PR #3 still needs changes

Changes requested

4 open findings · 3 P1 · 1 P2

305 baseline + 74 earlier probes pass · 5 new assertions fail

+454 / -61 since last review a8099f1

Reviewed exact head a8099f1 on 23 September 2026. The previous reproductions all pass, but four issues remain: three incompletely addressed findings and one newly introduced replay regression. There are three P1 findings and one P2 finding.

The largest risk is F54. An old confirmation can execute a newly presented transaction after restart. The matching cancellation case cancels unrelated new work. Both use the identical old command payload and a valid journal prefix, not corrupted records.

Verification

The controls show that complete command and reconciliation records behave correctly. The failures cut actual emitted journals at append boundaries. The null-result case fails on a complete journal. Tests and reports were written outside Circe; its source and active inspector worktree were left unchanged.

Architecture and complexity

The added durable fields are justified. Exact interaction IDs remove prefix ambiguity. The freeze completion marker prevents a subset from looking complete. Recording the actual operation key before invoke makes lost acknowledgements reconcilable. These changes repair missing information and do not require a broader framework.

The remaining weakness is that recovery infers command progress from which transaction a command created. Confirm, cancel, revise and resume work on existing transactions, so that inference cannot implement their replay contract. Persist the selected transaction/revision and enough application progress to distinguish an unstarted command from an applied command with a lost response. Do not add special cases for individual English acts.

The settlement code also duplicates the live runtime's decision with different guards. In particular, UNKNOWN blocks ordinary commands but can still be settled by reconciliation. A small shared lifecycle decision would remove that discrepancy. A new recovery framework, additional abstraction layers or a wholesale runtime rewrite would not be justified by these findings.

The unused manifested set can be removed as routine cleanup. It is not a merge blocker. Disk durability, compaction and later stack integrations remain outside this PR's review.

Current findings

F46, F47, F49, F51 and F52 are verified fixed. The original finding bodies below are retained as history; round-2 notes and current fix prompts describe what is still open.

This verdict covers PR #3 only. No GitHub comments, approvals, source changes or merges were made.

cd /tmp/circe-pr3-round2
pnpm exec vitest run --config /tmp/circe-pr3-round2-evidence/vitest.config.mjs recovery-round2.test.ts

At this head the command reports five failed and three passed. Downloaded configs contain local paths and need alias updates elsewhere.

New probes Failures and controls 74 earlier probes Baseline Typecheck Config Round 1 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.

Current fix prompt · Ctrl+`
Rerun prompt · Ctrl+Space
Finding status
P1 F48. Acceptance is treated as completion and reconstructed commands lose fingerprints openOriginal: packages/core/src/journal-recovery.ts:242-252 at 8d9cc15logicdata-integrity
Round 2 verification

D5 and D6 pass for typed commands. E6 still fails for ingest: recoverState ignores input records, so an observation whose final command-outcome is missing has a cached reconstructed result but no restored fingerprint. Reusing its ID with different text returns the old result instead of rejecting. E7 proves the intact journal rejects the same conflict.

Current source: packages/core/src/runtime.ts:2043-2052

Current fix prompt
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 2043-2052, D5 and D6 pass for typed commands. E6 still fails for ingest: recoverState ignores input records, so an observation whose final command-outcome is missing has a cached reconstructed result but no restored fingerprint. Reusing its ID with different text returns the old result instead of rejecting. E7 proves the intact journal rejects the same conflict. Persist an interaction-scoped observation reservation and original fingerprint before ingest applies work. Recover it just like typed command identity and compare it before returning a synthesized result. Do not infer interaction ownership from the existing unscoped input record. Retain safe replay and conflict checks for both entry points. Validate E6 and E7. Reproduce with `/tmp/circe-pr3-round2-evidence/recovery-round2.test.ts` at PR #3 head a8099f1ea7b509225d5b6572929c59f8551820e9. Keep all 74 earlier independent probes passing and run the repository tests and typecheck.

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

Captured source
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:     );
Original finding prompt
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 F53. Recovered effect outcomes do not settle the transaction before new commands openOriginal: packages/core/src/journal-recovery.ts:174-185 at 8d9cc15logiccorrectness
Round 2 verification

D11 passes for a COMMITTED transaction. E3 still fails after reconciliation: the previous durable transaction is UNKNOWN, the recovered effect is SUCCEEDED, and isTerminalTransaction causes settlement to skip it. Retrying reconcile cannot repair this because the effect is no longer UNKNOWN. E5 proves an intact reconciliation journal settles correctly.

Current source: packages/core/src/journal-recovery.ts:289-304

Current fix prompt
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 289-304, D11 passes for a COMMITTED transaction. E3 still fails after reconciliation: the previous durable transaction is UNKNOWN, the recovered effect is SUCCEEDED, and isTerminalTransaction causes settlement to skip it. Retrying reconcile cannot repair this because the effect is no longer UNKNOWN. E5 proves an intact reconciliation journal settles correctly. Derive recovered transaction settlement with the actual live lifecycle rules. UNKNOWN transactions can become SUCCEEDED or FAILED through reconciliation; they must not be skipped merely because UNKNOWN blocks ordinary commands. Preserve cancellation and complete-set checks. Prefer a small shared settlement decision over a second divergent copy. Validate E3 and E5, plus existing D11 and D14. Reproduce with `/tmp/circe-pr3-round2-evidence/recovery-round2.test.ts` at PR #3 head a8099f1ea7b509225d5b6572929c59f8551820e9. Keep all 74 earlier independent probes passing and run the repository tests and typecheck.

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

Captured source
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);
Original finding prompt
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 openOriginal: packages/core/src/journal-recovery.ts:314-328 at a8099f1logiccorrectness

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

Captured source
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;
Original finding prompt
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.
P2 F50. Clean recovery drops completed effect results and failure details openOriginal: packages/core/src/journal-recovery.ts:174-185 at 8d9cc15logicdata-integrity
Round 2 verification

Both D8 object-payload cases pass. E8 still fails on a clean restart with a valid null result: entry.result ?? entry.detail discards null and leaves result undefined. The writer persisted null correctly.

Current source: packages/core/src/journal-recovery.ts:204-205

Current fix prompt
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 204-205, Both D8 object-payload cases pass. E8 still fails on a clean restart with a valid null result: entry.result ?? entry.detail discards null and leaves result undefined. The writer persisted null correctly. Preserve explicit null outcome data. Use presence or an undefined check when falling back from result to the legacy detail field, not nullish coalescing. Validate E8 alongside object results, failures and reconciliation outcomes. Reproduce with `/tmp/circe-pr3-round2-evidence/recovery-round2.test.ts` at PR #3 head a8099f1ea7b509225d5b6572929c59f8551820e9. Keep all 74 earlier independent probes passing and run the repository tests and typecheck.

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

Captured source
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);
Original finding prompt
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 F46. Restart loses the presented gate and can redirect unaddressed actions fixedOriginal: packages/core/src/types.ts:560-570 at 8d9cc15logiccorrectness
Round 2 verification

D1 and D2 pass. The snapshot writer and startup restore now preserve the presented gate and tombstone.

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

Captured source
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;
Original finding prompt
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
Round 2 verification

D3 and D4 pass. Accepted commands and effect records now use exact interactionId checks.

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

Captured source
200:       case "command-accepted":
201:         if (entry.commandId) {
202:           acceptedKeys.set(`command:${entry.commandId}`, entry.commandId);
203:         }
Original finding prompt
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 F49. Crash recovery invents batch receipt keys that were never dispatched fixedOriginal: packages/core/src/journal-recovery.ts:229-238 at 8d9cc15logicdata-integrity
Round 2 verification

D7 passes. Lost-ack receipts retain the actual batch operation key and executor from pre-invoke records.

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

Captured source
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)!,
Original finding prompt
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.
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
Round 2 verification

D9 and D13 pass. The complete-set marker prevents a torn freeze from authorizing a partial set; complete paused sets still resume.

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

Captured source
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:   }
Original finding prompt
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
Round 2 verification

D10 passes. Workerless validation reservations are cleared when no effects are attached.

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

Captured source
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:         }
Original finding prompt
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.