Skip to content

feat(tui): the supervisor TUI moves into the runtime that produces its state - #689

Merged
drewstone merged 2 commits into
mainfrom
feat/tui
Aug 1, 2026
Merged

feat(tui): the supervisor TUI moves into the runtime that produces its state#689
drewstone merged 2 commits into
mainfrom
feat/tui

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Extracts the htop-style supervisor/worker TUI from the loops pi extension into agent-runtime as a ./tui subpath plus an agent-runtime-top bin.

Why here

The TUI reads the .agent/supervisor/<id> run layout that agent-runtime owns and publishes (supervisorRunsRoot, supervisorRunDir, workerInboxFile, writeWorkerSteer). A reader shipped separately from its writer version-skews against the layout it renders — the exact failure that cost this stack hours when cli-bridge's Python bridge required a field the npm client did not send. Reader and writer now ship together.

It also arrives free of baggage: zero third-party dependencies (raw ANSI + node:readline), and zero pi imports across 1,380 lines. It was trapped in a pi extension by history, not by design.

Extraction, not redesign

Names preserved exactly — loadTopSnapshot, renderTopFrame, renderTopFrameWithLayout, TopSnapshot, RenderOptions, RenderTarget, RenderedTopFrame — plus runTopApp and renderTopOnce as entry points. Hand-joined paths are replaced by the owned helpers, which is the point: a copy of the layout contract becomes a consumer of it.

Wiring the reader to its writers exposed that they disagreed

Four bugs, all real, all found by connecting the two halves rather than by reading:

The blocking one, caught by adversarial review after my own proof missed it: state.json and the spawn journal have different producers and their ids need not agree. The journal's root is the runtime's runId, which defaults to a value unrelated to the supervisor id the state file records. Only the spawned branch guarded for that; metered, settled, and cancelled checked state.id alone. So every driver event minted a worker named for the runId — one that never settles, sorts first as RUNNING so it is the default-selected row, holds the driver's spend while the driver's own totals read $0, and captures steers into an inbox nothing will ever read.

Measured on the default path: workers 4 / live 1 where the truth was 3 / 0.

The journal's root is now recovered from its own parentless root spawn, and either id counts as root in all four branches. A regression pins the mismatch case — the one my original proof was constructed to avoid.

Proof

A real createSupervisor().run over three workers, journalled by the runtime's own createFileRunContext, then the built bin pointed at that directory. No hand-written journal lines; the supervisor wrote spawn-journal.jsonl itself and the steer came from writeWorkerSteer.

Gates

tsc 0 · biome clean (540 files) · 2244 passed / 6 skipped across 198 files · docs freshness OK · typedoc 0 warnings · publint validates the new ./tui export and bin against a real pnpm pack · built artifact load-checked.

loops follow-up (not in this PR)

Sequence after refactor/pi-through-bridge lands: delete src/top.ts, src/top-model.ts, tests/top-model.test.ts; point pnpm top at the bin; drop the knip entry. Two visible changes — the header reads SUPERVISOR TOP, and the shell key's exported env vars are renamed LOOPS_*AGENT_*.

The terminal view over live supervisor runs lived in the loops repo, where it
carried its own hand-joined copy of the on-disk layout:

    [join(root, '.agent', 'supervisor'), join(root, '.loops', 'supervisor')]

agent-runtime owns and exports that contract, and agent-runtime is what WRITES
the state the view renders. A reader versioning independently of its writer
drifts; keeping the two in one package makes drift impossible by construction.

Moved verbatim except where a hand-joined path became a call into the contract:

  src/tui/top-model.ts  read the layout into a TopSnapshot, render an ANSI frame
  src/tui/top-app.ts    keypress/mouse loop, steer and cancel controls
  src/tui/bin.ts        agent-runtime-top [root] [--once] [--no-color]

Three fixes fall out of consuming the contract rather than copying it:

- The view read only `journal.jsonl`, the retired bare-event file. This repo's
  own `createFileRunContext` writes `spawn-journal.jsonl`, so the view showed
  zero workers for every current run. Both names are read, newest first.
- Worker tails are keyed by file stem, which the writer puts through
  `safeWorkerFile`; a label that stem alters never found its own events.
- `<label>.inbox.ndjson` was read as if it were a control-event log.

Steers go through `writeWorkerSteer` — the snapshot already carries the label
the layout keys inboxes by, so no id-to-label resolution sits in between.

run-layout gains the accessors the view needs so it never joins a path itself:
`legacySupervisorRunsRoot` (enumerating pre-rename runs), `supervisorWorkersDir`,
`workerControlLogFile`. The layout doc already named those files; nothing
exported them. The existing writers now use them too.

Zero new dependencies: raw ANSI and node:readline, as before.

Proof: a real `createSupervisor().run` journalled by `createFileRunContext`,
then `node dist/tui/bin.js <root> --once` renders its three workers, token
bars, latency histogram, and the steer appended by `writeWorkerSteer`.
state.json and the spawn journal have different producers and their ids
need not agree: the journal's root is the runtime's runId, which defaults
to a value unrelated to the supervisor id the state file records.

Only the spawned branch carried a second guard for that. The metered,
settled, and cancelled branches checked state.id alone, so every driver
event minted a worker named for the runId — one that never settles, sorts
first as RUNNING so it is the default-selected row, holds the driver's
spend where the driver's own totals then read zero, and captures steers
into an inbox nothing reads.

The journal's root is now recovered from its own parentless root spawn and
either id counts as root in all four branches. The regression pins the
mismatch case the original proof was constructed to avoid.

