Skip to content

Engineering archive

These notes describe work at the date shown. See the usage reference for current behavior.

Phase 5d recovery hardening review - 2026-09-05

Scope and outcome

Reviewed the original recovery-hardening series, 478dd6b~18..478dd6b, commit by commit, against sections 5.1, 7.1, 9, 12, 13, and 18 of docs/superpowers/specs/2026-09-03-agent-task-pool-design.md and findings 6 and 7 of docs/superpowers/reviews/2026-09-05-phase5d-review.md.

The operator confirmed that fix/p5d-deferred landed on main unchanged at 478dd6b; this branch therefore needed no rebase. The review found three Major findings: the two deferred findings are fixed, and one additional read-only-side-effect integration defect is fixed in 5f5f083. No Critical or Minor finding was opened. No original commit needs to be reverted.

Closing verdict: land listed fixes — land the original 18 commits together with 5f5f083 and this report.

Findings

1. Major - fixed: read-only task/run views recovered and deleted residue

Location: pre-fix 478dd6b:src/client_state.rs:1905,2091; current src/client_state.rs:1904-1912,2089-2096,2099-2137.

Evidence (pre-fix):

self.recover_task_replacement_residue(&tasks, &names)?;

The analogous load_run and list_runs paths called recover_run_replacement_residue as well.

Failure scenario: an exchange can publish the new task/run record while leaving a private replace-<uuid> displaced record after a crash or injected sync failure. worker task list, dashboard projection, or any other read-only path using task/run enumeration then retried the cleanup namespace and removed that residue. The displaced record is normally expected old state, but the deletion is still a mutation from a read-only command and could race with the owner recovery path. This violated the spec's requirement that list, status, result, diff, logs, and dashboard never mutate or start work.

Fix: list_tasks_locked, load_run, and list_runs now only read and filter private replacement names. Recovery is exposed through the mutating ClientStateStore::recover_replacement_residue path and is invoked from mutating update/reconciliation flows. The regression tests task_enumeration_does_not_recover_replacement_residue and run_enumeration_does_not_recover_replacement_residue leave the residue count unchanged after enumeration and prove that a later mutation cleans it up. Commit 5f5f083.

Verdict: FIX, then LAND the residue-recovery commits.

2. Major - fixed: post-create submission rollback

Location: src/task_client.rs:736-799; prior finding 6 in the earlier Phase 5d review.

Evidence (pre-fix):

let _ = transfer.release_base(self.runner, task_id);

Failure scenario: after create_task succeeded, a prompt write, run reference load, queue publication, report, or equivalent pre-handoff step could fail after only the transfer base was released. The task record, prompt, queue row, and task-owned run reservation could then disagree, while reconciliation had no complete durable description of the submission to repair.

Fix: submission intent is recorded before post-create work; failures use a durable rollback marker and compensating cleanup for the task-owned run reservation, transfer base, queue row, prompt tree, and task record. Cleanup is ownership-checked and retryable. Intent clearance is the explicit handoff boundary: after it, the complete submission is retained for reconciliation so it cannot remove a row a runner may already have adopted. The recovery path also reloads the current record under the transfer lock before acting.

Regression coverage includes prompt-write, run-reference, queue-publication, rollback-retry, restart, reopen, and handoff-boundary fault cases in the task-client and turn-runner suites.

Verdict: FIX confirmed and LAND the rollback series; no revert.

3. Major - fixed: local push origin target is pinned

Location: src/task.rs:188-255,581-589,662-685, src/job_service.rs:3127-3150, src/turn_runner.rs:641-646,1287-1289, and src/turn.rs:794-817; prior finding 7 in the earlier Phase 5d review.

Evidence (pre-fix):

(TaskSource::Local { .. }, true, Some(_)) => Ok(()),

Failure scenario: a local push task stored only TaskSource::Local, while the client reread the repository origin for every turn and the host accepted any supplied origin. Changing the repository origin after submit could send a retry or publication to a different normalized destination than the one used for admission and task creation.

Fix: local push metadata now stores a normalized credential-free URL and its derived origin:<host> requirement. Every turn uses that persisted target; it does not reread Git origin. Host validation and terminal publication require exact equality and return REQUEST_CONFLICT for missing, unexpected, or changed targets. Tests cover origin mutation after submit, accepted and rejected replay/request mismatches, and terminal publication.

Verdict: FIX confirmed and LAND the pinning series; no revert.

Commit-by-commit audit

