Repository navigation
fix(control): recover a damaged control journal without duplicate effects (#1240) - #1254
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lls; explicit acceptance (steering q13) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ects A torn tail, a schema-rejected row or a sequence gap made FileControlHistory.load() reject, cached, so every control call, reads included, failed with history_unavailable across restarts until events.jsonl was edited by hand (#1240). A failed append also poisoned the process. The journal is also the executor's dedupe ledger (steering q12), so it is recovered, not reset: - Torn tail only: the verified prefix is complete evidence (received and dispatched are durable before any effect), so full service resumes after the original bytes are preserved. - Other damage: every valid row is kept. New keyed intents are refused wherever a lost row could have held their key. Only a row valid except for an unknown kind proves its exact pair; anything else blocks all new keyed intents (q13). Reads, unkeyed calls and retries whose received survived keep working. - The block comes from the preserved evidence, re-analyzed on every load. A restart, deleting recovery.json, or a malformed marker (q14) cannot lift it; only naming the evidence digest in recovery-accepted.json does. The refusal message says so. - A failed append drops the cached load, so the next append re-reads the disk instead of poisoning the process. - Recovery is recorded as a control.history_recovered incident. Tests use real journal rows and are red on main; each guard is mutation-checked. Closes #1240 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Sep 25, 2026
…for #1240 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…records never lifted Review A (blocker): the executor stamps every row of a call with its request key (all 1,846 real calls agree), but the dedupe lookup reads only `received` rows. A downgrade-shaped `received` row with a corrupted key string, or a schema-valid one with its key deleted, let the real key dispatch again while its dispatched/result rows still named it. Keys named by a damaged call's kept rows are now blocked, and a call whose rows disagree about the key is damage in its own right. Review A (major): truncating a quarantine inside its damaged line made re-analysis call it a torn tail and replace the recorded block. Recorded guards now always apply; re-analysis only adds. Review B: a MISSING kind is corruption, not the downgrade shape; exact-pair precision, gap salvage + renumber, preserved invalid markers and load-failure un-caching are now pinned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… every launch (steering q17) Rewriting a call whose valid rows disagree about its request key could never make the ledger clean, so each launch re-quarantined the same bytes and filed another incident. Its rows now leave the rewritten ledger; the quarantine keeps the bytes and the recorded pairs keep its keys blocked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eyed call needs its intent Both round-2 reviewers: the task writer (tasks.ts) appends `step` rows on a call's id with no request key, so any keyed startControlTask call read as inconsistent. Its rows (including the durable task result) were dropped at the next load, and accepting that digest let the key run twice. The consistency check now reads only the rows the executor stamps (received/dispatched/result/duplicate); all 1,846 real calls agree there, while 660 real step rows carry no key. Review A: a keyed call left with dispatched/result rows but no received or duplicate row lost its intent (for example, rewritten into another valid call) and is now damage whose keys are blocked. There are 0 such calls in the real journal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
Review disposition (2 rounds: Grok A, Pi B; steering q12–q17)
CI is green on |
Juliusolsson05
added a commit
that referenced
this pull request
Sep 25, 2026
…nce rule Co-Authored-By: Claude Opus 5.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.
Closes #1240.
Problem
A damaged
events.jsonl(a crash mid-append, a newer build's event after a downgrade, or missing rows) made every control call fail withhistory_unavailable: renderer feed paging, terminals, workflows, External Control. It stayed that way across restarts until someone edited the file by hand. A failed append also blocked the rest of the process.Why it isn't "move the file aside and start fresh"
The journal is also the executor's dedupe ledger. A keyed retry finds its earlier
receivedrow and replays the stored result, or answersinterrupted. An empty ledger would turn that retry into a second real effect (steering q12). Plan:docs/plans/2026-09-25-control-history-recovery.md(plan-only commits8be3658band4ea65889).What changes
receivedanddispatchedare durable before any effect, so the torn row is the last write and its call's earlier rows survive.kind(downgrade)(caller, requestKey).receivedsurvived keep working.recovery.json, or a malformed marker (q14) cannot lift it. Lifting it takes an explicit acceptance: putting the evidence digest inrecovery-accepted.json. The refusal message and acontrol.history_recoveredincident name the file and the digest.Evidence
history_unavailable) where applicable.trust-record-over-fileis equivalent, because present files are always rescanned.tsc -bis clean. The control, control-sdk and incident suites pass (80 tests).Not in scope
🤖 Generated with Claude Code