PR #3 is ready to merge

Ready to merge

0 open findings · F46-F56 verified fixed

323 repository tests + 95 independent probes pass

+66 / -18 since last review 45288eb

Reviewed exact head 45288ebaf28e1e1581c03dd81514323e2e15d78d on 23 September 2026. F56 is verified fixed, and no open findings remain from this review. I found no new blocker in the latest diff and affected paths.

Why the last fix closes F56

Recovery now honors command-applied even when a completed observation selected no target and created no transaction. The original interrupt is returned as completed instead of being applied again to later live work. Both failing variants, pausing one and two pre-existing transactions before restart, now leave newly created B untouched on replay.

The earlier no-op affirmations and interrupts still pass. The tests also retain the distinction between accepted-only work, which can run, and selected-but-unapplied work, which returns an explicit incomplete result. Payload conflicts still reject before a cached result is returned, and uncertain external effects remain subject to reconciliation.

Verification

The independent suite contains 59 earlier kernel and accepted-turn probes, plus 36 recovery cases accumulated during this PR review. It covers complete restarts, valid journal prefixes, target identity, incomplete work, evidence, dispatch receipts, outcome recovery and the last replay failures. Testing used an isolated detached worktree at the exact PR head.

Architecture and complexity

This change simplifies the implementation. It removes the 15-line semantic-act special case and makes a small change in the shared recovery reader using a durable fact that was already recorded. Production code shrinks by 10 lines overall; the rest of the diff retains regression tests.

The journal records added during review serve concrete recovery requirements: exact interaction identity, complete frozen effect sets, actual dispatch keys, durable command progress and complete outcomes. Within this PR's scope, I do not see unnecessary framework machinery that should block merging.

This recommendation is specific to PR #3. The file-journal adapter, physical durability guarantees and later stack integrations still need their own reviews. Recovery may synthesize a conservative result when the original command-outcome is missing, so the guarantee tested here is safe replay and coherent recovered state rather than byte-for-byte recovery of every missing reply. No disk-durability certification is implied by the in-memory prefix tests.

No Circe source changes, GitHub review submission or merge were performed. All finding bodies and earlier review rounds remain available below as history.

Final interrupt probes95 independent probesBaselineTypecheckConfigRound 1 archiveRound 2 archiveRound 3 archiveRound 4 archiveRound 5 archiveSidecar

Original walkthrough, preserved

PR #3 adds startup reconstruction to the journal port. It records full intents and transaction snapshots, discourse snapshots, accepted commands and final command results; recoverState reconstructs maps and marks unresolved dispatches UNKNOWN.

Original scope: +885 / -40 at 8d9cc15.

flowchart TD
 A[Accepted command] --> B[Intent and transaction snapshots]
 B --> C[Per-effect freeze records]
 C --> D[Dispatch request records]
 D --> E[External invoke]
 E --> F[Effect outcome records]
 F --> G[Transaction settlement snapshot]
 G --> H[Discourse snapshot and command outcome]
 J[Restart from any durable prefix] --> K[recoverState]
 K --> L[Restore runtime maps and accept new commands]
Round 2 changes

The fix adds exact interaction scoping, accepted-command fingerprints, a complete-freeze marker, pre-dispatch operation keys and complete outcome fields. The original reproductions pass. Five new assertions show incomplete coverage in three original findings and one replay regression.

Round 3 changes

The fix adds scoped observation reservations, target-selection records for typed commands, a shared settlement decision and correct null-result recovery. All earlier reproductions pass. Semantic execution still bypasses target recording, target-only progress is mistaken for completion, and paused recovery still skips settlement.

Round 4 changes

F53, F54 and F55 now pass. The remaining gap is the no-target semantic case: completed affirmations and interrupts do not persist their empty selection, so replay can affect later work.

Round 5 changes

The patch adds a null-target record for four semantic acts only when no intent or transaction was touched. Previous no-op cases pass. An interrupt that actually paused existing work still replays against newly created transactions.

Round 6 changes

Recovery now treats command-applied as completion evidence in the no-target branch. The semantic-action no-op guard is removed. Both remaining interrupt reproductions pass, and no new actionable finding was identified in the incremental diff and affected paths.

Current fix prompt · Ctrl+`
Rerun prompt · Ctrl+Space
Finding status
P1 F46. Restart loses the presented gate and can redirect unaddressed actions fixedOriginal: packages/core/src/types.ts:560-570 at 8d9cc15logiccorrectness
Latest verification

Remains fixed. The complete 95-case independent suite passes on the exact reviewed head.

Original finding, preserved

DiscourseSnapshot omits presentedGateId and the fact that a gate has already been presented. Neither finalize nor the startup restore preserves these fields. Transactions are reconstructed, but the runtime forgets which one the user saw and incorrectly treats the interaction as never having presented a gate.

After proposing A and then B, a clean restart makes an unaddressed confirmation do nothing, instead of confirming B. If B has already succeeded before restart, an unaddressed cancellation falls back to older A and cancels it. D1 and D2 reproduce both behaviors from complete, valid journals; no crash truncation is needed.

This reintroduces the transaction-targeting behavior that PR #2 corrected. Persist and restore the presented identity even after that transaction settles. Keep the no-fallback semantics; do not infer the anchor from whichever live transaction happens to be first.

Original fix direction. Extend the durable discourse contract and its writer/reader with the presented anchor and its lifetime semantics. Preserve the settled anchor tombstone or an equivalent durable fact. Verify both current-gate confirmation and no fallback to older work after settlement.

Original probes: D1, D2

560: export interface DiscourseSnapshot {
561:   scopes: Scope[];
562:   scopeStack: ScopeId[];
563:   retainedScopes: ScopeId[];
564:   views: WorldView[];
565:   referents: Referent[];
566:   currentReferentId?: ReferentId;
567:   recentReferentIds: ReferentId[];
568:   pendingQuestion?: PendingQuestion;
569:   pendingFrame?: { capability: CapabilityId };
570:   metrics: MetricsBucket;
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/types.ts` around lines 560-570, DiscourseSnapshot omits presentedGateId and the fact that a gate has already been presented. Neither finalize nor the startup restore preserves these fields. Transactions are reconstructed, but the runtime forgets which one the user saw and incorrectly treats the interaction as never having presented a gate. Extend the durable discourse contract and its writer/reader with the presented anchor and its lifetime semantics. Preserve the settled anchor tombstone or an equivalent durable fact. Verify both current-gate confirmation and no fallback to older work after settlement. Reproduce D1, D2 in `/tmp/circe-pr3-evidence/recovery.test.ts` against PR #3 head 8d9cc151f6f62fb825d041b3a2b546773a8a3fff. Keep the three recovery controls and all 59 earlier reviewer probes passing. Run the repo test suite and typecheck. Change the durable record and recovery rule together; do not patch only the test scenario.
P1 F47. Recovery is not fully scoped to an exact interaction identity fixedOriginal: packages/core/src/journal-recovery.ts:200-203 at 8d9cc15logicdata-integrity
Latest verification

Remains fixed. The complete 95-case independent suite passes on the exact reviewed head.

Original finding, preserved

The command-accepted branch populates acceptedKeys without checking entry.interactionId, although the field is available. Later, recovery synthesizes cached results for these foreign keys. Effect records use startsWith(effectPrefix) instead of an exact interaction identity, so valid IDs A and A:child also overlap.

D3 writes command same in interaction A, then starts interaction B on the shared journal. B's own command same returns an empty reconstructed result and never invokes. D4 records an effect under A:child and recovers A; A imports the other interaction's effect despite having none of its intents or transactions. The existing isolation test uses different command IDs and does not assert effect isolation.

The journal is advertised as supporting multiple interactions, so every recovered namespace must apply the same exact boundary. The effect overlap can also collide with local transaction IDs such as tx:1 when those records exist in both interactions.

Original fix direction. Filter command acceptance by exact interactionId. Give effect, dispatch and outcome records an explicit interactionId or use a provably unambiguous identity encoding and exact ownership check. Test identical local command IDs across interactions and delimiter-containing interaction IDs.

Original probes: D3, D4

200:       case "command-accepted":
201:         if (entry.commandId) {
202:           acceptedKeys.set(`command:${entry.commandId}`, entry.commandId);
203:         }
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/journal-recovery.ts` around lines 200-203, The command-accepted branch populates acceptedKeys without checking entry.interactionId, although the field is available. Later, recovery synthesizes cached results for these foreign keys. Effect records use startsWith(effectPrefix) instead of an exact interaction identity, so valid IDs A and A:child also overlap. Filter command acceptance by exact interactionId. Give effect, dispatch and outcome records an explicit interactionId or use a provably unambiguous identity encoding and exact ownership check. Test identical local command IDs across interactions and delimiter-containing interaction IDs. Reproduce D3, D4 in `/tmp/circe-pr3-evidence/recovery.test.ts` against PR #3 head 8d9cc151f6f62fb825d041b3a2b546773a8a3fff. Keep the three recovery controls and all 59 earlier reviewer probes passing. Run the repo test suite and typecheck. Change the durable record and recovery rule together; do not patch only the test scenario.
P1 F48. Acceptance is treated as completion and reconstructed commands lose fingerprints fixedOriginal: packages/core/src/journal-recovery.ts:242-252 at 8d9cc15logicdata-integrity
Latest verification

Remains fixed. The complete 95-case independent suite passes on the exact reviewed head.

Original finding, preserved

Recovery turns every accepted key without a command-outcome into a completed cached TurnResult, even if no intent or transaction was written. It also restores fingerprints only from command-outcome or command-failed, ignoring the accepted command payload. Acceptance, execution progress and completion are therefore conflated.

D6 keeps a real journal prefix ending immediately after command-accepted. On restart, redelivery returns an empty result with no errors, creates no intent and does no work; that key is consumed permanently. D5 cuts only the final command-outcome after A already succeeded. Redelivering the same ID with target B is accepted as a cached A result instead of raising the required payload-conflict error.

A conservative no-retry policy is appropriate after uncertain external execution, but acceptance alone is not evidence of execution. Recovery needs a visible incomplete state or a safe resumption path, plus the immutable accepted fingerprint. Silently synthesizing completion hides lost work.

Original fix direction. Persist a scoped reservation with the original fingerprint before applying the command, and represent accepted, partially applied and completed states separately. Recover never-started commands or surface an explicit recoverable failure; retain no-reinvoke behavior after dispatch. Restore and compare fingerprints before any cached result is returned.

Original probes: D5, D6

242:   // Accepted commands whose outcome was never recorded are reconstructed
243:   // from the state they produced, so a replay cannot execute them again.
244:   for (const tx of transactions.values()) {
245:     if (tx.commandKey) acceptedKeys.set(tx.commandKey, tx.commandId ?? tx.commandKey);
246:   }
247:   for (const key of acceptedKeys.keys()) {
248:     if (commandResults.has(key)) continue;
249:     commandResults.set(
250:       key,
251:       synthesizeResult(key, transactions, intents, effects),
252:     );
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/journal-recovery.ts` around lines 242-252, Recovery turns every accepted key without a command-outcome into a completed cached TurnResult, even if no intent or transaction was written. It also restores fingerprints only from command-outcome or command-failed, ignoring the accepted command payload. Acceptance, execution progress and completion are therefore conflated. Persist a scoped reservation with the original fingerprint before applying the command, and represent accepted, partially applied and completed states separately. Recover never-started commands or surface an explicit recoverable failure; retain no-reinvoke behavior after dispatch. Restore and compare fingerprints before any cached result is returned. Reproduce D5, D6 in `/tmp/circe-pr3-evidence/recovery.test.ts` against PR #3 head 8d9cc151f6f62fb825d041b3a2b546773a8a3fff. Keep the three recovery controls and all 59 earlier reviewer probes passing. Run the repo test suite and typecheck. Change the durable record and recovery rule together; do not patch only the test scenario.
P1 F49. Crash recovery invents batch receipt keys that were never dispatched fixedOriginal: packages/core/src/journal-recovery.ts:229-238 at 8d9cc15logicdata-integrity
Latest verification

Remains fixed. The complete 95-case independent suite passes on the exact reviewed head.

Original finding, preserved

dispatch-requested stores only effectId and a timestamp. When acknowledgement is absent, recoverState fabricates a receipt with idempotencyKey equal to each member effect ID and executor set to unknown. The runtime actually submits a batch under the first member's key, so the persisted record is insufficient to reconstruct what was sent.

D7 runs a two-target batch and restarts from the prefix before its first acknowledgement. Both effects correctly become UNKNOWN. Reconciling the second effect asks about its own ID, although only the first effect's ID was used for the external operation. The recorded executor is also lost. A real executor cannot reliably find the second operation because it never existed under that key.

This is precisely the lost-response boundary that the PR promises to handle without retry. The dispatch identity has to be durable before the external call; an acknowledgement cannot be the first place the true batch identity is recorded.

Original fix direction. Write the actual dispatch envelope before invoking: interaction, executor, operation/idempotency key, and full member effect IDs. Build synthetic lost-ack receipts from that record for every member. Verify all recovered receipts use the actual batch key and no recovery path reinvokes uncertain work.

Original probes: D7

229:   for (const effect of effects.values()) {
230:     if (!dispatchedAt.has(effect.id) || terminal.has(effect.id)) continue;
231:     if (effect.status !== "PENDING" && effect.status !== "RUNNING") continue;
232:     effect.status = "UNKNOWN";
233:     effect.failure = undefined;
234:     effect.receipt ??= {
235:       effectId: effect.id,
236:       executor: "unknown",
237:       idempotencyKey: effect.id,
238:       dispatchedAt: dispatchedAt.get(effect.id)!,
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/journal-recovery.ts` around lines 229-238, dispatch-requested stores only effectId and a timestamp. When acknowledgement is absent, recoverState fabricates a receipt with idempotencyKey equal to each member effect ID and executor set to unknown. The runtime actually submits a batch under the first member's key, so the persisted record is insufficient to reconstruct what was sent. Write the actual dispatch envelope before invoking: interaction, executor, operation/idempotency key, and full member effect IDs. Build synthetic lost-ack receipts from that record for every member. Verify all recovered receipts use the actual batch key and no recovery path reinvokes uncertain work. Reproduce D7 in `/tmp/circe-pr3-evidence/recovery.test.ts` against PR #3 head 8d9cc151f6f62fb825d041b3a2b546773a8a3fff. Keep the three recovery controls and all 59 earlier reviewer probes passing. Run the repo test suite and typecheck. Change the durable record and recovery rule together; do not patch only the test scenario.
P2 F50. Clean recovery drops completed effect results and failure details fixedOriginal: packages/core/src/journal-recovery.ts:174-185 at 8d9cc15logicdata-integrity
Latest verification

Remains fixed. The complete 95-case independent suite passes on the exact reviewed head.

Original finding, preserved

Recovery reconstructs effects from effect-frozen and status-only outcome records. invokeBatch updates effect.result or effect.failure in memory but emits outcome without those payloads; reconciliation also omits successful results and failure details. The full command-outcome is loaded into the result cache, not used to reconstruct current effects.

The two D8 cases complete successfully and fail with a structured executor error, then restart from an intact journal. The recovered effect retains its status but loses the success result or failure object. Cached replay can still contain data that runtime.state() no longer has, giving different answers for the same durable outcome.

This is a data-fidelity defect on ordinary clean restart, independent of disk corruption or torn writes. Outcome records should be sufficient to rebuild the effect itself rather than relying on an unrelated command response cache.

Original fix direction. Journal the complete outcome data needed by Effect, including structured result, failure and timestamps, for invoke, observation and reconciliation writers. Reconstruct those fields consistently and test success/failure state equality after clean recovery.

Original probes: D8 success and failure

174:       case "outcome":
175:       case "reconciled": {
176:         const effect = effects.get(entry.effectId);
177:         if (!effect) break;
178:         const status: EffectStatus = VALID_EFFECT_STATUSES.has(entry.status)
179:           ? entry.status
180:           : "UNKNOWN";
181:         effect.status = status;
182:         if (entry.detail !== undefined) effect.result = structuredClone(entry.detail);
183:         if (TERMINAL_EFFECT_STATUSES.has(status)) {
184:           effect.endedAt = entry.at;
185:           terminal.add(entry.effectId);
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/journal-recovery.ts` around lines 174-185, Recovery reconstructs effects from effect-frozen and status-only outcome records. invokeBatch updates effect.result or effect.failure in memory but emits outcome without those payloads; reconciliation also omits successful results and failure details. The full command-outcome is loaded into the result cache, not used to reconstruct current effects. Journal the complete outcome data needed by Effect, including structured result, failure and timestamps, for invoke, observation and reconciliation writers. Reconstruct those fields consistently and test success/failure state equality after clean recovery. Reproduce D8 success and failure in `/tmp/circe-pr3-evidence/recovery.test.ts` against PR #3 head 8d9cc151f6f62fb825d041b3a2b546773a8a3fff. Keep the three recovery controls and all 59 earlier reviewer probes passing. Run the repo test suite and typecheck. Change the durable record and recovery rule together; do not patch only the test scenario.
P1 F51. A partially recorded effect set can execute as if it were complete fixedOriginal: packages/core/src/journal-recovery.ts:210-215 at 8d9cc15logicdata-integrity
Latest verification

Remains fixed. The complete 95-case independent suite passes on the exact reviewed head.

Original finding, preserved

commit writes one effect-frozen record per target and only later writes confirmation and the transaction transition containing the full effect list. Recovery reattaches whichever effects survived, but there is no durable manifest or completion marker proving that the recovered list is the complete authorized set.

D9 proposes targets A and B and restarts from the actual journal prefix ending after the first effect-frozen record. Resume executes only A and settles the transaction as SUCCEEDED. B vanishes from the effect set even though the recovered intent still contains both targets. A complete-freeze control correctly resumes both targets.

Reattaching an orphan effect is useful, but it cannot establish that no sibling is missing. Before dispatch, recovery must know whether the freeze was complete. Otherwise it silently changes the operation the user requested.

Original fix direction. Record the complete frozen effect set and its transaction/revision association as one durable unit, or use a manifest plus completion record. On recovery, incomplete freezes must be completed deterministically with stable identities or remain explicitly blocked. Never mark a subset successful as the whole requested operation.

Original probes: D9

210:   // A crash between freezing and the next transaction transition must not
211:   // orphan authorized effects or allow a second effect set for the revision.
212:   for (const effect of effects.values()) {
213:     const tx = transactions.get(effect.transactionId);
214:     if (tx && !tx.effects.includes(effect.id)) tx.effects.push(effect.id);
215:   }
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/journal-recovery.ts` around lines 210-215, commit writes one effect-frozen record per target and only later writes confirmation and the transaction transition containing the full effect list. Recovery reattaches whichever effects survived, but there is no durable manifest or completion marker proving that the recovered list is the complete authorized set. Record the complete frozen effect set and its transaction/revision association as one durable unit, or use a manifest plus completion record. On recovery, incomplete freezes must be completed deterministically with stable identities or remain explicitly blocked. Never mark a subset successful as the whole requested operation. Reproduce D9 in `/tmp/circe-pr3-evidence/recovery.test.ts` against PR #3 head 8d9cc151f6f62fb825d041b3a2b546773a8a3fff. Keep the three recovery controls and all 59 earlier reviewer probes passing. Run the repo test suite and typecheck. Change the durable record and recovery rule together; do not patch only the test scenario.
P2 F52. Recovery restores a validation reservation whose worker no longer exists fixedOriginal: packages/core/src/journal-recovery.ts:138-142 at 8d9cc15logiccorrectness
Latest verification

Remains fixed. The complete 95-case independent suite passes on the exact reviewed head.

Original finding, preserved

Transaction snapshots include pendingCommitRevision and recoverState restores it verbatim. This field is an in-process compare-and-set reservation for an outstanding authority await, not a durable confirmation. No validation worker survives createRuntime after a restart.

D10 starts confirmation with validation still awaiting, then records an interrupt. touchedTx journals the paused transaction with pendingCommitRevision still set. After restart, even explicitly resuming and confirming that transaction cannot invoke: commit sees the stale reservation and returns. There are no frozen effects and no surviving operation that could clear the reservation.

The reservation was necessary for live concurrency safety, but restoring it as an active lock makes recoverable work permanently uncommittable. Durable authorization and ephemeral in-flight ownership need different recovery rules.

Original fix direction. Normalize in-flight validation reservations during recovery. Clear or recoverably abandon pendingCommitRevision when no durable committed effect set owns it, then require fresh validation before commit. Preserve duplicate-confirm protection within a live runtime and do not treat the stale lock as authorization.

Original probes: D10

138:       case "transaction-transition":
139:         if (entry.interactionId === interactionId) {
140:           transactions.set(entry.transaction.id, structuredClone(entry.transaction));
141:           lastTransitionIndex.set(entry.transaction.id, index);
142:         }
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/journal-recovery.ts` around lines 138-142, Transaction snapshots include pendingCommitRevision and recoverState restores it verbatim. This field is an in-process compare-and-set reservation for an outstanding authority await, not a durable confirmation. No validation worker survives createRuntime after a restart. Normalize in-flight validation reservations during recovery. Clear or recoverably abandon pendingCommitRevision when no durable committed effect set owns it, then require fresh validation before commit. Preserve duplicate-confirm protection within a live runtime and do not treat the stale lock as authorization. Reproduce D10 in `/tmp/circe-pr3-evidence/recovery.test.ts` against PR #3 head 8d9cc151f6f62fb825d041b3a2b546773a8a3fff. Keep the three recovery controls and all 59 earlier reviewer probes passing. Run the repo test suite and typecheck. Change the durable record and recovery rule together; do not patch only the test scenario.
P1 F53. Recovered effect outcomes do not settle the transaction before new commands fixedOriginal: packages/core/src/journal-recovery.ts:174-185 at 8d9cc15logiccorrectness
Latest verification

Remains fixed. The complete 95-case independent suite passes on the exact reviewed head.

Original finding, preserved

The reducer updates effect status but leaves transaction state at the last separately written transaction-transition. After scanning the journal it reattaches effects, yet never derives the transaction lifecycle from those recovered outcomes. A valid crash prefix can therefore contain SUCCEEDED effects with a transaction still marked COMMITTED.

D11 keeps the prefix through a single effect's successful outcome, immediately before the final transaction transition. After restart, a named continuation of that completed intent passes the live-intent check and executes a second operation under a new key. The same continuation is correctly rejected once the uninterrupted runtime has settled; D14 is the positive control for that rejection.

Effect completion is already durable at this boundary. Recovery must reconcile the owning transaction before exposing state to command selection. It must also distinguish complete effect sets from partial freezes, which is the separate F51 requirement.

Original fix direction. After rebuilding verified complete effect sets, derive transaction settlement from recovered effect outcomes using the same lifecycle rules as the runtime. Preserve cancellations and superseding revisions. Expose a coherent state before accepting continuations or resumes; add an outcome-before-transition crash-prefix regression.

Original probes: D11

174:       case "outcome":
175:       case "reconciled": {
176:         const effect = effects.get(entry.effectId);
177:         if (!effect) break;
178:         const status: EffectStatus = VALID_EFFECT_STATUSES.has(entry.status)
179:           ? entry.status
180:           : "UNKNOWN";
181:         effect.status = status;
182:         if (entry.detail !== undefined) effect.result = structuredClone(entry.detail);
183:         if (TERMINAL_EFFECT_STATUSES.has(status)) {
184:           effect.endedAt = entry.at;
185:           terminal.add(entry.effectId);
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/journal-recovery.ts` around lines 174-185, The reducer updates effect status but leaves transaction state at the last separately written transaction-transition. After scanning the journal it reattaches effects, yet never derives the transaction lifecycle from those recovered outcomes. A valid crash prefix can therefore contain SUCCEEDED effects with a transaction still marked COMMITTED. After rebuilding verified complete effect sets, derive transaction settlement from recovered effect outcomes using the same lifecycle rules as the runtime. Preserve cancellations and superseding revisions. Expose a coherent state before accepting continuations or resumes; add an outcome-before-transition crash-prefix regression. Reproduce D11 in `/tmp/circe-pr3-evidence/recovery.test.ts` against PR #3 head 8d9cc151f6f62fb825d041b3a2b546773a8a3fff. Keep the three recovery controls and all 59 earlier reviewer probes passing. Run the repo test suite and typecheck. Change the durable record and recovery rule together; do not patch only the test scenario.
P1 F54. Replayed confirm or cancel commands can act on a different transaction fixedOriginal: packages/core/src/journal-recovery.ts:314-328 at a8099f1logiccorrectness
Latest verification

Remains fixed. The complete 95-case independent suite passes on the exact reviewed head.

Original finding, preserved

The new recovery rule treats an accepted command as never started when synthesizeResult finds no transaction created by that command. That inference is false for commands that modify an existing transaction. Transaction.commandKey names the original proposal, so a completed confirm or cancel has no matching transaction under its own key.

E1 confirms A, then restarts from the valid prefix immediately before the final command-outcome. After presenting B, redelivery of the identical unaddressed confirmation invokes B. E2 repeats the sequence for cancellation and cancels B. The complete-journal confirmation control E4 returns the recorded result and leaves B untouched. This is a regression introduced by allowing unmatched accepted commands to run again.

Persist command application identity and its selected transaction/revision before applying consequences. On recovery, distinguish never-applied work from an applied command whose reply was lost. An incomplete command may return a conservative explicit result, but replay must never resolve its target again against a later gate.

Original fix direction. Use durable per-command application/progress information for commands that target existing transactions. Bind the selected transaction and revision before applying work; reconstruct a safe replay result or explicit incomplete state. Do not fix this by disabling redelivery of all accepted-only commands, which would regress D6.

Original probes: E1, E2; E4 control

314:   // Commands with recorded work are reconstructed so replay never re-executes
315:   // them. A command accepted but never started has no work to reconstruct and
316:   // stays runnable: its reservation is the restored fingerprint.
317:   for (const tx of transactions.values()) {
318:     if (tx.commandKey) acceptedKeys.set(tx.commandKey, tx.commandId ?? tx.commandKey);
319:   }
320:   for (const key of acceptedKeys.keys()) {
321:     if (commandResults.has(key)) continue;
322:     const result = synthesizeResult(key, transactions, intents, effects);
323:     if (
324:       result.intents.length === 0 &&
325:       result.transactions.length === 0 &&
326:       result.effects.length === 0
327:     ) {
328:       continue;
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/journal-recovery.ts` around lines 314-328, The new recovery rule treats an accepted command as never started when synthesizeResult finds no transaction created by that command. That inference is false for commands that modify an existing transaction. Transaction.commandKey names the original proposal, so a completed confirm or cancel has no matching transaction under its own key. Use durable per-command application/progress information for commands that target existing transactions. Bind the selected transaction and revision before applying work; reconstruct a safe replay result or explicit incomplete state. Do not fix this by disabling redelivery of all accepted-only commands, which would regress D6. Reproduce E1 and E2 in `/tmp/circe-pr3-round2-evidence/recovery-round2.test.ts`; retain E4 and D6 as controls. Check revise and resume under the same contract. Keep all 74 earlier probes passing and run the repository tests and typecheck.
P1 F55. Target selection is cached as completion before the command applies fixedOriginal: packages/core/src/journal-recovery.ts:333-337 at 180759clogicdata-integrity
Latest verification

Remains fixed. The complete 95-case independent suite passes on the exact reviewed head.

Original finding, preserved

command-target is emitted before applying a confirmation, cancellation, revision or resume. Recovery treats its presence as sufficient to install a completed commandResults entry, even when the journal contains no consequence of that command. Recording a selected target prevents retargeting, but does not prove application.

G2 creates a gated transaction, cancels it, then recovers the actual journal prefix ending immediately after the cancellation's command-target record. Replaying that same explicit cancellation returns a synthesized result with no error and leaves the transaction AWAITING_CONFIRMATION. That command ID is permanently consumed although its cancellation never ran. G5 cuts one record earlier, after acceptance, and correctly completes the cancellation on replay.

The new target record creates an additional crash boundary. Represent selected-but-unapplied work separately from completed work. Recovery may safely continue against the pinned target with the necessary revision checks, or expose an explicit recoverable incomplete outcome. It must not silently cache ordinary completion, and must not reselect a later gate.

Original fix direction. Distinguish target selection from application/completion in the durable command contract. Preserve selected identity and required revision evidence, recover safely before external dispatch or surface an explicit incomplete result, and keep UNKNOWN external effects non-retriable. Retain both G2 and G5, plus the typed and semantic no-retarget controls.

Original probes: G2; G5 control

333:     if (commandTargets.has(key)) {
334:       // The command selected a transaction (or none) before applying
335:       // consequences: replay must never resolve that target again.
336:       const target = commandTargets.get(key);
337:       transactionIds = new Set(target ? [target] : []);
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/journal-recovery.ts` around lines 333-337, command-target is emitted before applying a confirmation, cancellation, revision or resume. Recovery treats its presence as sufficient to install a completed commandResults entry, even when the journal contains no consequence of that command. Recording a selected target prevents retargeting, but does not prove application. Distinguish target selection from application/completion in the durable command contract. Preserve selected identity and required revision evidence, recover safely before external dispatch or surface an explicit incomplete result, and keep UNKNOWN external effects non-retriable. Retain both G2 and G5, plus the typed and semantic no-retarget controls. Reproduce G2 in `/tmp/circe-pr3-round3-evidence/recovery-round3.test.ts`. A target-only prefix currently consumes an unapplied cancellation with no error. Check confirm/revise/resume against the same progress contract. Keep all 82 earlier independent probes passing and run the repository tests and typecheck.
P1 F56. Semantic no-op actions can retarget or pause work created after restart fixedOriginal: packages/core/src/runtime.ts:2153-2177 at c05efe6logiccorrectness
Latest verification

F56 is verified fixed. Both I1 variants and I2 pass, as do all H1-H4 no-op cases. The recovery reader now honors command-applied even without a target record or created transaction. The act-specific runtime no-op guard was removed. Acceptance-only and selected-but-unapplied controls still pass.

Original finding, preserved

recordTarget now protects consequence paths that have a transaction, but semantic no-op paths do not record that they selected no transaction. An affirmation with no gate falls through to reply = "none", and an interrupt pauses every live transaction without a target record. Recovery sees the accepted observation and its command-applied marker, but cannot distinguish a completed no-op from an observation that may be resolved again against later state.

H1 ingests an affirmation when no gate exists, cuts a valid journal at command-applied and at the following discourse snapshot, then creates B and replays the same observation. B is invoked even though the original affirmation was a completed no-op. H2 does the same with an interrupt: the replay pauses newly created B. H3 shows the intact-journal cache hides the problem, and H4 shows typed no-target commands have the intended behavior.

This is the same identity and progress contract that F54 fixed for targeted semantic confirmations, with the no-target case missing. A replayed observation must preserve its original selection result, including an explicit empty target, before it can mutate future state. The fix belongs at the shared semantic consequence boundary, not as another special case for affirm or interrupt.

Original fix direction. Record an interaction-scoped target decision for every consequence-bearing semantic observation, including an explicit no-target decision for completed no-ops. Recover that decision before accepting later commands. Keep no-op affirmations no-op and interrupts scoped to the transactions they originally selected; do not infer targets from current live state on replay.

Original probes: H1, H2; H3/H4 controls

2153:       case "interrupt": {
2154:         for (const tx of state.transactions.values()) {
2155:           if (!isTerminal(tx)) {
2156:             tx.paused = true;
2157:             touchedTx(ctx, tx);
2158:           }
2159:         }
2160:         ctx.reply = "ack";
2161:         break;
2162:       }
2163:       case "affirm": {
2164:         if (classification.ambiguous) {
2165:           // An ambiguous interpretation is a question, not authorization.
2166:           ctx.reply = "question";
2167:           break;
2168:         }
2169:         if (event.operations.includes("correction")) {
2170:           applyCorrection(ctx, references);
2171:         } else if (gateTransaction()) {
2172:           const tx = gateTransaction()!;
2173:           await commit(ctx, tx);
2174:           ctx.reply = ctx.errors.length > 0 ? "none" : "ack";
2175:         } else {
2176:           ctx.reply = "none";
2177:         }
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/core/src/runtime.ts` around lines 2153-2177, `recordTarget` now protects consequence paths that have a transaction, but semantic no-op paths do not record that they selected no transaction. An affirmation with no gate falls through to `reply = "none"`, and an interrupt pauses every live transaction without a target record. Recovery sees the accepted observation and its `command-applied` marker, but cannot distinguish a completed no-op from an observation that may be resolved again against later state. Record an interaction-scoped target decision for every consequence-bearing semantic observation, including an explicit no-target decision for completed no-ops. Recover that decision before accepting later commands. Keep no-op affirmations no-op and interrupts scoped to the transactions they originally selected; do not infer targets from current live state on replay. Reproduce H1 and H2 in `/tmp/circe-pr3-round4-evidence/recovery-round4.test.ts`; retain H3 and H4 controls, all 87 earlier probes, and run repository tests and typecheck.