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
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

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" |
| `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

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
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

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 |
| 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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

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.