Repository navigation
Purge Claude Code, Codex and Cursor native history and sweep orphans (OR-573) - #758
Conversation
A refused project delete (busy run artifacts, data-dir move, runs in flight, artifact cleanup failure, or contended native lock) no longer kills OpenCode history while the chat rows survive. The store now takes the native-deletion lock only after the run-artifacts guard and move check, then cleans artifacts, purges native sessions, and deletes rows, so the native lock is never held across the run-artifacts wait. Part of OR-569. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat and project deletes now remove the attachment files their messages showed once no remaining message shows them, skipping while a data-dir move is claimed. The periodic reclaim task also sweeps att-* files no message shows after a 24h settle, within the orphan-pass budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(OR-573) Chat and project deletes now remove the chat's sessions from orx's isolated Claude Code, Codex and Cursor homes, using every native id the chat recorded, and never touch the user's own homes. OpenCode deletes zero deleted content in place and checkpoint the WAL. The periodic reclaim task sweeps isolated sessions no chat records after a 24h settle (keeping Codex sub-agents of referenced threads), and VACUUMs orx's OpenCode database only while no OpenCode holds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The changes affect native chat history cleanup within the local chat workflow. The PR appears safe to merge with non-blocking follow-ups for cleanup completeness, freshness checks, Windows compaction, and scan limits. Findings
Fix with agent prompt### Issue 1
src/local/native_store.rs:242-245
The settle check reads only the top-level directory's modification time. Updating an existing file inside an old Cursor or Claude session directory does not refresh that time, so the sweep can remove history written within the last 24 hours. Check the newest descendant before removing a directory, and add a test with an old directory containing a fresh file.
### Issue 2
src/local/native_store.rs:207-210
Deleting a Codex chat removes only rollouts whose IDs appear directly in the chat records. Codex sub-agents have separate thread IDs and rollout files, so their transcripts remain after the parent chat is deleted. Expand the deleted IDs through `thread_spawn_edges`, while preserving descendants needed by surviving chats. The orphan sweep may remove these later, but only after they settle and the edges can be read.
### Issue 3
src/local/opencode_db.rs:326-327
On Windows, `compact` keeps its SQLite connection open while `ensure_no_external_users` tries to open the same file with `share_mode(0)`. That exclusive open conflicts with this process's own connection, so every attempt returns without running `VACUUM`. Close the connection before this check, then reopen it while holding the lease.
### Issue 4
src/local/native_store.rs:247-251
The five-second budget starts after `isolated_session_files` has walked every history tree and built the full map. Large histories therefore cause an unbounded scan while the data-directory gate is held, even when the deletion budget is zero. Start the deadline before discovery and make traversal stop or resume when it reaches that deadline.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryExtends native-history deletion to Claude Code, Codex, and Cursor. Adds periodic orphan cleanup, OpenCode database compaction, and stronger removal of deleted OpenCode text.
|
| let settled = |path: &PathBuf| { | ||
| std::fs::symlink_metadata(path) | ||
| .and_then(|metadata| metadata.modified()) | ||
| .is_ok_and(|at| at.elapsed().unwrap_or_default() >= settle) |
There was a problem hiding this comment.
Recent history is removed early
The settle check reads only the top-level directory's modification time. Updating an existing file inside an old Cursor or Claude session directory does not refresh that time, so the sweep can remove history written within the last 24 hours. Check the newest descendant before removing a directory, and add a test with an old directory containing a fresh file.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/local/native_store.rs
Line: 242-245
Comment:
**Recent history is removed early**
The settle check reads only the top-level directory's modification time. Updating an existing file inside an old Cursor or Claude session directory does not refresh that time, so the sweep can remove history written within the last 24 hours. Check the newest descendant before removing a directory, and add a test with an old directory containing a fresh file.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in fe1bc79: the settle check now uses newest_modified, the newest mtime anywhere under the path (no symlink following; any stat error counts as not settled, so the session is kept). Test: a Cursor chat dir with an old mtime but a fresh store.db survives the sweep.
| remove_isolated_sessions( | ||
| &crate::store::data_dir().join("agents"), | ||
| ids.difference(retained), | ||
| ) |
There was a problem hiding this comment.
Deleted chats leave child transcripts
Deleting a Codex chat removes only rollouts whose IDs appear directly in the chat records. Codex sub-agents have separate thread IDs and rollout files, so their transcripts remain after the parent chat is deleted. Expand the deleted IDs through thread_spawn_edges, while preserving descendants needed by surviving chats. The orphan sweep may remove these later, but only after they settle and the edges can be read.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/local/native_store.rs
Line: 207-210
Comment:
**Deleted chats leave child transcripts**
Deleting a Codex chat removes only rollouts whose IDs appear directly in the chat records. Codex sub-agents have separate thread IDs and rollout files, so their transcripts remain after the parent chat is deleted. Expand the deleted IDs through `thread_spawn_edges`, while preserving descendants needed by surviving chats. The orphan sweep may remove these later, but only after they settle and the edges can be read.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in fe1bc79: deleting a chat expands its ids through thread_spawn_edges and removes the spawn closure minus the closure of every id other chats still record (new Store::native_ids_outside, all harnesses). If the edges can't be read, nothing extra is removed and the sweep (which skips Codex while edges are unreadable) handles it later. Test: deleting parent 9 removes child 10 but keeps child 11, which a surviving chat records.
| if lock.try_lock().is_err() || ensure_no_external_users(&path).is_err() { | ||
| return Ok(()); |
There was a problem hiding this comment.
Windows skips database compaction
On Windows, compact keeps its SQLite connection open while ensure_no_external_users tries to open the same file with share_mode(0). That exclusive open conflicts with this process's own connection, so every attempt returns without running VACUUM. Close the connection before this check, then reopen it while holding the lease.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/local/opencode_db.rs
Line: 326-327
Comment:
**Windows skips database compaction**
On Windows, `compact` keeps its SQLite connection open while `ensure_no_external_users` tries to open the same file with `share_mode(0)`. That exclusive open conflicts with this process's own connection, so every attempt returns without running `VACUUM`. Close the connection before this check, then reopen it while holding the lease.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in fe1bc79: compact drops its connection before the .orx-lock try-lock and ensure_no_external_users, then reopens it under the lease for VACUUM. Its initial WAL checkpoint also uses a zero busy timeout now, so it never stalls OpenCode writers.
| let sessions = isolated_session_files(agents)?; | ||
| let started = std::time::Instant::now(); | ||
| for (id, paths) in sessions { | ||
| if started.elapsed() >= budget { | ||
| return Ok(true); |
There was a problem hiding this comment.
History scans exceed the budget
The five-second budget starts after isolated_session_files has walked every history tree and built the full map. Large histories therefore cause an unbounded scan while the data-directory gate is held, even when the deletion budget is zero. Start the deadline before discovery and make traversal stop or resume when it reaches that deadline.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/local/native_store.rs
Line: 247-251
Comment:
**History scans exceed the budget**
The five-second budget starts after `isolated_session_files` has walked every history tree and built the full map. Large histories therefore cause an unbounded scan while the data-directory gate is held, even when the deletion budget is zero. Start the deadline before discovery and make traversal stop or resume when it reaches that deadline.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in fe1bc79: the deadline is set before discovery. The sweep runs one target at a time (Claude, Codex, Cursor, OpenCode), rotated across passes by a counter, and checks the deadline before each target and after each removal. Discovery stops at target granularity: the most a pass overshoots is one target's walk. Partial discovery within a target isn't used, because a Claude session's settle check spans several trees and could otherwise judge a session from an incomplete set of paths. Each target removes at least one orphan before yielding, so a slow walk still makes progress, and an exhausted pass returns more for the 1s re-run. Test: a zero budget removes nothing.
# Conflicts: # src/store.rs
Attachment removal now holds a shared lock on the data-dir move lock, which a move takes exclusively before claiming its lease, so another dashboard cannot start copying mid-removal. Only removals count against the sweep budget, so a pass that walks many retained files still reaches later orphans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto myles/native-history-purge
…R-573) The sweep treats a session as settled only when its newest descendant is, starts its deadline before discovery and rotates which store goes first, with each store progressing at least one removal and failing on its own. Deleting a Codex chat also removes its sub-agent rollouts unless another chat still records them. OpenCode compaction closes its own connection before the Windows exclusive-open check and never waits on readers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # src/commands/up.rs # src/store.rs
Summary
Deleting a chat or project used to purge only OpenCode history. This PR covers the other harnesses and adds a periodic orphan sweep.
ChatHost::purge_native_history(_blocking)(renamed fromdelete_native_sessions) collects every native id the chat used:chat_sessions.native_session_idplus each message'sbase_/result_native_session_id. Ids shared with surviving chats are kept. Each harness group is scoped bynative_sessions_for_deletion(chat_ids, harnesses):delete_opencode_sessions). Claude/Codex/Cursor deletes no longer open the OpenCode databases.delete_isolated_sessionsremoves files from orx's isolated homes only. A home the user points at orx's (same_home, canonicalized) is never purged. The files are:agents/claude/projects/*/<id>.jsonl, the sibling<id>/(subagents, tool results),session-env/<id>andfile-history/<id>.agents/codex/{sessions,archived_sessions}/**/rollout-*-<id>.jsonl.agents/cursor/{chats/<md5>/<id>,acp-sessions/<id>}.PRAGMA secure_delete = ONand makes a non-blockingwal_checkpoint(TRUNCATE)attempt.Store::cleanup_orphaned_native_sessionsruns in the reclaim loop, under the data-dir gate and move-lease check.ORPHAN_PASS_BUDGET.thread_spawn_edges, recursive) are kept. If the edges can't be read, the Codex rollouts are skipped for that pass..orx-locklease is free (no orx OpenCode running) andlsoffinds no other user; otherwise it is retried on a later pass.idx_chat_messages_native_idskeeps the referenced-id scan offparts_json.Stacked on #755. Retarget to
mainafter it merges.Closes OR-573
Not covered / judgment calls:
state_5.sqlitethreadsrows (title/preview),history.jsonl/session_index.jsonl; Claudehistory.jsonl/todos/debug; OpenCodestorage/. These are not keyed cleanly by session or are owned by the agent's own DB.Testing
cargo fmt --all --check,cargo clippy --all-targets -- -D warnings,cargo build --locked,cargo test --lockedsecure_delete.agents/claude/projectsandagents/codex/sessions, and~/.claudeand~/.codexare unchanged.~/.claude/projectsremains.agents/opencode/opencode.db.🤖 Generated with Claude Code