feat(sessions): delist one-shot sessions instead of parking them - #387
Conversation
A hook-* session is spawned for one event batch and nobody ever resumes it, but finishing one left its registry entry behind for good. Three stopped hook-* entries had accumulated on the deployed box. Parking is the right end for a NAMED session: `stopped` keeps the entry's boxSessionId, which is the only record mapping the name to a conversation, so `agent-box-session restart` can resume it. A one-shot session has no such future, so the same flag means "finished" and the entry is litter that outlives the work. Nothing collected it. The spawn preamble asks the agent to `agent-box-session rm` itself, but that is a request to a MODEL, not a guarantee: `/quit` and Ctrl+D reach mark-stopped's clean-exit branch without it, and a session parked by `agent-box-session stop` never had an agent to ask. src/supervisor.sh already said as much next to sweep_session_state - "nothing makes a hook agent call `agent-box-session rm`". So mark it at the source instead: - `agent-box-session add --ephemeral` records ephemeral: true. The key is written only when set, so every other session keeps the entry shape it has always had. - agent-box-webhook-spawn passes --ephemeral, which is what makes every hook-* session one-shot by construction. - The supervisor's reconcile loop delists an entry that is both ephemeral and stopped, and sweeps the webhook filter file it cannot prune itself. sweep_session_state already reclaims the launch id on the same tick, so the next holder of a reused name cannot inherit this one's transcript. - `agent-box-session stop` no longer promises a `restart` that will never come for a one-shot session. A CRASH is unaffected: a non-zero exit takes the post-mortem branch and is never flagged stopped, so a hook session that died stays listed and attachable for inspection. Transcripts are untouched either way - they live under the harness's own state dir and already outlive every registry entry (315 transcripts against 7 listed sessions on the deployed box). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsqGhkpjsb4kKAdVz25xKN
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review. 📝 WalkthroughWalkthroughThe session CLI supports ephemeral sessions. The supervisor delists stopped ephemeral sessions after confirming that their tmux panes ended. Webhook sessions use this mode automatically. Crashed sessions remain listed, and lifecycle tests cover both session types. ChangesEphemeral session lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The change is merge-ready after normal checks and review; no actionable merge-blocking risk remains. Sequence Diagram(s)sequenceDiagram
participant Webhook
participant agent-box-webhook-spawn
participant agent-box-session
participant agent-box-supervisor
participant tmux
Webhook->>agent-box-webhook-spawn: dispatch hook
agent-box-webhook-spawn->>agent-box-session: add --ephemeral
agent-box-session->>tmux: create session
agent-box-supervisor->>tmux: check pane state
agent-box-supervisor->>agent-box-session: remove stopped ephemeral entry
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 3 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
…apper The hook-session cap has a fallback for when it cannot probe tmux: it counts hook-* registry keys instead of live panes, deliberately over-counting rather than uncapping spawns. Its test built the key-without-a-pane state by spawning a hook session and stopping it. A wrapper-spawned session is now --ephemeral, so parking it delists it within a tick and no lingering key is left to count. The test's own arithmetic then moved under it: `hook_keys == "3"` found 2, and the earlier `== "2"` was only passing inside the window before the reaper's next tick. Add the second entry directly instead, which is both non-ephemeral and deterministic. The divergence the fallback exists for did not go away with --ephemeral: a hook-* entry outlives its pane whenever it is listed while the supervisor is down, caught between respawns, or predates --ephemeral. The cap's primary accounting is untouched by the reap either way — it counts a live pane, or a listed entry that is NOT stopped, and every entry the reaper takes was already stopped and already holding no slot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsqGhkpjsb4kKAdVz25xKN
The problem
Three
stoppedhook-*entries had accumulated on the deployed box. Nothing was ever going to collect them.stoppedmeans two different things depending on the session:stoppedmeansmain,claude)boxSessionId, the only record mapping the name to a conversation, sorestartcan resume itThe spawn preamble asks the agent to
agent-box-session rmitself, but that is a request to a model, not a guarantee./quitand Ctrl+D both reachmark-stopped's clean-exit branch without it, and a session parked byagent-box-session stopnever had an agent to ask.src/supervisor.shalready said so next tosweep_session_state:The fix — mark it at the source
agent-box-session add --ephemeralrecordsephemeral: true. Written only when set, so every other session keeps the entry shape it has always had.agent-box-webhook-spawnpasses--ephemeral, which is what makes everyhook-*session one-shot by construction rather than by convention.ephemeralandstopped, then sweeps the webhook filter file it cannot prune itself.sweep_session_statealready reclaims the launch id on the same tick, so the next holder of a reused name cannot inherit this one's transcript.agent-box-session stopno longer promises arestartthat will never come for a one-shot session.Reaping keys on the flag, not on the
hook-name prefix, so it does not add a fourth place where that prefix carries meaning — andadd --ephemeralis available to anyone spawning a throwaway session.What is deliberately not reaped
A crash. A non-zero exit takes the post-mortem branch and is never flagged
stopped, so a hook session that died stays listed and attachable for inspection, exactly as today. That is the affordance worth keeping, and it is the one the old behavior was accidentally protecting for every session.Transcripts are untouched. They live under the harness's own state dir and already outlive every registry entry — the deployed box has 315 transcripts against 7 listed sessions. What goes is the name→conversation mapping, which is what makes the name reusable.
Verification
Two new subtests in
tests/sessions.nix, next to the existing issue #167 stop-semantics group:stop. Asserts the entry is gone, the pane is gone,lsno longer shows it, the per-session state file went with it, and thestopmessage says "one-shot".Checks run natively on
aarch64-linux:module-generated-up-to-dategolden-snapshotagentbox-renderassemble-module-escapingmulti-usermodule-single-filetests/test-registry.py,tests/test-envstore.pysessions,sessions-web,webhookandsettings-pageforx86_64-linuxevaluate cleanly; the VM runs need an x86 host, so CI covers those. The driver's ownty+ruffgates were run natively over the patchedtestScriptbefore pushing.Regenerated:
nix run .#assemble,nix run .#update-golden, andpython3 tests/test_agentbox.py --updatefor the nativeexpected/guide.🤖 Generated with Claude Code
https://claude.ai/code/session_01PsqGhkpjsb4kKAdVz25xKN