Skip to content

Purge Claude Code, Codex and Cursor native history and sweep orphans (OR-573) - #758

Merged
myles332 merged 8 commits into
mainfrom
myles/native-history-purge
Oct 23, 2026
Merged

myles332 merged 8 commits into
mainfrom
myles/native-history-purge

Conversation

@myles332

Copy link
Copy Markdown
Contributor

Summary

Deleting a chat or project used to purge only OpenCode history. This PR covers the other harnesses and adds a periodic orphan sweep.

  • Purge on delete: ChatHost::purge_native_history(_blocking) (renamed from delete_native_sessions) collects every native id the chat used: chat_sessions.native_session_id plus each message's base_/result_native_session_id. Ids shared with surviving chats are kept. Each harness group is scoped by native_sessions_for_deletion(chat_ids, harnesses):
    • OpenCode: unchanged (delete_opencode_sessions). Claude/Codex/Cursor deletes no longer open the OpenCode databases.
    • Claude Code, Codex, Cursor: delete_isolated_sessions removes files from orx's isolated homes only. A home the user points at orx's (same_home, canonicalized) is never purged. The files are:
      • Claude Code: agents/claude/projects/*/<id>.jsonl, the sibling <id>/ (subagents, tool results), session-env/<id> and file-history/<id>.
      • Codex: agents/codex/{sessions,archived_sessions}/**/rollout-*-<id>.jsonl.
      • Cursor: agents/cursor/{chats/<md5>/<id>,acp-sessions/<id>}.
    • Imported chats resume from the user's legacy homes and are never touched.
    • ACP agents keep history in homes orx doesn't own, so they are left alone.
  • Plaintext: the OpenCode purge sets PRAGMA secure_delete = ON and makes a non-blocking wal_checkpoint(TRUNCATE) attempt.
  • Periodic sweep: Store::cleanup_orphaned_native_sessions runs in the reclaim loop, under the data-dir gate and move-lease check.
    • It removes isolated sessions that no chat row or message records and that are untouched for 24h, within ORPHAN_PASS_BUDGET.
    • Codex sub-agents of referenced threads (thread_spawn_edges, recursive) are kept. If the edges can't be read, the Codex rollouts are skipped for that pass.
    • It then handles orx's OpenCode DB: unreferenced settled root sessions are deleted along with their children, the WAL is checkpointed, and the DB is VACUUMed once free pages reach 10%. The VACUUM happens only when an exclusive .orx-lock lease is free (no orx OpenCode running) and lsof finds no other user; otherwise it is retried on a later pass.
    • File leftovers are swept before OpenCode, so an unhealthy DB never holds them back. Repeated sweep errors are logged once.
  • A new covering index idx_chat_messages_native_ids keeps the referenced-id scan off parts_json.

Stacked on #755. Retarget to main after it merges.

Closes OR-573

Not covered / judgment calls:

  • Not purged: Codex state_5.sqlite threads rows (title/preview), history.jsonl/session_index.jsonl; Claude history.jsonl/todos/debug; OpenCode storage/. These are not keyed cleanly by session or are owned by the agent's own DB.
  • VACUUM vs chat start: while VACUUM holds the exclusive lease, an OpenCode V1 chat start fails instead of waiting. V2 waits.
  • Legacy OpenCode DB: the purge still deletes from the user's legacy DB (pre-existing; OpenCode chats can't be imported). Only orx's isolated DB is compacted.

Testing

  • cargo fmt --all --check, cargo clippy --all-targets -- -D warnings, cargo build --locked, cargo test --locked
  • New tests:
    • Isolated purge by id covers Claude transcript, subagents and session-env, and the Cursor chat.
    • The sweep keeps referenced, fresh, spawned-sub-agent and non-id files, and removes nothing when the budget is exhausted.
    • The OpenCode sweep deletes the settled unreferenced root and its child, keeps referenced and fresh roots, and shrinks the file.
    • Deleted OpenCode text is absent from the DB file and the WAL. The test fails without secure_delete.
    • Harness scoping excludes other harnesses' shared ids.
  • Manual: delete a Claude Code chat and a Codex chat. Their transcripts disappear from agents/claude/projects and agents/codex/sessions, and ~/.claude and ~/.codex are unchanged.
  • Manual: delete an imported Claude chat. The original in ~/.claude/projects remains.
  • Manual: with OpenCode idle after deleting chats, the next reclaim pass shrinks agents/opencode/opencode.db.

🤖 Generated with Claude Code

myles332 and others added 3 commits October 22, 2026 14:42
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>
@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[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

  1. P2 Recent history is removed early ▶
  2. P2 Deleted chats leave child transcripts ▶
  3. P2 Windows skips database compaction ▶
  4. P2 History scans exceed the budget ▶
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.

Summary

Extends native-history deletion to Claude Code, Codex, and Cursor. Adds periodic orphan cleanup, OpenCode database compaction, and stronger removal of deleted OpenCode text.

  • Deleting a chat removes its recorded history from each supported harness.
  • Old native history without chat records is swept during reclaim passes.
  • OpenCode deletion clears stored text and can compact idle databases.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Delete chat or project] --> B[Collect deleted and retained native IDs]
    B --> C[Delete OpenCode rows]
    B --> D[Remove isolated history files]
    E[Periodic reclaim loop] --> F[Read referenced native IDs]
    F --> G[Keep referenced Codex descendants]
    G --> H[Sweep settled isolated files]
    H --> I[Delete settled OpenCode roots]
    I --> J[Checkpoint and compact when idle]
Loading

Reviews (1) · Last reviewed commit: "Purge Claude Code, Codex and Cursor nati..." · Reviewed by Greptile

Comment thread src/local/native_store.rs Outdated
Comment on lines +242 to +245
let settled = |path: &PathBuf| {
std::fs::symlink_metadata(path)
.and_then(|metadata| metadata.modified())
.is_ok_and(|at| at.elapsed().unwrap_or_default() >= settle)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/local/native_store.rs Outdated
Comment on lines +207 to +210
remove_isolated_sessions(
&crate::store::data_dir().join("agents"),
ids.difference(retained),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/local/opencode_db.rs
Comment on lines +326 to +327
if lock.try_lock().is_err() || ensure_no_external_users(&path).is_err() {
return Ok(());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/local/native_store.rs Outdated
Comment on lines +247 to +251
let sessions = isolated_session_files(agents)?;
let started = std::time::Instant::now();
for (id, paths) in sessions {
if started.elapsed() >= budget {
return Ok(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

myles332 and others added 5 commits October 22, 2026 17:47
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>
…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
@myles332
myles332 changed the base branch from myles/chat-attachment-cleanup to main October 23, 2026 01:40
@myles332
myles332 merged commit 0fbfc74 into main Oct 23, 2026
28 of 33 checks passed
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