Skip to content

fix(sessions): move the supervisor's observations out of sessions.json - #302

Merged
lionello merged 1 commit into
masterfrom
fix/282-supervisor-state-side-file
Aug 19, 2026
Merged

fix(sessions): move the supervisor's observations out of sessions.json#302
lionello merged 1 commit into
masterfrom
fix/282-supervisor-state-side-file

Conversation

@defangdevs

Copy link
Copy Markdown
Owner

What this changes

sessions.json mixed two kinds of data: intent — what the operator asked for (name, agent, working directory, prompts, stopped) — and the supervisor's own observations (hasRun, boxSessionId). The observations lived in the file every human writer, the CLI and the web UI edit, which is what made the lost update in #254 expensive: reverting them turned a long-running session into a "first spawn" (new id, kickoff prompt re-fired, previous transcript orphaned).

And boxSessionId was documented as "the STABLE id WE own across respawns" while the supervisor rewrote it on every adopted rotation. It names a segment, not a conversation — the root of #274 and #277.

Closes #282.

1. The comment now says what the field is

The launch id is the id this session was last LAUNCHED with. A clear, a compact or a resume mints a new segment and the supervisor adopts it, so a consumer that wants "the conversation" has to follow the rotation record rather than trust the value. Reworded in supervisor.sh, session-cli.sh, settings-daemon.py and agent-box.nix.in.

2. Migration shape: side file first, registry copy as fallback, both written this release

The launch id moves to ~/.local/state/agent-box/session/<name>.json{"launchSessionId": "<uuid>"} — which only the supervisor writes.

  • Read: the side file first; sessions.json's boxSessionId is the fallback, which covers a session that last spawned before this release and the id agent-box-session add mints up front.
  • Write: record_launch writes the side file whenever the value changes (first spawn, adopted rotation, or the one respawn that migrates an existing session). mark_started still mirrors hasRun / boxSessionId / the cleared initialPrompt into the registry, on a first-ever spawn or an adopted rotation only.
  • Next release deletes mark_started and the fallback read; that is the point at which the supervisor becomes a read-only consumer of sessions.json.

Writing only on a change matters: a session that cannot start (a claude with no credentials) is respawned every couple of seconds, and neither file should be rewritten on that loop.

3. hasRun is derived, not stored

For claude it is claude_has_transcript "$bid" — the same check the resume arm already made, so a stored copy could only ever disagree with the disk, and did in both directions (a reverted registry promised a resume of a transcript that was there; a deleted transcript promised one that was not). The other harnesses have nothing equivalent to read — a codex rollout is found by a marker only a kickoff prompt carries, a shell session has no transcript at all — so they answer from the launch record itself (launched).

The rotation block is now gated on launched rather than on hasrun: what makes a live-id record meaningful is that this session has spawned before, and an adoption is its own proof of a resumable transcript.

4. "/clear rotation" → the triggers actually observed

Clear, compact and resume, in supervisor.sh, claude-session-start-hook.sh, settings-daemon.py, agent-box.nix.in and the test's subtest name.

One accessor per program (#284)

The key is the session name for now, and scope is deliberately not widened here — but the path is spelled in exactly one function per program, so re-keying it on a harness-minted id (tmux #{session_id} / AGENT_BOX_SESSION_ID, the transcript uuid) is a change to those functions rather than a migration:

program function
modules/src/supervisor.sh session_state_file
modules/src/session-cli.sh session_state_file
modules/src/settings-daemon.py session_state_path
tests/sessions.nix state_file

Reclaimed without anyone running rm

sweep_session_state in the supervisor is keyed on the registry, not on a delist path being taken, and runs on every reconcile tick (2s) as well as at startup — nothing makes a hook agent call agent-box-session rm, a session can be delisted while the unit is down, and a box that upgrades into this file has no delete path to have taken. One jq for the whole sweep, and a registry jq cannot answer for leaves everything alone; an empty registry is a different answer from an unreadable one and does sweep, because the last session deleted is exactly when the last file has to go.

agent-box-session rm and the settings page's delete do prune their own, as an optimisation only: it closes the window in which rm foo; add foo would hand the new session the dead one's launch id, and with it its transcript. Neither is what makes the state reclaimable. No second live-session-id/-shaped directory is added (#281 is untouched).

EAFP

Every read of the new state attempts and handles the miss — session_launch_id (jq, 2>/dev/null), read_launch_id (try / except OSError, ValueError, AttributeError), and the live-id record, whose [ -r ] check-then-act is now a plain read with a fallback. record_launch uses mktemp (O_EXCL on a name nobody can guess) + rename: not create-if-absent, because a rotation legitimately changes the value, so there is no existence test to lose a race with. The sweep skips an unmatched glob by the name charset check rather than by [ -e ].

Tests

  • tests/sessions.nix rotation subtest — the new second hop. Rotate, respawn, rotate again from the id the supervisor adopted, and assert it resumes the newest segment. One hop cannot tell a chain from a coincidence: a supervisor that only ever looks under its original launch id passes hop 1 and fails hop 2.
  • And that the SIDE FILE carries it, not sessions.json. After hop 2 the registry copy is corrupted exactly as a lost update did (stale id, hasRun back to false) and the session is bounced once more — it still resumes the newest segment.
  • Kickoff subtest now asserts the launch id lands in the supervisor's file, that the registry mirror agrees, and that rm prunes it.
  • New sweep subtest: a state file for a name the registry does not list is reclaimed with nobody running rm; a listed session's file survives being stopped.
  • tests/webhook.nix is unchanged — its hasRun == true and initialPrompt == null assertion still holds against the migration mirror. Deliberate: fix(webhook): cap dispatch on running hook sessions, not registry keys #286 was rebasing that file concurrently.
  • Both golden seeds (agent-box-agent-sessions.json) are byte-identical: only a comment above sessionsSeedFile changed, not the JSON. The rendered supervisor / session CLI / settings daemon / hook payloads did change, hence the tests/golden/ diff.

Checks

Ran natively (aarch64):

  • nix build .#checks.aarch64-linux.module-generated-up-to-date — green
  • nix build .#checks.aarch64-linux.golden-snapshot — green (regenerated with nix run .#update-golden)
  • nix build .#checks.aarch64-linux.module-single-file — green
  • nix build .#checks.aarch64-linux.{multi-user,download-route,webhook-route} — green
  • /home/agent/bin/check-testscript.sh tests/sessions.nixty and ruff both clean
  • nix-instantiate --parse tests/sessions.nix — clean
  • The new shell helpers and the two new Python helpers were exercised directly against a scratch $HOME (miss, hit, corrupt JSON, non-string value, path traversal in the recorded id, unreadable registry, empty registry, unmatched glob).

Only EVALUATED (VM tests are x86_64-only; this box is aarch64):

  • nix eval .#checks.x86_64-linux.sessions.drvPath — evaluates
  • nix eval .#checks.x86_64-linux.webhook.drvPath — evaluates
  • nix eval .#checks.x86_64-linux.settings-page.drvPath — evaluates

CI runs the VM tests for real.

Migration impact

None for an operator: a live session keeps its launch id through the fallback read, and the first respawn after the upgrade writes the side file. A rollback to the previous module still finds its ids in sessions.json, which is what the mirror is for. The new directory is agent-owned under ~/.local/state/, one small file per session, swept against the registry.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PsqGhkpjsb4kKAdVz25xKN

sessions.json mixed INTENT (name, agent, cwd, prompts, stopped) with the
supervisor's own OBSERVATIONS (hasRun, boxSessionId), in the one file every
human writer and the web UI edits. That is what made the lost update in #254
expensive: reverting the observations turned a long-running session into a
"first spawn" — new id, kickoff prompt re-fired, previous transcript
orphaned.

- The launch id moves to a supervisor-owned side file,
  ~/.local/state/agent-box/session/<name>.json, read first with the registry
  copy as a migration fallback. Both are written this release; the next one
  drops the registry copy and the fallback read.
- hasRun is DERIVED for claude from claude_has_transcript, which is the same
  check the resume arm already made. Other harnesses keep answering from the
  launch record.
- The path is spelled once per program (session_state_file in the supervisor
  and the CLI, session_state_path in the daemon), so re-keying it on a
  harness-minted id (#284) is a small change.
- Reclaimed by a sweep keyed on the registry, run on every reconcile tick —
  no delete path is guaranteed to run. Both delete paths prune their own as
  an optimisation, which closes the window in which a re-used name inherits
  the previous holder's launch id.
- boxSessionId is documented as what it is: the id this session was last
  LAUNCHED with, one SEGMENT of the conversation. Where the code said "/clear
  rotation" it now names the three observed triggers: clear, compact, resume.

Closes #282

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsqGhkpjsb4kKAdVz25xKN
@lionello
lionello merged commit 17c2b0a into master Aug 19, 2026
1 check passed
@lionello
lionello deleted the fix/282-supervisor-state-side-file branch August 19, 2026 23:53
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Agent-Box Aug 19, 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.

boxSessionId is documented as stable but names a segment; move the supervisor's observations out of sessions.json

2 participants