Conversation
…5180) /fork previously replaced the current session's runtime in place: the original's kernel was disposed, its resident subagents closed, its cron jobs and heartbeats rebound to the fork, and its identity taken over, so the original vanished from the agents view. Fork is now split into an additive, capability-gated fork_export daemon command (branch export with no teardown) plus client orchestration in DaemonAgentConnection.fork(): export -> create a new daemon session for the forked file -> reattach. The original keeps running with its kernel, subagents, heartbeats, and lease, and stays listed; the fork starts fresh (cold kernel, no subagent registry, no cron), which also keeps the fork as lazy as possible. Legacy fork remains as the fallback for old daemons and in-memory sessions. DAEMON_SCHEMA_REVISION 16 -> 17. Fork transitions are serialized on the connection; the exported file is only removed on a definitive create rejection, and a failed reattach best-effort kills the created fork session.
… into eng-5180 # Conflicts: # packages/coding-agent/CHANGELOG.md
| } | ||
| const forkActiveSessionId = summary.activeSessionId ?? summary.id; | ||
| try { | ||
| await this.reattachSession(sourceActiveSessionId, forkActiveSessionId); |
There was a problem hiding this comment.
🟠 High agent-connection/daemon-agent-connection.ts:1263
A chunked snapshot failure during reattachSession() leaves the connection attached to the fork, and this catch then kills that fork, so the client remains pointed at a dead activeSessionId while the original session is disconnected. reattachSession() sets reattached = true immediately after the daemon accepts the reattach, which prevents its rollback when waitForSnapshot() fails; restore or reattach the prior session before killing the fork worker.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts around line 1263:
A chunked snapshot failure during `reattachSession()` leaves the connection attached to the fork, and this catch then kills that fork, so the client remains pointed at a dead `activeSessionId` while the original session is disconnected. `reattachSession()` sets `reattached = true` immediately after the daemon accepts the reattach, which prevents its rollback when `waitForSnapshot()` fails; restore or reattach the prior session before killing the fork worker.
Evidence trail:
packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts:1131-1205 (REVIEWED_COMMIT)
packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts:1261-1267 (REVIEWED_COMMIT)
packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts:1534-1553,1853-1879 (REVIEWED_COMMIT)
packages/coding-agent/src/modes/daemon/daemon-supervisor.ts:1478-1519 (REVIEWED_COMMIT)
| return { cancelled: false, sessionPath: sessionPath ?? null, selectedText }; | ||
| } | ||
| const sourceManager = SessionManager.open(currentSessionFile, sessionDir); | ||
| const forkedSessionPath = sourceManager.createBranchedSession(targetLeafId); |
There was a problem hiding this comment.
🟠 High core/agent-session-runtime.ts:690
exportForkBranch can throw Entry ... not found or export a branch missing the latest conversation state when a prompt is still in progress. It reopens currentSessionFile before createBranchedSession, but the live SessionManager may contain unflushed entries because _persist defers user-message writes; flush the live manager (or otherwise branch from its live state) before reopening it.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/agent-session-runtime.ts around line 690:
`exportForkBranch` can throw `Entry ... not found` or export a branch missing the latest conversation state when a prompt is still in progress. It reopens `currentSessionFile` before `createBranchedSession`, but the live `SessionManager` may contain unflushed entries because `_persist` defers user-message writes; flush the live manager (or otherwise branch from its live state) before reopening it.
Evidence trail:
packages/coding-agent/src/core/agent-session-runtime.ts:669-696, REVIEWED_COMMIT
packages/coding-agent/src/core/session-manager.ts:1456-1474, REVIEWED_COMMIT
packages/coding-agent/src/core/session-manager.ts:2044-2049, REVIEWED_COMMIT
packages/coding-agent/src/core/session-manager.ts:2158-2183, REVIEWED_COMMIT
| entryId: string, | ||
| options?: AgentConnectionForkOptions, | ||
| ): Promise<{ cancelled: boolean; selectedText?: string }> { | ||
| return this.withSessionTransition(() => this.performFork(entryId, options)); |
There was a problem hiding this comment.
🟠 High agent-connection/daemon-agent-connection.ts:1213
switchSession() is not serialized with fork(), so a concurrent switch can be overwritten when performFork() resumes and calls reattachSession() using its captured sourceActiveSessionId. The connection can therefore end attached to the fork instead of the session selected by the user; include switchSession() in withSessionTransition as well.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts around line 1213:
`switchSession()` is not serialized with `fork()`, so a concurrent switch can be overwritten when `performFork()` resumes and calls `reattachSession()` using its captured `sourceActiveSessionId`. The connection can therefore end attached to the fork instead of the session selected by the user; include `switchSession()` in `withSessionTransition` as well.
Evidence trail:
packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts:1105-1128, 1209-1269, 1412-1419 at eca1294a36efb3fe1d407ce9351e11bf64e05e08; packages/coding-agent/src/modes/daemon/daemon-supervisor.ts:1478-1519 at eca1294a36efb3fe1d407ce9351e11bf64e05e08
Fixes ENG-5180.
What this does
/forkno longer kills the session you fork from. The original keeps running — with its subagents, scheduled heartbeats, and warm state — and stays visible in the agents view. You still land in the new forked chat immediately.What the problem was
Forking used to replace the current session in place: the running session was shut down, its subagents were closed, its scheduled heartbeats were silently handed to the fork, and its identity was taken over. The original survived only as a file on disk and vanished from the session list — "when I do /fork, the original session disappears" (user report).
How it works now
Changes
fork_exportdaemon command plus the client-side fork flow rewrite (+179/−21 source lines across five files).Checks
npm run checkclean; 138 targeted tests pass (108 re-run after merging the newest main).Known cosmetic quirk (pre-existing display behavior, left for a follow-up)
Right after forking, the agents view may briefly group the fork under the original (it is linked as the fork's parent) until the fork's own summary appears; a killed fork also shows as a completed child row when expanding the original. The underlying state is always correct.
Note
Medium Risk
Touches session lifecycle, daemon protocol, and multi-step fork/create/reattach flows with rollback paths; behavior is heavily tested but mistakes could strand workers or duplicate hook emissions.
Overview
Daemon
/forkno longer shuts down the session you fork from. When the daemon supportsfork_export, the client exports the chosen branch to a new session file, spawns a separate worker for that file, and switches you to it while the original keeps running and stays in the agents list.The runtime adds
exportForkBranch(and sharedresolveForkTarget) so export runssession_before_forkand writes a branched JSONL without replacing the source worker. The daemon exposes a capability-gatedfork_exportcommand (schema revision 17); legacyforkstill does in-place replacement for older clients and non-persisted sessions.The client serializes overlapping forks, cleans up orphan export files on definitive create failures, and kills a spawned fork worker if reattach fails. Docs describe daemon vs in-process extension event behavior; changelog notes ENG-5180.
Reviewed by Cursor Bugbot for commit eca1294. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Keep the original session running when forking in daemon mode
exportForkBranchtoAgentSessionRuntimethat writes the fork branch to a new session file without tearing down the current runtime.fork_exportdaemon command and server capability; the daemon routes this command to the source session's worker and returns the new session file path.DaemonAgentConnection.forknow usesfork_exportwhen the server supports it: creates a new worker on the exported file, reattaches to it, and leaves the original session running. Falls back to legacy in-place fork for non-persisted sessions or older servers.DaemonAgentConnectionviawithSessionTransition()to prevent race conditions./forkin daemon mode no longer stops the original session — it opens the fork as a new session and switches to it while the original continues running.📊 Macroscope summarized eca1294. 7 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.