- claim the lang42 prefix; iteration 42 story (readiness: ready), approved spec, and the 11-task implementation plan - board: pending row for 42; graph: node 42 with green edges (11, 24) - graph: porch track section added (same sweep) - parity studies that motivated 42: alacritty, tmux, zen-browser under docs/plan/exploration/ — staged path, gap lists, refused routes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.8 KiB
| track | iteration | status | readiness |
|---|---|---|---|
| language-runtime-database | 42 | pending | ready |
42 — bounded subprocess: spawn, supervise, and reap a child process
Part of Story — one language, one runtime, one database. Spec:
2026-09-01-bounded-subprocess-design.md(brainstormed and approved 2026-09-01; the six forks below are settled). First named as iteration 28's leading gap ("bounded subprocess, stdin/stdout transport"), promoted to its own iteration on 2026-09-01 when three further consumers arrived at once — the alacritty, tmux and zen-browser studies all bottom out on it as their first domino.The problem. A one-shot
proc.runalready exists (runtime/src/sysio.c,WO_B_PROC_RUN: fork/execvp on an argv vector, stdout/stderr captured through pipes, exit code returned) — but it is bounded in nothing that matters and blocking in the one place it must not be. It runs on the shard thread, not parked in a fiber, so one slow child stalls every actor on that shard; it has no deadline, and itswaitpidloop notices a runtime stop only if a signal happens to interrupt it; its output caps (8,192 / 4,096 bytes, fixed) truncate silently rather than refuse; there is no stdin, no streaming, no concurrency ceiling, and no story for what happens to a child when its owner dies. Suspected but not yet reproduced: a child that fills the stdout pipe past the cap while the parent is waiting for stderr EOF deadlocks both — needs a test before it is claimed. Every workload class beyond "server that owns all its state" — a script runner (28), a terminal (alacritty stage A), a multiplexer (tmux), a browser driver (zen C′) — begins by starting a process and ends by being responsible for it.
Why bounded — the doctrine half
The feature is deliberately not "exec". Every resource a child can consume carries a declared ceiling, and exceeding it is a refusal by name, never a hang or an OOM — the same fail-closed posture porch 1 proved for pool saturation and iteration 31 for mailbox caps:
- Time — a deadline after which the child is killed and the caller resumes with a named timeout error.
- Output bytes — a cap on captured stdout/stderr; past it the child is killed and the error names the cap, because an unbounded pipe buffer is an unbounded allocation.
- Concurrency — a ceiling on live children per program; at the ceiling, spawning fails closed rather than queueing invisibly.
- Lifetime — a child is owned by the actor that spawned it. Owner dies, child is killed and reaped. Program stops, children are terminated before exit (iteration 40's drain guarantee extends to them). A zombie or an orphan is a bug by definition, not a caveat.
Info — the forks, settled (brainstorm 2026-09-01)
- Retrofit, one-shot only. The existing
proc.rungains the bounds in place (deadline, declared output caps, fiber parking, owner-bound reaping, per-shard ceiling). The streaming form — long-lived child, stdout as mailbox messages, what tmux/alacritty/zen need — is the named follow-up, building on this slice's registry and pidfd machinery. - Transport — died with fork 1: a one-shot form streams nothing. Settled by the follow-up when it arrives.
- pidfd, no signal seam. Child exit is a pollable fd (
pidfd_open, Linux 5.3+) registered in the shard's io_uring beside the pipes and the deadline timer; the fiber parks in the iteration 35_dlmould. The general signals-as-events seam stays deferred by name to its real consumer (the multiplexer study's stage C). - Argv only; a shell form is refused permanently. A caller wanting a
shell types
sh -cinto their own argv and owns the quoting risk; consumer #1 is a confined script runner and must never get a shell-shaped API. - Env and cwd stay inherited, zero knobs. No consumer demands
control; skillhost confinement owns env scrubbing when it arrives, and
a clean-env knob breaks
execvppath search. - Ids.
WO_B_PROC_RUNkeeps its id and gains parking + defaults; the extended bounded form takes the next free id from 96+ (89/90 remain 31's holes); no.wobbump (38's precedent).
Bounds surface: bare proc.run(cmd, args) gets named defaults (30 s,
1 MiB stdout, 64 KiB stderr); an extended form states them per call;
exceeding any bound kills the child and raises a catchable error naming
the bound — silent truncation is removed.
Acceptance criteria — firmed in the spec, normative form there
- Given a child that exits normally, when it is run, then its exit code is an ordinary value in the language and both output streams are readable.
- Given a child that outruns its deadline, when the deadline passes, then the child is dead (verified by pid absence, not assumed), the caller has a named timeout error, and no fd has leaked.
- Given a child whose output exceeds the byte cap, then the refusal names the cap and the child is dead — measured with a deliberately chatty child.
- Given an owning actor that dies while its child lives, then the child is reaped — verified from the outside via the process table.
- Given SIGTERM to the program while children live, then every child is terminated before the program exits — the drain gate (iteration 40's battery) gains a subprocess leg.
- Given a spawn loop of one thousand sequential children, then the program's fd count is flat (the iteration 24 measurement style) and rss does not grow with the loop.
- A runnable example under
docs/examples/with ajustgate, in the residency/chat mould — the gate is the acceptance, not the unit tests alone.
Out of scope, by name
- PTY allocation — pipes only. Giving the child a pseudo-terminal is the alacritty/tmux stage-A follow-up and has its own ioctl surface.
- fd passing (SCM_RIGHTS), termios, terminfo — the tmux study's stage-C bill, separate iterations.
- WebSocket client — zen C′'s remaining item, not process work.
- A general signal API for user code — only if fork 3 resolves toward the private reaping path; otherwise this iteration carries the seam but not the surface.
- Pipelines between children — composition can wait for a consumer.
Consumers, for the record
Iteration 28 (script runner, "bounded subprocess first" was already its stated order); alacritty study stage A (headless PTY runner — needs this plus PTY); tmux study (spawn half of its kernel); zen study stage C′ (spawn the browser, then drive it). Four consumers is the most any single named gap has accumulated.