Skip to content

sessions: /branch creates generation chains — 23.9% of transcripts, and CodeV cannot tell them apart #142

Description

@grimmerk

What was measured (2026-09-05, live experiment)

/branch <name> copies the transcript to a new session id and lets the same process keep writing to the new file. Confirmed end to end on this machine:

before after /branch
session id b4feeab6 a8f5603a
pid 93499 93499 (unchanged)
sessions/<pid>.json id + name = branch-test-a id + name updated in place
transcripts 1 2 — parent intact (57 KB, ends cleanly), child is a copy
history.jsonl the /branch line lands under the parent, everything after under the child

Resuming the parent afterwards works (new pid, appends to the parent transcript). So the reported symptom is confusion, not failure.

This is not an edge case: 21 of 88 transcripts on disk (23.9%) carry forkedFrom.

The exact primitive

Claude Code records the link itself, on every copied line of the child:

"forkedFrom": { "sessionId": "<parent>", "messageUuid": "<branch point>" }

The root transcript has zero such lines. Enrichment already greps every transcript, so one more pattern yields a complete child → parent map. No heuristics, no content comparison.

Four consequences in CodeV

/branch copies the transcript but NOT history.jsonl. The child's prompts before the branch exist only in the transcript. CodeV's list is built from history.jsonl, so:

  1. Deep search cannot see them. Searching for anything said before the branch finds the parent, never the child — search systematically points at the older generation.
  2. messageCount is wrong. A child carrying a full conversation renders as "1 msgs" (observed: 5 msgs vs the parent's 38).
  3. Pins do not follow. Verified in ~/.config/codev/session-marks.json: the pin sat on the parent while work continued in the child.
  4. Ordering. Resuming a parent lifts it above the child that actually continues the work.

The sharper problem: identically-named live sessions

Because /branch moves you into the new session, the new name lands on the work you are continuing, while the original session keeps the old name — the wrong way round. The workaround in daily use is a three-step swap: branch with a temporary name, resume the parent and rename it <name>-2, then rename the child back to the original name.

Why the child gets the original name is the important part: /branch carries in-flight background subagents across, and their output lands in the child. So the unfinished task genuinely continues there, and the name follows the work. The parent is then resumed and repurposed for a different task, which is why it takes the -2.

That makes the rename dance a manual substitute for something CodeV does not provide: stable task identity across session ids. The user's mental model keys on name + task; CodeV keys on session id, and the two diverge every time a branch happens.

The result is that names are deliberately reused across generations. Measured right now:

pid=22290  id=b7f7c407  "agentic-fred harden again - pr2-1533-v7-readiness — deps ref…"
pid=20653  id=a96a4274  "agentic-fred harden again - pr2-1533-v7-readiness — deps ref…"
pid=27303  id=6fab4f73  "agentic-fred harden again - pr2-1533-v7-readiness — deps ref…"

Three distinct sessions, three live pids, identical names. CodeV is reporting this correctly — it is simply undisplayable, and scanning the terminals by eye does not separate them either.

And iTerm2 switching matches on the tab title, so all three rows switch to the same terminal. That is the "clicking any of them goes to the same session" report. Not a regression of the same-cwd work (activeStateRef is replaced wholesale each detection, and the direct pid→id mapping never reaches the cwd fallback) — it is the existing title-matching mechanism meeting a case where titles are intentionally not unique.

Proposed work, in priority order

  • C0 — make same-named live sessions distinguishable, and switch by TTY/pid rather than title. This is the one that hurts today. Display needs something unique per row (branch generation, divergence time, pid/tty).
  • C1 — surface the forkedFrom relation. Bind identity to the task, not blindly to the newest descendant. An earlier proposal here was to resolve pins to the tip of the chain. That is too crude: after the dance the child continues the original task while the parent is live under a different task, so both are meaningful and "newest" is not the same as "the thing you pinned". The relation should be shown, and any migration should follow the task — the signal for which is the name moving, not the chain advancing.
  • C2 — messageCount summed along the chain.
  • C3 — optional chain collapse in the list (plan §6's C3).
  • C4 — sessions: resume fails for sessions whose transcript was cleaned up — detect and mark as expired #135 expired-session marking, which shares the "can this id still be resumed" diagnostic.

Documentation this falsifies

  • docs/session-finding-plan.md §6 states C3 chain collapse is "essentially defunct … no generation chains exist; only meaningful if --fork-session / copy-fork become common." Generation chains are a daily occurrence and 23.9% of the corpus.
  • §4.4 lists forking under "Edge cases (v1 ignores them; re-pin manually if hit)". Same false premise.

Both should be corrected regardless of which items above get built.

🤖 On behalf of @grimmerk — generated with Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions