Skip to content

Replacement supervisor cannot orch_resume the batch it inherits: activation imports phase 'executing' into memory and the in-memory guard overrides disk eligibility #631

Description

@HenryLach

Summary

When a supervisor session dies or is replaced while its batch is executing, the replacement session cannot resume the batch it inherits. Supervisor activation copies the persisted phase ("executing") from batch-state.json into in-memory orchBatchState, and doOrchResume checks that in-memory phase before consulting disk — refusing with "A batch is currently executing … Cannot resume." Yet checkResumeEligibility on the persisted state explicitly treats executing as resumable ("was executing when the orchestrator disconnected"). The two disagree, and the in-memory check wins, so the designed disconnect-recovery path is unreachable from the session that is supposed to perform it.

Observed in penster batch henrylach-20260905T210935 (TP-2047), 2026-09-06, after the original supervisor session wedged on a provider-side 400 and a new session took over the lock.

Sequence

  1. Supervisor session A owned the batch; its engine/monitor loop ran in-process. Session A wedged (every provider request failed with the same 400 on a thinking block in the latest assistant message).
  2. The operator started supervisor session B. Lock takeover (stale heartbeat / dead pid path) ran buildTakeoverSummary and populated orchBatchState from batch-state.json, including phase = "executing" (extension.ts ~L4246 and ~L4292).
  3. No engine loop exists in session B for this batch. Evidence: registry.json updatedAt frozen at 1788657826268 for 17+ minutes; the dead worker pid (59452) never reconciled; orch_pause accepted (set pauseSignal) but nothing honored it; batch-state.json stayed phase: executing, updatedAt 1788656981285.
  4. orch_resume (with and without force) refused: in-memory phase executing. orch_retry_task / orch_skip_task / orch_force_merge carry the same active-phase guard. orch_stop is not in the supervisor toolset; orch_abort cleans up state (not resumable); supervisor_takeover is unsafe (supervisor_takeover destroys the paused lane: task marked skipped, batch completed 0/1, worktree+branch removed, uncommitted work lost #628).
  5. Recovery: back up state; edit one field in batch-state.json (phase: executing → paused); start a third supervisor session so activation imports paused; orch_resume then reconciled the dead worker as precedence-4 re-execute in the existing worktree and the lane relaunched cleanly.

Cause (code pointers, feat/review-boundary-supervisor-notifications @ a32cde3)

  • extensions/taskplane/extension.ts ~L2919 doOrchResume: refuses when orchBatchState.phase ∈ {launching, executing, merging, planning} — before loadBatchState.
  • extensions/taskplane/extension.ts ~L4246 / ~L4292 (lock takeover, stale/no-lockfile/corrupt and live-takeover branches): orchBatchState.phase = batchState.phase — imports an ACTIVE phase into a session that has no engine for it.
  • extensions/taskplane/resume.ts L396 checkResumeEligibility: executing → eligible ("orchestrator disconnected"). This is the intended semantics; the in-memory guard defeats it.

Expected

  • On lock takeover, if no engine for the batch runs in the taking-over process, the imported phase must not be an active one. Either import paused/disconnected (a phase meaning "persisted state says executing, no live engine here"), or set a flag engineAttached=false that the active-phase guards consult.
  • doOrchResume should treat "active phase in memory but no engine attached in this process" as resumable, deferring to checkResumeEligibility on the persisted state — which already encodes the disconnect case.
  • The supervisor toolset should carry a non-destructive stop (orch_stop or equivalent) that moves an orphaned batch to paused without cleanup, so the only out is not a hand edit of batch-state.json.
  • Test: session A persists executing and dies (or its lock heartbeat goes stale); session B takes over; orch_resume succeeds and reconciles dead workers via the existing precedence rules.

Notes

  • The stale heartbeat / dead-pid detection in the lock takeover already knows session A is gone; that knowledge should flow into the phase decision.
  • The registry did not need repair: resume's liveness check is !isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid), so a dead pid marked running is correctly treated as dead. Worth documenting so operators do not hand-edit registry.json unnecessarily.

Relation to other issues

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions