Let the daemon own an unattended relaunch; the pane waits for it - #312
Merged
Merged
Conversation
Restarting a loop from the app started a fresh session instead of resuming: the agent came up on its opening prompt, so a goal-based loop re-issued `/goal` and the conversation the restart existed to keep was orphaned. The restart machinery was not at fault. `graphcoded` kills and then starts with `--resume`, and the dial log says so — but the app remounts the pane on the same beat, and `localResumeOrFreshCommand`'s unbanked fallback had no `zmx get` guard and no resume branch. It unconditionally created the session fresh, and beat the daemon's detached start often enough to be the bug. Harmless while a session is alive, because `zmx attach` ignores its command argument then; fatal in the window right after a restart's kill. Local Copilot took that branch every time. Nothing banks a Copilot session id on this machine — `CopilotSessionLog`'s banker is the remote ensure's, and Copilot has no `SessionStart` hook the way Claude Code does — so `SessionIDStore.load` is permanently nil for it locally and the resume branch was unreachable. Claude Code, OpenCode and Codex all bank, so they resumed. So an unattended loop's pane now waits for the session the daemon is starting and attaches to it, rather than racing it with a launch that carries the prompt. That is the same ownership split the remote pane already makes for a missing unattended session, and the same one the Codex pane makes for every launch. An attended loop keeps the fresh launch: no daemon starts a turn-based loop, so a pane that waited for one would wait out its whole minute. `waitingAttachCommand` gains a dial line, since waiting was otherwise the one launch decision the log could not tell apart from a pane that never opened. Measured on the reported case — a Copilot goal loop open in the app logged `restart killed`, `ensure resume` and `open fresh` in the same second, twice over two presses, and the surviving process was the pane's fresh launch. The same backend and loop type with no pane open resumed on both restarts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KdnA1YedzfZVxRsNVGuKAN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The report
Restarting a loop started a fresh session instead of resuming it — a goal-based loop re-issued its
/goaldirective and the conversation the restart existed to keep was orphaned. Reported against both app affordances (Loop ▸ Restart Session, Restart All Sessions…), reproducible on the second press.What was measured
The restart machinery is not at fault. Every path I could drive resumed correctly:
graphcode node restartensure resumegraphcode node restartensure resumegraphcode node restart--resume, same directory, twiceopen resumeensure resumeThe failing case, a Copilot goal loop open in the app, logged this — twice, over two presses:
and the surviving process was the pane's:
copilot --yolo --name graphcode-… --interactive "$GRAPHCODE_TRIGGER_PROMPT"— no--resume, goal re-issued. The controlled comparison is the point: same backend, same loop type, no pane open → resumed on both restarts.Root cause
GhosttyTerminalView.localResumeOrFreshCommandguards onSessionIDStore.load(forNodeID:) != nil. Its unbanked fallback had nozmx getguard and no resume branch — it unconditionally created the session fresh. That is harmless while a session is alive, becausezmx attachignores its command argument then, and fatal in the window right after a restart's kill, where it racesZmxSessionLauncher.restart's detachedstartand wins often enough to be the bug.Local Copilot took that branch every time: nothing banks a Copilot session id on this machine —
CopilotSessionLog's banker is the remote ensure's, and Copilot has noSessionStarthook the way Claude Code does — soSessionIDStore.loadis permanently nil for it locally and the resume branch was unreachable. Claude Code, OpenCode and Codex all bank, which is why they resumed and Copilot did not.The Copilot half is a regression from
d8ea180, which reverted the local Copilot resume work (#248/#249) on 2026-09-02.The change
An unattended loop's pane now waits for the session the daemon is starting and attaches to it, instead of racing it with a launch that carries the prompt — the same ownership split the remote pane already makes for a missing unattended session, and the same one the Codex pane makes for every launch. An attended loop keeps the fresh launch: no daemon starts a turn-based loop, so a pane that waited for one would wait out its whole minute.
waitingAttachCommandgains a dial line (open await-daemon), since waiting was otherwise the one launch decision the dial log could not tell apart from a pane that never opened.Verification
From the worktree, private DerivedData, exit codes quoted:
xcodebuild … -scheme graphcode build→exit=0,** BUILD SUCCEEDED **, 0 errorsxcodebuild … -scheme graphcode test→exit=0,** TEST SUCCEEDED **, 1643 tests in 170 suites passedxcodebuild … -scheme graphcode-cli build→exit=0·-scheme graphcoded build→exit=0swiftlint lint→exit=0, 0 errors ·swift format lint --strict→exit=0New/changed tests, all observed running:
anUnattendedNodeWithNothingBankedWaitsForTheDaemonInsteadOfRacingIt— goal and time, asserts it waits, logsopen await-daemon, and never types the opening promptanAttendedNodeWithNothingBankedStillLaunchesFresh— wasaNodeWithNothingBankedStillLaunchesFresh, now pinned to a turn-based loop, which is the case that branch is still forNoted, not fixed here
A local project has no repeating liveness sweep —
ProjectRegistrystartsstartRemoteLivenessSweeponly for remote projects, so a local unattended loop whose session dies is not revived until the daemon restarts or the project reloads. Verified by raw-killing a probe's session and watching nothing bring it back. That is the already-tracked issue #276.🤖 Generated with Claude Code
https://claude.ai/code/session_01KdnA1YedzfZVxRsNVGuKAN