Skip to content

Copies between accounts: clearer rows, sync on resume, fork from a turn, and review fixes - #20

Merged
lr00rl merged 9 commits into
mainfrom
fix/20261022_copy-rows
Oct 23, 2026
Merged

lr00rl merged 9 commits into
mainfrom
fix/20261022_copy-rows

Conversation

@lr00rl

@lr00rl lr00rl commented Oct 23, 2026

Copy link
Copy Markdown
Owner

Summary

Work on sessions that live in more than one Claude Code account, plus the fixes from a code review of it.

Lists and plans

  • A session copied between accounts keeps its id, and the live-process map was keyed by id alone, so one running copy marked every account's copy as running. It is now keyed by account and id.
  • A copied or moved session's account reads talcum ← default in ccli sessions (JSON: copied_from).
  • The running and deleted marks stand one space apart from the id, so a double-click selects the id alone.
  • The last line of a copy, move or fork plan says Dry run, nothing was copied and is no longer dimmed.

Resume and fork

  • ccli resume <id> -a <account> brings that account's copy up to date by default. --no-sync restores the old check; --sync is still accepted. When a process runs the session, the error names the account to exit first.
  • ccli fork <id> -a <account> --turn N keeps the conversation through turn N of ccli log, in a new session. The cut must land on a line that starts a turn by the parser's own rule (transcript.StartsTurn); a fork through the last turn ends where the index stopped reading; the agent transcripts to leave out must all be in the session's folders; Fork re-checks all of it before writing. --turn 0 is refused. The plan and the result say that files on disk are not rolled back. -a is required; --to still works.

Write-path fixes from the review

  • checkParents now checks the account's own projects/ and file-history/ folders for symlinks.
  • Renames, links and new folders are synced to their directory, so a move never removes a source whose copy could be lost in a power cut.
  • A fast-forward copy re-checks the target just before its rename and leaves it alone if a process wrote to it meanwhile.
  • clean --undo no longer falls back to a rename that could replace a transcript written since the run.

Smaller

  • The quota window's usage includes web-search tokens and unlogged cost, like the other totals.
  • query.Service, which nothing used, is removed; DESIGN.md describes the real boundary and per-command flags.
  • The tutorial has a section on the pitfalls of switching accounts.

Deferred items are tracked in #19. The resume --best idea is #18.

Test plan

  • go vet ./..., gofmt -l ., go test ./..., and go test -race on transfer, index, store and query.
  • New tests: per-account liveness and copy source; resume syncing by default and refusing both sync flags; fork at a turn, through the last turn, at turn 0; cuts at a reply, a tool result and inside a line refused; a transcript replaced between plan and fork; a missing agent to leave out; a symlinked projects/ target (fails on the old code); a target changed during a copy; the exclusive copy for undo.
  • Real Claude Code resumed a fork cut at turn 1 of a two-turn session and recalled only turn 1.
  • Dry runs on the real index: sessions, fork --turn, fork --turn 0, resume -a while the session runs.

The plan prints a table whose ACTION column reads "create" or
"fast-forward", which looks like a result. The only hint that nothing
happened was a dimmed last line, and it was easy to miss: a copy ran
without --apply, and resuming the session in the target account then
failed with "No conversation found". The last line is no longer dimmed
and starts by saying nothing was copied, moved or forked.
A session copied between accounts keeps its id, and the live-process
map was keyed by id alone, so a process running the copy in one account
marked every account's copy as running. The map is now keyed by account
and id. The commands that must not touch a session running anywhere
(copy, move, hide, clean) keep their own checks across accounts.

The list also had no sign of the copy relationship that ccli show
prints. A copied or moved session's account now reads "talcum ←
default", from the newest copy or move recorded in the transfer ledger;
forks are left out because they get a new id. JSON rows carry it as
copied_from.

The running and deleted marks now stand one space apart from the id, so
a double-click in the terminal selects the id alone.

Tested: go test ./..., and on the real index: only the account whose
process runs 1087b979 shows it running, and its row reads
"talcum ← default".
…an account

