You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Replacement supervisor cannot orch_resume the batch it inherits: activation imports phase 'executing' into memory and the in-memory guard overrides disk eligibility #631
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
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).
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).
No engine loop exists in session B for this batch. Evidence: registry.jsonupdatedAt 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.
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.
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 persistedphase("executing") frombatch-state.jsoninto in-memoryorchBatchState, anddoOrchResumechecks that in-memory phase before consulting disk — refusing with "A batch is currently executing … Cannot resume." YetcheckResumeEligibilityon the persisted state explicitly treatsexecutingas 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
staleheartbeat / dead pid path) ranbuildTakeoverSummaryand populatedorchBatchStatefrombatch-state.json, includingphase = "executing"(extension.ts ~L4246 and ~L4292).registry.jsonupdatedAtfrozen at1788657826268for 17+ minutes; the dead worker pid (59452) never reconciled;orch_pauseaccepted (setpauseSignal) but nothing honored it;batch-state.jsonstayedphase: executing,updatedAt 1788656981285.orch_resume(with and withoutforce) refused: in-memory phaseexecuting.orch_retry_task/orch_skip_task/orch_force_mergecarry the same active-phase guard.orch_stopis not in the supervisor toolset;orch_abortcleans up state (not resumable);supervisor_takeoveris unsafe (supervisor_takeover destroys the paused lane: task marked skipped, batch completed 0/1, worktree+branch removed, uncommitted work lost #628).batch-state.json(phase: executing → paused); start a third supervisor session so activation importspaused;orch_resumethen reconciled the dead worker as precedence-4re-executein the existing worktree and the lane relaunched cleanly.Cause (code pointers,
feat/review-boundary-supervisor-notifications@ a32cde3)extensions/taskplane/extension.ts~L2919doOrchResume: refuses whenorchBatchState.phase∈ {launching, executing, merging, planning} — beforeloadBatchState.extensions/taskplane/extension.ts~L4246 / ~L4292 (lock takeover,stale/no-lockfile/corruptand live-takeover branches):orchBatchState.phase = batchState.phase— imports an ACTIVE phase into a session that has no engine for it.extensions/taskplane/resume.tsL396checkResumeEligibility:executing→ eligible ("orchestrator disconnected"). This is the intended semantics; the in-memory guard defeats it.Expected
paused/disconnected(a phase meaning "persisted state says executing, no live engine here"), or set a flagengineAttached=falsethat the active-phase guards consult.doOrchResumeshould treat "active phase in memory but no engine attached in this process" as resumable, deferring tocheckResumeEligibilityon the persisted state — which already encodes the disconnect case.orch_stopor equivalent) that moves an orphaned batch topausedwithout cleanup, so the only out is not a hand edit ofbatch-state.json.executingand dies (or its lock heartbeat goes stale); session B takes over;orch_resumesucceeds and reconciles dead workers via the existing precedence rules.Notes
!isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid), so a dead pid markedrunningis correctly treated as dead. Worth documenting so operators do not hand-editregistry.jsonunnecessarily.Relation to other issues
expectsReply: trueescalation timed out while the supervisor was being replaced, and nothing relaunched it.heldstate would have removed the timeout in the companion issue; this issue stands regardless — any supervisor replacement duringexecutinghits it.