Skip to content

Keep the original session running and listed when forking - #1389

Open
snimu wants to merge 5 commits into
mainfrom
eng-5180
Open

Keep the original session running and listed when forking#1389
snimu wants to merge 5 commits into
mainfrom
eng-5180

Conversation

@snimu

@snimu snimu commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Fixes ENG-5180.

What this does

/fork no 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

  • The daemon gets a new command that copies the chosen part of the conversation into a new session file and stops there — nothing about the running session is touched.
  • The client then opens that file as a genuinely new session (its own worker, its own identity) and switches you to it, using the same machinery that already exists for creating and attaching to sessions.
  • The fork starts fresh on purpose: no copied kernel state, no copied subagents, no stolen heartbeats. Subagents stay with the original, which keeps running. This also makes forking fast — there is nothing heavy to copy.
  • Old daemons that don't know the new command keep the old behavior (the client checks before using it), so nothing breaks across versions.

Changes

  • New fork_export daemon command plus the client-side fork flow rewrite (+179/−21 source lines across five files).
  • A regression test file covering: both sessions listed after a fork, the original stays promptable, its subagents stay open, its heartbeats stay put, the fork starts with no subagents — plus connection-level and protocol-compatibility tests (+703/−1 test lines).
  • Changelog entry and a small accuracy fix in the extensions doc.

Checks

  • npm run check clean; 138 targeted tests pass (108 re-run after merging the newest main).
  • Review went through two rounds; the first caught three real issues (double hook emission, a concurrency race between simultaneous forks, unsafe cleanup on failure) that the final version fixes.
  • Validated end-to-end in a real terminal with a fresh daemon (31 recorded steps): fork with a live subagent → both sessions listed, original responds afterwards with its subagent intact, fork starts empty, killing the fork leaves the original untouched. Two separate daemon workers confirmed in the logs.

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 daemon protocol, session identity, leases, and reattach/cleanup. Failure paths are covered, but a bad reattach can still leave the client on an unexpected session.

Overview
In daemon mode, /fork no longer shuts down the source session. It copies the chosen branch to a new session file, starts a separate worker, and switches the client to it. The original stays listed and running, including subagents, heartbeats, and leases. The fork starts fresh (no copied kernel state).

Adds exportForkBranch on AgentSessionRuntime and a capability-gated fork_export command (schema revision 21). DaemonAgentConnection.fork uses export → create → reattach, with cleanup on failed create/reattach, and serializes overlapping fork/switchSession so they cannot interleave. Falls back to the old in-place fork when the daemon lacks the capability or the session is not persisted.

In-process mode still replaces the session in place. Extension docs now distinguish daemon vs in-process fork lifecycle.

Reviewed by Cursor Bugbot for commit 3d02016. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Keep original session running when forking via new fork_export daemon command

  • Adds a fork_export daemon command and server capability (protocol schema rev 21) that exports a forked branch to a new session file without stopping the source runtime.
  • DaemonAgentConnection.fork now prefers fork_export: it exports the branch, creates a new worker for the forked session, and reattaches the client to it. The original session stays running and listed. Falls back to legacy in-place fork when the daemon lacks the capability or the session is not persisted.
  • Adds AgentSessionRuntime.exportForkBranch to flush the source session and write a branched session file, returning null for in-memory sessions.
  • Serializes fork and switchSession transitions via withSessionTransition to prevent interleaving races.
  • Risk: if reattach to the new fork worker fails after daemon-side acceptance, the client attempts recovery reattach to the source; if that also fails the fork attachment is kept alive rather than stranding the client — reviewers should check DaemonAgentConnection.fork in daemon-agent-connection.ts for the full error-path cleanup.

Macroscope summarized 3d02016.

snimu added 2 commits August 12, 2026 22:25
…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
Comment thread packages/coding-agent/src/core/agent-session-runtime.ts
return this.legacyFork(entryId, options);
}
const sourceActiveSessionId = this.activeSessionId;
const exported = await this.requestData<{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High agent-connection/daemon-agent-connection.ts:1240

A fork fails after reconnecting to an older daemon instead of falling back to legacyFork. The capability check occurs before requestData, but a recovered request can later throw DaemonCapabilityUnavailableError; catch that post-reconnect error and invoke legacyFork(entryId, options).

🚀 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 1240:

A fork fails after reconnecting to an older daemon instead of falling back to `legacyFork`. The capability check occurs before `requestData`, but a recovered request can later throw `DaemonCapabilityUnavailableError`; catch that post-reconnect error and invoke `legacyFork(entryId, options)`.

Evidence trail:
packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts:245-248, 1232-1249, 1301-1311, 1519-1533 at REVIEWED_COMMIT; packages/coding-agent/src/modes/daemon/daemon-client.ts:72-84, 282-285, 293-318, 431-460 at REVIEWED_COMMIT; git diff MERGE_BASE REVIEWED_COMMIT -- packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 93f9493. Configure here.


private legacyFork(
entryId: string,
options?: AgentConnectionForkOptions,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legacy naming left in fork path

Low Severity

The in-place fallback is named legacyFork, and related protocol comments and test titles also use “legacy” vocabulary. Project review rules disallow migration-era terms like legacy in names, comments, and test titles even when an older protocol path remains for compatibility.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by project rule: Review rules

Reviewed by Cursor Bugbot for commit 93f9493. Configure here.

// the create; on timeout/transport errors the worker may already be
// live on that file.
if (error instanceof Error && this.definitiveRequestErrors.has(error)) {
await unlink(sessionPath).catch(() => undefined);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High agent-connection/daemon-agent-connection.ts:1377

A definitive create error can cause this client to unlink a fork file that another client has already created and attached, deleting that session's backing JSONL from disk. Because definitiveRequestErrors only describes the response and does not establish ownership of sessionPath, a SessionAlreadyActiveError here can remove a live session; only clean up the export after proving this request created it.

🚀 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 1377:

A definitive `create` error can cause this client to unlink a fork file that another client has already created and attached, deleting that session's backing JSONL from disk. Because `definitiveRequestErrors` only describes the response and does not establish ownership of `sessionPath`, a `SessionAlreadyActiveError` here can remove a live session; only clean up the export after proving this request created it.

Evidence trail:
packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts:1363-1379, 1623-1632 @ 3d0201634920fb8fc40047316322399af0da8dbd
packages/coding-agent/src/modes/daemon/daemon-supervisor.ts:2233-2269, 2298-2312 @ 3d0201634920fb8fc40047316322399af0da8dbd
packages/coding-agent/src/core/session-lease.ts:20-32, 268-283 @ 3d0201634920fb8fc40047316322399af0da8dbd

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant