Severity: p1
Area: engine-store
Slug: attempt-row-corruption-strict-terminal
Claim
The #164 quarantine contract (journal AND take a state action so the next dispatch proceeds) landed only on the cache-hit branch. The succeeded-attempt-row branch (the second corruption detection site) journals the Inconsistency record and, under the default strict verdict, fails with the non-retryable CacheCorruptionDetected — but takes no repair/quarantine action on the durable attempt row, so every resume of that run re-reads the same row via attempts.get(attemptId), re-detects the identical corruption, and re-fails forever, with tolerant mode or time-travel as the only exits. Blind eviction/re-execution would violate exactly-once for irreversible activities (the gaps lens's dissent is valid), so the fix is not a copy of #164 — but the terminality is currently neither an explicit documented design decision nor pinned by any test, and it is the same permanent refuse→fail shape #99/#107/#164 closed elsewhere.
Evidence
Failure scenario
Run R's attempt seals; a disk fault corrupts one recorded output blob; R resumes → strict fail. Every subsequent resume repeats identically and R can never complete without hand-deleting the row.
Verifier confirmation
Read the code read-only; every element of the claim holds. (1) The succeeded-attempt-row branch at 570-623 never mutates the row. (2) The #164 quarantine (journal AND cache.evict) exists only on the cache-hit branch. (3) CacheCorruptionDetected is non-retryable with no opt-out, so the re-fail loop is deterministic. (4) No test pins whether the run stays failed or heals on a second dispatch. (5) The terminality is not documented as a design decision. The finding's own caveat is also correct: blind eviction of a succeeded attempt row would re-execute a durably-completed (possibly irreversible) activity, so the fix legitimately differs from #164 — but that nuance doesn't refute the claim, which is precisely that the terminality is neither quarantined, tested, nor documented.
Found by the iterate-until-dry loop, round 15 review.
Severity: p1
Area: engine-store
Slug: attempt-row-corruption-strict-terminal
Claim
The #164 quarantine contract (journal AND take a state action so the next dispatch proceeds) landed only on the cache-hit branch. The succeeded-attempt-row branch (the second corruption detection site) journals the Inconsistency record and, under the default strict verdict, fails with the non-retryable
CacheCorruptionDetected— but takes no repair/quarantine action on the durable attempt row, so every resume of that run re-reads the same row viaattempts.get(attemptId), re-detects the identical corruption, and re-fails forever, with tolerant mode or time-travel as the only exits. Blind eviction/re-execution would violate exactly-once for irreversible activities (the gaps lens's dissent is valid), so the fix is not a copy of #164 — but the terminality is currently neither an explicit documented design decision nor pinned by any test, and it is the same permanent refuse→fail shape #99/#107/#164 closed elsewhere.Evidence
packages/engine-store/src/internal/ActivityPersistence.ts:570-623— succeeded-row branch (attempts.get→ state'succeeded'→replayOutputsfailure →replayCorruption→receiver.noteCorruption→ verdict'fail'→Effect.fail(new CacheCorruptionDetected)at 611-620) contains no attempts-row repair, patch, or evict.packages/engine-store/src/internal/ActivityPersistence.ts:499,:559— the round-15: Cache-hit corruption never evicts the poisoned row #164cache.evictquarantine, with the comment at 484-496 explicitly framing quarantine as "journal AND evict". No counterpart on the attempt-row branch (~90 lines later).packages/engine/src/RetryPolicy.ts:295-297—CacheCorruptionDetectedis the sole entry indefaultNonRetryablewith "no per-callsite or per-policy opt-out"; the docblock concedes "each retry re-reads the same corrupt row and appends another durable corruption record".packages/engine-store/test/ReplayCorruptionClassification.test.ts:229-257— the strict succeeded-row cell performs exactly one corrupt re-dispatch; the round-15: Cache-hit corruption never evicts the poisoned row #164 evict-then-heal test (line 330+) exercises only the cache branch. No test dispatches the corrupt attempt row a second time.ActivityPersistence.ts:580-589document the round-7: Refused stale read set hit re-executes then permanently fails the run #99/round-8: replayOutputs failure on a verified cache hit is a permanent failure loop; no mkdir of parents #107 fix only for the host-refusal classification; the corruption→strict-fail terminality is stated nowhere.Failure scenario
Run R's attempt seals; a disk fault corrupts one recorded output blob; R resumes → strict fail. Every subsequent resume repeats identically and R can never complete without hand-deleting the row.
Verifier confirmation
Read the code read-only; every element of the claim holds. (1) The succeeded-attempt-row branch at 570-623 never mutates the row. (2) The #164 quarantine (journal AND
cache.evict) exists only on the cache-hit branch. (3)CacheCorruptionDetectedis non-retryable with no opt-out, so the re-fail loop is deterministic. (4) No test pins whether the run stays failed or heals on a second dispatch. (5) The terminality is not documented as a design decision. The finding's own caveat is also correct: blind eviction of a succeeded attempt row would re-execute a durably-completed (possibly irreversible) activity, so the fix legitimately differs from #164 — but that nuance doesn't refute the claim, which is precisely that the terminality is neither quarantined, tested, nor documented.Found by the iterate-until-dry loop, round 15 review.