A board that refuses

Austin Wagner

Many AI sessions coordinate through one append-only board, and the board refuses what it cannot trust: a label that is not yours, a closure with no result, a delivery claim with no commit, a session id one keystroke from a real one. A surface that records is a log. One that refuses is infrastructure.

Problem
78 session labels wrote to the board in one week, editing shared trees, with no way to know who held what, who had read what, or whether a claimed result had happened.
Result
A Postgres queue and a session-stamped trail the application role cannot update or delete, typed refusals at the write path, a read receipt with no revoke.
Identity
One session, one label, forever. A relabel is refused. A session id one character from a known one is refused.
Stack
TypeScript, PostgreSQL with row-level security and per-role grants, an MCP server the sessions call.

Append-only is a grant, not a promise

The trail's table carries no update or delete grant for the application role and no policy that would admit one, so a session cannot retract what it said. Copied from the migration:

GRANT SELECT, INSERT, UPDATE ON agent_tasks TO corvis_app;
GRANT SELECT, INSERT ON agent_task_updates TO corvis_app;

packages/db/migrations/415_agent_tasks.sql, lines 187 to 188, private repository.

Identity is enforced at the same chokepoint. A session's first write binds its real id to a label, and a later write under a different label is refused with a typed error that names the rule.

The typo the identity check could not see

One session posted forty times under its id and once under an id that differed at index 33. A person had typed it. The identity check could not catch that: a mistyped id does not look like a session changing its label, it looks like a new session, and new sessions are admitted. The phantom is permanent, since the trail cannot be edited.

So a second check runs only for an id the board has never seen, and refuses one that differs from an established id at exactly one character, naming both. The window is sound because a version-4 id carries 122 random bits: a fresh id landing one hex character from a known one is vanishingly unlikely, so the only plausible cause is a person typing it.

The suite ran red first, against the unguarded module: 4 failed and 3 passed, with the phantom write succeeding beside the real id as in production. Then green. A guard that cannot be made to fail on purpose has not been shown to work.

The read nobody could see

Mentions reach a session only when it asks for its own queue, and nothing said whether it ever had. One session read the feed about ten times in a shift and its own queue zero times, while holding 85 unread events addressed to it, among them a check-in naming the exact fix that session then reported as an unfixed bug.

The existing audit log could not answer the question: 735 of 736 post rows carried an agent label and 0 of 21 queue reads did, because that read took no identity. A check built on it would have reported every session as never having read.

So the receipt is a row written only when an agent is actually served its own queue, keyed to the agent rather than the filter, with no delete grant. A receipt has no revoke. Every post reply now carries the count of unread mentions since the last read.

The audit that found eleven problems in five rows

The audit lists agents that post repeatedly without being served their own queue. Its first scheduled run used a 1440-minute window against a table 75 minutes old and printed eleven findings from five receipts. It was judging time in which no agent could have held one.

The window is now floored at the migration's own applied-at timestamp, read from the schema table rather than the receipts, since the earliest receipt moves forward whenever anyone re-reads. The judged span renders beside the count, because "nobody is behind" and "too young to tell" otherwise look identical.

Record

Every figure above is bound to a named section of the wiki pages agent-task-board-service and agent-session-ledger, checked mechanically before this page can deploy. Both pin the git blob hash of every source file they cite, so a change to the enforcing code invalidates the citation rather than outdating it silently. The 735-of-736 census was re-derived from the audit log; a 48-hour window ending 2026-09-10 reproduces it.

Known limits. The refusal log has recorded which rule fired only since 2026-09-12; in that window 12 writes were refused and neither identity guard has fired, and the 448 refusals before it are logged as one undifferentiated word. A label exported to a tool that never consults the board is still unvalidated, and that gap is filed as an open task rather than claimed as closed here.

Sources

  • The board, its migrations and both wiki pages are in a private repository; the excerpt above is copied from it with path and lines.
  • The shell-side reader of the same queue is public: API/tools/board-watch/src/board_watch/watch.py at c447b0c, whose _identity docstring records why a receipt is keyed to the caller and not the filter.
  • This article's manifest: provenance.json.