Skip to content

fix(cat): let the Cat write its reply, by separating authorization from writing - #771

Merged
github-actions[bot] merged 2 commits into
mainfrom
fix/cat-can-write
Aug 26, 2026
Merged

fix(cat): let the Cat write its reply, by separating authorization from writing#771
github-actions[bot] merged 2 commits into
mainfrom
fix/cat-can-write

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Found by running the real path against production. The worker claimed its mentions, read the conversation, produced an answer — and then threw:

claimed: 2, answered: 0, failed: 2
queue state: pending  attempts=1  err=Unauthorized

Why no unit test could have caught it

sendMessage begins with getServerUser() and throws Unauthorized when there's no session, then asserts the sender id matches that user. Both are correct for a person typing in a browser, and impossible for the Cat, which answers from a cron worker with no session at all.

A mocked Supabase client has no opinion about sessions. This needed the real thing.

The fix is not to relax sendMessage

"The sender must be the authenticated user" is a real control on the human path, and it is unchanged.

What was actually wrong is that one function decided authorization and performed the write, so a sender that isn't a browser session had no way in at all.

writeMessage now owns the writing half:

  • the insert
  • last_message_at / last_message_preview — without which the conversation list still shows the previous message as the latest, so a reply that was written looks like it never arrived
  • the sender's own read time

sendMessage keeps its checks and delegates. A caller reaching writeMessage is asserting that authorization already happened — which is why it takes an admin client explicitly rather than constructing one.

The evidence was precise about where it broke

The participant upsert had worked throughout. The live test showed the Cat already in the conversation —

is the Cat a visible participant now?
  messaging_test, test2, cat

— while its message was being lost. So the failure was exactly at the write, and nowhere else.

Verification

  • npm run verify — exit 0 (2364 tests)
  • the same end-to-end test will be re-run against production after deploy; everything it wrote was cleaned up (messages left in window: 0, queue rows left anywhere: 0)

🤖 Generated with Claude Code

catomean and others added 2 commits August 26, 2026 13:45
Caught by checking production rather than by reading the code: the queue
had deployed, and the Cat still had no account. Following that through,
it never would have got one.

The circularity: resolveMentions can only flag @cat when a Cat PROFILE
exists, so with no account nothing is ever queued — and the worker called
ensureCatAccount AFTER its empty-queue early return, so an empty queue
established nothing. Nothing queued, so nothing was established, so
nothing queued. A freshly deployed platform would sit there with @cat
resolving to nobody, looking exactly like a working feature that nobody
had used yet.

The worker now establishes the account before it looks at the queue. One
primary-key lookup per tick when it is a no-op, which is every tick after
the first, and the every-minute timer therefore becomes what brings the
Cat into existence.

Second half of the same bug, in the timer: `Persistent=true` only
replays a run MISSED while the machine was down. A timer enabled for the
first time just waits for the next OnCalendar — for a daily timer, up to
24 hours. Production confirmed it: the unit was installed and enabled,
LAST was "-", NEXT was 12 hours away. OnActiveSec=1min makes a freshly
deployed Cat exist within the minute.

Pinned by a test that asserts the ORDERING, not just the call, and proven
by restoring the old arrangement — two tests go red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…om writing

Found by running the real path against production. The worker claimed
its mentions, read the conversation, produced an answer — and then threw:

  claimed: 2, answered: 0, failed: 2
  queue state: pending  attempts=1  err=Unauthorized

sendMessage begins with getServerUser() and throws Unauthorized when
there is no session, then asserts that the sender id matches that user.
Both are correct for a person typing in a browser and impossible for the
Cat, which answers from a cron worker with no session at all. No unit
test could have caught it: a mocked client has no opinion about sessions.

The fix is NOT to relax sendMessage. "The sender must be the
authenticated user" is a real control on the human path and is unchanged.
What was wrong is that one function decided authorization AND performed
the write, so a sender that is not a browser session had no way in.

writeMessage now owns the writing half — the insert plus the two
follow-ups that make a reply look like it arrived: the conversation's
last_message_at/preview, without which the list still shows the previous
message as the latest, and the sender's own read time. sendMessage keeps
its checks and delegates. A caller reaching writeMessage is asserting
authorization already happened, which is why it takes an admin client
explicitly instead of making one.

The participant upsert had worked throughout — the live test showed the
Cat already in the conversation (messaging_test, test2, cat) while its
message was being lost — so the failure was precisely at the write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions
github-actions Bot merged commit 44d88d1 into main Aug 26, 2026
7 checks passed
@github-actions
github-actions Bot deleted the fix/cat-can-write branch August 26, 2026 12:27
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