Skip to content

fix(session): reconcile restart policy authority - #2777

Draft
zaxbysauce wants to merge 2 commits into
mainfrom
codex/fix-2668
Draft

zaxbysauce wants to merge 2 commits into
mainfrom
codex/fix-2668

Conversation

@zaxbysauce

@zaxbysauce zaxbysauce commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Closes #2668

Summary

  • make restart reconciliation publish only while the exact session generation and process-local authority incarnation that started the work still own it
  • make ledger recovery authoritative when projections are missing, malformed, stale, or quarantined, without allowing superseded work to publish partial state
  • fence snapshots, coordination, caches, subscriptions, aggregate hydration, recovery markers, replay, quarantine, and projection writes at their asynchronous commit boundaries
  • document the restart policy, recovery behavior, operator surfaces, and the J08 end-to-end journey

Root cause

Restart and hydration work was keyed too coarsely. A delayed task could survive an ordinary generation change or an evict-and-reintroduce cycle, then publish a result for authority it no longer owned. Several recovery paths also validated ownership before an asynchronous operation instead of immediately before publication. Together those gaps allowed stale projections, caches, coordination outcomes, markers, subscriptions, or replay results to overwrite the newer accepted state.

Fix

  • introduce exact authority tokens combining generation with a process-monotonic incarnation epoch
  • keep the epoch stable across ordinary generation bumps and replace it on eviction, reset, or reintroduction
  • require post-await exact-token validation at every affected publication boundary
  • propagate typed supersession through ledger replay, integrity, quarantine, initialization, replacement, projection, marker, and snapshot paths
  • rebuild missing or corrupt projections from the authoritative ledger through loadPlan
  • bind rehydration caches and hydrated aggregate ownership to the same epoch
  • use atomic temporary writes followed by a final authority predicate and synchronous rename for markers and projections

Recurrence prevention

The Phase 4.2 sweep classified 43 related sites across 12 predicates: 40 are fixed and 3 are explicitly out of class. Focused mutation probes removed or moved the reducer, coordinator, recovery, replay, marker, cache, and aggregate fences; each probe turned its targeted regression test red before the exact implementation was restored.

Acceptance evidence

  • C1 discriminating restart reconciliation: the frozen-base run (0 pass / 4 fail / 9 expect()) was performed in a disposable external workspace; ISSUE2668_EXPECTED_FAILURE_SET was a harness label, not a tracked repository file. Reproduce the current head suite with bun --smol test tests/unit/session/restart-reconciliation-2668.test.ts --timeout 60000 (11 pass / 0 fail / 46 expect() on feedback commit 97a7de56a).
  • C2 preserving restart safety/idempotence: base and head both 5 pass / 0 fail / 36 expect()
  • C3 preserving J04: base and head both 1 pass / 0 fail / 17 expect()
  • exact-head focused isolation suite: 94 pass / 0 fail / 398 expect() across 17 files; feedback regressions: 55 pass / 0 fail / 229 expect() across seven modified files, plus the C1 head suite above
  • broader cache/aggregate isolation: 132 pass / 0 fail / 431 expect()
  • typecheck, build, Node ESM import, lint, invariants, tool registration, registry citations, retention registry, drift enforcement, package smoke, security/adversarial checks, pending release fragment, checkpoint verification, Phase 4.2, deferred scan, and git diff --check passed
  • independent implementation review: MiniMax-M3/high APPROVE on the complete feedback-fixed diff, committed as 97a7de56a9100b81f923881a97d8a110114a7b23
  • final critic: GLM 5.3/high APPROVE on the complete feedback-fixed diff, committed as 97a7de56a9100b81f923881a97d8a110114a7b23

Invariant audit

  • 1 (plugin init): not touched — no plugin registration or initialization-path code changed
  • 2 (runtime portability): not touched — no bundle entry, package export, Bun-only runtime import, or plugin shape changed; build and Node ESM import still passed
  • 3 (subprocesses): not touched — no subprocess call site changed
  • 4 (.swarm containment): not touched — no root-resolution or runtime-storage location changed
  • 5 (plan durability): touched — authoritative ledger replay/recovery, projections, markers, quarantine, and snapshot coordination are covered by C1, focused recovery tests, checkpoint verification, and mutation probes
  • 6 (test_runner safety): not touched — validation used bounded per-file shell isolation, not broad test_runner
  • 7 (test writing): touched — new bun:test files stay below the 500-line cap, use dependency seams rather than new mock.module targets, and pass per-file isolation
  • 8 (session state): touched — generation plus bounded process-local incarnation ownership is tested across bump, eviction, reset, reintroduction, caches, subscriptions, and aggregate hydration
  • 9 (guardrails/retry): touched — typed supersession and exact action-local publication fencing are validated across asynchronous recovery boundaries and mutation probes
  • 10 (chat/system msg): not touched — no chat transform or guidance-carrier code changed
  • 11 (tool registration): not touched — no tool surface changed; registration validation passed for 134 tools
  • 12 (release/cache): touched — pending release documentation was added/updated; no package version, changelog version, or plugin cache layout changed

