Skip to content

acpmux: a chat keeps its Claude transcript past Claude's retention cleanup - #24802

Open
austinywang wants to merge 23 commits into
feat-cmux-nextfrom
feat-cmux-next-chat-resume-full-history
Open

austinywang wants to merge 23 commits into
feat-cmux-nextfrom
feat-cmux-next-chat-resume-full-history

Conversation

@austinywang

@austinywang austinywang commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

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:

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 new the_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_one and a_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 d380872 and 90ec2b1. The hosted run also caught a lock guard held across an await in the pooled path, fixed in bbb952c.

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 Autofix with [code]smith
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.

Written for commit 599195a. Summary will update on new commits.

View guided diff Turn on auto-fix

austinywang and others added 5 commits October 23, 2026 21:52
…tion cleanup

Red: keep and restore do nothing yet, and a first turn cut short is still
refused even when Claude stored it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eanup

Claude Code deletes transcripts past cleanupPeriodDays (here about a day).
A chat resumed after that found nothing to --resume and started a fresh
conversation with only its plain text: the agent lost every tool call and
what it had read. Each chat now holds a hard link to its transcript in its
own session folder, kept at each turn's end, and links it back (with a fresh
time) before a start resumes it. A first turn cut short (an app update
mid-turn) now resumes what Claude stored instead of starting over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… chats

Review fixes:
- Pooled agents and agents adopted from their host after a restart note
  their Claude store too, so their turns keep the transcript.
- A transcript still in the store is made new before a resume, so the
  resuming process's own cleanup cannot take it.
- A first turn cut short resumes only when Claude stored a prompt.
- Keeps run one at a time and read a recreated file to its end before the
  rename; lines the kept file already holds are not appended again.
- A fork (and a fork through a turn) puts back the transcript the chat it
  forks kept.
- A chat deleted meanwhile is not given its folder back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After putting back the transcript a fork starts from, only the chat's own
conversation is kept in its folder: the parent's goes with the parent. A
keep that finds the kept file already holds Claude's lines points Claude's
name at it again instead of leaving later lines unkept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guard was still in scope at the await, so every future that pools a
chat stopped being Send.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@austinywang austinywang added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 24, 2026
@austinywang
austinywang temporarily deployed to cmux-tui-merge-tree October 24, 2026 05:53 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review e105ab9: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr24802 -f ref=feat-cmux-next-chat-resume-full-history -f platform=macos -f backend_mode=local

web and size checks: green

@austinywang

Copy link
Copy Markdown
Contributor Author

Correctness review (subagent, two rounds) and resolution

Round 1 on da660e3: nothing blocking; six should-fix findings, all fixed in d380872:

  1. Pooled agents and agents adopted from their host never noted their Claude store, so their turns kept nothing. Fixed: note_claude_store in pool/mod.rs and hosts.rs.
  2. A transcript still in the store but past retention was not made new before a resume. Fixed: restore sets the time to now on both paths (test a_stored_transcript_past_the_retention_is_made_new_before_a_resume).
  3. The relaxed first-turn refusal accepted a file with no prompt, which --resume cannot load. Fixed: stored_with_prompt via fork_at::is_prompt (refusal test covers both).
  4. Merge races: concurrent keeps could duplicate a tail or lose lines written mid-merge. Fixed: process-wide KEEPING lock, read-to-end loop before the rename, holds_line guard.
  5. Forks of chats older than retention failed. Fixed: restore searches sibling session folders; cold_start restores for fork_from; fork.rs restores before claude_transcript_through (test a_fork_puts_back_the_transcript_another_chat_kept).
  6. A keep landing after a chat's delete recreated its folder. Fixed: keep returns when the session folder is gone (test a_deleted_chat_keeps_nothing).

Round 2 on d380872: all six confirmed fixed, no deadlock (the two lock takes are sequential, never across an await). New should-fix: a fork kept a link to its parent's transcript in its own folder. Fixed in 90ec2b1 (keep after restore only for the chat's own conversation); the relink nit in the already-merged case is fixed there too.

