docs(rt2): track-wide brainstorm — all five iterations ready, graph remapped

- spec 2026-09-01-runtime-v2-design.md: the one principle (PULL — a
  child is fds, the net verbs drive them; runtime-v2 adds acquisition
  verbs, never transport), the full surface (ids 97+: spawn/spawn_pty/
  wait_dl/signal/resize, signal.on delivering the sig number, term.raw/
  restore with runtime-guaranteed restore, send_fd/recv_fd/connect_unix),
  actor-owned lifecycle, mechanics notes, refusals by name
- push transport rejected with reasons recorded (mailbox-cap collision,
  new delivery machinery); death-notice verb refused (a two-line fiber
  composes wait_dl)
- five stories flip readiness: ready; fork sections rewritten as settled
- graph section 6 remapped: pull broke the 1->2->3 chain — only 1->2
  remains; 3, 4, 5 and the VTE grid startable alone today
- board section + registry follow; linkcheck 0 broken

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit d313cdeebbe53c83b83f31f4480631568d9d0743)
This commit is contained in:
shoney.arickathil 2026-09-01 23:08:10 +02:00
parent ab8ef64cd9
commit 3190b609af
10 changed files with 213 additions and 107 deletions

View file

@ -322,7 +322,8 @@ exists to make visible. Streaming responses' runtime prerequisites
The tmux study's gaps, remapped as buildable edges now that iteration 42 The tmux study's gaps, remapped as buildable edges now that iteration 42
landed. Every yellow node is an iteration of the landed. Every yellow node is an iteration of the
[runtime-v2 track](stories/runtime-v2/00-story.md) ("the runtime beyond [runtime-v2 track](stories/runtime-v2/00-story.md) ("the runtime beyond
sockets", its own folder since 2026-09-01), brainstormed on demand — sockets"), ALL `readiness: ready` since the track-wide brainstorm
([spec](superpowers/specs/2026-09-01-runtime-v2-design.md), 2026-09-01) —
[1 streaming subprocess](stories/runtime-v2/01-streaming-subprocess.md) · [1 streaming subprocess](stories/runtime-v2/01-streaming-subprocess.md) ·
[2 PTY](stories/runtime-v2/02-pty.md) · [2 PTY](stories/runtime-v2/02-pty.md) ·
[3 signals as events](stories/runtime-v2/03-signals-as-events.md) · [3 signals as events](stories/runtime-v2/03-signals-as-events.md) ·
@ -351,21 +352,22 @@ flowchart TD
I42w --> GSTREAM I42w --> GSTREAM
GSTREAM --> GPTY GSTREAM --> GPTY
GPTY --> GSIG
GSTREAM --> GVTE
GPTY --> WMUX GPTY --> WMUX
GSIG --> WMUX GSIG --> WMUX
GTERMIOS --> WMUX GTERMIOS --> WMUX
GFDPASS --> WMUX GFDPASS --> WMUX
GVTE --> WMUX GVTE --> WMUX
TINFO -.settled at brainstorm.-> WMUX TINFO -.settled at wmux's brainstorm.-> WMUX
TMONO -.v2.-> WMUX TMONO -.v2.-> WMUX
``` ```
Sibling reuse, for the record: the alacritty study's Wayland stage D **Remapped by the 2026-09-01 track brainstorm** (pull transport: a child
reuses GFDPASS + GVTE; the zen CDP driver shares GSTREAM only; skillhost is fds, the net verbs drive them): the old 1→2→3 chain broke — signals
(28) consumes GSTREAM's stdin transport. GTERMIOS and GFDPASS have no never needed PTY, only the resize pairing, and the VTE grid needs no
incoming edges — startable any time, alone. subprocess (a replay corpus feeds it). Only 1 → 2 remains chained;
**3, 4, 5 and the grid are all startable alone, today.** Sibling reuse:
the alacritty Wayland stage reuses GFDPASS + GVTE; the zen CDP driver
shares GSTREAM only; skillhost (28) consumes GSTREAM's stdin transport.
## Maintenance rule ## Maintenance rule

View file

@ -46,7 +46,7 @@ features cannot collide.
| `query-corpus` | databasev2 query-grammar corpus #1 | on `dev` (`4c82461`). Conclusion was "no new grammar needed" | | `query-corpus` | databasev2 query-grammar corpus #1 | on `dev` (`4c82461`). Conclusion was "no new grammar needed" |
| `lang42` | iteration 42 — bounded subprocess: `proc.run` bounded + parked (pidfd, caps, ceiling, owner-bound reaping), `proc.run_dl`; carries the alacritty/tmux/zen parity studies and the porch dependency-graph section from the same sweep | ✅ on `master` 2026-09-01 | | `lang42` | iteration 42 — bounded subprocess: `proc.run` bounded + parked (pidfd, caps, ceiling, owner-bound reaping), `proc.run_dl`; carries the alacritty/tmux/zen parity studies and the porch dependency-graph section from the same sweep | ✅ on `master` 2026-09-01 |
| `wmux` | the wmux track (`docs/stories/wmux/`, iteration 1 was language 43) — the terminal multiplexer, first of the softwares built with writeonce; story + gap-chain remap first, code follows gap by gap | on `dev` 2026-09-01 | | `wmux` | the wmux track (`docs/stories/wmux/`, iteration 1 was language 43) — the terminal multiplexer, first of the softwares built with writeonce; story + gap-chain remap first, code follows gap by gap | on `dev` 2026-09-01 |
| `rt2` | the runtime-v2 track (`docs/stories/runtime-v2/`) — the runtime beyond sockets: streaming subprocess, PTY, signals-as-events, termios, fd passing; five refine stories, wmux is the driving workload | on `dev` 2026-09-01 | | `rt2` | the runtime-v2 track (`docs/stories/runtime-v2/`) — the runtime beyond sockets: streaming subprocess, PTY, signals-as-events, termios, fd passing; all five `readiness: ready` (track-wide spec 2026-09-01), wmux is the driving workload | on `dev` 2026-09-01 |
## Cherry-picks onto master ## Cherry-picks onto master

View file

@ -1181,17 +1181,22 @@ this track adds the missing third — **processes, terminals, signals** —
five builtin-sized seams, each `runtime/src/` work with a `types.ml` row five builtin-sized seams, each `runtime/src/` work with a `types.ml` row
as its whole compiler cost (the iteration 42 precedent). Iteration 42 as its whole compiler cost (the iteration 42 precedent). Iteration 42
(bounded subprocess, ✅ on `master` 2026-09-01) opened the arc from the (bounded subprocess, ✅ on `master` 2026-09-01) opened the arc from the
language track before it had a name. Build order 1 → 2 → 3; 4 and 5 language track before it had a name. **All five `readiness: ready`** —
startable alone. All ⬜ `refine`; edges in one track-wide brainstorm settled every fork
[dependency graph section 6](../00-dependency-graph.md). ([spec](../superpowers/specs/2026-09-01-runtime-v2-design.md),
2026-09-01). The keystone decision: PULL transport — a child is fds and
the existing net verbs drive them, so no new transport machinery exists
anywhere in the track, and the old 1→2→3 chain broke. Only 1 → 2
chained; 3, 4, 5 startable alone. Plan per iteration, written when it
starts. Edges in [dependency graph section 6](../00-dependency-graph.md).
| # | Iteration | State | | # | Iteration | State |
| --- | --- | --- | | --- | --- | --- |
| 1 | [streaming subprocess](runtime-v2/01-streaming-subprocess.md) | ⬜ `refine` — 42's named follow-up: long-lived child, output as events, stdin, exit notice. Five forks (verb shape, push-vs-pull transport, the mailbox-cap collision, stdin backpressure, idle-deadline semantics). First up | | 1 | [streaming subprocess](runtime-v2/01-streaming-subprocess.md) | ⬜ ready — `proc.spawn -> Child{id,in,out,err}` (fds driven by the net verbs; kernel pipe = backpressure), `proc.wait_dl`, `proc.signal`; actor-owned lifecycle, 42's sweeps. First up |
| 2 | [PTY](runtime-v2/02-pty.md) | ⬜ `refine` — openpty + controlling terminal + resize; `.dev/reference/tmux` `spawn.c`/`fdforkpty.c` is the reading | | 2 | [PTY](runtime-v2/02-pty.md) | ⬜ ready, after 1 — `proc.spawn_pty(cmd, args, cols, rows)` (master raw, in==out), `proc.resize`; `-lutil` link check flagged |
| 3 | [signals as events](runtime-v2/03-signals-as-events.md) | ⬜ `refine` — SIGWINCH/SIGCHLD as mailbox messages; signalfd lean; the seam 42 deferred to its real consumer | | 3 | [signals as events](runtime-v2/03-signals-as-events.md) | ⬜ ready, **startable alone** — `signal.on(sig, addr)` delivering the sig number as a scalar; signalfd on shard 0's plane; TERM/INT refused by name |
| 4 | [termios adoption](runtime-v2/04-termios.md) | ⬜ `refine`, **startable alone** — raw mode + guaranteed restore on the process's own tty | | 4 | [termios adoption](runtime-v2/04-termios.md) | ⬜ ready, **startable alone** — `term.raw(fd)`/`term.restore(fd)`; restore is a runtime obligation (unwind/stop), no wrecked tty ever |
| 5 | [fd passing](runtime-v2/05-fd-passing.md) | ⬜ `refine`, **startable alone** — SCM_RIGHTS over unix sockets; detach/attach's foundation | | 5 | [fd passing](runtime-v2/05-fd-passing.md) | ⬜ ready, **startable alone** — `net.send_fd`/`net.recv_fd` (one fd, SCM_RIGHTS) + `net.connect_unix` (38 pending, verified) |
### ▸ wmux — the terminal multiplexer track ### ▸ wmux — the terminal multiplexer track

View file

@ -34,9 +34,14 @@ cross a unix socket. Five seams, each builtin-sized, each in
| 4 | [termios adoption](04-termios.md) | the process's OWN tty into raw mode and back — adopting a terminal it was given | | 4 | [termios adoption](04-termios.md) | the process's OWN tty into raw mode and back — adopting a terminal it was given |
| 5 | [fd passing](05-fd-passing.md) | SCM_RIGHTS over unix sockets — detach/attach's foundation, and the Wayland stage's later | | 5 | [fd passing](05-fd-passing.md) | SCM_RIGHTS over unix sockets — detach/attach's foundation, and the Wayland stage's later |
Build order is 1 → 2 → 3 (each leans on the last); 4 and 5 have no All five are `readiness: ready` since the track-wide brainstorm
incoming edges and are startable alone. Edges live in ([spec](../../superpowers/specs/2026-09-01-runtime-v2-design.md),
[dependency graph section 6](../../00-dependency-graph.md). 2026-09-01), which also settled the build order: only 1 → 2 is chained
(spawn_pty extends spawn's plumbing); **3, 4 and 5 are startable alone,
today** — the pull-transport decision (a child is fds; the net verbs
drive them) broke the old 1→2→3 chain. Edges live in
[dependency graph section 6](../../00-dependency-graph.md); each
iteration writes its own implementation plan when it starts.
## The driving workload ## The driving workload

View file

@ -2,12 +2,14 @@
track: runtime-v2 track: runtime-v2
iteration: "1" iteration: "1"
status: pending status: pending
readiness: refine readiness: ready
--- ---
# runtime-v2 1 — streaming subprocess: a long-lived child as a peer # runtime-v2 1 — streaming subprocess: a long-lived child as a peer
> Part of [Story — runtime-v2: the runtime beyond sockets](00-story.md). > Part of [Story — runtime-v2: the runtime beyond sockets](00-story.md).
> Spec: [`2026-09-01-runtime-v2-design.md`](../../superpowers/specs/2026-09-01-runtime-v2-design.md)
> (track-wide brainstorm, approved 2026-09-01 — the forks below are settled).
> Iteration [42](../language-runtime-database/42-bounded-subprocess.md)'s > Iteration [42](../language-runtime-database/42-bounded-subprocess.md)'s
> named follow-up, promoted to this track's opening slice. 42 built the > named follow-up, promoted to this track's opening slice. 42 built the
> machinery a streaming form reuses whole: the `wo_child` registry, the > machinery a streaming form reuses whole: the `wo_child` registry, the
@ -21,33 +23,31 @@ readiness: refine
> actor as it happens, stdin must reach the child, and exit must arrive > actor as it happens, stdin must reach the child, and exit must arrive
> as an event — all without violating a single 42 guarantee. > as an event — all without violating a single 42 guarantee.
## Info — the forks (open; why this is `refine`) ## Info — the forks, settled (brainstorm 2026-09-01)
1. **Verb shape.** A new `proc.spawn(cmd, args, …)` returning a child 1. **Verb shape: `proc.spawn(cmd, args) -> ?Child`** — a new verb, and
HANDLE, versus spawn-options on `proc.run_dl`. Sub-fork: the handle the handle is a RECORD, not an actor: `Child {id, in, out, err}`,
as an actor address (the child looks like an actor — `send` to feed all Int-shaped, joining the predeclared records.
stdin, `monitor` for exit, iteration 24 machinery free) versus an 2. **Transport: PULL, and it already exists.** The child's fds are
opaque scalar with its own verb family. ordinary conn-like values; `net.read_dl`/`net.write_dl`/`net.close`
2. **Output transport.** PUSH — stdout/stderr chunks delivered as drive them unchanged through the park plane. This iteration adds NO
`Bytes` messages into the owner's mailbox (the fd-event pattern) — transport code at all — only acquisition, ownership and exit.
versus PULL — a `read`-style verb the fiber parks on, the 3. **The mailbox-cap collision never starts** — nothing is pushed. The
`net.read_dl` mould. Push composes with actors; pull composes with kernel pipe is the backpressure: an owner that stops reading makes
backpressure. the child block on write, exactly tmux under a slow client.
3. **The mailbox-cap collision** — the fork that decides whether push is 4. **stdin mirror settled by the same move**: `net.write_dl` on
viable at all: a chatty child versus `wo_mailbox_cap`'s fail-fast `child.in` parks with a deadline; torn-write semantics as on sockets.
1024. Drop chunks? Kill the child by name? Or stop reading the pipe 5. **Bounds:** the idle deadline is `read_dl`'s per-call ms; output caps
and let the KERNEL buffer be the backpressure (the lean — the child are the caller's read sizes (no runtime buffer exists); exit is
blocks on a full pipe exactly as it would under a slow tmux). `proc.wait_dl(id, ms) -> ?Int` parking on the slot's pidfd — one
4. **stdin and its mirror.** Child not reading, pipe full: park the waiter per id, a second refuses by name. `proc.signal(id, sig)`
writing fiber with a deadline (the `net.write_dl` mould) versus completes the surface. A death-notice verb is refused: a two-line
refuse at a byte cap. fiber composes `wait_dl` into an event.
5. **Bounds semantics shift.** Total-output cap and total deadline stop
meaning anything for a shell that runs for days — per-chunk caps and
an IDLE deadline replace them; exit notification as a monitor-style
death notice carrying the code, versus a blocking `wait` verb.
Ownership does not fork: 42's rule stands — the owner unwinding kills Ownership (stated, not forked): the spawning ACTOR owns the child (the
the child; engine stop kills them all; a zombie or an orphan is a bug. program, when spawned outside one). Actor death, engine stop and
`wo_vm_destroy` kill, reap and close — 42's doctrine, streaming edition.
Ceiling stays 32 per shard, failing closed by name.
## Acceptance sketch (firmed at brainstorm) ## Acceptance sketch (firmed at brainstorm)

View file

@ -2,7 +2,7 @@
track: runtime-v2 track: runtime-v2
iteration: "2" iteration: "2"
status: pending status: pending
readiness: refine readiness: ready
--- ---
# runtime-v2 2 — PTY: a child that believes it owns a terminal # runtime-v2 2 — PTY: a child that believes it owns a terminal
@ -19,19 +19,19 @@ readiness: refine
> `compat/fdforkpty.c` (~450 lines of C covering five platforms; Linux > `compat/fdforkpty.c` (~450 lines of C covering five platforms; Linux
> alone is far smaller). > alone is far smaller).
## Info — the forks (open) ## Info — the forks, settled (brainstorm 2026-09-01; spec:
[`2026-09-01-runtime-v2-design.md`](../../superpowers/specs/2026-09-01-runtime-v2-design.md))
1. **Surface.** A `pty: true` option on iteration 1's spawn verb (the 1. **Separate verb**: `proc.spawn_pty(cmd, args, cols, rows) -> ?Child`
lean — one spawn shape, two transports) versus a separate — its own builtin id (static arity table stays static), same Child
`proc.spawn_pty`. record with `in` and `out` both the master fd, `err` nil.
2. **Resize.** A verb on the handle (`resize(cols, rows)` → TIOCSWINSZ) 2. **Resize ships HERE**: `proc.resize(id, cols, rows)` → TIOCSWINSZ on
— needed the moment [3](03-signals-as-events.md) delivers SIGWINCH. the slot; refusal by name on a pipe child.
The fork is only whether it ships here or with 3. 3. **Transport settled by iteration 1's pull decision** — the master fd
3. **The master fd's transport** is settled by iteration 1's fork 2 — is driven by the existing net verbs, nothing new.
whatever won there carries the PTY master identically. 4. **Master raw by default, no knobs** — until a consumer asks by name.
4. **Encoding edge.** A PTY master delivers the child's output with tty (`openpty` may need `-lutil` — the plan verifies the link line
post-processing (ONLCR and friends): raw the master by default versus before assuming.)
expose termios knobs. Lean: raw, no knobs, until a consumer asks.
## Acceptance sketch ## Acceptance sketch

View file

@ -2,7 +2,7 @@
track: runtime-v2 track: runtime-v2
iteration: "3" iteration: "3"
status: pending status: pending
readiness: refine readiness: ready
--- ---
# runtime-v2 3 — signals as events: SIGWINCH into a mailbox # runtime-v2 3 — signals as events: SIGWINCH into a mailbox
@ -21,25 +21,23 @@ readiness: refine
> once: the stop flag is a signal made safe by latching. This iteration > once: the stop flag is a signal made safe by latching. This iteration
> generalizes that shape without handing user code a signal handler. > generalizes that shape without handing user code a signal handler.
## Info — the forks (open) ## Info — the forks, settled (brainstorm 2026-09-01; spec:
[`2026-09-01-runtime-v2-design.md`](../../superpowers/specs/2026-09-01-runtime-v2-design.md))
1. **Registration surface.** `signal.on(SIGWINCH, addr, msg)` in the 1. **`signal.on(sig, addr)`** — a standing subscription delivering the
`time.after` mould (the msg MOVES to the runtime, delivered on SIGNAL NUMBER as a scalar message. No moved-message ownership rules:
arrival) versus a process-level subscription table in `wo.toml`. scalars copy, so a repeating SIGWINCH needs no per-delivery payload.
Lean: the builtin — dynamic, one consumer today. 2. **signalfd on shard 0's plane** — one more registered fd with a
2. **Delivery mechanics.** `signalfd` on shard 0's plane (a signal sentinel `user_data` (the wake-eventfd precedent); reads drain
becomes an fd event — no async-signal-safety questions at all, the `signalfd_siginfo` records and fan out ordinary sends.
kernel-primitive taste) versus a latch array swept like deadlines. 3. **Offerable set: SIGWINCH, SIGCHLD, SIGHUP, SIGUSR1, SIGUSR2.**
Lean: signalfd; the runtime is Linux-first and the plane already SIGTERM/SIGINT registration is refused by name — the stop latch
multiplexes fds. stays the engine's, iteration 40 is load-bearing.
3. **Which signals are offerable.** SIGWINCH and SIGCHLD certainly; 4. **Coalescing disclosed, not sequenced** — what SIGWINCH consumers
SIGTERM/SIGINT stay the ENGINE's (the stop latch is load-bearing — expect anyway.
iteration 40's drain). The fork is whether user registration for the
stop signals is refused by name or layered before the latch. Also settled: this iteration is STANDALONE — the old edge from PTY was
4. **Coalescing.** Signals coalesce in the kernel; a mailbox message per only the resize pairing; signalfd needs nothing from iteration 2.
delivery can't promise one-per-resize. Disclose coalescing (lean —
it is what SIGWINCH consumers expect anyway) versus sequence-number
them.
## Acceptance sketch ## Acceptance sketch