@tangletools tangletools left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Auto-approved drewstone PR — 79bc444c

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

tangletools · auto-approval · reason: drewstone_author · 2026-08-01T08:08:11Z

@tangletools tangletools left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Value Audit — sound-with-nits

Verdict sound-with-nits
Concerns 3 (1 medium-concern, 1 low, 1 weak-concern)
Heuristic 0.0s
Duplication 0.0s
Interrogation 115.5s (2 bridge agents)
Total 115.5s

💰 Value — sound-with-nits

Promotes the supervisor-run TUI from an external pi extension into the runtime that writes its state, co-locating reader and writer so they can't drift — a sound move that also surfaced and fixed a real phantom-worker bug.

  • What it does: Extracts the htop-style supervisor/worker terminal view (1,380 lines, zero third-party deps) from the loops pi extension into agent-runtime as a ./tui subpath export plus an agent-runtime-top bin. It reads the .agent/supervisor/<id> run layout the runtime owns and renders live runs/workers with spend, token, and latency bars; it writes back via two operator controls — steer a worker (throu
  • Goals it achieves: Eliminate reader/writer version skew: the TUI is the reader for a layout the runtime writes, and a separately-versioned reader is exactly the failure that bit this stack (cli-bridge's Python bridge vs its npm client). By consuming the SAME run-layout.ts helpers the writers use, the contract has one definition instead of a hand-joined copy. The payoff is concrete — wiring the reader to the writer
  • Assessment: Sound and in the grain of the codebase. The central architectural argument is correct and self-proving: the phantom-worker bug was found BY connecting the two halves, which is the strongest possible evidence that co-location is the right move. The fix (recover the journal's own root id from its parentless label:'root' spawn at top-model.ts:319-322 and treat either id as root) is correct and prec
  • Better / existing approach: none — this is the right approach. The only inconsistency with the change's own thesis is noted below.
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 2
  • Bridge warning: opencode/kimi-for-coding/k2p7: opencode: opencode error

🎯 Usefulness — sound-with-nits

A well-wired, zero-dependency extraction of the supervisor TUI that consumes the owned run-layout contract; observability and steer are fully live, but the cancel control writes a file no reader in this repo consumes.

  • Integration: Fully reachable. package.json:97-101 exports the ./tui subpath and package.json:107 registers the agent-runtime-top bin; tsdown.config.ts:24-25 builds both entry points; vitest.config.ts:20-22 and typedoc.json:20 wire test/doc resolution; scripts/gen-primitive-catalog.mjs:80 lists the surface. The reader consumes the OWNED layout helpers imported at src/tui/top-model.ts:17-22 (`supervisorRunsR
  • Fit with existing patterns: Fits cleanly. Zero third-party deps (raw ANSI + node:readline), matching other dependency-free modules. Follows the established bin pattern (mcp/bin, loop-runner-bin in tsdown.config.ts). No competing supervisor viewer exists — grepping src outside tui for TUI/renderTop/loadTopSnapshot returned nothing. The extraction rationale is real: a separately-versioned reader would skew from the on-disk lay
  • Real-world viability: Reader is defensively built: corrupt journal lines and half-written files are skipped (src/tui/top-model.ts:296-298, 1008-1012); the state.json-vs-journal root-id disagreement is handled and proven by a test against the real FileSpawnJournal (tests/tui/top-model.test.ts:24-64). The steer path is durable (best-effort control log + durable inbox append, run-layout.ts:170-185). The weak spot is the c
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 1

🔎 Heuristic Signals

🟡 Cruft: magic number added src/tui/top-app.ts

  • timer = setInterval(draw, 1000)

💰 Value Audit

🟡 Cancel write hand-joins a path, breaking the PR's own 'no hand-joined paths' rule [against-grain] ``

The PR's stated principle (top-model.ts:1-13, top-app.ts:1-13) is that the reader becomes a CONSUMER of the owned layout helpers, not a copier of path joins — and the read side plus the steer write both honor this (steer calls writeWorkerSteer). But the cancel control still hand-joins: join(supervisor.stateDir, 'cancel.request.json') at src/tui/top-app.ts:395, with no corresponding cancelRequestFile(dir) helper promoted from run-layout.ts. To stay consistent with the change's grain, promot

🎯 Usefulness Audit

🟠 Cancel control writes a file no reader owns — contradicts the PR's owned-layout premise [problem-fit] ``

src/tui/top-app.ts:393-406 hand-writes cancel.request.json via raw writeFileSync(join(supervisor.stateDir, 'cancel.request.json')). This path is NOT part of the owned run-layout contract (no helper, no export in src/runtime/index.ts), directly contradicting the PR thesis that 'hand-joined paths are replaced by the owned helpers.' Grep across the whole repo finds NO reader of cancel.request.json — the only match is the TUI's own write. The real cancellation paths are in-process: the super


What this audit checks

It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.

Pass What it asks
Heuristic Vague title? Whitespace-only or cruft-bearing diff? (content signals only)
Duplication Do added function/class names already exist elsewhere in the repo?
Value Audit What does it do? What goal does it achieve? Is it good? Better architecture or already-exists?
Usefulness Audit Does it integrate and fit? Will it hold up in real use and actually get used?

Findings are concerns, not blocks — the human reviewer decides what to do with them.

value-audit · 20260801T081036Z

@drewstone
drewstone merged commit 72913a1 into main Aug 1, 2026
4 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.

2 participants