#CommitAdversarial resultVerdict
1dcdca5a - roll back failed task submission stateReal initial post-create rollback; incomplete on its own, completed by the following durable-marker and cleanup commits.LAND
2e4630ef - preserve state after task handoff failureReal ownership boundary: state is retained when a runner may already have adopted the queue row.LAND
36151ee8 - recover incomplete submission rollbackReal durable rollback marker and restart/retry path; bounded to the task's own resources.LAND
4db25522 - pin local push origin targetReal immutable target capture and host requirement for local push.LAND
50cb6a25 - avoid origin reads after task submissionReal removal of mutable Git-origin rereads from the turn path.LAND
6306ae15 - preserve submission rollback recoveryReal ordering fix that keeps pre-handoff recovery possible across fallible boundaries.LAND
73cc91dc - preserve pinned origin task metadataReal preservation of immutable origin metadata through host/request parsing.LAND
8afa5740 - retain parked submission after publication faultReal protection against deleting a submission whose parked row may have transferred ownership.LAND
993e6054 - cover origin recovery and replayDeterministic regression coverage for origin replay/recovery; no speculative production mechanism.LAND
10c0e71fe - recover rollback after turn retirementReal retention of rollback identity after the prompt/turn tree is retired.LAND
1198ebac2 - recover submission intent after marker failureReal durable intent-before-failure ordering and explicit durable intent clearing.LAND
126450fec - compile retention fixture with pinned originTest-fixture compatibility for the new immutable field; no runtime risk found.LAND
13346e5c2 - guard submission intent recovery racesReal transfer-lock/current-record reload guard against stale rollback or adoption.LAND
14e08794e - preserve submission rollback ownershipReal task-scoped run-branch ownership; rollback cannot release another task's branch.LAND
15a6b7684 - treat intent clear as handoff boundaryReal atomic-exchange/final-sync ambiguity handling; avoids compensating after ownership may transfer.LAND
16d15bb95 - recover task replacement residue after exchangeReal rooted task-record recovery, but its enumeration call made read-only views mutate.FIX, THEN LAND
172723ba9 - serialize task replacement residue recoveryReal serialized recovery and rooted-FS fault coverage; retain with the read-side-effect fix.FIX, THEN LAND
18478dd6b - recover run replacement residue and report runnerReal run-residue recovery and runner reporting; load_run/list_runs inherited the same read-side-effect defect.FIX, THEN LAND

No commit was speculative or harmful enough to revert. The defect spanning commits 16-18 was a call-site integration error, not evidence that the underlying replacement recovery should be removed.

Seam checks

  • Recovery mutation is confined to mutating command paths and reconciliation. Read projections use owner-only state locks and reads; they do not enqueue, start runners, remove replacement residue, or reread mutable origin state.
  • Submission rollback cannot remove a live runner's adopted work: it requires the pre-handoff state, checks for no runner and no active task, and only removes a queue row in the waiting state with the original enqueue owner. Transfer locking plus a fresh record load closes the stale-snapshot race.
  • Durable markers are canonical bounded IDs only. Task records are owner-only, unknown fields are rejected, and markers contain no prompt, environment, credential, or filesystem path data.
  • Local push targets are normalized before persistence; credentials, query, and fragment material are not retained. A legitimate Git-origin change after submit does not change the pinned task target; a mismatched request fails with REQUEST_CONFLICT before publication.
  • Run publish reservations are task-owned and release checks preserve another task's reservation. Rooted-FS replacement recovery retains root-name, owner-only regular-file, no-follow, device/inode, exchange, and fsync invariants.
  • Recovery helpers remain reachable from mutating update and runner reconciliation paths. Fault injection is deterministic and covers exchange, sync, prompt, queue, origin, restart, and handoff boundaries.

Verification and gate

Per-fix verification passed:

  • cargo fmt --check
  • cargo test --locked --test task_model --test turn_runner (29 + 31 passed)
  • cargo clippy --locked --all-targets -- -D warnings
  • git diff --check

The required final cargo test --locked --all-targets passed every target except the three dashboard tests that were blocked by the restricted environment's loopback bind and returned DASHBOARD_BIND_FAILED. Per the brief, cargo test --locked --test dashboard_command was rerun with host network permission and all 4 dashboard tests passed. No serial timing rerun was needed. No worker or Mac mini was contacted.

Closing verdict

LAND LISTED FIXES. Land the original 18 recovery-hardening commits with 5f5f083 and this report; do not revert any of the 18 commits.