docs(rt2): close out runtime-v2 1-5

- five stories status: done; 00-story records the one-run landing
- spec History: three implementation amendments (Signal record not
  scalar, caller-owned stdio fds, handler-latch instead of signalfd)
- board NEXT PLAN entry with measured findings (zero transport code
  added; the tty-across-the-socket handover proven; the double-raw
  refusal restoring the terminal — the "bug" that was the design
  working); section rows flipped; graph nodes green
- CODE-LOGIC.md: the runtime-v2 section
- full belt quoted on the board: suites 0 fail both flavors (test_proc
  193/0, test_term 60/0), woc 557/0, subprocess 12/0, site 23/0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit bc1b4f070693eb755ad6a9fd0c853fb3e2bda347)
This commit is contained in:
shoney.arickathil 2026-09-02 00:14:54 +02:00
parent f52ee83ff4
commit 6e997759e5
11 changed files with 365 additions and 22 deletions

View file

@ -340,11 +340,11 @@ flowchart TD
classDef later fill:#6e7781,color:#fff,stroke:none
I42w["42 bounded subprocess ✅ 2026-09-01"]:::done
GSTREAM["runtime-v2 1 streaming subprocess: long-lived child, output as mailbox messages, stdin (42's named follow-up)"]:::gap
GPTY["runtime-v2 2 PTY: openpty, controlling terminal, resize ioctls"]:::gap
GSIG["runtime-v2 3 signals as events: SIGWINCH (and SIGCHLD beyond pidfd) as mailbox messages"]:::gap
GTERMIOS["runtime-v2 4 termios adoption: the CLIENT's own tty raw + restored"]:::gap
GFDPASS["runtime-v2 5 SCM_RIGHTS fd passing over the unix socket (detach/attach's foundation)"]:::gap
GSTREAM["runtime-v2 1 ✅ 2026-09-02 streaming subprocess: Child fds driven by the net verbs, wait_dl, signal"]:::done
GPTY["runtime-v2 2 ✅ 2026-09-02 PTY: spawn_pty + resize"]:::done
GSIG["runtime-v2 3 ✅ 2026-09-02 signals as events: signal.on delivers Signal records"]:::done
GTERMIOS["runtime-v2 4 ✅ 2026-09-02 termios: raw/restore, restore a runtime obligation"]:::done
GFDPASS["runtime-v2 5 ✅ 2026-09-02 fd passing: send_fd/recv_fd/connect_unix"]:::done
GVTE["VTE grid in pure .wo + unicode width tables (pinned against recorded sessions)"]:::gap
WMUX["wmux 1 (was language 43): server owns sessions/PTYs in durable tables, thin client hands over its tty — reattach after server RESTART replays from the WAL"]:::product
TINFO["terminfo fork: parse the db in .wo vs fixed xterm-256color + refusal by name (decide at 43's brainstorm)"]:::later
@ -361,13 +361,12 @@ flowchart TD
TMONO -.v2.-> WMUX
```
**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 track landed whole on 2026-09-02** — every runtime edge into wmux
is green; what remains for wmux 1 is its own `.wo` work (the VTE grid +
unicode width node) and its brainstorm's terminfo fork. Sibling reuse:
the alacritty Wayland stage reuses GFDPASS + GVTE; the zen CDP driver
shares GSTREAM only; skillhost (28) consumes GSTREAM's stdin transport.
now lacks only a WebSocket client; skillhost (28) has its stdin
transport.
## Maintenance rule

View file

@ -70,6 +70,56 @@ behind this board; live Obsidian Dataview views:
## ▶ NEXT PLAN
### Landed 2026-09-02 — runtime-v2 COMPLETE: all five iterations in one run
**Implemented last time (2026-09-02):** the whole
[runtime-v2 track](runtime-v2/00-story.md) — streaming subprocess
(`proc.spawn`/`wait_dl`/`signal`, Child record, kernel-pipe
backpressure), PTY (`spawn_pty` via posix_openpt + `resize`), signals as
events (`signal.on` delivering Signal records), termios
(`term.raw/restore` with runtime-guaranteed restore), and fd passing
(`send_fd`/`recv_fd`/`connect_unix`). Ids 97–107, five commits, one
[plan](../superpowers/plans/2026-09-01-runtime-v2.md) against the
[track spec](../superpowers/specs/2026-09-01-runtime-v2-design.md).
**Key findings (measured, not asserted):** the PULL design paid off
exactly as argued — the five iterations added ZERO transport code; the
existing net verbs drove pipes, PTY masters and received fds unchanged
(`cat` echo, `stty size`, cross-socket pipe reads all through
`read_dl`/`write_dl`). A real child's SIGUSR1 landed in an actor's multi
as one coalesced Signal record. The tty that crossed the unix socket was
raw'd through the RECEIVED copy and restored at vm destroy — wmux's
detach/attach handover, proven in miniature. `test_proc` 193/0,
`test_term` 60/0, both dispatch flavors ASan clean; woc 557/0;
subprocess-accept 12/0; site-accept 23/0.
**Learned (three spec amendments, recorded in its History):** message
payloads are unconditionally dropped as heap objects, so scalar messages
crash by construction — anything delivered to an actor must be a record;
a streaming slot must NOT own the caller-visible fds (recycled numbers —
the sweep would close a stranger); and signalfd was the wrong mechanics —
the stop-latch pattern generalized (handler latch + wake eventfd + drain
at `wo_io_wait`'s loop head) needs no mask plumbing at all. Bonus: the
double-raw refusal is itself a trap, so the termios obligation restores
the terminal even THERE — the test caught it as a "bug" that was the
design working.
**Dependencies unblocked:** every runtime edge into
[wmux 1](wmux/01-wmux.md) is green — what remains for wmux is its own
`.wo` work (VTE grid + unicode width tables, server/client, the gate)
plus its brainstorm's terminfo fork. The alacritty stage A (headless PTY
runner) is fully unblocked; the zen CDP driver now lacks only the
WebSocket client; skillhost's stdin transport exists.
**Next steps:** cherry-pick `rt2` to master when declared ready; then
wmux 1's brainstorm (terminfo fork, v1 surface) — the first product
slice of the goal recorded 2026-09-01: acceptance by Linux-based
developers.
**`.dev/reference` used:** tmux (`spawn.c`, `imsg-buffer.c` — the
fd-passing and PTY shapes), the kernel's pidfd/termios/SCM_RIGHTS
interfaces.
### Landed 2026-09-01 — iteration 42, bounded subprocess (brainstorm to gate in one day)
**Implemented last time (2026-09-01):** iteration
@ -1192,11 +1242,11 @@ starts. Edges in [dependency graph section 6](../00-dependency-graph.md).
| # | Iteration | State |
| --- | --- | --- |
| 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) |
| 1 | [streaming subprocess](runtime-v2/01-streaming-subprocess.md) | ✅ **DONE 2026-09-02** — `proc.spawn -> Child{id,stdin,stdout,stderr}` (fds driven by the net verbs; kernel pipe = backpressure), `proc.wait_dl` (nil at deadline, one waiter), `proc.signal`; actor-owned lifecycle |
| 2 | [PTY](runtime-v2/02-pty.md) | ✅ **DONE 2026-09-02** — `proc.spawn_pty` via posix_openpt (no -lutil), `proc.resize`; `test -t` and live `stty size` legs |
| 3 | [signals as events](runtime-v2/03-signals-as-events.md) | ✅ **DONE 2026-09-02** — `signal.on(sig, addr)` delivering a fresh Signal record (scalar payloads crash by construction — spec amendment); handler-latch + wake eventfd instead of signalfd (amendment); TERM/INT refused by name |
| 4 | [termios adoption](runtime-v2/04-termios.md) | ✅ **DONE 2026-09-02** — `term.raw/restore`; restore proven a runtime obligation twice (DIV0 while raw, and the double-raw refusal itself) |
| 5 | [fd passing](runtime-v2/05-fd-passing.md) | ✅ **DONE 2026-09-02** — `net.send_fd`/`recv_fd`/`connect_unix`; a tty crossed the socket, was raw'd through the received copy and restored at destroy — the wmux handover in miniature |
### ▸ wmux — the terminal multiplexer track

View file

@ -34,7 +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 |
All five are `readiness: ready` since the track-wide brainstorm
**ALL FIVE LANDED 2026-09-02, one execution run** (plan:
[`2026-09-01-runtime-v2.md`](../../superpowers/plans/2026-09-01-runtime-v2.md);
three implementation amendments in the spec's History). Gates:
`test_proc` 193/0 + `test_term` 60/0 inside a fully green ASan suite on
both dispatch flavors, woc-test 557/0, subprocess-accept 12/0,
site-accept 23/0. The board's NEXT PLAN entry carries the findings.
All five were `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,