View file

@ -2,7 +2,7 @@
track: runtime-v2 track: runtime-v2
iteration: "4" iteration: "4"
status: pending status: pending
readiness: refine readiness: ready
--- ---
# runtime-v2 4 — termios adoption: raw mode on a terminal we were given # runtime-v2 4 — termios adoption: raw mode on a terminal we were given
@ -17,20 +17,18 @@ readiness: refine
> including a trap. A terminal left raw is the classic way a program > including a trap. A terminal left raw is the classic way a program
> makes a user's shell unusable. > makes a user's shell unusable.
## Info — the forks (open) ## Info — the forks, settled (brainstorm 2026-09-01; spec:
[`2026-09-01-runtime-v2-design.md`](../../superpowers/specs/2026-09-01-runtime-v2-design.md))
1. **Surface size.** Exactly two verbs — `term.raw()` returning a 1. **Exactly two verbs**: `term.raw(fd)` and `term.restore(fd)`. Saved
restore token and `term.restore(token)` (the lean: wmux needs termios live in a per-shard table keyed by fd (one thread — no
nothing else; tmux itself uses little more than `cfmakeraw`) — locks); double-raw on one fd refuses by name; no flag knobs.
versus exposing termios flag knobs. YAGNI says two verbs and a 2. **Restore is a RUNTIME OBLIGATION.** Fiber unwind, engine stop and
refusal for the rest. `wo_vm_destroy` restore every saved tty, newest first — "no orphan"
2. **Restore guarantee.** Tie restoration to the unwind machinery (the has its terminal-state sibling: no wrecked tty, ever, trap paths
42 ownership pattern: fiber dies, terminal restored — a runtime included.
obligation) versus caller's-problem-with-a-doc-note. Lean: runtime 3. **fd-taking from day one** — a tty received via
obligation; "no orphan" has a terminal-state sibling: no wrecked tty. [5](05-fd-passing.md) works without the verb growing an argument
3. **Scope.** stdin only, versus any tty fd (a client adopting a tty it
received via [5](05-fd-passing.md) — the wmux SERVER's need). This
fork decides whether the verb takes an fd argument now or grows one
later. later.
## Acceptance sketch ## Acceptance sketch

View file

@ -2,7 +2,7 @@
track: runtime-v2 track: runtime-v2
iteration: "5" iteration: "5"
status: pending status: pending
readiness: refine readiness: ready
--- ---
# runtime-v2 5 — fd passing: SCM_RIGHTS over the unix socket # runtime-v2 5 — fd passing: SCM_RIGHTS over the unix socket
@ -22,21 +22,19 @@ readiness: refine
> SCM_RIGHTS ancillary data is the only mechanism, and it is runtime > SCM_RIGHTS ancillary data is the only mechanism, and it is runtime
> work by nature. > work by nature.
## Info — the forks (open) ## Info — the forks, settled (brainstorm 2026-09-01; spec:
[`2026-09-01-runtime-v2-design.md`](../../superpowers/specs/2026-09-01-runtime-v2-design.md))
1. **Surface.** `net.send_fd(conn, fd)` / `net.recv_fd(conn)` — two 1. **Two verbs, fd travels alone**: `net.send_fd(conn, fd) -> Bool` /
verbs, fd travels alone (the lean; tmux sends its imsg header as `net.recv_fd(conn) -> ?Int` — `sendmsg` + SCM_RIGHTS, fixed
ordinary bytes beside it) — versus fd-attached-to-a-message framing ancillary buffer; framing stays the caller's ordinary bytes (the
in the runtime. tmux imsg shape, composed in `.wo`).
2. **What arrives.** The received fd as an opaque scalar the existing 2. **A received fd is a plain Int** every existing fd verb accepts —
`net.read`/`net.write`/termios verbs accept (lean — every fd verb net reads/writes, termios adoption included.
already takes an Int-shaped conn) versus a new wrapped type. 3. **This iteration CARRIES `net.connect_unix(path) -> Int`** —
3. **The unix-socket client side.** Iteration 38's `net.connect` covers iteration 38 verified pending at brainstorm time, not assumed.
outbound TCP; the CLIENT half of a unix-socket connection may or may 4. **One fd per message**, refusal by name past it; non-unix sockets
not exist by then — this iteration carries `net.connect_unix` if 38 refuse by name; multi-fd arrays wait for a consumer.
has not landed it first. Verify at brainstorm, not assumed.
4. **Bounds.** One fd per message, refusal by name past it (lean),
versus SCM_RIGHTS' multi-fd arrays. YAGNI: no consumer sends two.
## Acceptance sketch ## Acceptance sketch

