Skip to content

Parked-session surfaces can never report a parked session: wire the writer and separate empty from unavailable #3453

Description

@chernistry

Problem

Four operator surfaces report parked sessions. None of them can ever report one.

SpawnSupervisor.spawn() (src/bernstein/core/agents/spawn_supervisor.py:276)
is the only path to _consume_respawn (:348), _park (:361-369) and
SupervisorState.PARKED. Grepping SpawnSupervisor and spawn_supervisor
across src/ finds the module itself, docstring cross-references, and three
read-only CLI call sites: src/bernstein/cli/commands/status_cmd.py:428-430
and src/bernstein/cli/commands/agents_cmd.py:579-581 and :601-603, which
call parked_sessions() and resume(). Nothing in the orchestrator or the
spawner ever calls .spawn().

So the respawn budget cannot be consumed in production, and
bernstein agents resume <id> can only resume a session parked by a unit test.

The aggregator has the same shape independently.
load_parked_sessions() reads .sdd/runtime/spawn_supervisor/parked.json
(src/bernstein/core/orchestration/supervisor_aggregator.py:168-179). Grepping
parked.json across src/ and tests/ returns only that reader. Its
fallback branch keys on .sdd/runtime/failures/*.json entries with
kind == "respawn_exhausted" (supervisor_aggregator.py:193); grepping
respawn_exhausted across src/ and tests/ finds only that reader and one
test name. The function structurally returns the empty set.

Everything built on it inherits that: bernstein status, bernstein supervisor, bernstein fleet and the TUI status bar
(src/bernstein/tui/status_bar.py:465) report "nothing parked" unconditionally
and always, on every run, whatever happened.

This is the worst shape a recovery path can have. It is not absent, so nobody
notices it is missing; it is not broken in a way that errors; it answers, and
the answer is always the reassuring one.

Proposal

Two things, in order.

Wire the writer. The spawner's failure path calls SpawnSupervisor.spawn()
so respawn attempts are budgeted and exhaustion parks the session, and _park
persists to the path the aggregator already reads. The read surfaces do not
change; they start being fed.

Then make the silence impossible to repeat. A surface that reports the
absence of something must be able to distinguish "none occurred" from "the
producer never ran". load_parked_sessions() returns a result that carries
whether the store has ever been written in this run, and the surfaces render
0 parked and parked state unavailable differently.

The second half is the part that generalises. An empty recovery surface should
be a claim the run can support, not a default.

Why this shape

Every other guarantee in this project is checkable rather than assumed, and this
is a surface that has been quietly asserting a fact for its whole life with
nothing behind it. Making the unavailable case distinct from the empty case is
what turns the status bar from a decoration into evidence, and it is cheap: the
store either has a write for this run or it does not.

Scope

  1. Call SpawnSupervisor.spawn() from the spawner's failure and respawn path so
    the budget is consumed and exhaustion parks.
  2. _park persists to .sdd/runtime/spawn_supervisor/parked.json, the path
    supervisor_aggregator.load_parked_sessions already reads.
  3. load_parked_sessions() distinguishes empty from unavailable.
  4. bernstein status, bernstein supervisor, bernstein fleet and the TUI
    status bar render the two cases differently.
  5. bernstein agents resume <id> resumes a session parked by a real run.

Done means

  • A session whose respawn budget is exhausted appears in
    bernstein status and in the TUI status bar
  • bernstein agents resume <id> resumes that session
  • With no supervisor writes in the run, the surfaces say the parked state is
    unavailable rather than reporting zero
  • A test drives a real spawn failure to exhaustion and asserts the parked
    entry is on disk at the path the aggregator reads
  • The respawn_exhausted fallback branch is either fed by a real writer or
    removed, not left as a third unreachable path

Start here

Picking this up cold? Land this first and stop.

Step 1 - make the empty answer honest, before wiring anything. Change
load_parked_sessions() in
src/bernstein/core/orchestration/supervisor_aggregator.py:168-179 to return a
result that distinguishes three cases: the store file is absent (unavailable),
present and empty (zero parked), present with entries (the list). Render the
unavailable case distinctly in src/bernstein/cli/commands/status_cmd.py:428-430.
Do not touch the spawner yet.

Proves it: a test named for the property that absent and empty are different
answers. Assert the unavailable result with no file on disk; write an empty
store and assert the zero-parked result; write one entry and assert it is
returned. Then assert the status output text differs between the first two -
that assertion is the whole point of the step, because it is the one that would
have caught this.

Not in step 1: calling SpawnSupervisor.spawn() from the spawner. That is
the larger half and it changes live failure handling, so it wants its own PR
with its own failure-path tests. One thing worth knowing before planning step 2:
_publish_exhausted (spawn_supervisor.py:375-392) already exists alongside
_park, so there are two exhaustion outputs with one unreachable caller between
them - decide which is authoritative before wiring, rather than feeding both.

Partial work is welcome and normal here. Title the PR "Partial implementation of
this issue" and say what remains.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-welcomeSuitable for AI agent contributorsbugSomething isn't workingcorehelp wantedExtra attention is neededsize/mup-for-grabsListed on up-for-grabs.net — no commitment, low friction

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions