Skip to content

Report fleet progress to the operator unprompted - #430

Merged
TheGreatAxios merged 10 commits into
mainfrom
cl-5761-orchestrator-reporting
Aug 9, 2026
Merged

Report fleet progress to the operator unprompted#430
TheGreatAxios merged 10 commits into
mainfrom
cl-5761-orchestrator-reporting

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Collaborator

The orchestrator had a complete picture of the fleet at all times and said nothing about it unless interrupted and asked. An operator watched half a fleet finish with the remaining work undispatched, stopped the run to find out whether it was stuck, and lost a queued message doing it.

Three changes, smallest first.

Interrupting no longer discards queued input. The queue survives the stop and is handed over at the interrupt itself. Delivering it there rather than waiting on an idle boundary matters: a stop does not reliably produce one, so the alternative was a message stranded in the badge forever. The host serialises the send behind the agent rebuild the same interrupt started.

/status answers where the fleet is without touching the run. One row, read from the live session store: running lanes with their clocks, which of them have gone quiet, and the finished tally. Palette commands do not go through the prompt queue, so asking costs nothing.

The fleet reports itself, unprompted. Lane changes drive the report directly, so a completion lands with what it produced and a failure with what went wrong at the moment it happens, not at the next turn boundary. A five-second poll covers the one change that produces no event at all, a lane going quiet.

The noise boundary is the design, not a detail on top of it:

  • only transitions are reported — dispatch, done, failed, cancelled, stalled; a lane working normally produces nothing
  • a quiet lane is announced once, never on every tick it stays quiet
  • more than three changes in one observation collapse into a tally, and a parallel dispatch is allowed to settle first so six lanes read as fleet · 6 dispatched rather than six lines
  • each update is clipped to one transcript row, because a wrapped line doubles the cost of every update on screen
  • the fleet running dry gets its own line, since that is the silence that started this

Stalled is not re-derived here; agentProgress is the one definition and this asks it.

Observed on a real six-lane fleet:

fleet · 6 dispatched
fleet · First lines of briefs done — The `briefs/` directory exists and con…
fleet · First lines of top-level md done — Read the first lines of all 10 t…
fleet · Count docs files done — Counted 19 files under `docs/`. No `docs/pl…
fleet · Exports of tui/runner done — Enumerated every export in `src/tui/ru…
fleet · Exports of subagent/index done — Read `src/subagent/index.ts` and e…
fleet · Count src TS files done — Counted `.ts` files under `src/`, grouped…
fleet · 6 done — nothing running

Eight rows for six lanes, and the last one is the answer to the question that forced the interrupt.

Closes CL-5761.

@linear-code

linear-code Bot commented Aug 9, 2026

Copy link
Copy Markdown

CL-5761

@TheGreatAxios
TheGreatAxios force-pushed the cl-5761-orchestrator-reporting branch from be3abea to 760af89 Compare August 9, 2026 01:34
Interrupting discarded every pending queue and steer item. The common
gesture is to type a correction and then interrupt so it lands sooner,
which meant the interrupt destroyed exactly the input the operator most
wanted delivered. Pending items now survive the stop and hand over at
the next drain boundary.
Interrupting was the only way to ask whether a fleet was progressing, and
it stopped the work to answer. /status reads the live session store and
prints one row -- running lanes with their clocks, which of them have
gone quiet, and the finished tally -- from the palette, which does not
touch the run.
Every lane completion, stall and failure already passed through the
parent session, which said nothing about any of it unless interrupted
and asked -- an operator watched half a fleet finish with the remaining
work undispatched and had to stop the run to find out. Lane changes now
drive the report directly, so a completion lands with what it produced
and a failure with what went wrong, at the moment it happens.

The noise boundary is the whole design: only transitions are reported,
a quiet lane is announced once rather than on every tick it stays quiet,
more than three changes in one observation collapse into a single tally
so a dozen lanes cost a line rather than a screen, and the fleet running
dry gets its own line because that is the silence that started this.
A stop does not reliably produce an idle event to drain against, so a
message the operator had queued sat in the badge with nothing left to
release it. The interrupt delivers it directly; the host serialises the
send behind the agent rebuild the same interrupt started.
Running a real fleet showed both halves of the noise boundary leaking.
A parallel dispatch arrives as one store change per lane, so six lanes
printed six dispatch lines instead of the one decision they were; the
report now settles briefly before observing, and six lanes read as
"6 dispatched". Long worker summaries also wrapped, costing two rows an
update, so a line is clipped to the width a transcript row has.
The deliver path caught failures silently on the theory a dropped message
was harmless. It wasn't: the shell had already popped the queue item and
painted it as delivered, and a failed rebuild left currentAgent pointing at
the closed agent, so deliver threw and the catch ate it. Extracted the guard
into a small testable function that checks fatalBuildError up front (mirroring
the send path) and routes any failure through the system notice.
…ty lands

Rebasing onto #429's call-id-keyed tool clock left FleetLane's structural type
one field short of SubAgentSession, so the fleet report and the agents panel
were reading two different lane shapes.
docs/TUI.md described Ctrl+C as discarding the queue, which stopped being
true once the interrupt started keeping pending input. It also had no mention
of the fleet-report notice channel added alongside the agents panel, so the
two surfaces read as unrelated when they draw from the same store and stall
definition. Also suppress the fleet's coalesced tally line when the idle
line already says the same thing verbatim.
@TheGreatAxios
TheGreatAxios force-pushed the cl-5761-orchestrator-reporting branch from 760af89 to 03c5ec2 Compare August 9, 2026 04:53
Ctrl+C JSDoc still said the path cleared pending items after the
implementation started keeping them. The fleet-report section credited
the 400ms settle timer with quiet detection and named /fleet; quiet is
polled on the 5s stall interval, settle only coalesces burst store
changes, and the command is /status. Kept-queue drain is at the
interrupt itself, not a later boundary via applyShellInterrupt.
Docs already split FLEET_REPORT_SETTLE_MS from FLEET_STALL_POLL_MS; the runner wiring comment still blamed quiet detection on the settle timer. Match the two-timer design so the next doc pass cannot re-poison from this call site.
@TheGreatAxios
TheGreatAxios merged commit 48908d2 into main Aug 9, 2026
2 checks passed
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.

1 participant