View file

@ -0,0 +1,100 @@
# runtime-v2 — the runtime beyond sockets, all five iterations settled
> Brainstormed and approved 2026-09-01, one pass across the whole track:
> [runtime-v2](../../stories/runtime-v2/00-story.md) iterations 1–5.
> Scope settled: PULL transport (a child is fds; the net verbs already
> drive them), `wait_dl` over death notices, PTY as a spawn variant,
> signals via signalfd delivering the signal number, termios as two
> fd-taking verbs with runtime-guaranteed restore, fd passing as two
> verbs plus `net.connect_unix`. This spec is track-wide; each iteration
> gets its own implementation plan when it starts.
## The one principle
**A child is fds plus a registry slot — runtime-v2 adds ACQUISITION
verbs, never new transport.** `net.read_dl`, `net.write_dl` and
`net.close` already park on any fd through the shard plane (POLL_ADD is
fd-agnostic; pipes and PTY masters qualify today). So:
- Backpressure is the kernel pipe. An owner that stops reading makes
the child block on write — exactly tmux under a slow client. The
mailbox-cap collision never starts because nothing is pushed.
- The idle deadline is `read_dl`'s own per-call ms. Total-output caps
disappear because no runtime buffer exists — the caller's read sizes
ARE the bound.
- A wmux pane actor is chat's Reader actor with a different fd.
The rejected alternative, recorded: PUSH (child output as mailbox
messages) required new delivery machinery in `vm.c`, a policy for the
`wo_mailbox_cap` collision (drop, kill, or park — each wrong somewhere),
ordering rules across two streams, and per-chunk allocation. Everything
pull gets from three landed iterations, push would rebuild.
## The surface
New builtin ids from 97 up. Every id is FOUR registrations — `wob.h`
enum, `loader.c` arity table, `builtin.c` dispatch range, `types.ml`
stdlib row — the iteration 42 lesson; missing one reads as "unknown
stdlib builtin" from a valid image. No `.wob` version bump (the 38/42
precedent). `Child` joins the predeclared records (`types.ml`
`stdlib_records`): `id`, `in`, `out`, `err` — all Int-shaped; the fds
are ordinary conn-like values every existing fd verb accepts.
| Verb | Contract |
| --- | --- |
| `proc.spawn(cmd, args) -> ?Child` | iteration 1. Pipes on all three stdio fds, parent ends `O_NONBLOCK`; the registry slot is claimed (ceiling 32/shard, fails closed by name) and OWNED (below) |
| `proc.wait_dl(id, ms) -> ?Int` | iteration 1. Parks on the slot's pidfd (the 42 machinery); the exit code, or nil at the deadline. ONE waiter per id — a second concurrent wait refuses by name |
| `proc.signal(id, sig)` | iteration 1. `pidfd_send_signal` through the slot — no pid-reuse race |
| `proc.spawn_pty(cmd, args, cols, rows) -> ?Child` | iteration 2. openpty; child on the slave as controlling terminal; `in` and `out` are both the master, `err` is nil; master raw by default, no knobs |
| `proc.resize(id, cols, rows)` | iteration 2. TIOCSWINSZ on the slot's master; refusal by name on a pipe child |
| `signal.on(sig, addr)` | iteration 3. Standing subscription; delivery is an ordinary send of the SIGNAL NUMBER as a scalar (scalars copy — no moved-message ownership rules). Offerable: SIGWINCH, SIGCHLD, SIGHUP, SIGUSR1, SIGUSR2. SIGTERM/SIGINT refused by name — the stop latch stays the engine's (iteration 40 is load-bearing). Kernel coalescing disclosed, not sequenced |
| `term.raw(fd)` / `term.restore(fd)` | iteration 4. Save termios into the shard table, `cfmakeraw`, restore from the table. Restore is a RUNTIME OBLIGATION: fiber unwind, engine stop and `wo_vm_destroy` restore every saved tty, newest first. fd-taking from day one — a tty received via iteration 5 works |
| `net.send_fd(conn, fd) -> Bool` | iteration 5. `sendmsg` + SCM_RIGHTS, exactly one fd, fixed ancillary buffer; refusal by name on a non-unix socket or a second fd |
| `net.recv_fd(conn) -> ?Int` | iteration 5. The received fd as a plain Int; nil when the peer sent none |
| `net.connect_unix(path) -> Int` | iteration 5 carries it — iteration 38 (`net.connect`) has not landed; verified pending, not assumed |
## Lifecycle — 42's doctrine, streaming edition
The `wo_child` slot grows: pid, pidfd, owner, and the fds the runtime
must close if the owner never does. **Owner is the spawning ACTOR when
one exists, else the program.** Actor death (iteration 24's dead mark),
engine stop, and `wo_vm_destroy` each sweep: kill via pidfd, reap,
close the slot's fds, restore any termios the shard saved. The
per-shard ceiling stays 32; a zombie, an orphan, or a wrecked tty is a
bug by definition. `wait_dl` parking reuses `fb->dl_active`/`dl_at`
and parks on the pidfd directly — no epoll bundle needed for a single
fd.
## Mechanics notes (for the plans)
- signalfd joins shard 0's plane as one more registered fd with a
sentinel `user_data`, the wake-eventfd precedent in `park.c`; reads
drain `signalfd_siginfo` records and fan out sends.
- The termios table is per-shard (one thread — no locks), keyed by fd;
double-raw on one fd refuses by name.
- Raw syscalls wherever glibc 2.35 lacks wrappers (the 42 rule);
`signalfd` and `openpty` are fine (`openpty` needs `-lutil` — check
the Makefile link line before assuming).
- Each iteration's gate legs: the 42 batteries repeat — fd-flat churn,
stop/unwind reaping, refusals asserted verbatim — plus the
iteration's own proof (PTY prompt bytes, SIGWINCH round trip, termios
restore-after-trap, fd-across-socket byte echo).
## Sequencing (the graph remap this settles)
Pull transport cuts the old 1→2→3 chain: signals never needed PTY, only
the resize PAIRING. Edges now: **1 → 2** (spawn_pty extends spawn's
plumbing); **3, 4, 5 startable alone, today**; the VTE grid is wmux's
own `.wo` work, also standalone (a replay corpus needs no subprocess).
wmux 1 consumes all five plus the grid.
## Out of scope, by name
- PUSH delivery of child output — rejected above, revisit only with a
consumer pull cannot serve.
- Death notices (`proc.watch`) — a two-line fiber composes `wait_dl`
into an event; build it in `.wo`, not in C.
- termios knobs beyond raw/restore; multi-fd SCM_RIGHTS arrays;
sequence-numbered signals; PTY termios shaping — each waits for a
named consumer.
- SIGTERM/SIGINT user handling — permanently the engine's.