fix(cat): the outcome gate reported a cause it had never tested - #829
Merged
Conversation
The nightly gate failed with "assistant messages exist in the window but 0 Cat
actions were logged — the cat_action_log write path looks dead (check RLS
policies and the executor)" while cat_action_log held TEN rows for that window,
the newest from 2026-08-25. The write path was alive. The gate had never looked
at it.
`proposed` counts only rows that are BOTH status=completed AND a create_*
action. In the window there were none — the single create_* was `create_cause`,
DENIED. Three different worlds produce proposed === 0:
1. nothing is written at all → the audit trail is dead. Page.
2. writes happen, no completed creates → nothing creatable was asked for, or
every attempt was denied. Not an outage.
3. no assistant activity at all → nothing to grade.
The old predicate could not tell 1 from 2, so it asserted the diagnosis for 1
whenever it saw either. It now fetches the window's rows with no status or
action filter and fails only on the shape it was built for: the log EMPTY while
the Cat is talking. Otherwise it reports what it found — including denials,
which are a real signal worth seeing and were being swallowed.
This matters beyond the noise: a tripwire that cries every night is one nobody
believes on the night it is right, which is the exact failure it exists to catch.
The predicate is now a pure function and main() only runs on direct execution,
so the decision can be tested without a database — it stayed wrong because
nothing could reach it. Seven cases added, run in a child process against the
real module (matching eval-cat-retry's pattern). Proven by mutation: restoring
the old predicate turns three of them red, and the artifact was verified
byte-identical afterwards.
Live against production: exits 0 with
"the write path is alive — 10 action(s) logged in window (denied 4,
completed 5, failed 1), just none of them a completed create_*."
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqzRcMP1uJzd7Tav5fNxQz
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.
The nightly gate paged with
The write path is not dead.
cat_action_logheld ten rows for that window, the newest from 2026-08-25. The gate had never looked at it.What it actually measured
proposedcounts only rows that are bothstatus=completedand acreate_*action. In that window there were none — the singlecreate_*wascreate_cause, and it was denied. Three different worlds produceproposed === 0:The old predicate could not tell 1 from 2, so it asserted the diagnosis for 1 whenever it saw either — a cause it had never tested.
This matters beyond the noise: a tripwire that cries every night is one nobody believes on the night it is right, which is precisely the failure this gate exists to catch.
The fix
It now fetches the window's rows with no status or action filter — "is anything written at all" is the only question that distinguishes the dead-write-path bug — and fails only on that shape. Otherwise it reports what it found, including denials, which are a real signal that was being swallowed.
Testability was the root cause
The predicate stayed wrong because nothing could reach it: the decision lived inline in
main(), behind network calls, in a script that ran on import. It is now a puregateVerdict()andmain()runs only on direct execution.Verification
eval-cat-retry's pattern.Worth a separate look
Of the 10 actions in the window, 4 were denied (
publish_entity×2,create_cause,add_wallet) and onesend_paymentfailed. Not an outage, but not obviously intended either — the fixed gate surfaces it instead of hiding it behind a wrong diagnosis.🤖 Generated with Claude Code
https://claude.ai/code/session_01AqzRcMP1uJzd7Tav5fNxQz