fix(sandbox): purge per-session _cache/<id> at delete time, not only via the reaper - #2414
Conversation
…via the reaper Commit db1ebfa (#2347) added the per-session /root/.cache bind-mount source — <workspace_root>/_cache/<session_id>, built by the same _per_session_child helper as /tmp — together with the asynchronous _reap_session_cache reaper, but, unlike af52d8a did for /tmp, omitted the matching (session_cache_dir(session_id), ...) entry from purge_session_directories. So a deleted session's _tmp/<id> was removed the moment its row was gone, while _cache/<id> survived on disk until the periodic host_dir_reaper aged it out. Under the documented P1 disk-fill override (host_dir_reaper_enabled=false) the reaper short-circuits to a no-op, and since the synchronous purge never gated on the kill-switch, _cache had zero cleanup and accumulated indefinitely — the exact warm-cache workload db1ebfa names as the regrowth driver. Register session_cache_dir(session_id) in the purge_session_directories candidate tuple, restoring the symmetry with session_tmp_dir. The directory is per-session and never shared (only the owner session mounts it), so it satisfies the same _purge_target_if_owned proof the _tmp entry already passes; the ownership guard's refusals (sibling dirs, shared roots, out-of-jail, the account root) are unchanged. Tests: - test_purge_session_directories_jail.py: _session_owned_dirs now lists all six per-session dirs (workspace, uploads, attachments, repos, _tmp, _cache) — closing a blind spot where the suite omitted BOTH mounts — and the legitimate-purge, per-candidate-reclaim, and shared-root-refusal cases now cover _tmp and _cache, with sibling-session bystanders proving scoping. - test_delete_session_purges_cache_dir.py: drives the real delete_session service against a migrated Postgres testcontainer; asserts _cache/<id> (and _tmp/<id>) are gone the moment delete_session returns, with host_dir_reaper_enabled both true and false. Co-Authored-By: Detail
Code reviewVerdict: pass — head What the change does
(session_cache_dir(session_id), (session_cache_dir(session_id),)),The claim in the PR body is accurate and I verified each link in it against the tree:
Verification I ran (scoped to the changed behavior)
The strengthened unit fixture is a real improvement, not padding: Not fully evaluated (declared, not treated as green)
CI's Notes (non-blocking, no action required)
No blocking issues. The diff is minimal, symmetric with the existing |
|
@cursor update to latest master |
|
This branch is already on latest
Nothing new to merge or rebase. The only commits on this branch that are not on |
…on-cache-id-at-delete-t-4ca1fd
Code reviewVerdict: pass — head What the change does
(session_cache_dir(session_id), (session_cache_dir(session_id),)),Verified against the tree rather than taken from the PR body:
Verification run (scoped to the changed behavior)
Test qualityThe strengthened unit fixture is substantive, not padding. Notes (non-blocking, no action required)
No blocking issues. Minimal diff, symmetric with the existing |
…on-cache-id-at-delete-t-4ca1fd
Code reviewVerdict: pass — head This head is a second What the change does
(session_cache_dir(session_id), (session_cache_dir(session_id),)),Verified against this tree, not taken from the PR body:
Verification run on this head (scoped)
Test qualityThe strengthened unit fixture is substantive rather than padding: Not executed here (declared, not treated as green)
I am recording this as UNKNOWN rather than green. It is not blocking because the consequential behavior change is the single candidate registration, which is independently proven RED→GREEN against the real Notes (non-blocking, no action required)
No blocking issues. Minimal diff, symmetric with the existing |
…on-cache-id-at-delete-t-4ca1fd
Code reviewVerdict: pass — head This head is a third The merge: what it brought in, and why it does not touch this change
What the change does
(session_cache_dir(session_id), (session_cache_dir(session_id),)),Re-verified against this tree:
Verification run on this head (scoped)
Test qualityThe strengthened unit fixture is substantive, not padding: Not executed locally (declared, resolved via CI on this SHA)
Notes (non-blocking, no action required)
No blocking issues. Minimal diff, symmetric with the existing Structured result (for the launcher): {"verdict": "pass", "issues": [], "artifact_posted": true} |


Detail bug report: View on Detail
Summary
purge_session_directories(the synchronous, delete-time host-dir cleanup run fromdelete_session) removed every per-session host directory except the per-session/root/.cachebind-mount source. Commitdb1ebfa4(#2347) added<workspace_root>/_cache/<session_id>and its asynchronous reaper but — unlikeaf52d8acdid for/tmp— never registeredsession_cache_dir(session_id)in the purge candidate tuple, so a deleted session's_tmp/<id>vanished the moment its row was gone while_cache/<id>lingered until the periodichost_dir_reaperaged it out (and under the documented P1 disk-fill overridehost_dir_reaper_enabled=falsethe reaper is a no-op, so_cachehad zero cleanup and accumulated indefinitely). This registers the cache dir inpurge_session_directories, restoring the symmetry with_tmp.Substrate state changes
None — code-only change. The
_cache/<id>bind mount and its host dir already exist (created byensure_session_cache_dirat provision time); this only adds the missing delete-timermtreeof that already-existing dir.Test plan
tests/unit/test_purge_session_directories_jail.py:_session_owned_dirsnow lists all six per-session dirs (it previously omitted both mounts —_tmpand_cache), and the legitimate-purge, per-candidate-reclaim, and shared-root-refusal cases now cover_tmp/_cachewith sibling-session bystanders proving per-session scoping. 17/17 pass.tests/integration/test_delete_session_purges_cache_dir.py: drives the realdelete_sessionservice against a migrated Postgres testcontainer and asserts_cache/<id>(and_tmp/<id>) are gone the momentdelete_sessionreturns, withhost_dir_reaper_enabledboth true and false. 2/2 pass._tmpstill passes) across the unit, integration, and a real-Docker e2e smoke; re-applying turns all green.mypy src tests(strict, 1084 files),ruff check/ruff format --check, fulluv run pytest tests/unit -q(6093 passed, 1 unrelated skip, 0 failures).aios-sandbox:ci) driving a scripted model — provisioned a sandbox, confirmed the host_cache/<id>existed post-provision, deleted the session via the service, and confirmed the host dir was gone immediately, both with the reaper on and off. Excluded from the PR since the integration test already guards the realdelete_sessioncall site at far lower maintenance cost.Risk / rollback
Low. The new candidate is structurally identical to the existing
_tmpentry (same_per_session_childhelper, same per-session exclusivity proof) and passes the same_purge_target_if_ownedownership guard, so no refusal is weakened — verified by the unchanged jail-suite refusal tests (account root, sibling sessions, shared roots including_cacheitself, out-of-jail, symlink-escape). Roll back by reverting the one-line addition; the asynchronous reaper continues to backstop cleanup as it does today.Automatic Fixes PRs can be configured here.