Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ Both platforms share one Session Manager and use the same stream-relay logic via

**Message queue** sits between platform bots and Session Manager. Rapid messages are debounced (3s window) into a single prompt. Messages arriving while Claude is processing are collected (up to 20) and delivered as a combined followup after the current turn completes.

**Context injection:** Each message includes metadata — current time, chat type (DM/group/topic), topic name, sender username, and emoji reactions. The agent knows where it is, when it is, and who it's talking to. Reactions are delivered as messages so the agent can respond to a thumbs-up or a ❤️ without the user typing anything.

**Cron jobs** run separately via launchd plists. Each plist calls `run-cron.sh <task-name>`, which invokes `cron-runner.ts` to spawn a one-shot `claude -p` session with the cron's prompt.

**Config:** `config.yaml` defines agents (workspace + model) and bindings (chatId/channelId -> agentId). User-specific overrides live in `config.local.yaml` (gitignored, deep-merged over `config.yaml`). At least one platform (Telegram or Discord) must be configured. Tokens are read from macOS Keychain at runtime.
Expand Down
52 changes: 52 additions & 0 deletions docs/plans/completed/080-no-reply-trim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Plan: Fix NO_REPLY check to use trim + word-boundary regex

GitHub issue: #80

## Problem

When a cron LLM response starts with `NO_REPLY` but includes additional text (e.g. `NO_REPLY\n\nExplanation...`), the exact match `output === "NO_REPLY"` fails and the entire response gets delivered to the user.

Real example from bedtime-reminder cron:
```
NO_REPLY

Завтра (1 апреля) нет событий с конкретным временем...
```

## Root cause

`bot/src/cron-runner.ts:394`:
```ts
if (cron.type === "llm" && output === "NO_REPLY") {
```

Exact match doesn't handle trailing whitespace or extra text after `NO_REPLY`.

## Fix

Change line 394 from:
```ts
if (cron.type === "llm" && output === "NO_REPLY") {
```
to:
```ts
if (cron.type === "llm" && /^NO_REPLY\b/.test(output.trim())) {
```

Uses `\b` (word boundary) instead of `startsWith` — this catches `NO_REPLY`, `NO_REPLY: reason`, `NO_REPLY\ntext`, but correctly rejects `NO_REPLY_EXTRA` (underscore is a word character, so no boundary).

Also check `bot/src/stream-relay.ts` and `bot/src/message-queue.ts` for similar NO_REPLY checks — apply the same pattern everywhere.

## Files to change

- [ ] `bot/src/cron-runner.ts` — line 394, fix the check
- [ ] Search all `NO_REPLY` checks in `bot/src/` — apply same fix if exact match found
- [ ] Add/update tests for NO_REPLY with trailing text, whitespace, and clean NO_REPLY

## Tests

- [ ] `NO_REPLY` exact — should be swallowed
- [ ] `NO_REPLY\n\nSome text` — should be swallowed
- [ ] ` NO_REPLY ` — should be swallowed
- [ ] `NO_REPLY_EXTRA` — should NOT be swallowed (`\b` correctly rejects this — underscore is a word character)
- [ ] Regular output — should be delivered
Loading