View file

@ -1,7 +1,7 @@
---
track: runtime-v2
iteration: "1"
status: pending
status: done
readiness: ready
---

View file

@ -1,7 +1,7 @@
---
track: runtime-v2
iteration: "2"
status: pending
status: done
readiness: ready
---

View file

@ -1,7 +1,7 @@
---
track: runtime-v2
iteration: "3"
status: pending
status: done
readiness: ready
---

View file

@ -1,7 +1,7 @@
---
track: runtime-v2
iteration: "4"
status: pending
status: done
readiness: ready
---

View file

@ -1,7 +1,7 @@
---
track: runtime-v2
iteration: "5"
status: pending
status: done
readiness: ready
---

View file

@ -0,0 +1,235 @@
# runtime-v2 (iterations 1–5) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use
> superpowers:executing-plans to implement this plan task-by-task. Steps
> use checkbox (`- [ ]`) syntax for tracking.
>
> **Project rule (overrides the plan-skill template):** plan docs carry
> concept, reason and actions in words — no code blocks. Steps name exact
> functions, ids, fields and expected outcomes; the implementer writes
> the code at the keyboard against the cited anchors.
**Goal:** land all five runtime-v2 iterations — streaming subprocess,
PTY, signals-as-events, termios adoption, fd passing — as builtins on the
existing park plane, per the track spec.
**Architecture:** acquisition verbs only, never transport: children and
received fds are ordinary fds the existing `net.read_dl`/`write_dl`/
`close` drive. The `wo_child` registry grows a streaming variant; a
per-shard termios table and shard-0 signalfd are the only new state.
**Tech Stack:** C (runtime), OCaml table rows (compiler), `.wo` +
bash (gate legs).
**Spec:** `docs/superpowers/specs/2026-09-01-runtime-v2-design.md`.
## Global Constraints
- Ids 97–107, in this order: 97 `PROC_SPAWN`, 98 `PROC_WAIT_DL`,
99 `PROC_SIGNAL`, 100 `PROC_SPAWN_PTY`, 101 `PROC_RESIZE`,
102 `SIGNAL_ON`, 103 `TERM_RAW`, 104 `TERM_RESTORE`, 105 `NET_SEND_FD`,
106 `NET_RECV_FD`, 107 `NET_CONNECT_UNIX`. `WO_B_MAX` → 107. No `.wob`
bump.
- Every id = FOUR registrations: `wob.h` enum, `loader.c` `b_arity`,
`builtin.c` dispatch range (extend the `<= WO_B_PROC_RUN_DL` bound to
`<= WO_B_NET_CONNECT_UNIX`), `types.ml` row. The 42 lesson.
- **Spec amendment 1 (verified 2026-09-01):** message payloads are
unconditionally `wo_drop_obj`'d (`vm.c:887`, `actor_die`,
`actor_activate`) — a scalar payload is a crash. `signal.on` therefore
delivers a fresh predeclared `Signal {sig Int}` record per delivery,
class id appended by the compiler (loader arity 3). Record the
amendment in the spec's History when closing.
- **Spec amendment 2:** a streaming slot does NOT own the caller-visible
stdio fds (fd-reuse hazard: the sweep could close a recycled number).
The caller owns Child.in/out/err and closes them with `net.close`; the
slot owns pid + pidfd + (PTY only) a private `dup` of the master for
resize. Sweep = kill, reap, close pidfd (+ master dup).
- PTY via `posix_openpt`/`grantpt`/`unlockpt`/`ptsname_r` — plain libc,
NO `-lutil`, no Makefile change.
- Raw syscalls where glibc 2.35 lacks wrappers (pidfd already handled);
`signalfd` has a wrapper since 2.8 — use it.
- SIGTERM/SIGINT registration refused by name; the stop latch stays the
engine's.
- All work on `dev`, commits prefixed `feat(rt2):`/`docs(rt2):`; builds
and tests only via just recipes; ASan+UBSan clean is a gate.
- Suite home: `runtime/test/test_proc.c` grows spawn/wait/pty legs; new
`runtime/test/test_term.c` for termios + signals + fd-passing (auto-
globbed).
---
### Task 1: streaming spawn — `proc.spawn`, `proc.wait_dl`, `proc.signal` (rt2 iteration 1)
**Files:**
- Modify: `runtime/src/wob.h` (ids 97–99), `runtime/src/loader.c`
(arities: spawn 3 — cmd, argv, cls; wait_dl 2; signal 2),
`runtime/src/builtin.c` (dispatch bound), `runtime/src/sysio.c`
(three cases + slot changes), `runtime/src/vm.h` (`wo_child` gains
`streaming`, `waiter`, `master_dup` fields), `runtime/src/vm.c`
(`actor_die` sweeps children owned by the dying actor),
`compiler/src/types.ml` (`Child` record — id/in/out/err, all Int —
in `stdlib_records`; rows for the three verbs).
- Test: `runtime/test/test_proc.c`.
**Interfaces:**
- Produces: `proc.spawn(cmd, args) -> ?Child`, `proc.wait_dl(id, ms) ->
?Int`, `proc.signal(id, sig) -> 0`; slot ownership field
`owner_actor` (a `wo_actor*`, NULL = program) that Tasks 2–5 reuse.
- [ ] **Step 1 (red):** legs in `test_proc.c` driving the new ids from
bytecode: (a) spawn `cat`, `net.write_dl` a line to `Child.in`, read
it back from `Child.out` via `net.read_dl` — the echo round trip;
(b) `wait_dl` on a fast child answers its code, on a `sleep 10` with
ms=100 answers nil and the child is STILL alive (then `proc.signal`
SIGKILL, wait again, code observed, ECHILD after vm destroy);
(c) second concurrent `wait_dl` on one id refuses by name (two
fibers); (d) spawn-loop fd-flat leg with the caller closing all three
fds each round. Run `just wovm-test` — all four fail (unknown builtin).
- [ ] **Step 2 (green):** implement. Spawn: three pipes (stdin write end
stays parent-side as Child.in), parent ends `O_NONBLOCK`, fork/execvp
(argv rules identical to 42's), pidfd, claim slot (`streaming = 1`,
no epoll bundle, no buffers), owner = `vm->cur->actor` (NULL when
none), build the Child record via `record_of` (4 fields). `wait_dl`:
slot lookup by id (id = slot index + a generation counter to refuse a
stale id by name), waiter-claim refusal, park on the pidfd with
`dl_active`/`dl_at`, on exit reap + release slot + answer code, nil at
deadline (child untouched). `signal`: `pidfd_send_signal` through the
slot. `actor_die` calls a new `wo_proc_abandon_actor(vm, a)` killing
every slot whose owner is `a`. Sweeps close pidfd only (amendment 2).
- [ ] **Step 3:** `just wovm-test` green both flavors; commit
`feat(rt2): proc.spawn/wait_dl/signal — the streaming child` with the
compiler row in the same commit (`just woc-build && just woc-test`
first).
### Task 2: PTY — `proc.spawn_pty`, `proc.resize` (rt2 iteration 2)
**Files:** same four registration files (ids 100–101; spawn_pty arity 5
— cmd, argv, cols, rows, cls; resize 3), `runtime/src/sysio.c`,
`runtime/test/test_proc.c`.
**Interfaces:**
- Consumes: Task 1's slot, Child record, ownership.
- Produces: `proc.spawn_pty(cmd, args, cols, rows) -> ?Child` (in==out=
master, err nil), `proc.resize(id, cols, rows) -> 0`.
- [ ] **Step 1 (red):** legs: (a) spawn_pty `sh -c 'test -t 0 && echo
yes-tty'` — read "yes-tty" back (isatty proof); (b) spawn_pty with
24x80 then a child running `stty size` — read "24 80"; resize to
40x120, re-ask via a second child? No — one child that sleeps then
prints size after a marker write; simpler: child = `sh -c 'read x;
stty size'` — resize between spawn and the marker write, expect
"40 120"; (c) resize on a Task-1 pipe child refuses by name. Red run.
- [ ] **Step 2 (green):** `posix_openpt(O_RDWR|O_NOCTTY)`, `grantpt`,
`unlockpt`, `ptsname_r`; child: `setsid`, open slave (becomes
controlling tty), dup2 onto 0/1/2, `TIOCSWINSZ` initial size, exec.
Parent: master `O_NONBLOCK`, slot stores a private `dup` of the master
(`master_dup`) for resize; Child.in == Child.out == master, err = 0
(nil). Resize: `ioctl(master_dup, TIOCSWINSZ)` + refusal by name when
the slot is not a PTY child. Sweep closes `master_dup`.
- [ ] **Step 3:** suites green; commit `feat(rt2): spawn_pty + resize —
a child that believes it owns a terminal`.
### Task 3: signals as events — `signal.on` (rt2 iteration 3)
**Files:** registrations (id 102, arity 3 — sig, addr, cls),
`runtime/src/sysio.c` or `builtin.c` for the case, `runtime/src/park.c`
(signalfd on shard 0's plane beside the wake eventfd, sentinel
user_data), `runtime/src/vm.h` (per-vm subscription list {sig, actor}),
`compiler/src/types.ml` (`Signal {sig Int}` record + row),
`runtime/test/test_term.c` (new).
**Interfaces:**
- Consumes: `runtime_notify` (`vm.c:1326`, the timer delivery path) for
handing a fresh payload to an actor.
- Produces: `signal.on(sig, addr) -> 0`; delivery = fresh `Signal{sig}`
record per arrival (spec amendment 1).
- [ ] **Step 1 (red):** test_term.c: module with an actor whose receive
pushes `msg.sig` into a shared multi; main registers
`signal.on(SIGUSR1, addr)`, then `proc.run("sh", ["-c", "kill -USR1
$PPID"])`, then sleeps briefly; assert the multi holds SIGUSR1's
number. Second leg: `signal.on(SIGTERM, …)` refuses naming the stop
latch. Red.
- [ ] **Step 2 (green):** first registration on shard 0 creates the
signalfd (mask grows per registration; `pthread_sigmask` blocks the
sig process-wide first — document: registration must happen before
worker shards spawn or the mask is per-thread incomplete; v1 rule:
register from shard 0/main, refusal by name elsewhere). park.c: the
signalfd is registered like the wake eventfd (oneshot POLL_ADD under
uring, level under epoll) with its own sentinel; on readiness drain
`signalfd_siginfo` records, for each match allocate `Signal{sig}` via
`wo_obj_new` and `runtime_notify` the subscribed actor(s).
- [ ] **Step 3:** suites green (both WO_IO backends — the fibers gate
pattern proves uring AND epoll); commit `feat(rt2): signal.on —
signalfd delivers Signal records to actors`.
### Task 4: termios — `term.raw`, `term.restore` (rt2 iteration 4)
**Files:** registrations (ids 103–104, arity 1 each),
`runtime/src/sysio.c` (cases + the per-shard saved-termios table in
`wo_vm` — 8 entries {fd, termios, owner fiber}), `runtime/src/vm.c`
(restore sweep in `fib_reap` and `wo_vm_destroy` beside the proc
sweeps), `runtime/test/test_term.c`.
- [ ] **Step 1 (red):** legs using a PTY pair made in the TEST via
`posix_openpt` (C-side, no builtin): (a) `term.raw(slave_fd)` then
`tcgetattr` shows ECHO/ICANON cleared; `term.restore(slave_fd)`
brings the saved flags back bit-identically; (b) double-raw refuses by
name; (c) raw then DELIBERATE trap in the fiber — after the trap the
fd's termios are restored (the runtime obligation); (d) restore on an
fd never raw'd refuses by name. Red.
- [ ] **Step 2 (green):** table claim (full table refuses by name),
`tcgetattr` save, `cfmakeraw`, `tcsetattr`; restore verb frees the
entry; `fib_reap` and `wo_vm_destroy` restore entries owned by the
dying fiber / all, newest first.
- [ ] **Step 3:** suites green; commit `feat(rt2): term.raw/restore —
no wrecked tty, ever`.
### Task 5: fd passing — `net.send_fd`, `net.recv_fd`, `net.connect_unix` (rt2 iteration 5)
**Files:** registrations (ids 105–107; arities 2/1/1),
`runtime/src/sysio.c`, `runtime/test/test_term.c`.
- [ ] **Step 1 (red):** legs: (a) `net.listen_unix` + `net.connect_unix`
pair inside one vm (two fibers: acceptor and connector); (b) create a
pipe in C, `send_fd` its read end across the socket, `recv_fd` it,
write into the pipe's write end, `net.read_dl` from the RECEIVED fd
answers the bytes; (c) `send_fd` on a TCP socket refuses by name;
(d) `recv_fd` when the peer sent plain bytes answers nil; (e) a tty
fd (test PTY slave) crosses and `term.raw` works on it — the wmux
handover in miniature. Red.
- [ ] **Step 2 (green):** `connect_unix`: socket AF_UNIX, connect,
`O_NONBLOCK` after. `send_fd`: `SO_DOMAIN` check (refusal), `sendmsg`
with one `SCM_RIGHTS` fd in a fixed `CMSG_SPACE(sizeof(int))` buffer
and one sentinel data byte; EAGAIN parks (POLLOUT, the write mould).
`recv_fd`: `recvmsg` with the same buffer; EAGAIN parks (POLLIN);
a message without ancillary fd answers nil; received fd set
`O_NONBLOCK`.
- [ ] **Step 3:** suites green; commit `feat(rt2): send_fd/recv_fd/
connect_unix — an fd crosses the socket`.
### Task 6: close-out
**Files:** `runtime/src/CODE-LOGIC.md` (runtime-v2 section),
`docs/superpowers/specs/2026-09-01-runtime-v2-design.md` (History —
the two amendments), the five story files (status: done + Progress),
`docs/stories/00-status.md` (NEXT PLAN entry, section rows ✅),
`docs/00-dependency-graph.md` (nodes → done class).
- [ ] **Step 1:** full belt: `just wovm-test`, `just woc-test`,
`just subprocess`, `just site` — quote results, never assert.
- [ ] **Step 2:** write the docs; commit `docs(rt2): close out
runtime-v2 1–5`.
---
## Self-review (at write time)
- Spec coverage: every surface row has a task; both amendments carried
into Tasks 1 and 3 and recorded for the spec's History in Task 6.
Out-of-scope items appear in no task.
- Ids consistent 97–107 across tasks; `Child`/`Signal` records named
identically throughout.
- Deliberate verify-first flags: the shard-0-only registration rule for
signals (mask is per-thread — confirm where worker threads inherit
the mask), and `runtime_notify`'s exact signature before reuse.

View file

@ -88,6 +88,25 @@ 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.
## History — three amendments found at implementation (2026-09-02)
1. **Signal delivery is a record, not a scalar.** Message payloads are
unconditionally `wo_drop_obj`'d (`vm.c` — delivery, actor death,
fiber reap), so a scalar payload is a crash by construction.
`signal.on(sig, addr)` delivers a fresh predeclared `Signal {sig}`
record per arrival; the class id rides the call as the appended
record operand.
2. **A streaming slot does not own the caller's stdio fds** — fd numbers
get recycled, so a sweep closing them could close a stranger. The
caller owns `Child.stdin/stdout/stderr` (released with `net.close`);
the slot owns pid + pidfd and, for a PTY child, a private `dup` of
the master so resize survives the caller closing its copy.
3. **No signalfd.** The stop-latch pattern generalized instead: an
async-signal-safe handler latches the number, bumps a sequence and
pokes shard 0's wake eventfd; `wo_io_wait`'s loop head drains latches
into deliveries. Same observable contract, no mask plumbing, no
fork-child mask restoration, EINTR itself is the wake.
## Out of scope, by name
- PUSH delivery of child output — rejected above, revisit only with a

View file

@ -113,6 +113,39 @@ orphan is a bug by definition; `test_proc` pins all of it (deadline,
caps, ceiling, thousand-spawn fd flatness, stop/unwind), and
`scripts/subprocess-accept.sh` proves the language-level half.
## runtime-v2 (ids 97–107): processes, terminals, signals
The track's one principle: **a child or received fd is an ORDINARY fd
the existing net verbs drive** — these are acquisition verbs, never
transport. `proc.spawn` returns `Child {id, stdin, stdout, stderr}`; the
CALLER owns those fds (`net.close`), the slot owns pid + pidfd only
(recycled fd numbers make a sweeping close a stranger-killer). The id is
`(gen << 6) | slot` so stale handles refuse by name. `wait_dl` parks on
the pidfd (one waiter per id); `spawn_pty` (posix_openpt, child setsid +
opens the slave as controlling tty) returns the master as both stdin and
stdout, with a private `dup` in the slot so `resize` (TIOCSWINSZ)
survives the caller closing its copy. Streaming children are owned by
the spawning ACTOR — `actor_die` calls `wo_proc_abandon_actor`.
`signal.on(sig, addr)`: the stop-latch pattern generalized — an
async-signal-safe handler latches the number, bumps a sequence, pokes
shard 0's wake eventfd; `wo_io_wait`'s loop head drains latches into
fresh `Signal {sig}` records via `wo_actor_notify` (payloads MUST be
heap objects: vm.c drops them unconditionally — a scalar payload is a
crash). Coalescing disclosed. SIGTERM/SIGINT refused: the stop latch is
load-bearing.
`term.raw/restore`: saved termios in the shard's 8-entry table; restore
is a RUNTIME obligation — `vm_unwind` at depth 0 (uncaught trap, fiber
reap) restores the dying fiber's entries newest-first, `wo_vm_destroy`
sweeps the rest. Even the double-raw REFUSAL (itself a trap) restores.
`net.send_fd/recv_fd`: sendmsg/recvmsg, one SCM_RIGHTS fd + a sentinel
byte, `SO_DOMAIN` gates to unix sockets; the received fd arrives
nonblocking as a plain Int. `net.connect_unix` rides here until
iteration 38. `test_term` pins signals/termios/fd-passing; `test_proc`
the spawn family.
## Class metadata and json (`.wob` v2)
The class table carries, per field, its name constant, the class it refers to