Skip to content

Add ccli clean: find empty sessions and set them aside after asking - #17

Merged
lr00rl merged 6 commits into
mainfrom
feature/20261020_clean-empty
Oct 21, 2026
Merged

lr00rl merged 6 commits into
mainfrom
feature/20261020_clean-empty

Conversation

@lr00rl

@lr00rl lr00rl commented Oct 21, 2026

Copy link
Copy Markdown
Owner

ccli clean lists sessions that hold nothing and, once you agree, sets them aside. It does this without deleting anything, and --undo puts a run back.

Rules

  • Empty. A session counts as empty when it has no prompt, reply, tool call, tokens, cost, subagent, workflow run or name, is not running, and has been idle for at least --idle (1h). A name counts as content: a named Cursor chat whose messages ccli cannot read is not empty. Billed Cursor usage is read first and counts as content.
  • --unanswered. Adds sessions whose prompts never got a reply. It is opt in, because those hold text you typed.
  • Left alone. Sessions you hid or tagged, unused Cursor IDE drafts, and Codex, Pi or Cursor sessions with no recorded time.
  • No folder recorded. A transcript that names no folder takes the folder that other sessions under its project key recorded, so --dir finds it.

What it does

  • Asking first. It prints the plan and asks on a terminal. Without a terminal, or with --json, it changes nothing unless --apply is given. --no-index is refused.
  • Claude Code. A transcript moves to archive/clean/<stamp>/<account>/ with the path checks move uses, and leaves the index (store.Forget). A session whose transcript is already gone only leaves the index.
  • Codex, Pi and Cursor. These sessions are hidden, because ccli does not write their homes.
  • Checks before each change. Each session is checked again just before it changes. One that is running, moved or changed since the plan is left with a note.
  • Run record. The run claims its folder with an exclusive mkdir. It writes manifest.json before the first change and after each session, and stops cleanly on an interrupt.
  • Undo. --undo <stamp> works from the folder even without the record. It links transcripts back, so it never replaces one the tool wrote meanwhile, and leaves anything that is not a session in place. It unhides what the run hid and updates the index, even when one account fails.

Also in this PR

  • Screenshot script. Its exit trap ran under set -e, so it stopped before killing its tmux server and removing its temp folder; every run leaked both. It now cleans up.
  • Moves across devices. A file copied because a rename could not cross devices now keeps its mode and time. This applies to move too.
  • Demo data. The demo home gains four empty sessions, written after the others so the ids in the docs stay the same. The README, tutorial and DESIGN describe clean, with a screenshot.
  • Cleanup. A stray empty internal/store/tags.go.new is removed.

Test plan

  • go vet ./... and go test -race ./...
  • End to end through Main on a temporary home: dry run, --unanswered, --dir, a tagged session, apply, undo with the record removed, a transcript written to between the plan and the apply, an interrupted run, and --no-index
  • transfer: archive and restore round trip, a conflict, a foreign file beside a session, and a linked project folder. store: Forget removes one session in one account only.
  • Demo home: plan, apply and undo, after which the archive folder is gone
  • Dry run on real data (Claude Code, Codex, Pi): 4 Claude Code archives and 9 Codex hides, or 48 sessions with --unanswered. Nothing was changed.

ccli clean lists sessions that hold nothing: no prompt, reply, tool call,
tokens, cost, subagent, workflow run or name, not running, and idle for at
least --idle (1h). A name counts as content, since a named Cursor chat whose
messages ccli cannot read is not empty. --unanswered adds sessions whose
prompts never got a reply; it is opt in because those hold typed text.

The plan is printed, and on a terminal ccli asks before changing anything;
without one, or with --json, nothing changes unless --apply is given. A
Claude Code transcript moves to archive/clean/<stamp>/<account>/ with the
path checks move uses and leaves the index (store.Forget); one whose
transcript is already gone only leaves the index. Codex, Pi and Cursor
sessions are hidden, since ccli does not write their homes. Each session is
checked again just before it changes: running, moved or changed since the
plan means it is left with a note. A manifest beside the archive lets
--undo <stamp> move the transcripts back and unhide what was hidden.

A transcript that names no folder takes the folder its project key's other
sessions recorded, so --dir finds it.

Tested: plan, --unanswered, --dir, apply, undo and a transcript written to
between plan and apply, end to end through Main on a temporary home; a dry
run on real data plans 4 Claude Code archives and 9 Codex hides.
The exit trap ran under set -e, and wait on the killed sleep returns 143,
so the trap stopped before it killed the private tmux server and removed
the temp folder. Every run left a tmux server with a ccli view in it and a
demo home behind. The cleanup now runs with set +e.
The demo home gains two sessions that stopped at their first metadata
line, one whose prompt got no reply and an empty Codex thread, written
after every other session so the ids in the docs stay the same. The README,
tutorial and DESIGN.md describe clean and its rules; the projects sample and
image change with the demo home.
From a review of clean:

- The run's record was written only at the end, so an interrupt, a crash
  or a failed session could leave files in the archive that --undo did not
  know about. The run now claims its folder with an exclusive mkdir (which
  also refuses a second run in the same second), writes its record before
  the first change and after each session, and stops at an interrupt. Undo
  works from the folder, with or without the record.
- Restore failed on any file it did not expect, such as a .DS_Store, and
  then skipped the remaining accounts, the unhide and the index update. It
  now leaves such files where they are, and undo always unhides and updates
  the index. A transcript is linked back into place, so one the tool wrote
  meanwhile is never replaced, and a linked project folder is refused.
- A moved file kept neither its mode nor its time across devices; it now
  keeps both, for move too.
- Tagged and hidden sessions are left alone, a Codex, Pi or Cursor session
  with no recorded time is not taken as idle, billed Cursor usage is read
  before deciding, and --no-index is refused.
- --json prints one shape, {sessions, undo}, with undo only when there is
  something to undo, and an account that no longer exists is reported as
  such on undo.
@lr00rl
lr00rl merged commit e5a74a8 into main Oct 21, 2026
2 checks passed
@lr00rl
lr00rl deleted the feature/20261020_clean-empty branch October 21, 2026 03:40
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