Skip to content

feat(sessions): delist one-shot sessions instead of parking them - #387

Merged
defangdevs merged 2 commits into
masterfrom
feat/ephemeral-hook-sessions
Aug 26, 2026
Merged

feat(sessions): delist one-shot sessions instead of parking them#387
defangdevs merged 2 commits into
masterfrom
feat/ephemeral-hook-sessions

Conversation

@defangdevs

Copy link
Copy Markdown
Owner

The problem

Three stopped hook-* entries had accumulated on the deployed box. Nothing was ever going to collect them.

stopped means two different things depending on the session:

What stopped means Right end?
A named session (main, claude) parked — the entry keeps its boxSessionId, the only record mapping the name to a conversation, so restart can resume it yes
A hook-* session finished — spawned for one event batch, nobody will ever resume it no, it is litter

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 both 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 so next to sweep_session_state:

nothing makes a hook agent call agent-box-session rm

The fix — mark it at the source

  • agent-box-session add --ephemeral records ephemeral: true. 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 rather than by convention.
  • The supervisor's reconcile loop delists an entry that is both ephemeral and stopped, then 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.

Reaping keys on the flag, not on the hook- name prefix, so it does not add a fourth place where that prefix carries meaning — and add --ephemeral is 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:

  • one-shot is delisted, not parked — covers both paths: the clean exit a finished hook agent takes, and stop. Asserts the entry is gone, the pane is gone, ls no longer shows it, the per-session state file went with it, and the stop message says "one-shot".
  • a plain session is still parked, not reaped — the regression guard: a named session parked by the very same flag keeps its entry.

Checks run natively on aarch64-linux:

Check Result
module-generated-up-to-date ok
golden-snapshot ok
agentbox-render ok
assemble-module-escaping ok
multi-user ok
module-single-file ok
tests/test-registry.py, tests/test-envstore.py ok

sessions, sessions-web, webhook and settings-page for x86_64-linux evaluate cleanly; the VM runs need an x86 host, so CI covers those. The driver's own ty + ruff gates were run natively over the patched testScript before pushing.

Regenerated: nix run .#assemble, nix run .#update-golden, and python3 tests/test_agentbox.py --update for the native expected/ guide.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PsqGhkpjsb4kKAdVz25xKN

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
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9057f370-160b-439d-b2ab-6948001f9205

📥 Commits

Reviewing files that changed from the base of the PR and between 188d43b and 1c0850b.

📒 Files selected for processing (1)
  • tests/webhook.nix

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

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

Changes

Ephemeral session lifecycle

Layer / File(s) Summary
Session CLI contract
modules/agent-box.nix, modules/src/session-cli.sh, tests/golden/vm/payloads/agent-box-session/bin/agent-box-session
agent-box-session add accepts --ephemeral and stores the flag. Stop output distinguishes ephemeral sessions from restartable sessions.
Supervisor reaping and reconciliation
modules/agent-box.nix, modules/src/supervisor.sh, tests/golden/vm/payloads/agent-box-supervisor/bin/agent-box-supervisor
The supervisor rechecks tmux and registry state before removing stopped ephemeral sessions. It also prunes orphaned webhook filters.
Webhook integration and lifecycle validation
modules/src/default-agents-webhook.md, modules/src/webhook-spawn.sh, tests/golden/web/..., tests/native/expected/..., tests/sessions.nix, tests/webhook.nix
Webhook sessions are created as ephemeral. Documentation and golden outputs describe clean-exit cleanup, crash retention, transcript preservation, and persistent named sessions. Tests cover ephemeral cleanup and capacity counting with persistent sessions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 1c085

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
Loading

Suggested reviewers: lionello, claude

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: delisting one-shot sessions instead of parking them.
Description check ✅ Passed The description directly explains the problem, implementation, preserved behaviors, tests, and verification for ephemeral session cleanup.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

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

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ephemeral-hook-sessions

Comment @coderabbitai help to get the list of available commands.

…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
@defangdevs
defangdevs merged commit 5c42dda into master Aug 26, 2026
3 checks passed
@defangdevs
defangdevs deleted the feat/ephemeral-hook-sessions branch August 26, 2026 23:30
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Agent-Box Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants