PR #3 still needs one recovery fix
1 open finding · F56 · P1
320 baseline + 92 prior probes pass · 2 new assertions fail
+92 / -0 since last review 486b83bReviewed exact head 486b83b8855ea266348a53429add7ee9ed4f094c on 23 September 2026. All previous reproductions pass, including the completed no-op cases. F56 remains partially addressed because an interrupt that actually paused existing work is still replayed against later work.
Reproduction
Create A, then ingest “wait” to pause it. Recover the emitted journal with only that observation’s final command-outcome missing. The command-applied record and paused transaction are both durable. Create B, then redeliver the identical old “wait”. B becomes paused even though it did not exist when that input was first applied.
The failure reproduces with one and two pre-existing transactions. The intact-journal control correctly leaves B unpaused. These tests use public APIs and writable copies of valid journal prefixes.
Verification
- Repository baseline: 320 tests pass.
pnpm typecheck: passes.- All 92 earlier independent probes pass, including all five round-4 cases.
- New suite: two failing assertions and one passing control.
- All five recovery probe files pass strict TypeScript checking.
Architecture judgment
The latest patch adds 15 production lines and 77 test lines. Its scope is small, but it adds another condition based on the semantic act and the number of touched objects. That condition covers the reported no-op examples without completing the general replay contract.
The durable log already has command-applied for the failing observation. Recovery only uses that fact when a command-target entry exists. In the other branch, it assumes a command that created no transaction never ran. Interrupts modify existing transactions, so that assumption is false.
Use the existing completion evidence centrally. A completed turn must not run again merely because it created no transaction or lacked a target record. Keep the separate rules for acceptance-only work and selected-but-unapplied work. This should simplify the recovery decision rather than grow the semantic-act list or require a new framework.
The earlier statement that the architecture was ready after one small fix was too broad. The durable records are useful, but merge readiness still depends on every recovery path honoring their meaning consistently. This review keeps F56 open with a concrete counterexample; it does not reopen the fixed findings or add a new number for the same gap.
This review covers PR #3. Disk-journal durability and later stack integrations remain separate. Circe source, PR comments and approval state were not changed.
cd /tmp/circe-pr3-round5
pnpm exec vitest run --config /tmp/circe-pr3-round5-evidence/vitest.config.mjs recovery-round5.test.ts
At this head the new suite reports two failed and one passed. Downloaded configs contain local paths that need adjustment elsewhere.
New probesFailures and control92 prior probesBaselineTypecheckConfigRound 1 archiveRound 2 archiveRound 3 archiveRound 4 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.
- createRuntime calls recoverState before exposing command and ingestion entry points.
- Mutation writers append intent, transaction, effect and outcome records at different points in each command.
- Recovery loads the latest snapshots, reattaches frozen effects and caches reconstructed command results.
- Reconciliation operates on recovered UNKNOWN effects without an automatic retry.
Original scope: +885 / -40 at 8d9cc15.
- packages/core/src/index.ts +1 / -0
- packages/core/src/journal-recovery.ts +271 / -0
- packages/core/src/runtime.ts +107 / -36
- packages/core/src/types.ts +91 / -4
- packages/evals/src/journal-recovery.test.ts +415 / -0
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.
- 305 repository tests and typecheck pass.
- All 74 prior independent probes pass, including 15 recovery cases.
- Eight new cases: five failed assertions and three passing controls. The probe files pass strict TypeScript.
- Five prior findings close: F46, F47, F49, F51, F52. F48, F50 and F53 remain partially addressed; F54 is new.
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.
- 311 repository tests and typecheck pass.
- All 82 earlier independent probes pass.
- Five new cases: three failures and two passing controls; all recovery probe files typecheck.
- F48 and F50 close. F53 and F54 remain partially addressed. F55 is new.
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.
- 316 repository tests and typecheck pass.
- All 87 earlier independent probes pass.
- Five new cases: three failures and two passing controls.
- F53, F54 and F55 close. F56 is new and remains open.
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.
- 320 repository tests and typecheck pass.
- All 92 earlier independent probes pass.
- New suite: two failing interrupt variants and one passing complete-journal control.
- F56 remains open; no new finding number.
Current fix prompt · Ctrl+`
Rerun prompt · Ctrl+Space
Finding status
- FIXED · P1 · F46. Restart loses the presented gate and can redirect unaddressed actions
- FIXED · P1 · F47. Recovery is not fully scoped to an exact interaction identity
- FIXED · P1 · F48. Acceptance is treated as completion and reconstructed commands lose fingerprints
- FIXED · P1 · F49. Crash recovery invents batch receipt keys that were never dispatched
- FIXED · P2 · F50. Clean recovery drops completed effect results and failure details
- FIXED · P1 · F51. A partially recorded effect set can execute as if it were complete
- FIXED · P2 · F52. Recovery restores a validation reservation whose worker no longer exists
- FIXED · P1 · F53. Recovered effect outcomes do not settle the transaction before new commands
- FIXED · P1 · F54. Replayed confirm or cancel commands can act on a different transaction
- FIXED · P1 · F55. Target selection is cached as completion before the command applies
- OPEN · P1 · F56. Semantic no-op actions can retarget or pause work created after restart
P1 F46. Restart loses the presented gate and can redirect unaddressed actions fixedOriginal: packages/core/src/types.ts:560-570 at 8d9cc15logiccorrectness
Remains fixed. All 92 prior independent probes pass.
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
Remains fixed. All 92 prior independent probes pass.
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
Remains fixed. All 92 prior independent probes pass.
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
Remains fixed. All 92 prior independent probes pass.
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
Remains fixed. All 92 prior independent probes pass.
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
Remains fixed. All 92 prior independent probes pass.
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
Remains fixed. All 92 prior independent probes pass.
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
Remains fixed. All 92 prior independent probes pass.
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
Remains fixed. All 92 prior independent probes pass.
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
Remains fixed. All 92 prior independent probes pass.
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.
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. Sidecar: /home/abstergo/Documents/reviews/Circe-journal-recovery-abbyview.json HTML: /home/abstergo/Documents/reviews/Circe-journal-recovery-abbyview.html In packages/core/src/journal-recovery.ts around lines 338-367, honor durable command-applied evidence even when commandTargets has no entry and the observation created no transaction. Preserve a safe completed replay result instead of reclassifying or reapplying the observation to later live state. Keep acceptance-only work runnable and selected-but-unapplied work explicitly incomplete; never retry uncertain external effects. Avoid adding another act-specific condition in runtime.ts. A completed interrupt may affect multiple existing transactions without creating one, so transaction creation and target-record presence are not completion predicates. Verify both I1 variants and I2 in /tmp/circe-pr3-round5-evidence/recovery-round5.test.ts, all 92 prior probes, repository tests and typecheck.
/abbyview journal-recovery Round 6. Sidecar: /home/abstergo/Documents/reviews/Circe-journal-recovery-abbyview.json Incremental: preserve baseline and prior finding bodies. Review changes since 486b83b8855ea266348a53429add7ee9ed4f094c. Verify F56 with round-5 probes; retain all 92 earlier probes. Append genuinely new findings after F56. This does not approve later stack PRs.
P1 F56. Semantic no-op actions can retarget or pause work created after restart openOriginal: packages/core/src/runtime.ts:2153-2177 at c05efe6logiccorrectness
All H1-H4 cases now pass, but F56 is only partially addressed. I1 pauses one or two existing transactions, then recovers the actual journal prefix missing only that observation’s final command-outcome. After a new transaction B is presented, replaying the original interrupt pauses B. The prefix includes command-applied. An interrupt that changed existing work has ctx.transactions.size > 0, so the new no-op guard skips recordTarget; recovery then ignores appliedKeys in its no-target branch and treats the completed observation as runnable. I2 passes with an intact journal.
Current source: packages/core/src/journal-recovery.ts:355-367
Current fix prompt
Original finding, preserved
recordTargetnow 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 toreply = "none", and an interrupt pauses every live transaction without a target record. Recovery sees the accepted observation and itscommand-appliedmarker, 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-appliedand 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: }