From 3190b609afba4d9d34c05983fee5cf441bf38d31 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Tue, 1 Sep 2026 23:08:10 +0200 Subject: [PATCH] =?UTF-8?q?docs(rt2):=20track-wide=20brainstorm=20?= =?UTF-8?q?=E2=80=94=20all=20five=20iterations=20ready,=20graph=20remapped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 (cherry picked from commit d313cdeebbe53c83b83f31f4480631568d9d0743) --- docs/00-dependency-graph.md | 18 ++-- docs/00-git-commit-history.md | 2 +- docs/stories/00-status.md | 21 ++-- docs/stories/runtime-v2/00-story.md | 11 +- .../runtime-v2/01-streaming-subprocess.md | 52 ++++----- docs/stories/runtime-v2/02-pty.md | 26 ++--- .../runtime-v2/03-signals-as-events.md | 36 +++---- docs/stories/runtime-v2/04-termios.md | 26 +++-- docs/stories/runtime-v2/05-fd-passing.md | 28 +++-- .../specs/2026-09-01-runtime-v2-design.md | 100 ++++++++++++++++++ 10 files changed, 213 insertions(+), 107 deletions(-) create mode 100644 docs/superpowers/specs/2026-09-01-runtime-v2-design.md diff --git a/docs/00-dependency-graph.md b/docs/00-dependency-graph.md index 15ca21b..ae2987a 100644 --- a/docs/00-dependency-graph.md +++ b/docs/00-dependency-graph.md @@ -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 landed. Every yellow node is an iteration of the [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) · [2 PTY](stories/runtime-v2/02-pty.md) · [3 signals as events](stories/runtime-v2/03-signals-as-events.md) · @@ -351,21 +352,22 @@ flowchart TD I42w --> GSTREAM GSTREAM --> GPTY - GPTY --> GSIG - GSTREAM --> GVTE GPTY --> WMUX GSIG --> WMUX GTERMIOS --> WMUX GFDPASS --> WMUX GVTE --> WMUX - TINFO -.settled at brainstorm.-> WMUX + TINFO -.settled at wmux's brainstorm.-> WMUX TMONO -.v2.-> WMUX ``` -Sibling reuse, for the record: the alacritty study's Wayland stage D -reuses GFDPASS + GVTE; the zen CDP driver shares GSTREAM only; skillhost -(28) consumes GSTREAM's stdin transport. GTERMIOS and GFDPASS have no -incoming edges — startable any time, alone. +**Remapped by the 2026-09-01 track brainstorm** (pull transport: a child +is fds, the net verbs drive them): the old 1→2→3 chain broke — signals +never needed PTY, only the resize pairing, and the VTE grid needs no +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 diff --git a/docs/00-git-commit-history.md b/docs/00-git-commit-history.md index 74d47cc..d6f3be9 100644 --- a/docs/00-git-commit-history.md +++ b/docs/00-git-commit-history.md @@ -46,7 +46,7 @@ features cannot collide. | `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 | | `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 diff --git a/docs/stories/00-status.md b/docs/stories/00-status.md index 5effb25..5335c67 100644 --- a/docs/stories/00-status.md +++ b/docs/stories/00-status.md @@ -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 as its whole compiler cost (the iteration 42 precedent). Iteration 42 (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 -startable alone. All ⬜ `refine`; edges in -[dependency graph section 6](../00-dependency-graph.md). +language track before it had a name. **All five `readiness: ready`** — +one track-wide brainstorm settled every fork +([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 | | --- | --- | --- | -| 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 | -| 2 | [PTY](runtime-v2/02-pty.md) | ⬜ `refine` — openpty + controlling terminal + resize; `.dev/reference/tmux` `spawn.c`/`fdforkpty.c` is the reading | -| 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 | -| 4 | [termios adoption](runtime-v2/04-termios.md) | ⬜ `refine`, **startable alone** — raw mode + guaranteed restore on the process's own tty | -| 5 | [fd passing](runtime-v2/05-fd-passing.md) | ⬜ `refine`, **startable alone** — SCM_RIGHTS over unix sockets; detach/attach's foundation | +| 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) | ⬜ 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) | ⬜ 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) | ⬜ 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) | ⬜ ready, **startable alone** — `net.send_fd`/`net.recv_fd` (one fd, SCM_RIGHTS) + `net.connect_unix` (38 pending, verified) | ### ▸ wmux — the terminal multiplexer track diff --git a/docs/stories/runtime-v2/00-story.md b/docs/stories/runtime-v2/00-story.md index 372a4e4..171f6e0 100644 --- a/docs/stories/runtime-v2/00-story.md +++ b/docs/stories/runtime-v2/00-story.md @@ -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 | | 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 -incoming edges and are startable alone. Edges live in -[dependency graph section 6](../../00-dependency-graph.md). +All five are `readiness: ready` since the track-wide brainstorm +([spec](../../superpowers/specs/2026-09-01-runtime-v2-design.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 diff --git a/docs/stories/runtime-v2/01-streaming-subprocess.md b/docs/stories/runtime-v2/01-streaming-subprocess.md index 2ec66c6..0038ce7 100644 --- a/docs/stories/runtime-v2/01-streaming-subprocess.md +++ b/docs/stories/runtime-v2/01-streaming-subprocess.md @@ -2,12 +2,14 @@ track: runtime-v2 iteration: "1" status: pending -readiness: refine +readiness: ready --- # runtime-v2 1 — streaming subprocess: a long-lived child as a peer > 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 > named follow-up, promoted to this track's opening slice. 42 built 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 > 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 - HANDLE, versus spawn-options on `proc.run_dl`. Sub-fork: the handle - as an actor address (the child looks like an actor — `send` to feed - stdin, `monitor` for exit, iteration 24 machinery free) versus an - opaque scalar with its own verb family. -2. **Output transport.** PUSH — stdout/stderr chunks delivered as - `Bytes` messages into the owner's mailbox (the fd-event pattern) — - versus PULL — a `read`-style verb the fiber parks on, the - `net.read_dl` mould. Push composes with actors; pull composes with - backpressure. -3. **The mailbox-cap collision** — the fork that decides whether push is - viable at all: a chatty child versus `wo_mailbox_cap`'s fail-fast - 1024. Drop chunks? Kill the child by name? Or stop reading the pipe - and let the KERNEL buffer be the backpressure (the lean — the child - blocks on a full pipe exactly as it would under a slow tmux). -4. **stdin and its mirror.** Child not reading, pipe full: park the - writing fiber with a deadline (the `net.write_dl` mould) versus - refuse at a byte cap. -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. +1. **Verb shape: `proc.spawn(cmd, args) -> ?Child`** — a new verb, and + the handle is a RECORD, not an actor: `Child {id, in, out, err}`, + all Int-shaped, joining the predeclared records. +2. **Transport: PULL, and it already exists.** The child's fds are + ordinary conn-like values; `net.read_dl`/`net.write_dl`/`net.close` + drive them unchanged through the park plane. This iteration adds NO + transport code at all — only acquisition, ownership and exit. +3. **The mailbox-cap collision never starts** — nothing is pushed. The + kernel pipe is the backpressure: an owner that stops reading makes + the child block on write, exactly tmux under a slow client. +4. **stdin mirror settled by the same move**: `net.write_dl` on + `child.in` parks with a deadline; torn-write semantics as on sockets. +5. **Bounds:** the idle deadline is `read_dl`'s per-call ms; output caps + are the caller's read sizes (no runtime buffer exists); exit is + `proc.wait_dl(id, ms) -> ?Int` parking on the slot's pidfd — one + waiter per id, a second refuses by name. `proc.signal(id, sig)` + completes the surface. A death-notice verb is refused: a two-line + fiber composes `wait_dl` into an event. -Ownership does not fork: 42's rule stands — the owner unwinding kills -the child; engine stop kills them all; a zombie or an orphan is a bug. +Ownership (stated, not forked): the spawning ACTOR owns the child (the +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) diff --git a/docs/stories/runtime-v2/02-pty.md b/docs/stories/runtime-v2/02-pty.md index 0ca6030..64a79c9 100644 --- a/docs/stories/runtime-v2/02-pty.md +++ b/docs/stories/runtime-v2/02-pty.md @@ -2,7 +2,7 @@ track: runtime-v2 iteration: "2" status: pending -readiness: refine +readiness: ready --- # 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 > 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 - lean — one spawn shape, two transports) versus a separate - `proc.spawn_pty`. -2. **Resize.** A verb on the handle (`resize(cols, rows)` → TIOCSWINSZ) - — needed the moment [3](03-signals-as-events.md) delivers SIGWINCH. - The fork is only whether it ships here or with 3. -3. **The master fd's transport** is settled by iteration 1's fork 2 — - whatever won there carries the PTY master identically. -4. **Encoding edge.** A PTY master delivers the child's output with tty - post-processing (ONLCR and friends): raw the master by default versus - expose termios knobs. Lean: raw, no knobs, until a consumer asks. +1. **Separate verb**: `proc.spawn_pty(cmd, args, cols, rows) -> ?Child` + — its own builtin id (static arity table stays static), same Child + record with `in` and `out` both the master fd, `err` nil. +2. **Resize ships HERE**: `proc.resize(id, cols, rows)` → TIOCSWINSZ on + the slot; refusal by name on a pipe child. +3. **Transport settled by iteration 1's pull decision** — the master fd + is driven by the existing net verbs, nothing new. +4. **Master raw by default, no knobs** — until a consumer asks by name. + (`openpty` may need `-lutil` — the plan verifies the link line + before assuming.) ## Acceptance sketch diff --git a/docs/stories/runtime-v2/03-signals-as-events.md b/docs/stories/runtime-v2/03-signals-as-events.md index 887b075..6b1701c 100644 --- a/docs/stories/runtime-v2/03-signals-as-events.md +++ b/docs/stories/runtime-v2/03-signals-as-events.md @@ -2,7 +2,7 @@ track: runtime-v2 iteration: "3" status: pending -readiness: refine +readiness: ready --- # 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 > 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 - `time.after` mould (the msg MOVES to the runtime, delivered on - arrival) versus a process-level subscription table in `wo.toml`. - Lean: the builtin — dynamic, one consumer today. -2. **Delivery mechanics.** `signalfd` on shard 0's plane (a signal - becomes an fd event — no async-signal-safety questions at all, the - kernel-primitive taste) versus a latch array swept like deadlines. - Lean: signalfd; the runtime is Linux-first and the plane already - multiplexes fds. -3. **Which signals are offerable.** SIGWINCH and SIGCHLD certainly; - SIGTERM/SIGINT stay the ENGINE's (the stop latch is load-bearing — - iteration 40's drain). The fork is whether user registration for the - stop signals is refused by name or layered before the latch. -4. **Coalescing.** Signals coalesce in the kernel; a mailbox message per - delivery can't promise one-per-resize. Disclose coalescing (lean — - it is what SIGWINCH consumers expect anyway) versus sequence-number - them. +1. **`signal.on(sig, addr)`** — a standing subscription delivering the + SIGNAL NUMBER as a scalar message. No moved-message ownership rules: + scalars copy, so a repeating SIGWINCH needs no per-delivery payload. +2. **signalfd on shard 0's plane** — one more registered fd with a + sentinel `user_data` (the wake-eventfd precedent); reads drain + `signalfd_siginfo` records and fan out ordinary sends. +3. **Offerable set: SIGWINCH, SIGCHLD, SIGHUP, SIGUSR1, SIGUSR2.** + SIGTERM/SIGINT registration is refused by name — the stop latch + stays the engine's, iteration 40 is load-bearing. +4. **Coalescing disclosed, not sequenced** — what SIGWINCH consumers + expect anyway. + +Also settled: this iteration is STANDALONE — the old edge from PTY was +only the resize pairing; signalfd needs nothing from iteration 2. ## Acceptance sketch diff --git a/docs/stories/runtime-v2/04-termios.md b/docs/stories/runtime-v2/04-termios.md index dff0a64..8a9ffb4 100644 --- a/docs/stories/runtime-v2/04-termios.md +++ b/docs/stories/runtime-v2/04-termios.md @@ -2,7 +2,7 @@ track: runtime-v2 iteration: "4" status: pending -readiness: refine +readiness: ready --- # 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 > 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 - restore token and `term.restore(token)` (the lean: wmux needs - nothing else; tmux itself uses little more than `cfmakeraw`) — - versus exposing termios flag knobs. YAGNI says two verbs and a - refusal for the rest. -2. **Restore guarantee.** Tie restoration to the unwind machinery (the - 42 ownership pattern: fiber dies, terminal restored — a runtime - obligation) versus caller's-problem-with-a-doc-note. Lean: runtime - obligation; "no orphan" has a terminal-state sibling: no wrecked tty. -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 +1. **Exactly two verbs**: `term.raw(fd)` and `term.restore(fd)`. Saved + termios live in a per-shard table keyed by fd (one thread — no + locks); double-raw on one fd refuses by name; no flag knobs. +2. **Restore is a RUNTIME OBLIGATION.** Fiber unwind, engine stop and + `wo_vm_destroy` restore every saved tty, newest first — "no orphan" + has its terminal-state sibling: no wrecked tty, ever, trap paths + included. +3. **fd-taking from day one** — a tty received via + [5](05-fd-passing.md) works without the verb growing an argument later. ## Acceptance sketch diff --git a/docs/stories/runtime-v2/05-fd-passing.md b/docs/stories/runtime-v2/05-fd-passing.md index 4137662..50f1856 100644 --- a/docs/stories/runtime-v2/05-fd-passing.md +++ b/docs/stories/runtime-v2/05-fd-passing.md @@ -2,7 +2,7 @@ track: runtime-v2 iteration: "5" status: pending -readiness: refine +readiness: ready --- # 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 > 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 - verbs, fd travels alone (the lean; tmux sends its imsg header as - ordinary bytes beside it) — versus fd-attached-to-a-message framing - in the runtime. -2. **What arrives.** The received fd as an opaque scalar the existing - `net.read`/`net.write`/termios verbs accept (lean — every fd verb - already takes an Int-shaped conn) versus a new wrapped type. -3. **The unix-socket client side.** Iteration 38's `net.connect` covers - outbound TCP; the CLIENT half of a unix-socket connection may or may - not exist by then — this iteration carries `net.connect_unix` if 38 - 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. +1. **Two verbs, fd travels alone**: `net.send_fd(conn, fd) -> Bool` / + `net.recv_fd(conn) -> ?Int` — `sendmsg` + SCM_RIGHTS, fixed + ancillary buffer; framing stays the caller's ordinary bytes (the + tmux imsg shape, composed in `.wo`). +2. **A received fd is a plain Int** every existing fd verb accepts — + net reads/writes, termios adoption included. +3. **This iteration CARRIES `net.connect_unix(path) -> Int`** — + iteration 38 verified pending at brainstorm time, not assumed. +4. **One fd per message**, refusal by name past it; non-unix sockets + refuse by name; multi-fd arrays wait for a consumer. ## Acceptance sketch diff --git a/docs/superpowers/specs/2026-09-01-runtime-v2-design.md b/docs/superpowers/specs/2026-09-01-runtime-v2-design.md new file mode 100644 index 0000000..9736f7c --- /dev/null +++ b/docs/superpowers/specs/2026-09-01-runtime-v2-design.md @@ -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.