feat(sandbox): session folder retention and cleanup - #11
Merged
Conversation
Records session start times to <sandbox_id>.json, makes age the single folder-deletion policy, and adds an hourly sweeper plus a `sandbox cleanup` command. Two findings reshaped the original request: - The elapsed-time panel is rendered by agentfleet, not retask-cli, and TUIConfig exposes no formatting hook — that item must land upstream. - Stop and disconnect already never deleted session folders; only delete_session did. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverses the earlier "defer deletion to the sweeper" call: explicit deletes now reclaim disk immediately, and retention covers only folders left behind by stop/disconnect. delete_sandbox becomes a full teardown — stop, drain, delete folders, delete the log file, exit the CLI. Two hazards this design has to handle: - PtyAgent.Stop returns on SIGTERM delivery, not process exit, so deleting straight after it races the agent's own cleanup. Every delete path now drains Runner.Done() with a timeout first. - Deleting the log file while the sweeper is alive lets a later tick recreate it, so the store latches closed on teardown. Stop, StopAll and CLI-stop remain deletion-free, with regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 TDD tasks across two repos. Task 1 (agentfleet elapsed format) lands upstream first and needs a manual release; Task 11 bumps the dep and is blocked on it. Tasks 2-10 are independent of that. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
time.ParseDuration has no day unit, so 30d needs handling. --retention additionally accepts off; 0 is rejected there because it means delete everything for --older-than. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records session start times to <sandbox_id>.json next to the session folders. The log is the only source of truth for what may be deleted — a session-* folder with no entry is never touched. Mutations are read-modify-write under a mutex plus an atomic rename, since session starts and the retention sweep both write it. Files that aren't ours (package.json) and logs from newer CLIs are left alone. One sweep serves both the hourly goroutine and the cleanup command. Its skip predicate keeps a live session's own working directory from being deleted when it outlives the retention window. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sessions are recorded before bootstrap, since setupFolder creates the folder early but bootstrap can fail later — an unlogged folder could never be reaped. Every delete path now drains the PTY first. PtyAgent.Stop returns when SIGTERM is delivered, not when the process exits, so deleting straight after it destroyed the working directory while the agent was still flushing into it. agentfleet never escalates to SIGKILL for a process that ignores SIGTERM, so the 5s timeout is the only backstop. delete_session deletes the folder and entry. delete_sandbox stops and drains every session, deletes every logged folder and the log file, then exits the CLI — previously the data lane goroutine just returned, leaving a TUI attached to a sandbox that no longer existed. Closing the log store stops a late sweep tick from recreating the deleted file. --retention 30d (default; off disables) sweeps at startup and hourly, skipping live sessions. Stop, StopAll and the CLI-stop path stay deletion-free, with a regression test pinning that. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sweeps every session log in the working directory, or one named sandbox. --older-than 0 deletes everything and prompts first, since a separate process cannot know which sessions another process has live; --yes skips the prompt and --dry-run reports without deleting. Files failing the log schema check are skipped, so package.json and friends are never touched. help-llm gains the cleanup entry and --retention on connect; the manifest sync test in cmd/retask enforces both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picks up the session panel timer that scales precision to duration: mm:ss under an hour, h:mm:ss under a day, and 2d3h beyond that. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the design spec and implementation plan under docs/superpowers/; they were working artifacts and don't belong in the repo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avoids a bare <sandbox_id>.json sitting in the session working directory next to other JSON files with no indication of what wrote it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hoaitan
approved these changes
Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Updated 2026-08-21: rebased onto current
main(was 5 weeks stale and conflicting). See the new section at the bottom — the session-lane merge is the part worth a close look.Session working folders now survive being stopped, are deleted immediately on an explicit delete, and are reaped by age otherwise.
What changed
sandbox connectrecords each session's start time tosandbox_<sandbox_id>.jsonbeside the session folders. It is the only source of truth for what may be deleted — asession-*folder with no entry is never touched. The entry is written before bootstrap, sincesetupFoldercreates the folder early but bootstrap can fail later at git clone; recording after success would leak every failed bootstrap permanently.delete_sessiondeletes the folder and its entry.delete_sandboxis a full teardown: stop, drain, delete every logged folder, delete the log file, then exit the CLI.PtyAgent.Stopreturns when SIGTERM is delivered, not when the process exits, so deleting straight after it destroyed the working directory while the agent was still flushing into it. Every delete path now waits onRunner.Done()(5s cap) first.--retention 30d(default;offdisables) sweeps aged folders at startup and hourly. Live sessions are skipped, so a session outliving the window keeps its own cwd.retask sandbox cleanup. Manual sweep of every log in the working directory, or one named sandbox.--older-than 0takes everything (prompts unless--yes),--dry-runreports only.Notes for reviewers
Three things worth knowing, since they weren't obvious going in:
os.RemoveAll, reached only fromdelete_session.Stop,StopAlland the CLI-stop path stay deletion-free, pinned by a regression test.delete_sandboxdidn't exit the CLI.dl.Run(ctx)is a goroutine, so returningerrSandboxDeletedsignalled nothing and left a TUI attached to a deleted sandbox. It now cancelsctxdown the same path a Ctrl-C takes (stop()is idempotent).ctxfirst would kill the TUI before teardown finished.Log-only, by design: folders already on disk before this ships have no entry and are never auto-reaped; they need a manual
rm. An mtime-based adoption scan was considered and rejected.--retention 0is an error pointing atoff, so the same value can't mean both "delete nothing" and "delete everything".Agentfleet dependency
Includes the agentfleet v0.7.0 bump (hoaitan/agentfleet#11, merged), which scales the session panel's elapsed timer.No longer needed:mainmoved to agentfleet v0.8.0 while this PR sat open, and v0.8.0 is a strict superset of v0.7.0 (same elapsed-timer scaling, plus an unrelated rotating-log-file feature and a Go toolchain CVE bump). The rebase drops this PR's own version bump entirely and takesmain's v0.8.0 as-is.Rebase notes (2026-08-21)
mainmerged substantially since this PR opened, including a full rework ofSessionManager's session-lane handling (durable reconnect: idempotentStart,Reattach/reconnect_session, per-session output buffering across a dropped lane,sessionEntryreplacing the old flatmap[string]*agentfleet.Runner). None of that overlaps with the retention/cleanup feature itself — nothing like it exists elsewhere onmain— but it did mean this PR'ssessionlane.gochanges no longer applied as a textual diff.The rebase re-threads this PR's additions through that new architecture:
recordSessionStartnow fires fromcreate()(the new architecture's brand-new-session path) right after the session lane connects, in the same "before bootstrap" position as before.Removedrains viaentry.runner(still waiting on real PTY exit, not just SIGTERM delivery) and keeps this PR's folder+log cleanup with error logging, replacingmain's bare uncheckedos.RemoveAll.RemoveAllis unchanged in behavior, adapted to iteratemap[string]*sessionEntryand drainentry.runnerfor each.sessionEntrytype already existed insessionlog.go(the on-disk log record: name/dir/created-at) and collided by name withmain's new live-sessionsessionEntry. Renamed the log-record type tosessionLogEntry— a mechanical rename, no behavior change.sessionlane_test.go(new in this PR) built aSessionManagerdirectly against the old flat runner map; updated to construct againstmap[string]*sessionEntry{}. The tests it covers (drain-before-delete, folder+log removal onRemove, concurrent draining inRemoveAll, the stop/delete distinction) all exercise the "no live entry" teardown path, so no test logic changed — just the map's value type.helpcmd/command.go'ssandbox connectmanifest entry andconnect.go's flag list/help text were combined withmain's own additions to that same command (--session-buffer,--log-filefamily) — purely additive, no behavioral overlap with--retention.<sandbox_id>.jsontosandbox_<sandbox_id>.json, so it reads unambiguously as ours next to a working directory's other JSON files (package.json,tsconfig.json, etc.) instead of looking like an arbitrary id-named file.sessionLogPath, its doc comments, theconnect/cleanuphelp text, and the help-manifest descriptions were all updated together; every place that names the file dynamically viasessionLogPath(retention sweep, cleanup) needed no change.Verification
go build ./...,go vet ./..., andgo test ./...all green post-rebase and after the log-filename rename, includingTestHelpManifestMatchesCommandTree(the manifest/command-tree sync check) and every test ininternal/cmd/sandbox(retention, cleanup, session log, and the rewritten session-lane teardown tests).gofmt -lclean on every touched file.Not re-run in this pass: the original PR's manual end-to-end exercise of the built binary (dry-run, 30d sweep, orphan-folder skip,
--older-than 0prompt). Worth doing before merge, but out of scope for a conflict-resolution rebase.Task: RETA-97
🤖 Generated with Claude Code