After the first fork, every subsequent fork (of the fork, or of the original) silently creates an empty session — no transcript copy

Status Open
Reported on v2.1.227
Maintainer reply None cached
Activity 0 comments · opened Aug 11, 2026

After the first fork, every subsequent fork (of the fork, or of the original) silently creates an empty session — no transcript copy

Version: 2.1.227 (native installer, macOS Darwin 25.5.0)

Summary

The first fork of a session works: the full transcript is copied into the new session. After that, forking either the forked session or the original session again produces a session whose jsonl contains only ai-title/agent-name rows — zero history — while the job's state.json still records correct forkParentSessionId / forkBoundaryAt metadata. The new session starts cold with no conversation context. No error is surfaced; the parent even receives the normal "The fork runs as its own separate session…" success notice.

Secondary bug (naming): the fork marker is derived purely from the parent's display name (child = parent.name + " ⑂"). Because the first fork silently re-identifies the original session under a new session id that keeps the unmarked name, a second fork of "the original" is named identically to the first fork (both <name> ⑂), making them indistinguishable in the roster, and the fork depth is not reflected.

Repro

  1. Run an interactive session for a while, then fork it. → Fork #1 gets the full transcript. (As a side effect, the original session is re-identified: a new session id is created via the snapshot + --resume <snapshot> --fork-session path, with a full physical copy of the jsonl and the original name.)
  2. Fork again — either fork #1, or the entry that now represents the original. → The new session's transcript is empty (~2 lines), context lost.
  3. Roster naming: fork #1 and the fork-of-original from step 2 both display as <name> ⑂ (name collision, no depth indication); only fork-of-fork gets <name> ⑂ ⑂.

Root cause analysis (from the bundled code of 2.1.227)

In spawnBackgroundFork (Ger), the parent transcript is snapshotted only when the REPL executing the fork already knows its transcript path:

let O = Y_a();                 // = Pu().sessionFile — transcript path of the REPL executing the fork
let M = O;
if (c?.keepParent && O !== null) { M = await HEm(O, D); }   // copy to jobs/<child>/tmp/parent-transcript.jsonl
let Y = [ ...M !== null ? ["--resume", M, "--fork-session"] : [], ... ];   // <-- silent fallthrough
...
{ forkSourceAlive: !0, forkBoundaryAt: z, forkSessionId: H, forkParentSessionId: Mt() }

When Y_a() returns null, the child is spawned without --resume, i.e. as a brand-new empty session — but the fork metadata is still written, and the success path (fork notice in the parent, tengu_session_fork telemetry) still runs.

Y_a() is null exactly in the post-first-fork state: both fork #1 and the re-identified original are freshly (re)spawned via --resume <snapshot> --fork-session and have not claimed/written their session file yet. Any fork issued from a session in that state loses the entire history. (Consistent with this, the failure went away in our data only after a session had written at least one new message.)

On-disk evidence (one incident, 4 sessions)

| session | how created | name | jsonl content |
|---|---|---|---|
| 959ef4ed | original interactive session (10 MB) | <name> | full history |
| a944660c | fork #1, issued inside the live original | <name> ⑂ | full copy, message uuids identical to original, sessionId rewritten; snapshot jobs/a944660c/tmp/parent-transcript.jsonl (10 MB) present |
| 4f6837c2 | the original, silently re-identified at fork time (no fork metadata, full copy) | <name> (unmarked) | full copy |
| f070993d | fork of fork #1, ~64 s later, before it wrote anything new | <name> ⑂ ⑂ | 2 lines (ai-title + agent-name); no snapshot in its job dir |
| 534fe509 | fork of the re-identified original, same pattern | <name> ⑂collides with fork #1 | no history; first user message has parentUuid: null; state.json has correct forkParentSessionId/forkBoundaryAt; no snapshot in its job dir |

In both failing cases the parent's first new jsonl write happened only after the fork attempt, matching the "session file not yet claimed" condition.

Related (not duplicates)

  • #85004 — session forking has no parent-child linkage (adjacent metadata problem; here the metadata is present but the transcript copy is skipped)
  • #79764 — background subagents unresumable after background-session fork
  • #77649 — reconnect re-forks a duplicate idle session
  • #73354 — agent view collapses same-named live sessions (the naming collision above feeds this)

Expected

  1. The fork resolves the parent transcript path from the session id / job metadata when Pu().sessionFile is not yet set — the file exists on disk and is readable — or, failing that, the fork fails loudly ("can't fork yet — transcript not available") instead of silently creating an empty session that looks successful.
  2. Fork naming reflects lineage rather than string-appending to the parent's display name, so a second fork of the same parent doesn't collide with the first, and fork depth (⑂ ⑂) is consistent regardless of the parent's silent re-identification.

View original on GitHub ↗