CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Failing after 8s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 36s
CI & Build / Python tests (push) Successful in 1m6s
CI & Build / Build & push image (push) Successful in 21s
The prior-art and tool-rule hooks record every rule id they have named in <state>/<sid>.rules.ids and hand it back as exclude_rule_ids, so a rule is surfaced once per session and then goes quiet. That is correct while the session still HOLDS what it was told. A compaction breaks it in the worst available way: it summarizes the earlier injections out of context and does not touch the filesystem. The rule ends up absent from context AND still excluded — unreachable for the rest of the session. The compaction banner this hook already prints tells the model to re-pull its ALWAYS-ON rules, but a rule an arm surfaced is conditional and is not in that set, so it has no other way back. The rules most likely to be in that state are the ones that fire most often. The stale ledger is genuinely found again rather than orphaned: the etag marker further down this same hook is rewritten on `compact` and keyed by session_id, which is only meaningful if the id survives a compaction. CLEARED ON THE SOURCES THAT DESTROY CONTEXT, AND ONLY THOSE. `compact` and `clear` destroy it while the file survives. `resume` does not — the context came back intact, so clearing there would re-surface every rule after a restore that lost nothing, which is the same defect from the other side. `startup` is a no-op against a new session id. `fork` keeps it, and the answer holds whichever way forks are keyed: a fork carries the conversation, so an inherited id means an accurate ledger and a new id means an empty file. Only the RULE ledger. The same directory holds .ids / .sync.ids / .derive.ids for the note arms; whether a surfaced note should return after a compaction is a different question with a different answer, and a `rm` glob would have decided it silently. The guard pins the DISCRIMINATION, not the deletion: the whole source table is asserted in one statement, so a blanket delete (all False) and a no-op (all True) both fail, and neither can be made to pass by editing one case. A second test pins the scope against that glob, and a third proves an event with no session id clears nothing rather than falling back to a wildcard. Runs with no SCRIBE_URL/SCRIBE_TOKEN on purpose — the clear is local, keyless and networkless, and must still happen against an unreachable instance. That is also why it sits above the config read rather than inside the dynamic tier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