Test plan

  • frozen-base versus exact-head C1/C2/C3 acceptance checks
  • 17-file per-file isolation suite
  • mutation/falsification probes in disposable copies
  • bun run typecheck
  • bun run lint:ci
  • bun run build and Node ESM import
  • invariant, registration, citation, retention, drift, package, security, and adversarial gates
  • issue-tracer checkpoint, recurrence, implementation-review, and final-critic gates

Risk and rollback

The principal risk is a legitimate delayed operation being rejected after ownership changes. That is intentional fail-closed behavior and is observable through typed supersession rather than silent partial publication. Rollback is the single commit on this branch; no persistent schema migration is required.

Waivers

None.

Merge status

Ready for CI and maintainer review. Keep as draft until required checks are green.

@github-actions

Copy link
Copy Markdown
Contributor

Drift check report

Found 2 drift finding(s): 0 error, 0 warning, 2 notice.

required-check-contract (2)

  • 🔵 notice scripts/required-check-contract.json: [RULESET_DIVERGENCE] intended-required context "drift" is not yet required by the captured ruleset
  • 🔵 notice scripts/required-check-contract.json: [RULESET_DIVERGENCE] intended-required context "drift" is not present for every expected event in captured external workflow evidence

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It makes deep, interdependent changes to plan-durability (invariant 5) and session-state (invariant 8) recovery paths across many files whose correctness depends on subtle ordering and authority-fencing invariants that warrant final human verification.

Pull request overview

This PR (Closes #2668) hardens restart/hydration reconciliation so that a delayed or superseded operation can never publish stale durable state over newer accepted state. It introduces a process-monotonic "authority epoch" paired with the existing per-project hydration generation to close an ABA window (numeric generations are reusable after FIFO eviction/reset), and threads a synchronous authority predicate (preCommitCheck) into every recovery-path publication boundary, plus a typed PlanRecoverySupersededError that broad recovery catches must rethrow. It also makes the post-resolution coordinator rebuild missing/corrupt projections authoritatively from the ledger via loadPlan.

Changes:

  • Adds hydrationAuthorityEpoch to hydration ownership + session state, fencing caches, aggregate ownership, subscriptions, and rehydration at their async commit boundaries.
  • Makes plan recovery (loadPlan/savePlan/rebuildPlan/ledger init/replace/quarantine/marker/spec-staleness) fail-closed on supersession via preCommitCheck + commitAsyncPreparedFile, and switches the projection writer to a synchronous rename with an adjacent authority check.
  • Adds a coordination superseded readiness state, extensive #2668 unit/journey tests, a J08 registered-host journey, and documentation/release fragments.
File summaries
File Description
src/session/hydration-ownership.ts Replaces generation counters with bounded authority records (generation + monotonic epoch); adds capture/current predicates and epoch-scoped caches/aggregate keys.
src/state.ts Stamps hydrationAuthorityEpoch, returns a commit result from buildRehydrationCache, fences PR-subscription rehydration, adds _internals/re-export seams.
src/session/snapshot-reader.ts Authority-epoch-fenced eviction/protection and cache/rehydrate commit gating in rehydrateState/loadSnapshot.
src/session/snapshot-writer.ts Sync rename + shouldCommit predicate; restructured atomic write try/finally; adds hydrationAuthorityEpoch transient-field doc.
src/session/snapshot-coordination-init.ts Adds superseded outcome, authoritative loadPlan recovery before cache/projection, and per-boundary isCurrent() fences.
src/plan/manager.ts Adds PlanRecoverySupersededError, commitAsyncPreparedFile, preCommitCheck threading, and rebuild marker-cleanup supersession handling.
src/plan/ledger.ts Threads preCommitCheck through init/append/snapshot/replace/replay and moves quarantine write outside its broad catch.
src/observability/catalog.ts Updates producer line citations to match moved emit sites.
scripts/retention-registry.data.ts Updates loadPlan/writeSnapshot reader/writer line citations.
scripts/registry-citation-baseline.json Removes the now-resolved loadPlan out-of-range debt entry.
docs/* Documents restart authority boundaries, recovery runbook, and J08 journey; adds/updates release fragments.
tests/unit/** (session/plan/execute-journey/commands) Adds ABA, supersession, recovery-replay/quarantine, marker, subscription-fence, and J08 journey coverage; updates existing coordination/parity tests.
Review details
  • Files reviewed: 31/31 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

Swarm PR Review — #2777 (issue #2668, restart policy authority)

Reviewed at head 7c308370221ee03f6f7d4e9a22ec1238b2b54d70 against merge-base 37ee0ce8be11aa105ef97a829ef6038a6a90df1c (31 files, +3442/-215). Full layered review: 6 base-dimension explorer lanes + 4 risk-family micro-lanes (36 raw candidates) → 3 independent reviewer passes → 1 critic challenge that resolved a direct contradiction between two reviewers and disproved two overstated findings. No CRITICAL or HIGH finding survived validation.

⚠️ Process note before anything else

PR #2767 (feat/issue-2668-restart-policy-reconciliation, opened 2026-09-14T14:58:04Z, non-draft, 24 files/+1674/-30) also declares Closes #2668 and was opened ~4h20m before this PR. These appear to be two independent submissions against the same issue. Recommend the maintainer confirm which is the intended landing PR before merging either — merging both would double-close #2668 and likely conflict in src/plan/manager.ts, src/session/snapshot-coordination-init.ts, and src/state.ts, which both touch.

Also: this PR is currently a draft, and several required checks (unit × 18 shards, coverage, smoke × 3) were still pending/empty at review time — per the PR's own "Merge status" note ("Keep as draft until required checks are green"), this is expected and not itself a finding.


Confirmed findings (post-reviewer, post-critic)

F-001 — LOW — superseded snapshot-coordination readiness has no automatic re-drive (PR-introduced)
src/session/snapshot-coordination-init.ts — pre-PR, a superseded init's .then() unconditionally set the entry to 'succeeded' and snapshot writes continued. This PR makes 'superseded' a settled-but-terminal state: ensureSnapshotCoordinationReady only re-drives when the entry is entirely absent (not when it's settled-but-superseded), and evictSettledEntries() only reclaims entries once the ready-map hits its 32-root cap, so a single-project process never triggers it. Recovery requires the operator to run /swarm recover --coordination. This is visible via /swarm status (status-service.ts:952-968) and documented in the updated recovery runbook — only the per-write failure inside writeSnapshot (snapshot-writer.ts:568-572) is gated behind OPENCODE_SWARM_DEBUG=1.
No production trigger was demonstrated in review — /swarm close and /swarm reset-session both install a 'closing' guard that prevents this path, and the PR's own test suite manufactures supersession by calling beginHydrationScope directly rather than through a real concurrent trigger. Raise to MEDIUM if a maintainer can name a reachable production trigger (e.g., a second plugin server() init for the same directory in one process — not confirmed or ruled out in this review).
Suggested one-line fix: in ensureSnapshotCoordinationReady, also re-drive when entry.settled && entry.state === 'superseded'; and promote the writeSnapshot catch from log() to criticalWarn().

F-002 — LOW — FIFO (not LRU) eviction + epoch renewal can delete genuinely-live sessions after reintroduction
src/session/hydration-ownership.ts (captureCurrentHydrationAuthority) + src/session/snapshot-reader.ts:490-494. After a project's authority record is FIFO-evicted from the bounded 32-project map and later reintroduced, it gets a fresh authorityEpoch. The next hydration for that project then evicts every live session of that project regardless of hydrationStamp, because they all carry the old epoch. Pre-PR this was survivable (generation-based, not epoch-based); this is a real behavior change (introduced_by_pr: YES). It is also deliberately tested (hydration-authority-aba-2668.test.ts:215,242), so it's a documented design tradeoff, not an accidental bug — but the doc claim in state.ts:718-728 ("live sessions created after generation g began survive") is now only true within an epoch and should be corrected for accuracy. Reachability requires both >31 distinct project keys touched in one process (real via turbo-lane worktrees) and a mid-process rehydration of the affected project (in practice, only /swarm recover --coordination) — a narrow window.

F-003 — LOW/MEDIUM — resource leak: rebuildPlan's temp markdown file is not cleaned up on supersession
src/plan/manager.ts:2408-2422. Unlike its sibling regeneratePlanMarkdown (:683-696), which wraps its temp write in finally { unlinkSync(tempPath) }, rebuildPlan's inline plan.md temp-file write has no such cleanup. This PR adds a new preCommitCheck throw trigger at this exact site, and superseded recovery is a routine, expected outcome of the new design — so orphaned .swarm/plan.md.rebuild.<ts>.<rand> files will now accumulate over the life of a long-running process. No correctness/security impact; recommend adding the same finally block as the sibling function.

F-004 — LOW — no cumulative counter/metric for supersession events
src/plan/manager.ts:276-281 (PlanRecoverySupersededError), src/session/snapshot-coordination-init.ts (~15 return sites for 'superseded'). Only the latest attempt's transient state is observable via /swarm status; there's no historical count or per-site breakdown, so an operator can't tell if supersession is firing repeatedly (a sign of a flapping restart) versus once. This is an observability enhancement, not a violation of a cited convention — recommend a follow-up issue for a counts-only telemetry entry.

F-005 — LOW — malformed-plan.json recovery path has zero test coverage for its new branch
src/plan/manager.ts:1092-1104. The new rawPlanJsonParseFailed && !catchIntegrity.truncated branch broadens when a ledger-driven rebuild can proceed without a plan-identity comparison — but on review this branch is actually more verified than the migration path it replaces (every reconstruction source is PlanSchema-validated; the branch is also gated on !catchIntegrity.truncated), so this is a safety improvement, not a regression. However, rawPlanJsonParseFailed has zero references anywhere under tests/ — particularly the negative case (malformed plan.json + truncated ledger, which should skip rebuild) is unverified. Recommend adding both positive and negative test cases.

F-006 — LOW — two ledger preCommitCheck fence positions are unpinned by their ordinal-count test assertions
src/plan/ledger.ts:1103 (pre-writeFileFsyncedThenRename in initLedger) and :1960 (pre-replaceSqliteLedger). The corresponding tests assert call-count and durable-state outcomes that would still pass if these two specific fences were moved to just after their rename/write rather than immediately before — a narrower version of the fence-relocation mutation class the PR's own body says was probed. Not a demonstrated bug, but worth tightening the assertions.

F-007 — LOW — PR's cited acceptance-evidence mechanism doesn't exist in the repository
The PR body's "Acceptance evidence" section cites ISSUE2668_EXPECTED_FAILURE_SET and specific frozen-base pass/fail counts (C1). git grep and git log --all -S both return zero hits for this identifier anywhere in history. This makes the C1 discriminating-regression claim not independently reproducible by a reviewer from the shipped tree — it must have run in an external/ephemeral harness. Separately, the other quantitative claims in the PR body (94 pass/398 expect/17 files) were independently reproduced exactly by re-running the 16 changed test files plus the unchanged j04-restart-generation-continuity.test.ts regression guard. Recommend either committing the C1 harness or restating it as a manual procedure in the PR description.

Disproved / downgraded-to-no-action (for the record, so they aren't re-raised)

  • A plan.json/plan.md rename-atomicity race under concurrent loadPlan was proposed and disproved — the startupLedgerCheckedWorkspaces gate's check-and-add is synchronous with no intervening await; double-entry is not possible.
  • A claim that a marker is "permanently" left in_progress: true on supersession, silently reversing a named protection was disproved on both counts: the next savePlan resets the marker (not permanent), and the removed comment was replaced with one that names the new tradeoff explicitly (not silent).
  • Several root-vs-canonicalized-directory divergence concerns in snapshot-coordination-init.ts were raised by two independent lanes but disproved on deeper trace: the change actually fixes a pre-existing key mismatch under symlinked project roots.
  • A same-epoch <= eviction boundary in snapshot-reader.ts is pre-existing (unchanged by this diff) and already tracked against the prior [Workstream D] PR 14 of 17: Make live state and hydration project-owned and generation-fenced #2667/fix(state): make live state and hydration project-owned and generation-fenced #2742 review round — not a new issue.
  • A concern that advisoryWarn embedding raw error.message needed catalog.ts registration was disproved — catalog.ts only tracks structured emit() telemetry producers, and this PR added no new emit() call.
  • The in-place edit of an already-merged PR's pending release-notes fragment was checked against scripts/release-notes-fragments.mjs's reconciliation logic and against the currently-open release-please PR (chore(main): release 7.181.2 #2778) — no hazard: the fragment isn't referenced by the open release PR's published block.

Coverage note

buildRehydrationCache's new shouldCommit/planOverride/{committed} result contract in src/state.ts (~113 changed lines) was not independently deep-dived by any lane beyond the base-dimension passes — flagging as a residual coverage gap rather than a finding.

Verdict

APPROVE_WITH_NOTES, conditional on:

  1. Reconciling with PR fix(session): preserve durable QA policy and owner-named reconciliation across restart #2767 before merge (same closing issue, both open).
  2. CI going green (currently draft, most required checks still pending).
  3. F-001–F-007 above are all LOW severity and non-blocking; recommend addressing F-001, F-002, and F-003 as fast follow-ups given they touch the exact restart/durability surface this PR is meant to harden, and filing F-004/F-005/F-006/F-007 as tracked follow-up issues.

🤖 Generated via /swarm-pr-review (Claude Code, Profile B — native parallel subagents: 6 base-dimension explorer lanes, 4 risk-family micro-lanes, 3 independent reviewer passes, 1 critic challenge)

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01XqUz4WeqEXrAiNybUpFGTP

@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

🤖 Multi-Stage PR Review

Pipeline: MiniMax-M2.7-highspeed (orientation) (context pack) → MiniMax-M2.7-highspeed (explorer) (explore → candidates) → MiniMax-M2.7-highspeed (fallback arbiter) (challenge + blind-spot)
Commit reviewed: 7c308370221e


PR Reviewer — opencode-swarm

🔍 PR Intent

Reconstructed from PR description, linked issue #2668, and diff:

  • O-001: Add authority fence (preCommitCheck) to all ledger/snapshot recovery publication boundaries, preventing stale coordinators from publishing after supersession
  • O-002: Introduce process-monotonic hydrationAuthorityEpoch to prevent ABA revive after FIFO eviction or reset
  • O-003: Add PlanRecoverySupersededError and propagate typed supersession through all async recovery paths
  • O-004: Preserve durable plan/QA policy across restart; ephemeral authority (overrides, leases, child handles) must not be resurrected
  • O-005: Publish new test fixtures j08, ledger-recovery, quarantine-supersession, manager-recovery-replay, rebuild-plan-marker, hydration-authority-ABA, restart-coordination-supersession, restart-reconciliation, restart-subscription-fence

📦 Implementation Summary

The PR adds an exact authority token (generation + authorityEpoch) to all hydration-snapshot-recovery paths, threads a preCommitCheck: () => boolean fence through every async publication boundary in loadPlan, rebuildPlan, savePlan, appendLedgerEvent, quarantineLedgerSuffix, and writeSnapshotProjection, introduces PlanRecoverySupersededError, and updates snapshot-coordination-init.ts to return typed outcomes (succeeded | superseded). Five new test files and one new release doc are added; existing tests are updated.


✅ / ⚠️ / ❌ Intended vs Actual

Obligation Status Evidence
O-001 (authority fence in recovery paths) SUPPORTED src/plan/manager.ts:2292–2319commitAsyncPreparedFile calls preCommitCheck before renameSync; every ledger/snapshot call site propagated
O-002 (authority epoch) SUPPORTED src/session/hydration-ownership.ts:66–69hydrationAuthorityEpoch is process-monotonic and never reset
O-003 (PlanRecoverySupersededError propagation) SUPPORTED src/plan/manager.ts:27–32 — class defined; throw error / if (e instanceof PlanRecoverySupersededError) throw e appears at 19 call sites
O-004 (durable vs ephemeral authority) SUPPORTED docs/plan-durability.md:466–496 — explicit table distinguishing durable from ephemeral; j08 fixture asserts it
O-005 (new tests) SUPPORTED 7 new test files; tests/unit/execute-journey/j08-restart-policy-reconciliation.test.ts and 6 unit test files

🚨 Confirmed Findings

[HIGH] renameWithTransientRetry silently drops false when _internals.rename is void

  • Location: src/session/snapshot-writer.ts:91–98
  • Why it matters: The renameWithTransientRetry function is typed Promise<boolean> and returns true on success / false on fence-reject. However, _internals.rename is typed as (from, to, shouldCommit?) => void | Promise<void>. When the DI adapter is the production renameSync (returns undefined/void), the async loop's inner call await _internals.rename(tempPath, targetPath, shouldCommit) resolves to await undefined — a no-op — and control falls through to the next iteration attempt. The retry loop will call shouldCommit() again, but if the fence has flipped false on a retry after the loop already entered with true, the false is never surfaced and renameWithTransientRetry incorrectly returns true. More critically, when _internals.rename is a void-returning mock (as in snapshot-writer-rename-retry.test.ts line 224), await undefined makes the for-loop iterate all SNAPSHOT_RENAME_MAX_ATTEMPTS times without ever returning true.
  • Evidence: snapshot-writer.ts:84 — the try body assigns await _internals.rename(tempPath, targetPath, shouldCommit) where the return type is void | Promise<void>; src/session/snapshot-writer.ts:96return true is reached only if _internals.rename resolves; if _internals.rename is void-sync, the try body resolves immediately to undefined and the loop continues.
  • Fix direction: Wrap the inner call to detect void-sync adapters: if _internals.rename(tempPath, targetPath, shouldCommit) returns a non-undefined value, await it; if it returns undefined (void-sync), check shouldCommit?.() synchronously before the synchronous rename and return false if rejected. Or guard the return path: after the try body, explicitly check if (!renamed) return false by storing the call's return value (or checking shouldCommit?.() inline for void-sync paths).

🔬 Unverified but Plausible Risks

None — all plausible concerns are structurally covered by the preCommitCheck fence propagation or the typed PlanRecoverySupersededError catch chain.


🧪 Test / Coverage Gaps

  • Gap: snapshot-writer-rename-retry.test.ts adds a test for writeSnapshotProjection with a void-sync _internals.rename adapter, but the test renameStarted is captured via mock() and await renameStartedPromise — the mock resolves immediately on call, not after bunWrite inside writeSnapshotProjection. The test path does not exercise the real ordering constraint (shouldCommit check before renameSync inside the mock adapter). This is acceptable as a unit-test limitation since the end-to-end j08 fixture drives the real production path.
  • Gap: The commitAsyncPreparedFile function (src/plan/manager.ts:2292–2319) uses renameSync (synchronous) and is tested indirectly through rebuild-plan-marker-supersession-2668.test.ts. No dedicated unit test isolates it in isolation from rebuildPlan.

📋 Shipped-vs-Claimed Gaps

  • Gap: tests/unit/plan/write-marker-in-progress-manager.test.ts adds two new tests at line 201. The PR description does not mention these tests in the acceptance evidence. However, they are present in the diff, so this is not a stealth addition — the acceptance report listed 7 new test files, not every new test case.

📝 Merge Recommendation

[APPROVE_WITH_FIXES]

One HIGH finding: _internals.rename is typed void | Promise<void> but renameWithTransientRetry silently falls through when the adapter is void-sync, never surfacing the fence-reject (false). The fix is small (one conditional guard at snapshot-writer.ts:91–98) and mechanically correct.

Check Result
No CRITICAL findings
No unresolved STEALTH_CHANGE
No UNSUPPORTED obligations
Test coverage adequate
No hardcoded secrets
All async errors handled ✅ (PlanRecoverySupersededError chain)
Input validation present
No broken agent role boundaries
Prompt format contracts intact
Lockfile consistent ✅ (no lockfile in diff)

🔁 Validation provenance

Reviewer-REFUTED findings (dropped):

  • writeSnapshotProjection missing shouldCommit propagationwriteSnapshotProjection is called with shouldCommit at snapshot-writer.ts:610 (renameWithTransientRetry(tempPath, resolvedPath, shouldCommit)). The LOW finding was refuted: shouldCommit is propagated to renameWithTransientRetry, and the call to writeSnapshotProjection from snapshot-coordination-init.ts uses the _internals seam which is typed correctly. The claim that writeSnapshot does not propagate shouldCommit to writeSnapshotProjection is irrelevant because writeSnapshot is the SQLite authority path and writeSnapshotProjection is the derived-compatibility shadow path — they are separate write surfaces.

  • Mock resource leak in write-marker-in-progress-manager.test.ts — the existing afterEach covers tempDir and closePlanTerminalState. The new tests patch mock.module inside the test body; Bun's mock.module patches are function-scoped and self-cleaning on test exit. No global state is mutated by these patches.

  • verifyWrittenPlanJson monkey-patch never restored — the patch at write-marker-in-progress-manager.test.ts:202 sets _internals.verifyWrittenPlanJson to a no-op. This is set on the module-level _internals export and is never restored. However, Bun's mock.module creates fresh module instances per test file; the _internals patch from this file cannot bleed into other test files. Within this file, both new tests set the same no-op, so no inter-test interference occurs.

Blind-spot findings added:
None — the type mismatch was the only structural defect identified.

Confirmed findings kept:

  1. [HIGH] renameWithTransientRetry silently drops false when _internals.rename is void-sync — exact file:line cited above.

🔒 Reviewed by a multi-stage local-first funnel (architect context pack → explorer candidates → critic challenge/author) for high recall with low false-positive noise. Findings are advisory — verify before acting.

@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

Feedback resolution — commit 97a7de56a9100b81f923881a97d8a110114a7b23\n\nFixed: TF-001/TF-002 (mock export and current temp-writer citations); IA-001 (typed supersession propagation); subprocess-platform-001/BOT-001 (commit-aware rename/cache invalidation and declined-adapter behavior); F-001 (bounded readiness re-drive); F-002 (epoch-scoped state comment); F-003 (JSON/Markdown temp cleanup); F-005 (positive/negative malformed-projection recovery coverage); F-006 (ordinal ledger-fence assertions); GAP-001/GAP-002 (public rehydration supersession and cache-observable rename tests). Focused tests pass: 55 tests / 229 assertions across seven modified test files, plus the tracked C1 head suite 11 / 46.\n\nDisposition without code changes: F-004 is optional cumulative telemetry and needs an observability decision; GAP-003 remains covered through the public rebuildPlan surface rather than widening the helper API. F-007 is corrected in the PR body: the frozen-base harness was a disposable external artifact, and the current-head test command/result is now recorded. PRE-001..PRE-004 and compat-001 predate this PR; DIS-001..DIS-006 were disproved or pre-existing; DUP-001 is moot because #2767 is merged; CLAIM-001/COMMENT-001 require no code change. COMMENT-002 remains two notice-only drift findings, with zero errors/warnings.\n\nThe original TF-001/TF-002 CI failures are fixed locally and a fresh workflow has started on this commit. The PR remains draft until all required checks pass; then I will mark it ready and merge if repository policy permits.

@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

🤖 Multi-Stage PR Review

Pipeline: MiniMax-M2.7-highspeed (orientation) (context pack) → MiniMax-M2.7-highspeed (explorer) (explore → candidates) → MiniMax-M2.7-highspeed (fallback arbiter) (challenge + blind-spot)
Commit reviewed: 97a7de56a910


🔍 PR Intent

Reconstructed from PR description, linked issue #2668, commit messages, and diff:

  • O-001 Introduce a process-monotonic authority epoch paired with per-project generation counters to prevent ABA reuse after FIFO eviction or reset
  • O-002 Thread typed supersession (PlanRecoverySupersededError) through every ledger/plan recovery publication boundary (ledger append, init, replace, snapshot, quarantine, markdown, marker, replay, coordination init)
  • O-003 Make rehydrateState refuse superseded hydration authority after all awaits (including pending-rehydration joins) before mutating state
  • O-004 Preserve durable plan identity and QA policy across restart; session overrides/auto-proceed must not survive as execution authority
  • O-005 Bind rehydration cache publication to exact authority epoch so an evicted-then-reintroduced project cannot bleed old plan/evidence state
  • O-006 Document restart reconciliation behavior, recovery runbook, and execute-journey j08

📦 Implementation Summary

The PR adds an authorityEpoch scalar (process-monotonic, not reset by resetSwarmState) to the per-project hydration authority record alongside the generation counter. Every recovery publication point — ledger appends, init, replace, snapshot, quarantine, markdown regeneration, plan-write markers, spec-staleness writes, and snapshot projections — now calls an optional preCommitCheck() synchronous guard immediately before the atomic OS rename. The guard throws PlanRecoverySupersededError, which propagates through all nested callers and prevents superseded recoveries from publishing stale projections, markers, or quarantine files. The loadSnapshot / startSnapshotCoordinationInitialization paths gate on isHydrationScopeCurrent after every await. New tests cover ABA eviction/reintroduction, marker-preservation through cleanup paths, coordination supersession, and the j08 end-to-end journey.


✅ / ⚠️ / ❌ Intended vs Actual

Obligation Status Evidence (file:line)
O-001 SUPPORTED src/session/hydration-ownership.ts:174-179nextHydrationAuthorityEpoch increments; epoch returned in beginHydrationScope at :120-130
O-002 SUPPORTED src/plan/ledger.ts:1100 (initLedger), :1439/:1473 (appendLedgerEvent), :1687 (takeSnapshotEvent); src/plan/manager.ts:2339 (rebuildPlan), :2163 (regeneratePlanMarkdown), etc.
O-003 SUPPORTED src/session/snapshot-reader.ts:477-482 — re-checks isHydrationAuthorityCurrent after Promise.allSettled(pendingRehydrations)
O-004 SUPPORTED tests/unit/execute-journey/j08-restart-policy-reconciliation.test.ts — asserts session overrides absent post-restart
O-005 SUPPORTED src/state.ts:3770-3782setRehydrationCache requires exact epoch match
O-006 SUPPORTED docs/releases/pending/2668-restart-policy-reconciliation.md, docs/troubleshooting/recovery-runbook.md, docs/testing/execute-journey.md

🚨 Confirmed Findings

[HIGH] rebuildPlan silently discards the markdown write error when the final marker write fails with a non-supersession error

  • Location: src/plan/manager.ts:2443-2455

  • Why it matters: If commitAsyncPreparedFile for the final .plan-write-marker throws a non-PlanRecoverySupersededError (e.g. disk full, permissions), the finally block at :2443 unconditionally throws that error. The markdownWriteError stored by the earlier catch block at :2437 is discarded, so callers cannot distinguish a markdown failure from a marker failure and the root cause of a degraded save is obscured.

  • Evidence: Diff lines:

    } catch (error) {
        markdownWriteFailed = true;
        markdownWriteError = error;
    } finally {
        try {
            ...
            await commitAsyncPreparedFile(markerPath, marker, ...);
        } catch (error) {
            if (error instanceof PlanRecoverySupersededError) { markerSupersededError = error; }
            /* Advisory only */
        }
    }
    +if (markerSupersededError) throw markerSupersededError;
    +if (markdownWriteFailed) throw markdownWriteError;  // ← markdownWriteError discarded if markerSupersededError is set
    

    The control flow is linear: finally runs after every path through the try/catch above it. If the marker catch captures a non-supersession error, markerSupersededError stays undefined and the if (markerSupersededError) guard passes, but finally already threw. The if (markdownWriteFailed) line is unreachable.

  • Fix direction: Capture both errors (e.g. let finalError = markerSupersededError ?? (markdownWriteFailed ? markdownWriteError : undefined)) and throw the aggregated context, or throw markdownWriteError first and let the marker error surface in the error chain.


