Skip to content

feat(cat): answer @cat in private messages - #767

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/cat-mention-queue
Aug 26, 2026
Merged

feat(cat): answer @cat in private messages#767
github-actions[bot] merged 1 commit into
mainfrom
feat/cat-mention-queue

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Tagging the Cat in a conversation now gets an answer in that conversation, from the Cat's own account.

Shape: a queue with two payers

The write path records that a reply is owed and returns — an LLM round trip must not sit inside the sender's POST. A worker pays the debt.

The systemd timer is the durability path, not the latency path: the write also kicks a run, so a reply normally lands in seconds. The tick is what guarantees the question survives a process dying mid-thought. An assistant that silently drops a question is worse than one that's slow.

property mechanism
one mention → one reply UNIQUE (source_type, source_id); 23505 treated as success, which is what makes an at-least-once producer safe
two workers never collide FOR UPDATE SKIP LOCKED in claim_cat_mentions
a failed attempt retries back to pending until MAX_ATTEMPTS, then failed with the reason kept

Concurrency proven on production with two simultaneous sessions:

seeded: 2
A claimed: 1
B claimed: 1
distinct rows claimed: 2 of 2
still pending: 0
cleaned up: 0

The bug I wrote and caught

The dedupe key must be the message id. My first draft fell back to the conversation id when no message id was passed — which would have meant the first question ever asked in a conversation was the only one ever answered, every later insert colliding with the unique constraint and reading as "already queued".

The parameter is now required, the fallback is gone, and a test fails if it comes back.

"@cat what do you think about this?"

That works because "this" means the conversation. The Cat reads CAT_CONTEXT_MESSAGE_WINDOW recent messages of the thread it was tagged in — no other conversation, no history beyond the window. That's a stated promise about what tagging consents to, which is why the number lives in config rather than inside a query.

Two product calls, taken as documented assumptions since they were open and both are reversible:

  • The Cat stays in the conversation once tagged — a follow-up needs no second tag, both people can see it's present, and it can be removed like any participant.
  • A failure to think is still answered. CAT_FALLBACK_REPLY goes out rather than nothing, because a tag that produces silence is indistinguishable from a broken feature — and this codebase has shipped exactly that before.

Layering

parse (pure)  →  resolve (one query)  →  note (the seam a write path calls)
              →  queue (durability)   →  worker (dispatch)  →  reply (what it says)

Nothing was added to chat-orchestrator.ts or memory.ts — already 837 and 1062 lines. A new surface (wall posts, group chat) gains all of this by calling noteCatMention.

Verification

  • npm run verify — exit 0 (2359 tests); check:rpc-exists picked up claim_cat_mentions, baseline still zero
  • 10 new tests, including the dedupe-key regression and "never throws — a person's message must be stored whatever the Cat does"
  • migration dry-run inside BEGIN … ROLLBACK, plus the live concurrency test above

🤖 Generated with Claude Code

Tagging the Cat in a conversation now gets an answer in that
conversation, from the Cat's own account.

The shape is a queue with two payers. The write path records that a
reply is owed and returns, because an LLM round trip must not sit inside
the sender's POST; a worker pays the debt. The systemd timer is the
DURABILITY path rather than the latency path — the write also kicks a
run, so a reply normally lands in seconds, and the tick is what
guarantees the question survives a process dying mid-thought. An
assistant that silently drops a question is worse than one that is slow.

Idempotency is the unique key on (source_type, source_id), which is what
makes an at-least-once producer safe, and 23505 is therefore treated as
success rather than failure. Concurrency is FOR UPDATE SKIP LOCKED
inside claim_cat_mentions, so an inline run overlapping a timer tick
cannot answer the same mention twice. Proven on production with two
concurrent sessions: two mentions seeded, "A claimed: 1", "B claimed: 1",
distinct rows claimed 2 of 2, nothing left pending, scratch table
dropped.

The dedupe key is the MESSAGE id, and the first draft got that wrong. It
fell back to the conversation id when no message id was passed, which
would have meant the first question ever asked in a conversation was the
only one ever answered — every later insert colliding with the unique
constraint and being read as "already queued". The parameter is now
required and the fallback is gone, with a test that fails if it returns.

"@cat what do you think about this?" works because "this" means the
conversation. The Cat reads CAT_CONTEXT_MESSAGE_WINDOW recent messages
of the thread it was tagged in and nothing else — no other conversation,
no history beyond the window. That is a stated product promise about
what tagging consents to, which is why the number lives in config and
not in a query.

The Cat STAYS in the conversation once tagged, so a follow-up needs no
second tag and both people can see it is there, and it can be removed
like any participant. That was one of the two open product calls; it is
recorded here as an assumption and is reversible.

A failure to think is still answered. CAT_FALLBACK_REPLY goes out rather
than nothing, because a tag that produces silence is indistinguishable
from a broken feature, and this codebase has shipped exactly that before.

Layering: parse (pure) -> resolve (one query) -> note (the seam a write
path calls) -> queue (durability) -> worker (dispatch) -> reply
(what it says). Nothing was added to chat-orchestrator.ts or memory.ts,
already 837 and 1062 lines. A new surface gains all of this by calling
noteCatMention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions
github-actions Bot merged commit 57431cb into main Aug 26, 2026
9 checks passed
@github-actions
github-actions Bot deleted the feat/cat-mention-queue branch August 26, 2026 11:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant