Skip to content

basou: Import a forked Claude Code transcript without the records it copied - #377

Merged
takashi-matsuyama merged 1 commit into
mainfrom
import/fork-dedup
Oct 19, 2026
Merged

takashi-matsuyama merged 1 commit into
mainfrom
import/fork-dedup

Conversation

@takashi-matsuyama

Copy link
Copy Markdown
Member

Summary

A Claude Code transcript that forks another (a session resumed into a new session id, or one started from another session's conversation) opens with copies of the records it took over, under their original uuid and timestamp. Each transcript was imported as a session of its own, so the shared history was counted once per transcript: commands, decisions, file changes, output tokens and per-session active time, with every fork's session starting at the original's first record.

What changes

  • basou import claude-code groups the transcripts of the source roots by conversation: every transcript of one conversation opens with the same first uuid-bearing record, so each file is read only up to that record. Within a conversation the file created first (birth time; the session id decides where none is recorded or two are equal) keeps the shared records, and each later transcript is derived from the records it added (claudeTranscriptOwnRecords, new in @basou/core). A fork seen for the first time that added no action is not imported.
  • A session imported with the copies is re-derived in place on the next import, even though its transcript did not change. reimportPreservingId takes dropDerivedBefore: a prior derived event dated before the fresh start may be dropped; one from the session's own time that is not reproduced still refuses the re-import (prior_derived_dropped). The session id and the ids of the kept events are preserved, and the hash chain is rewritten.
  • A re-import, --force included, leaves out records dated before the start of the session an earlier import made from the same transcript, so the copies stay out after the older transcript is deleted.
  • The import's summary and basou refresh say how many re-imported sessions dropped copies; the --json result of basou import gains dropped_copies_count (sessions that held copies and no longer do), and reimported_count now also counts these in-place re-derivations. A fork that added no action of its own, and a session imported before source sizes were recorded (it waits for --force), keep their copies.
  • docs/spec/terminal-and-import.md section 14.2 describes the rule and a known limit: the order is read from birth times on every import, so if they change after an import (a backup restored, a copy to another disk) and the order flips, the shared records end up counted in neither session. CHANGELOG Unreleased has the entry. The shapes of session.yaml and events are unchanged.

Verification

  • New tests: transcript-fork.test.ts (the filter), session-reimport.test.ts (dropDerivedBefore, including the exact-bound case), import.test.ts (15 fork scenarios: beside the original, creation-time order vs. name order, unknown birth times, --session, nothing of its own, in-place re-derivation with kept ids and a verified chain, idempotence, --dry-run, original deleted then fork grows, --force, a non-append change still refused, the older transcript vanishing mid-run, copies that derived no event, a fork with no action of its own), refresh.test.ts.
  • build, typecheck, lint, lint:lang, lint:paths, test (core 2,929 / sdk 67 / cli 1,581), test:scripts: all exit 0.
  • Mutation testing on a copy: 29 mutants, 28 killed; the survivor (older.length >= 0) is behaviourally equivalent because the holds_copies path already skips a session from which nothing was left out.
  • On a copy of a real store, the new build re-derived the 6 forked sessions whose transcripts still exist (each matched an independent re-derivation), a second run changed nothing, and basou verify reported no tampered session.
  • A convergent review (one round, review-only) reported four findings; three were applied (the message and docs for a fork with no action of its own, the dropped-copies count, the note on legacy sessions) and one was left as not observed in real transcripts (a timestamped non-uuid record trailing the copies).

…copied

A transcript that forks another (a session resumed into a new session id,
or one started from another session's conversation) opens with copies of
the records it took over, under their original uuids and timestamps, and
each transcript was imported as a session of its own. The shared history
was counted once per transcript: its commands, decisions, file changes,
output tokens and per-session active time, and every fork's session
started at the original's first record.

basou import claude-code now groups the transcripts of the source roots by
the conversation they hold: every transcript of one conversation opens
with the same first record, so each is read up to its first uuid. The
transcript whose file was created first keeps the shared records (birth
time; the session id decides where none is recorded or two are equal),
and each later one is derived from the records it added
(claudeTranscriptOwnRecords). A fork that added no action is not imported.

A session imported with the copies is re-derived in place on the next
import even though its transcript did not change (its start is at or
before the conversation's first record). reimportPreservingId takes
dropDerivedBefore: a prior derived event from before the fresh start may
be dropped; one from the session's own time that is not reproduced still
refuses the re-import. A fork that added no action of its own, and a
session imported before source sizes were recorded (it waits for --force),
keep their copies. The JSON result counts these re-derived sessions in
reimported_count and in a new dropped_copies_count, and refresh says how
many there were. A re-import, --force included, leaves out records from
before the start of the session an earlier import made, so the copies stay
out after the older transcript is deleted.

docs/spec/terminal-and-import.md section 14.2 describes the rule and a
known limit: the order is read from birth times on every import, so if they
change after an import and the order flips, the shared records end up
counted in neither session. The shapes of session.yaml and events are
unchanged.
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