🔬 Unverified but Plausible Risks

  • Epoch exhaustion: nextHydrationAuthorityEpoch (src/session/hydration-ownership.ts:174-179) throws Error('hydration authority epoch exhausted') when hydrationAuthorityEpoch >= Number.MAX_SAFE_INTEGER. The reviewer confirmed this; however, the epoch increments only on project eviction or reset (not per session/task), and the maximum tracked project count is 32, making practical exhaustion impossible. The PR explicitly chooses process-monotonic semantics where "an old token can never become current again," which would be violated by graceful degradation. Confidence: low — this is intentional design for a practically unreachable boundary; no runtime guard needed.

  • quarantineLedgerSuffix path-null silent return: If assertProjectRoot or the hash computation throws (before quarantinePath is assigned), the catch block returns { path: null, salvagedCount } silently. The caller in replayFromLedgerWithStatus does not use the returned path on the non-error path, so this is not a correctness issue — but callers testing the return value for the path !== null case would get a false negative on these rare I/O failures. Confidence: low — no caller checks the returned path.


🧪 Test / Coverage Gaps

  • Gap: rebuildPlan has no test covering a non-PlanRecoverySupersededError throw from commitAsyncPreparedFile in the final marker path (the finally / error-shadowing scenario above).

    • Evidence: tests/unit/plan/rebuild-plan-marker-supersession-2668.test.ts tests supersession only; tests/unit/plan/manager-recovery-replay-supersession-2668.test.ts tests quarantine/marker supersession but not disk-write failure.
  • Gap: No test exercises nextHydrationAuthorityEpoch throwing in production. The ABA test suite (hydration-authority-aba-2668.test.ts) triggers eviction via FIFO flooding but never hits the numeric ceiling.

    • Evidence: No test file contains a probe that forces hydrationAuthorityEpoch >= Number.MAX_SAFE_INTEGER.

📋 Shipped-vs-Claimed Gaps

None found. The PR description's acceptance evidence, C1/C2/C3 checks, 17-file isolation suite, mutation probes, and invariant audit are all structurally verifiable from the diff and test files.


📝 Merge Recommendation

[APPROVE_WITH_FIXES]

The HIGH finding — markdownWriteError discarded when the final marker write throws a non-supersession error — is a real, fixable defect introduced by this PR. The finally/if ordering means callers receive the wrong error type on a specific degraded-save path. Fixing it (aggregate both errors or throw in the correct order) is a small, targeted change. No other PR-introduced defect meets the bar.

The epoch-exhaustion "finding" is not a bug: it is intentional design (process-monotonic semantics require an unrecoverable ceiling to prevent stale-token revival), it is practically unreachable (9 quadrillion eviction cycles on a 32-slot FIFO), and the PR explicitly documents it. The reviewer's confirmation is technically accurate but conflates "could theoretically throw" with "is a fixable defect" — it is neither.

Check Result
No CRITICAL findings
No unresolved STEALTH_CHANGE
No UNSUPPORTED obligations
Test coverage adequate ⚠️ gap for non-supersession marker-throw path
No hardcoded secrets
All async errors handled
Input validation present
No broken agent role boundaries
Prompt format contracts intact
Lockfile consistent

🔁 Validation provenance

Survived challenge:

  • HIGH — rebuildPlan error shadowing (kept): concrete code path in src/plan/manager.ts:2437-2455 where finally throw makes markdownWriteError unreachable when the final marker write fails with a non-supersession error. A test covering this specific path is absent.

Dropped:

  • Epoch exhaustion (refuted as a fixable defect): intentional design choice with a practically unreachable ceiling (2^53-2 increments on a 32-entry FIFO); violates process-monotonic semantics if degraded gracefully; documented in PR design notes. Kept in Unverified Risks at low confidence.
  • 31 reviewer-refuted items (pre-existing list; no diff evidence contradicts those refutations).
  • Several speculative concerns were scoped away after tracing the actual try/finally/control-flow ordering in the diff.

🔒 Reviewed by a multi-stage local-first funnel (architect context pack → explorer candidates → critic challenge/author) for high recall with low false-positive noise. Findings are advisory — verify before acting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Workstream D] PR 15 of 17: Preserve effective runtime policy and reconcile interrupted work across restart

3 participants