Repository navigation
An authored spawner never hears an unsolicited child exit: stopped is only ever the answer to its own stop effect #9227
Description
Summary
An authored program that spawns a child with spawn(program, {on}) never hears that the child ended on its own. The authoring layer produces a stopped event in exactly one place — stopHandler in apps/tuval/src/authoring/define-program.ts, as the answer to the program's own stop effect. A child that exits unsolicited publishes {kind: "stopped"} to the ProcessTable stream (apps/tuval/src/process/Processes.ts) and its live finalizer in apps/tuval/src/commands/core/process.ts deletes the entry; nothing subscribes to that and dispatches a stopped into the spawner's inbox.
What I was doing
Review of the cron program (#9225, PR #9226): the reviewer graded criterion 2's middle clause — "a child that ends before answering is recorded as a failed run" — and found the stopped cell unreachable under the live kernel. git grep of stopped( / "stopped" across apps/tuval/src at 0e6b5750 finds no producer other than stopHandler. Verdict: #9226 (comment).
What I observed
cron.unit.test.tsdrives thestoppedcell by hand throughtestProgram, so the cell is proven and the behaviour is not.- Live: a job that crashes before
resultleaves cron'schildset, and every later tick is dropped until a restart. The same holds for thepr-reviewexample: a reviewer session that dies beforeverdictleaves it waiting forever. spawned(Every authored spawn logs an error: the spawned event has no cell in the update table #8927) and theon-routed child out-ports (An authored ask never gets its reply, and spawn's child-port routing is dropped #8756, landed in feat: An authored ask never gets its reply, and spawn's child-port routing is dropped #8845) are delivered; the child's end is the one lifecycle edge that is not.
Why it matters
spawn with on is the authoring layer's whole story for "start a program and hear back". Without the end-of-life edge every authored parent has a silent hang mode for a crashed child, and the only recovery is a restart (which cron reconciles on resume; other programs will not).
Pointers
apps/tuval/src/authoring/define-program.ts—stopHandler, the onlystoppedproducerapps/tuval/src/process/Processes.ts— theProcessTablestoppedpublicationapps/tuval/src/commands/core/process.ts—spawnwithon, thelivefinalizer;onrouting applied at the child's emit (feat: An authored ask never gets its reply, and spawn's child-port routing is dropped #8845)apps/tuval/src/process/inbox.ts—deliver, the primitive feat: An authored ask never gets its reply, and spawn's child-port routing is dropped #8845 added for answering into a running process- feat: An authored ask never gets its reply, and spawn's child-port routing is dropped #8845 / An authored ask never gets its reply, and spawn's child-port routing is dropped #8756, Every authored spawn logs an error: the spawned event has no cell in the update table #8927, Tuval has no scheduler: a cron program on defineProgram that starts a job program on a timer and reports each run on its tile #9225
Suggested next step (non-binding)
Where on routing is applied at the child's emit, also apply it at the child's end: when a child spawned with a parent leaves the table on its own, deliver a stopped for that child into the parent's inbox, distinguishable from the answer to the parent's own stop. The existing stopped event shape already carries the process id.
Acceptance criteria
- 1. A child spawned with a parent that leaves the process table on its own — ended from outside, or crashed — reaches its spawner as the authored
stoppedevent for that child, delivered into the spawner's inbox (apps/tuval/src/process/inbox.ts). - 2. One child end is one
stopped. Astopthe parent issued itself does not produce a second event, and it is made unwritable rather than filtered: one of the two producers no longer exists, with no dedupe flag anywhere. - 3. The delivery cannot fail or wedge the child's finalizer, including when the spawner is itself already stopped or is the very fiber that closed the child.
- 4. A restored child (
apps/tuval/src/durability/restore.ts, which spawns with noon/parent wiring) is stated in a docblock as still uncovered; restore is not changed here. - 5.
apps/tuval/src/cron/cron.ts— thestoppedcell's docblock, the module header and the test name that today say the branch is unreachable "until An authored spawner never hears an unsolicited child exit: stopped is only ever the answer to its own stop effect #9227" are true again: they state the behaviour cron now has. - 6. A test drives the whole path through the real kernel — spawn through the compiled handlers, end the child from outside, assert the parent's
stoppedran — nottestProgram(pattern: the real-kernel spawn test A spawn on a program-valued arg looks up the arg's service key as a program id #8955 added indefine-program.unit.test.ts). Its assertion fails if the delivery is removed. - 7. Gates from
apps/tuval:pnpm typecheck,npx biome check src/commands src/process src/authoring src/cron,pnpm vitest run --project unit,pnpm vitest run --project integration, plus a clean boot withfeatures.cronon and noHandlerFailed. -
authoring/effect.tsno longer exports astopped(process)constructor with no caller; the finalizer literal incommands/core/process.tsis the event's only producer and the only place its shape is built.
Filed by an agent on behalf of cansirin · from the review of PR #9226 · 2026-09-15
Activity
docs(tuval): cron's `stopped` cell says what the kernel actually deli…
feat(tuval): a child's end reaches its spawner, from one producer