Hosted run on d380872 found a std MutexGuard in scope across an await in the pooled path (not Send). Fixed in bbb952c; the run on that head passes the Rust tests: https://github.com/manaflow-ai/cmux/actions/runs/40066863762

Left as accepted nits: a few-syscall window between the last read and the rename in the merge; the sibling scan costs one read_dir per chat on a start whose transcript is gone and was never kept; kept transcripts live as long as their chat.

Own verdict on bbb952c: diff read in full, the pool change only scopes the guard; lint red is base-only (hub/dispatch.rs, server/requests.rs, cmux-link, the same on the red commit). SHIP.

@austinywang austinywang added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Oct 24, 2026
@github-actions github-actions Bot added risk:high Risk tier (docs/ci/risk-tiers.md) and removed risk:normal Risk tier (docs/ci/risk-tiers.md) labels Oct 24, 2026
@blacksmith-sh

This comment has been minimized.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Bead cx-q2sx7 created for this PR by the cmux coordinator.

@austinywang austinywang removed the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Oct 24, 2026
@austinywang
austinywang marked this pull request as draft October 24, 2026 06:31
@austinywang

Copy link
Copy Markdown
Contributor Author

Held: root cause corrected, and a regression found

The root cause in this description is wrong. Claude Code's own retention here is its 30-day default: an isolated sweep deleted only a 40-day-old probe and kept the 20-, 8-, 3-, 1.5- and 0.5-day ones. The deleter is OptChat Chief. Its turn and subagent project settings carry cleanupPeriodDays: 2, and Claude Code's cleanup sweeps every project of the configuration it runs on. Chief turns on claude-sr and Chief subagents run on ~/.claude (their transcripts are under ~/.claude/projects/*-optchat-session and *-optchat-subagent), so each one swept the person's own chat transcripts.

Reproduced: a Claude started in a folder whose project settings say 2 deleted the 3-, 8-, 20- and 40-day probes of another project, transcripts and companion folders alike. The root-cause fix is a separate Chief PR.

Regressions this PR would add, as it stands:

  • Chief turn sessions are acpmux sessions that are never deleted (1,412 in the nightly's Chief home since Oct 16). Each would hard-link its Claude turn transcript until the session is deleted, which undoes Chief's 2-day transcript retention, on disk and for privacy.
  • Claude's cleanup also deletes the session's companion folder (<id>/, which holds tool results and subagent transcripts). This PR keeps only the .jsonl, so a restored conversation can point at tool-result files that are gone.
  • Claude's 30-day sweep no longer frees space for any chat that is still listed.

What was verified with the real Claude CLI: a hard link survives Claude's appends (same inode across turns). With Claude's copy deleted, --resume fails with "No conversation found". After the link is put back, the resumed conversation remembers its first turn.

Converted to draft. With the Chief fix in, this PR only helps chats idle for more than 30 days, so it needs narrowing (no preset or Chief sessions, plus the companion folder) before it is worth its cost.

@austinywang
austinywang marked this pull request as ready for review October 26, 2026 00:09
…still writing

Review findings on #24802: keep the old file open across the merge's rename
so a line appended after the last read still joins the kept transcript; leave
a recreated file alone while its first line is half written; never make a
deleted chat's folder again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@austinywang
austinywang temporarily deployed to cmux-tui-merge-tree October 26, 2026 00:16 — with GitHub Actions Inactive
…s, on any volume

After the merge's rename, append what the old file still held through Claude's
name, so a copy on another volume gets it too; the kept file when Claude's name
was deleted again meanwhile. Review follow-up on #24802.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@austinywang
austinywang temporarily deployed to cmux-tui-merge-tree October 26, 2026 00:25 — with GitHub Actions Inactive
@austinywang

Copy link
Copy Markdown
Contributor Author

Review (claude-subagent): SHIP at 9a76842

Independent correctness review (read-only subagent), two passes: d8ef448 then 9a76842.

Fixed in this PR

  • Crash ratchet: the three claude_store.lock().unwrap() calls now use lock_or_recover() (d8ef448). Local scripts/cmux-next/check-crash-safety.sh is ok.
  • Merge race: a line Claude appends between the merge's last read and the rename was lost. The old file now stays open across the rename and the rest is appended through Claude's name, so it also reaches the copy on another volume. If Claude's name is gone again, the rest goes to the kept file (508d74b, 9a76842).
  • Half-written first line: a recreated file whose first line had no newline yet could match a kept line and be replaced, which corrupted the transcript. keep now waits for the next turn. Regression test: a_first_line_still_being_written_is_left_alone (508d74b).
  • Deleted chat: create_dir_all could recreate the folder of a chat deleted while its keep waited, leaving an orphan transcript. The folder levels are now made one at a time below the chat's folder, and a NotFound keeps nothing (508d74b).

Advisory, not fixed

  • A late line caught by the post-rename read can land after one Claude writes at the same instant (the window is narrowed, not closed; records chain by parentUuid).
  • No test of copy mode across volumes, or of the late-line path. a_chat_deleted_before_its_keep_gets_no_folder_back checks make_dir only, not the race itself.
  • In copy mode the whole transcript is copied at each turn end under a process-wide lock, and restore scans every chat folder when the transcript was never kept. Both cost time, not correctness.
  • A preset that sets CLAUDE_CONFIG_DIR would point keep and restore at the profile's store. The resume-refusal check already had this.

Also merged the latest feat-cmux-next into the branch (a merge, no force push). The remaining reds (clippy E0308 in prompt_images.rs, queue.rs and queue_edit.rs, the Rust panic ratchet in untouched crates, the live conformance check and nightly-guards) fail on files this PR does not touch.

@austinywang austinywang added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Oct 26, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator

For awareness: this conflicts with feat-cmux-next (c5af877) in one hunk, cmux-tui/crates/acpmux/src/hub/pool/mod.rs around line 853 (the pooled Claude start). Base now sets the session id through m.set_agent_session_id(state.session_id, "claude") (ebce631 / #27082 era) instead of m.agent_session_id = state.session_id. The resolution looks like base's block (with set_agent_session_id) plus this PR's self.note_claude_store(session).await; after the lock is dropped. The other ten files merge cleanly.

Failing checks at e105ab9: clippy (changed cmux-tui packages), seven-language live conformance and the build musl/darwin jobs (the last three failed fetching the ghostty-next Zig packages, which looks like infra). Once the base merge is in, a fresh run should show whether the clippy red is this PR's own.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Review (codex gpt-6-astra high, fast): BLOCK at e105ab9

This is for awareness; nothing was pushed here. The PR also still conflicts with feat-cmux-next in hub/pool/mod.rs (the hunk noted in the earlier comment), so GitHub can't land it as it stands. The root-cause half is now cmux#27978: Chief project settings no longer set cleanupPeriodDays, so Chief turns stop sweeping other projects of ~/.claude.

The reviewer's findings at this head. I spot-checked 1 and 2 in claude_keep.rs.

  1. Blocking. A fork or resume of a recreated transcript loses the earlier history (claude_keep.rs restore ~290-315, lifecycle.rs ~727-733). Claude's cleanup deletes <sid>.jsonl, and a still-running Claude then writes a new file with only the newer lines. restore takes the Some(live) branch and leaves that file as it is. Only the mtime is reset, and mirror(.., refresh=false) keeps the suffix. The fork start passes own = false, so the following keep() never folds the kept copy back in. --resume --fork-session then starts from the suffix alone. Suggested fix: fold the kept copy into an existing live file before any resume or fork, companion files included, and add a lifecycle-level fork test.
  2. Blocking. The first-record heuristic can drop lines (keep ~166-175). If the recreated file's first record equals the kept file's first record, replace(&live, &kept) makes Claude's shorter file the whole transcript. If holds_line(&kept, &first) matches, replace(&kept, &live) drops any later new lines in Claude's file. Suggested fix: compare by record identity and append only unseen records, instead of deciding on the first line alone.
  3. Blocking (per the reviewer). The keep is fire-and-forget (turns.rs ~786, claude_keep.rs ~48-60). The turn settles after queuing a spawn_blocking. A daemon shutdown, or a sweep, before that task runs leaves no kept copy. Suggested fix: await the keep, still off the runtime thread.
  4. Blocking (per the reviewer). Companion files copied across volumes can be torn (~231-235). fs::copy writes straight into the kept destination while Claude may still append. If the source is swept later, the truncated copy is all that remains. Suggested fix: copy to a temp file, then rename it into place.
  5. Should-fix. note_claude_store() (claude_keep.rs ~35-43, hosts.rs ~477-488, pool/mod.rs ~850-860) rebuilds the store from the base profile env only. A preset- or route-resolved CLAUDE_CONFIG_DIR is missed, so adopted or pooled agents on a custom store keep nothing.
  6. Should-fix. stored_with_prompt() (~118-121, lifecycle.rs ~741-744) parses a whole JSONL transcript synchronously on the async cold-start path.
  7. Should-fix. Chief classification is checked only when a keep is queued. A chat tagged cmux.chief later keeps its existing copy under ~/.acpmux/.../claude indefinitely.

Checks at this head, for when it is updated: the failing jobs are clippy (changed cmux-tui packages), seven-language live conformance, the musl/darwin build jobs, the fleet compiles and fast tier, and nightly-guards release-notary/Guard status. They need comparing against base, test by test, after the base update.

Resolves hub/pool/mod.rs: base's set_agent_session_id(.., "claude")
keeps the id history, and this branch's note_claude_store still runs
after the lock is released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@austinywang

Copy link
Copy Markdown
Contributor Author

Sweep fix. Blocker: conflict in cmux-tui/crates/acpmux/src/hub/pool/mod.rs (1780 commits behind). Merged origin/feat-cmux-next (new head 599195a7feb80ee656274f6efdc93b35c3044009). Resolution uses base's m.set_agent_session_id(state.session_id, "claude"), which keeps prior Claude ids, inside this PR's scoped lock, then still calls self.note_claude_store(session).await after the lock drops. Not built locally (no cargo on this Mac). The head needs CI and a review receipt before relabel.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Independent review for awareness (codex gpt-6-astra, high, fast; read-only, no builds), at head 599195a7feb80ee656274f6efdc93b35c3044009 against the merge base with current feat-cmux-next:

  • P2 — Interrupted merge retries discard new records. In claude_keep.rs:171, finding the first live record in the kept file triggers wholesale replacement. If merging B into kept A succeeds but staging/renaming fails, then Claude appends C, the retry replaces live B,C with kept A,B, permanently losing C. Reconcile the remaining suffix before replacing.

  • P2 — Pooled/adopted agents can preserve from the wrong store. claude_keep.rs:35 reads the raw profile environment. Actual spawning also applies defaults, preset/session environment, and template expansion. A pooled chat whose preset supplies CLAUDE_CONFIG_DIR searches the default store instead, silently retaining nothing. Record the store from the effective spawn environment.

Static review only; no modifications, builds, or tests.

VERDICT: BLOCK at 599195a — Merge retries can lose transcript records, and pooled/adopted sessions can miss retention entirely.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

For awareness: this PR conflicts with feat-cmux-next, so it can't land until it's rebased or base is merged in. Its current review verdict stays as is; a hand-resolved merge gets a fresh review at the new head.

This branch had an error being deployed

1 failed (outdated) and 3 inactive deployments
artifacts — 599195a7 Deployed Oct 30, 2026 by austinywang via add the Windows daemon to the tree #85585
sentry-dsyms — 599195a7 Deployed Oct 30, 2026 by austinywang via upload cmux-tui dSYMs to Sentry #85585
ci-routing-agent — 599195a7 Deployed Oct 30, 2026 by austinywang via cmux-tui tree preflight #85585
cmux-tui-merge-tree — 136313ae Deployed Oct 26, 2026 by austinywang via publish the merge tree #6661
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants