feat(cat): answer @cat on the wall, with the thread as context - #774
Merged
Conversation
Tagging the Cat under a post now gets a public reply on that post. Wall posts have no server seam. post-composer.ts calls a Postgres function straight from the BROWSER, so unlike a private message there is nowhere in the app to notice a mention — the notice has to happen in the database. A trigger is also the stronger choice on its own merits: it cannot be skipped by a client that forgets to call an endpoint, or by a second client written later. THE TRIGGER IS A PREFILTER, NOT THE RULE. Detection has one implementation — domain/mentions/parse.ts plus services/mentions/resolve.ts — and it stays there, because getting `@cat.`, `@catalogue` and `bob@example.com` right is not something to write twice in two languages and hope they agree. The trigger asks only "does this contain @cat at all?"; the worker applies the resolver's verdict. It may over-select and must never under-select, which is why the pattern is a plain case-insensitive substring rather than a clever one. An over-selected row is completed, not failed — retrying a post that never asked anything three times and then logging an error would be noise about nothing. Context is the THREAD, not the tagged post. On X, tagging Grok under a reply gets you an answer about that reply. "@cat is this realistic?" three replies deep is a question about the conversation, and answering it from the last sentence alone is the difference between an assistant and an autocomplete. writeTimelineReply is the same split as write-message.ts, for the same reason: create_timeline_event re-imposes the RLS actor check by hand (`v_actor_id IS DISTINCT FROM auth.uid()` raises "Actor mismatch"), which is right for a browser and impossible for a worker that has no auth.uid() at all. Relaxing it is not an option — it is the only thing between one authenticated user and posting as another — so authorization stays there and this exists for callers with authority established elsewhere. The reply inherits its parent's visibility. Answering a followers-only post in public would republish the question to people who could not see it. It also writes the timeline_event_visibility row, without which the reply exists and appears on nobody's timeline. Trigger behaviour proven against the real schema inside BEGIN ... ROLLBACK: a tagging post is queued, a plain post is not, `@catalogue` is queued and later discarded, THE CAT'S OWN POST IS NEVER QUEUED (no self-answer loop), and a second fire returns INSERT 0 0 — one post owes one reply. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
20260826160000 was taken by 20260826160000_profile_username_history.sql, merged from a parallel session while this branch was open. Supabase keys schema_migrations on the version alone, so two files sharing a timestamp fail on the second INSERT — caught by the Migration Replay gate: ERROR: duplicate key value violates unique constraint "schema_migrations_pkey", Key (version)=(20260826160000) already exists Neither migration is wrong; the number is. Renamed rather than merged, because they do unrelated things.
catomean
force-pushed
the
feat/cat-wall-posts
branch
from
August 26, 2026 13:36
07d63ca to
7e566b1
Compare
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.
Tagging the Cat under a post now gets a public reply on that post.
Why a database trigger
Wall posts have no server seam.
post-composer.tscalls a Postgres function straight from the browser, so unlike a private message there is nowhere in the app to notice a mention.A trigger is also stronger on its own merits: it can't be skipped by a client that forgets to call an endpoint, or by a second client written later.
The trigger is a prefilter, not the rule
Detection has one implementation —
domain/mentions/parse.ts+services/mentions/resolve.ts— and it stays there. Getting@cat.,@catalogueandbob@example.comright is not something to write twice in two languages and hope they agree.So the trigger asks only "does this contain
@catat all?" and the worker applies the resolver's verdict. It may over-select; it must never under-select — which is why the pattern is a plain case-insensitive substring rather than a clever one.An over-selected row is completed, not failed. Retrying a post that never asked anything three times and then logging an error would be noise about nothing.
Context is the thread, not the tagged post
On X, tagging Grok under a reply gets you an answer about that reply.
@cat is this realistic?three replies deep is a question about the conversation — answering it from the last sentence alone is the difference between an assistant and an autocomplete.writeTimelineReplyThe same split as
write-message.ts, for the same reason:create_timeline_eventre-imposes the RLS actor check by hand (v_actor_id IS DISTINCT FROM auth.uid()→ "Actor mismatch"), which is right for a browser and impossible for a worker with noauth.uid()at all.Relaxing it isn't an option — it's the only thing between one authenticated user and posting as another — so authorization stays there, and this exists for callers whose authority was established elsewhere.
Two details that decide whether the reply is actually correct:
timeline_event_visibilityrow, without which the reply exists and appears on nobody's timelineTrigger behaviour, proven against the real schema
Inside
BEGIN … ROLLBACK:The Cat's own post is never queued, so there is no self-answer loop.
Verification
npm run verify— exit 0 (2373 tests)🤖 Generated with Claude Code