`ccli fork <id> --turn N -a <account>` keeps the conversation through
turn N of `ccli log` and leaves the rest out, in a new session beside
the original. The cut is the byte offset of the next turn's prompt, taken
from the index and checked against the file: the byte before it must end
a line, and the line there must be a user message, where every turn
starts. Agent transcripts started after the turn are left out, by the
turn the index ties them to, or by start time when it could not place
them. Only the conversation goes back; the plan and the result both say
that files on disk stay as they are and the fork does not know the later
changes. Without --turn a fork keeps everything, as before. `-a` names
the account the fork goes into and is required; `--to` still works.

`ccli resume <id> -a <account>` now brings that account's copy up to
date from a newer one without --sync, which was the step people forgot.
It still only appends to a copy the newer one extends exactly, still
refuses diverged copies, and when a process runs the session it names
the account to exit first. `--no-sync` restores the old check; `--sync`
is accepted and is the default.

The tutorial gains the pitfalls of switching accounts: a copy planned
but not applied, switching while Claude Code runs, the account-less
resume line Claude Code prints on exit, and a fresh copy showing $0.

Tested: go test ./...; a real Claude Code resumed a fork cut at turn 1
of a two-turn session and recalled only turn 1; dry runs on the real
index and the demo home.
checkParents started its walk one level down, so the account's own
projects/ and file-history/ folders were taken on trust. A target whose
projects folder is a symlink (another disk, a mount) was written
through, which DESIGN.md says cannot happen. The walk now starts at the
first folder below the account.
…replace a target that changed

Only temporary files were synced. A rename into the target and a new
folder were not synced to their directory, and `move` then renamed the
source into the archive, so after a power cut the copy's rename could be
lost while the source's removal was kept, leaving the transcript in
neither account. Every rename, link and new folder is now synced to its
directory, and the archive move syncs both folders it touches.

A fast-forward copy wrote the whole transcript to a temporary file and
renamed it over the target. A Claude Code process that resumed the
session in the target meanwhile lost every line it appended. The copy
now checks, just before the rename, that the target is still the size
and age it was, and leaves it alone otherwise.
…through the last turn keeps

The review of --turn found four ways the cut could go quietly wrong.

The check on the cut accepted any user line, so a stale index could cut
at a tool result and leave a dangling tool call. The parser's turn rule
is now one function, transcript.StartsTurn, which the indexer and the
fork check both use; the fork passes it in, so transfer still imports
only account. Fork repeats the check just before it writes, against the
file as it is then.

Asking for the last turn read "no next turn" as "keep everything", so
lines written after the index ran, a new turn included, came along and
the plan said nothing. A fork through the last turn now ends where the
index stopped reading, and the plan says how much it leaves out.

The agents to leave out were trusted as given; a path that did not match
the walk would have been copied anyway. Every one must now be found in
the session's folders, at plan time and again before writing.

`--turn 0` was taken as "no cut" although `ccli log --all` can show a turn
0. It is now refused with what turn 0 is. `resume --sync --no-sync` is
refused instead of letting one win.
- fork's plan looked up its one session by loading every session of the
  account, and missed it when the session was hidden; it reads the one
  session now.
- The quota window's usage left out web-search tokens and the share of
  its cost that came from process totals (unlogged), which the session,
  account and overview totals include.
- In `sessions -v`, the account cell was dimmed inside a line that is
  dimmed as a whole, and its reset ended the dim early.
- The copy/move plan's last line had an empty past tense for any verb but
  copy and move.
- sessionModels' doc comment had ended up above copiedFrom.
- DESIGN.md listed `fork --to` without -a or --turn, and called --json,
  --no-index and --color global although they follow the command.
prepare returns *query.DB and commands call a dozen methods the
interface did not list, so the interface promised a boundary that did
not exist. The TUI already declares the narrower Source it reads
through, which is the pattern a future web server would follow too.
DESIGN.md now describes it that way.
Restore hard-links an archived transcript into place, which fails when a
file has appeared there. Where hard links do not work (another device, a
file system without them) it fell back to a rename, which replaces such a
file. The fallback now copies into a file it creates only if none exists,
syncs it, and removes the archived copy afterwards.
@lr00rl
lr00rl merged commit 58f1ff2 into main Oct 23, 2026
2 checks passed
@lr00rl
lr00rl deleted the fix/20261022_copy-rows branch October 23, 2026 09:34
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