Skip to content

feat(sandbox): session folder retention and cleanup - #11

Merged
hoaitan merged 11 commits into
nwebxyz:mainfrom
nwebbot:feat/sandbox-session-retention
Aug 21, 2026
Merged

feat(sandbox): session folder retention and cleanup#11
hoaitan merged 11 commits into
nwebxyz:mainfrom
nwebbot:feat/sandbox-session-retention

Conversation

@nwebbot

@nwebbot nwebbot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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

  • Session log. sandbox connect records each session's start time to sandbox_<sandbox_id>.json beside the session folders. It is the only source of truth for what may be deleted — a session-* folder with no entry is never touched. The entry is written before bootstrap, since setupFolder creates the folder early but bootstrap can fail later at git clone; recording after success would leak every failed bootstrap permanently.
  • Explicit delete reclaims disk now. delete_session deletes the folder and its entry. delete_sandbox is a full teardown: stop, drain, delete every logged folder, delete the log file, then exit the CLI.
  • Drain before delete. 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. Every delete path now waits on Runner.Done() (5s cap) first.
  • Retention. --retention 30d (default; off disables) 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 0 takes everything (prompts unless --yes), --dry-run reports only.

Notes for reviewers

Three things worth knowing, since they weren't obvious going in:

  1. Stop and disconnect never deleted folders to begin with. The repo had exactly one os.RemoveAll, reached only from delete_session. Stop, StopAll and the CLI-stop path stay deletion-free, pinned by a regression test.
  2. delete_sandbox didn't exit the CLI. dl.Run(ctx) is a goroutine, so returning errSandboxDeleted signalled nothing and left a TUI attached to a deleted sandbox. It now cancels ctx down the same path a Ctrl-C takes (stop() is idempotent).
  3. The log store latches closed on teardown. Deleting the log file while the sweeper is alive would otherwise let a later tick recreate a log for a sandbox that no longer exists. Ordering alone can't fix it — cancelling ctx first 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 0 is an error pointing at off, 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: main moved 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 takes main's v0.8.0 as-is.

Rebase notes (2026-08-21)

main merged substantially since this PR opened, including a full rework of SessionManager's session-lane handling (durable reconnect: idempotent Start, Reattach/reconnect_session, per-session output buffering across a dropped lane, sessionEntry replacing the old flat map[string]*agentfleet.Runner). None of that overlaps with the retention/cleanup feature itself — nothing like it exists elsewhere on main — but it did mean this PR's sessionlane.go changes no longer applied as a textual diff.

The rebase re-threads this PR's additions through that new architecture:

  • recordSessionStart now fires from create() (the new architecture's brand-new-session path) right after the session lane connects, in the same "before bootstrap" position as before.
  • Remove drains via entry.runner (still waiting on real PTY exit, not just SIGTERM delivery) and keeps this PR's folder+log cleanup with error logging, replacing main's bare unchecked os.RemoveAll.
  • RemoveAll is unchanged in behavior, adapted to iterate map[string]*sessionEntry and drain entry.runner for each.
  • A second, unrelated sessionEntry type already existed in sessionlog.go (the on-disk log record: name/dir/created-at) and collided by name with main's new live-session sessionEntry. Renamed the log-record type to sessionLogEntry — a mechanical rename, no behavior change.
  • sessionlane_test.go (new in this PR) built a SessionManager directly against the old flat runner map; updated to construct against map[string]*sessionEntry{}. The tests it covers (drain-before-delete, folder+log removal on Remove, concurrent draining in RemoveAll, 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's sandbox connect manifest entry and connect.go's flag list/help text were combined with main's own additions to that same command (--session-buffer, --log-file family) — purely additive, no behavioral overlap with --retention.
  • The session log filename was changed from a bare <sandbox_id>.json to sandbox_<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, the connect/cleanup help text, and the help-manifest descriptions were all updated together; every place that names the file dynamically via sessionLogPath (retention sweep, cleanup) needed no change.

Verification

go build ./..., go vet ./..., and go test ./... all green post-rebase and after the log-filename rename, including TestHelpManifestMatchesCommandTree (the manifest/command-tree sync check) and every test in internal/cmd/sandbox (retention, cleanup, session log, and the rewritten session-lane teardown tests). gofmt -l clean 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 0 prompt). Worth doing before merge, but out of scope for a conflict-resolution rebase.

Task: RETA-97

🤖 Generated with Claude Code

nwebbot and others added 11 commits July 17, 2026 11:40
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
hoaitan merged commit a3b4c19 into nwebxyz:main Aug 21, 2026
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.

2 participants