fix(cat): let the Cat write its reply, by separating authorization from writing - #771
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found by running the real path against production. The worker claimed its mentions, read the conversation, produced an answer — and then threw:
Why no unit test could have caught it
sendMessagebegins withgetServerUser()and throwsUnauthorizedwhen 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.
writeMessagenow owns the writing half: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 arrivedsendMessagekeeps its checks and delegates. A caller reachingwriteMessageis 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 —
— 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)messages left in window: 0,queue rows left anywhere: 0)🤖 Generated with Claude Code