acpmux: a chat keeps its Claude transcript past Claude's retention cleanup
Summary
Chats resumed after a while came back without their history: the agent started a fresh conversation with only the chat's plain text (at most 24,000 characters, no tool calls or results) and forgot what it had read and done. Claude Code deletes transcripts past its retention (cleanupPeriodDays), so --resume found nothing. The ~1-day loss on the reporting machine came from OptChat Chief: its turn and subagent settings carry cleanupPeriodDays: 2, and Claude's cleanup sweeps every project of the store it runs on (see the 2026-10-24 hold comment; that root cause is a separate Chief fix). Claude's own default is 30 days, and this PR keeps a chat's conversation past either. acpmux also refused to resume any chat whose first turn never finished (an app update mid-turn), even when Claude had written that turn to disk.
Each chat now keeps a hard link to its Claude transcript in its own session folder (~/.acpmux/sessions/<chat>/claude/<project>/<id>.jsonl), taken at each turn's end. Before a start resumes or forks a conversation, a transcript Claude deleted is linked back into Claude's store and its time is set to now, so the resuming process's own cleanup can't take it. A Claude that recreated the file after a deletion has its new lines merged onto the kept transcript. A link costs no copy and sees every line Claude appends; across volumes it falls back to a copy. Claude's companion folder (<id>/: tool results, subagent transcripts) is kept the same way, file by file, and every resume puts back whatever of it Claude's cleanup took (Claude's own newer files win; a file Claude wrote again never cuts its kept copy short). Deleting the chat deletes its folder and the transcript with it.
Chief sessions (turns, compactors, subagents: cmux.chief tag, optchat- name or optchat- preset) keep nothing, so Chief's own short retention still applies to them. Those sessions are never deleted, and keeping their transcripts would keep them for good (the regression the hold found).
The first-turn refusal now resumes when Claude stored a prompt for the conversation, and still starts fresh when it did not.
Covered entry points: a cold start (resume and --fork-session), a pooled agent, an agent adopted from its host after a restart, and a fork through an earlier turn (the parent's kept copy is found in its folder).
Testing
Eight regression tests in cmux-tui/crates/acpmux/src/hub/claude_keep.rs, run with ./scripts/verify-cmux-tui-hosted.sh --filter claude_keep:
-
Base with stubs (9b91d6fd2e7f7): 4 of 5 failed (
a_transcript_claude_deleted_is_put_back_whole,a_transcript_put_back_is_not_past_the_retention,lines_written_after_a_deletion_join_the_kept_transcript,a_first_turn_cut_short_resumes_what_claude_stored); the fifth asserts nothing is restored, which a stub also satisfies. https://github.com/manaflow-ai/cmux/actions/runs/40064180785 -
Head (bbb952c52dcc6): Rust tests pass (Linux, filtered to
claude_keep, 8 tests). https://github.com/manaflow-ai/cmux/actions/runs/40066863762 -
Head (9a76842e5c2fd), after merging the latest feat-cmux-next and the review fixes: Rust tests pass (Linux, filtered to
claude_keep, 10 tests, including the newa_first_line_still_being_written_is_left_alone). https://github.com/manaflow-ai/cmux/actions/runs/40249428388
The hosted lint job is red on files this PR does not touch (hub/dispatch.rs, server/requests.rs, cmux-link), on both the red and the fix commits.
- Head 688b6fd (narrowing, review fixes, latest feat-cmux-next merged):
./scripts/verify-cmux-tui-hosted.sh --filter claude_keep, 14 tests including the newthe_companion_folder_is_put_back_with_the_transcript,the_companion_folder_comes_back_when_claude_wrote_its_transcript_again,a_companion_file_claude_wrote_again_does_not_cut_the_kept_oneanda_chief_session_keeps_no_transcript. Each companion test fails on the commit before its fix (traced by the reviewer; the companion files are missing or the kept one is cut short).
Observed / Not run
Observed: the reporting chat's acpmux log shows resume_failed: Claude conversation … never finished a turn, so a fresh one starts at 04:35:09Z, after ~/.claude/.last-cleanup at 04:34:38Z; neither that conversation's transcript nor another chat's (resumed exact three times the day before) exists anywhere under the home folder. The chat pane's history paging was replayed against the daemon and loads every turn, so the pane is not involved.
Not run: a tagged build dogfood of a resume after a real retention cleanup, and the macOS test job (it stops at the base lint failure). Transcripts Claude already deleted before this change cannot be recovered.
Review: an independent correctness subagent reviewed two rounds; its findings (pooled and adopted agents, stale times, prompt-less files, keep races, forks of old chats, deleted chats, forks keeping the parent's transcript) are fixed in d380872d65b33 and 90ec2b1438670. The hosted run also caught a lock guard held across an await in the pooled path, fixed in bbb952c52dcc6.
Changelog
Fixed: A chat resumed after a day or more keeps its whole conversation instead of starting over with only its text
Proof
No capture: daemon-side change with no UI.
🤖 Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes chats losing their Claude transcript after Claude's retention cleanup, so a resume after a day or more keeps the full conversation instead of starting fresh with only the chat's plain text.
Claude Code deletes transcripts past cleanupPeriodDays (about a day), leaving --resume with nothing to load. Each chat now keeps a hard link to its transcript in its own session folder, refreshed at each turn's end. Before a start resumes or forks, a deleted transcript is linked back into Claude's store with a fresh timestamp so the resuming process's own cleanup can't remove it. If Claude recreated the file after a deletion, its new lines are merged onto the kept transcript without loss or duplication; a half-written first line is left alone. A first turn cut short by an app update now resumes when Claude stored the prompt, instead of starting over.
- The companion folder (tool results, subagent transcripts) is kept the same way and restored with the transcript; Claude's own newer files win.
- Chief sessions keep no transcript, so Chief's own short retention still applies.
- Transcripts from pooled agents, agents adopted after a restart, and forks of older chats are kept and restored too.
- Deleting a chat removes its kept transcript with it.
- Transcripts Claude already deleted before this change can't be recovered.