Public: anyone can read this; the crew can change it.
/boards: forges made, time to live, and failures #2950
Check · Review · Land · Live done — Landed and live in production.
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
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:
startedis written when a new child's ledger is created.failedis written at stepplacewhen placement returns an unusable body id, and at stepcopywhen copying the child fails. A placement refusal is policy, not a failure, so it records nothing. - Live: the new
src/spawn/live.tspolls the child's/healthzevery second. The first 200 appendsforge.spawn.livewithmssince the spawn began. If it never answers within 10 minutes, it appendsfailedat stephealth. 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: 1becomes its own series in the sources and panels. The drill (#2931) isn't built yet, so nothing sets it today.SpawnDeps.drillandwatchLive'sdrilloption 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
msis measured from just beforespawnForgeis called, not from thestartedevent itself. - A failure while booting the child's apps (
bootHostedChild) has no step of its own. It surfaces ashealthif 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"}.
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
failedwithstep: placeand retires the body. - A copy failure appends
failedwithstep: copy. deps.drilladdsdrill: 1to 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
forgelive count of 6 in that test depends on events appended by earlier tests. Use a fresh app, or count the delta. - The
budgetrow exists only for days with aforge.spawn.liveevent. A day with only failures has no budget line. Take the days from all spawn kinds. - The comments in
live.tsandsources.tssay/health, but the probe fetches/healthz. Make them agree. - The watcher dies with the process, so a restart mid-spawn leaves a
startedevent with no outcome. Say so in the docs, or reconcile unfinished spawns at boot.
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) drivesspawnForgewith 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
watchLiveand an existing child (existed) does not. I added a smalluseProbe(app, …)seam insrc/spawn/door.ts, likeusePlacement, 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.
spawnTimebudget row: it now takes its days from all three spawn kinds, so a failure-only day has a budget line./healthzwording: the comments inlive.tsandsources.tsand the column docs now say/healthz, matching the probe.- Restart gap:
live.tsanddocs/APPS.mdnow say the watcher dies with the process. A restart mid-spawn leaves astartedwith 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.
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.
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:
Apphas a newclosingabort controller thatclose()aborts.watchLivechecks it before and after each probe and during its sleep, so a closed forge gets noapp.bus.appendand no unhandled rejection. It returns"stopped"and appends nothing. - Backoff: the poll is every 1s, then every 5s once 30s have passed.
- Cap:
startWatchallows at most 20 watchers per forge. A spawn past the cap is still made but never timed;docs/APPS.mdsays so. - Door:
spawnDoorcallsstartWatchinstead ofvoid watchLive(...). Thespawn-api,spawn-anon,tui-newforgeandnew-forgesuites now useuseProbe, so no test touches the network for the probe. - Failures vs. forges made: resuming an unfinished ledger now appends a
startedtoo, so everyfailedhas astarted.startedtherefore counts attempts, not distinct forges. I chose that over suppressingfailed, since each retry really did fail.docs/APPS.mdnotes 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 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:
- Reconcile unfinished spawns at boot. A
startedwith no outcome is currently invisible on the board, so add a panel or count for unfinished spawns. - Drop the unused
Watch.namefield, or log it. Thedrilloption on Watch and SpawnDeps is only for #2931, so wire it up there. Thelive.tsheader comment cites #2932 while docs/APPS.md and sources.ts cite #2914, so make them agree.
Spawn now records
forge.spawn.started,forge.spawn.liveandforge.spawn.failedon the outbox, and a new seventh starting board, Forges, charts them. The type check is clean.bun testover the spawn, boards and TUI-boards files passes, including the newtest/spawn-live.test.ts. I did not run the whole suite.What changed
startedis written when a new child's ledger is created.failedis written at stepplacewhen placement returns an unusable body id, and at stepcopywhen copying the child fails. A placement refusal is policy, not a failure, so it records nothing.src/spawn/live.tspolls the child's/healthzevery second. The first 200 appendsforge.spawn.livewithmssince the spawn began. If it never answers within 10 minutes, it appendsfailedat stephealth. 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.-
spawnshas one row per event, withoutcome,series(forgeordrill),stepandms. The forge's name is not exposed. -spawnTimehas per-day p50 and p95, plus abudgetrow of 180000 ms so a line chart shows the margin. The p50/p95 rows reuse the nearest-rank approach ofreach.- 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: 1becomes its own series in the sources and panels. The drill (#2931) isn't built yet, so nothing sets it today.SpawnDeps.drillandwatchLive'sdrilloption are what #2931 passes in.- The README and
docs/APPS.mdare updated. -test/boards-starting.test.tsnow 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
msis measured from just beforespawnForgeis called, not from thestartedevent itself.bootHostedChild) has no step of its own. It surfaces ashealthif the child never answers.To try it: sign in as admin and open
/boards/forgesafter a spawn. Or queryPOST /api/v1/boards/querywith{"source":"spawns","aggregate":"count","groupBy":"outcome"}.Closes #2932