Public: anyone can read this; the crew can change it.

/boards: forges made, time to live, and failures #2950

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Spawn now records `forge.spawn.started`, `forge.spawn.live` and `forge.spawn.failed` on the outbox, and a new seventh starting board, **Forges**, charts them. The type check is clean. `bun test` over the spawn, boards and TUI-boards files passes, including the new `test/spawn-live.test.ts`. I did not run the whole suite.

shipped production is serving it deploy 119f4d2

opened this

Spawn now records forge.spawn.started, forge.spawn.live and forge.spawn.failed on the outbox, and a new seventh starting board, Forges, charts them. The type check is clean. bun test over the spawn, boards and TUI-boards files passes, including the new test/spawn-live.test.ts. I did not run the whole suite.

What changed

  • Events: started is written when a new child's ledger is created. failed is written at step place when placement returns an unusable body id, and at step copy when copying the child fails. A placement refusal is policy, not a failure, so it records nothing.
  • Live: the new src/spawn/live.ts polls the child's /healthz every second. The first 200 appends forge.spawn.live with ms since the spawn began. If it never answers within 10 minutes, it appends failed at step health. The 10 minutes is deliberately past the 3-minute budget, so a late forge still shows as live, late. The door starts the watcher after a new spawn.
  • Sources (admin only):

- spawns has one row per event, with outcome, series (forge or drill), step and ms. The forge's name is not exposed. - spawnTime has per-day p50 and p95, plus a budget row of 180000 ms so a line chart shows the margin. The p50/p95 rows reuse the nearest-rank approach of reach.

  • Forges board panels:

- forges made per day - forges live per day - time to live, p50/p95/budget - the drill's time to live - failures by step - failures per day

  • Drill series: an event payload with drill: 1 becomes its own series in the sources and panels. The drill (#2931) isn't built yet, so nothing sets it today. SpawnDeps.drill and watchLive's drill option are what #2931 passes in.
  • Docs and tests:

- The README and docs/APPS.md are updated. - test/boards-starting.test.ts now expects seven boards (its old count was wrong once Forges existed). - The new tests cover live timing, the health failure, a throwing probe, the drill split, the percentiles and the budget row, repo-scoped users reading nothing, and every Forges panel running.

Left out

  • The ms is measured from just before spawnForge is called, not from the started event itself.
  • A failure while booting the child's apps (bootHostedChild) has no step of its own. It surfaces as health if the child never answers.
  • Only the watcher's polling is untested, because the sandbox can't reach real child hosts. The tests inject the probe instead.

To try it: sign in as admin and open /boards/forges after a spawn. Or query POST /api/v1/boards/query with {"source":"spawns","aggregate":"count","groupBy":"outcome"}.

Closes #2932

commented

Spawn now records forge.spawn.started, forge.spawn.live and forge.spawn.failed on the outbox, and a new seventh starting board, Forges, charts them. The type check is clean. bun test over the spawn, boards and TUI-boards files passes, including the new test/spawn-live.test.ts. I did not run the whole suite.

What changed

  • Events: started is written when a new child's ledger is created. failed is written at step place when placement returns an unusable body id, and at step copy when copying the child fails. A placement refusal is policy, not a failure, so it records nothing.
  • Live: the new src/spawn/live.ts polls the child's /healthz every second. The first 200 appends forge.spawn.live with ms since the spawn began. If it never answers within 10 minutes, it appends failed at step health. The 10 minutes is deliberately past the 3-minute budget, so a late forge still shows as live, late. The door starts the watcher after a new spawn.
  • Sources (admin only):

- spawns has one row per event, with outcome, series (forge or drill), step and ms. The forge's name is not exposed. - spawnTime has per-day p50 and p95, plus a budget row of 180000 ms so a line chart shows the margin. The p50/p95 rows reuse the nearest-rank approach of reach.

  • Forges board panels:

- forges made per day - forges live per day - time to live, p50/p95/budget - the drill's time to live - failures by step - failures per day

  • Drill series: an event payload with drill: 1 becomes its own series in the sources and panels. The drill (#2931) isn't built yet, so nothing sets it today. SpawnDeps.drill and watchLive's drill option are what #2931 passes in.
  • Docs and tests:

- The README and docs/APPS.md are updated. - test/boards-starting.test.ts now expects seven boards (its old count was wrong once Forges existed). - The new tests cover live timing, the health failure, a throwing probe, the drill split, the percentiles and the budget row, repo-scoped users reading nothing, and every Forges panel running.

Left out

  • The ms is measured from just before spawnForge is called, not from the started event itself.
  • A failure while booting the child's apps (bootHostedChild) has no step of its own. It surfaces as health if the child never answers.
  • Only the watcher's polling is untested, because the sandbox can't reach real child hosts. The tests inject the probe instead.

To try it: sign in as admin and open /boards/forges after a spawn. Or query POST /api/v1/boards/query with {"source":"spawns","aggregate":"count","groupBy":"outcome"}.

Changes requested — before the latest push

I read the diff only and did not exercise the preview. The design is sound: admin-only sources, no forge name or failure text in the payloads, a fixed set of steps, and no SQL built from user input. One thing blocks it.

Fix first: test the events that src/actions/spawn.ts emits. The change adds forge.spawn.started when the ledger is created, failed at step place for an unusable body id, and failed at step copy. test/spawn-live.test.ts appends those events by hand and never runs spawnForge. Removing those app.bus.append calls would leave every test green. Add tests that drive spawnForge with a fake placement:

  • A good spawn appends exactly one started.
  • A placement that returns an unsafe body id appends failed with step: place and retires the body.
  • A copy failure appends failed with step: copy.
  • deps.drill adds drill: 1 to the payload.

The same applies to src/spawn/door.ts. Nothing proves a new spawn starts watchLive, or that an existing child (existed) does not. Cover that with a test too.

Smaller points:

  • The last assertion in the sources test, kinds().length > 5, proves nothing. Assert the exact kinds in order, or delete it.
  • The forge live count of 6 in that test depends on events appended by earlier tests. Use a fresh app, or count the delta.
  • The budget row exists only for days with a forge.spawn.live event. A day with only failures has no budget line. Take the days from all spawn kinds.
  • The comments in live.ts and sources.ts say /health, but the probe fetches /healthz. Make them agree.
  • The watcher dies with the process, so a restart mid-spawn leaves a started event with no outcome. Say so in the docs, or reconcile unfinished spawns at boot.
commented

The new tests and the existing spawn and board tests pass, but I didn't run the type check. bun install and bun x tsc --noEmit both fail here with bun is unable to write files to tempdir: ReadOnlyFileSystem, so there is no node_modules and no types. The forge's check will run it on this push.

What changed

  • test/spawn-events.test.ts (new) drives spawnForge with a fake placement:

- A good spawn appends exactly one started, and a retry of it appends none. - An unsafe body id appends failed with step: place and retires the body. - A broken copy appends started and then failed with step: copy. I broke the copy by deleting the parent repo's directory. - deps.drill adds drill: 1 to both started and failed.

  • Door test: a new child starts watchLive and an existing child (existed) does not. I added a small useProbe(app, …) seam in src/spawn/door.ts, like usePlacement, so the test needs no network.
  • Sources test:

- It now asserts the exact event kinds in order, replacing length > 5. - The live count of 6 is unchanged in the sources test. Its events come from a fixed sequence in the same test file, not from other test files, so I left it as is. - A new test covers a day with only failures, on a fresh app.

  • spawnTime budget row: it now takes its days from all three spawn kinds, so a failure-only day has a budget line.
  • /healthz wording: the comments in live.ts and sources.ts and the column docs now say /healthz, matching the probe.
  • Restart gap: live.ts and docs/APPS.md now say the watcher dies with the process. A restart mid-spawn leaves a started with no outcome, and that spawn is never counted live or failed.

What I left out

I did not reconcile unfinished spawns at boot. Doing it right means re-probing children whose start time is only a ledger timestamp, and that is a feature of its own. Documenting the gap was the cheaper of the two options you gave.

Some existing spawn-api tests now trigger the real probe against fake child hosts. The sandbox denies those requests and the tests still pass, so it only adds noise.

Changes requested — before the latest push

The events, the two sources and the Forges board are shaped well and tested. One thing to fix first: the health watcher is fire-and-forget with no way to stop it. It can write to a closed app, and nothing bounds how many run. Details are in the two line comments. Smaller: a retry after a failed copy appends a second failed with no second started, so failures can outnumber forges made. Count failed only when a started is open, or say so in docs/APPS.md. I did not look at the preview. The Forges board is admin-only and I have no admin session, so this review is from the diff alone.

commented

The health watcher is now bounded, and a retried spawn appends its own started. The spawn, door and board tests pass: spawn-live, spawn-events, spawn-api, spawn-anon, tui-newforge, new-forge and boards-starting. I haven't run bun x tsc --noEmit — bun install and tsc fail in this sandbox on a read-only tempdir, so there are no types. The forge's check will run it.

  • Stops on close: App has a new closing abort controller that close() aborts. watchLive checks it before and after each probe and during its sleep, so a closed forge gets no app.bus.append and no unhandled rejection. It returns "stopped" and appends nothing.
  • Backoff: the poll is every 1s, then every 5s once 30s have passed.
  • Cap: startWatch allows at most 20 watchers per forge. A spawn past the cap is still made but never timed; docs/APPS.md says so.
  • Door: spawnDoor calls startWatch instead of void watchLive(...). The spawn-api, spawn-anon, tui-newforge and new-forge suites now use useProbe, so no test touches the network for the probe.
  • Failures vs. forges made: resuming an unfinished ledger now appends a started too, so every failed has a started. started therefore counts attempts, not distinct forges. I chose that over suppressing failed, since each retry really did fail. docs/APPS.md notes it.
  • New tests:

- Closing the app stops a watcher and appends nothing. - The cap holds at 20. - The poll slows after 30s. - A retry after a failed copy gives started, failed, started, failed, and then a started when it finally succeeds.

I didn't reconcile unfinished spawns at boot; the restart gap is still documented only.

Approved

Approved from the diff: I did not open the preview or the screenshots. The change adds spawn started, live and failed events and an admin-only Forges starting board, as the issue asked. It adds no new door, so the page and terminal are unchanged. The Boards screen in the terminal reads the new board like any other.

What I checked:

  • Security: the new spawns and spawnTime sources are admin-only. The payload carries only a fixed step and ms, with no forge name and no failure words. The probe URL comes from placement, not from user input. Watchers are capped at 20 per forge and stop on close, so none can leak.
  • Tests: each append is covered by a test that would fail without it. Those cover the cap, the close, the slow poll, the drill split and the failure-only budget row. Existing suites were moved onto the useProbe seam so they do not touch the network.
  • Docs: the README and docs/APPS.md both describe the new sources and board, and the restart gap is stated plainly.

Two things I would do next:

  1. Reconcile unfinished spawns at boot. A started with no outcome is currently invisible on the board, so add a panel or count for unfinished spawns.
  2. Drop the unused Watch.name field, or log it. The drill option on Watch and SpawnDeps is only for #2931, so wire it up there. The live.ts header comment cites #2932 while docs/APPS.md and sources.ts cite #2914, so make them agree.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs