diff --git a/docs/00-dependency-graph.md b/docs/00-dependency-graph.md index ae2987a..2c9c6d7 100644 --- a/docs/00-dependency-graph.md +++ b/docs/00-dependency-graph.md @@ -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 diff --git a/docs/stories/00-status.md b/docs/stories/00-status.md index 5335c67..81a3657 100644 --- a/docs/stories/00-status.md +++ b/docs/stories/00-status.md @@ -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 diff --git a/docs/stories/runtime-v2/00-story.md b/docs/stories/runtime-v2/00-story.md index 171f6e0..b76abae 100644 --- a/docs/stories/runtime-v2/00-story.md +++ b/docs/stories/runtime-v2/00-story.md @@ -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, diff --git a/docs/stories/runtime-v2/01-streaming-subprocess.md b/docs/stories/runtime-v2/01-streaming-subprocess.md index 0038ce7..bb8194b 100644 --- a/docs/stories/runtime-v2/01-streaming-subprocess.md +++ b/docs/stories/runtime-v2/01-streaming-subprocess.md @@ -1,7 +1,7 @@ --- track: runtime-v2 iteration: "1" -status: pending +status: done readiness: ready --- diff --git a/docs/stories/runtime-v2/02-pty.md b/docs/stories/runtime-v2/02-pty.md index 64a79c9..d4e7d09 100644 --- a/docs/stories/runtime-v2/02-pty.md +++ b/docs/stories/runtime-v2/02-pty.md @@ -1,7 +1,7 @@ --- track: runtime-v2 iteration: "2" -status: pending +status: done readiness: ready --- diff --git a/docs/stories/runtime-v2/03-signals-as-events.md b/docs/stories/runtime-v2/03-signals-as-events.md index 6b1701c..1b5a4ea 100644 --- a/docs/stories/runtime-v2/03-signals-as-events.md +++ b/docs/stories/runtime-v2/03-signals-as-events.md @@ -1,7 +1,7 @@ --- track: runtime-v2 iteration: "3" -status: pending +status: done readiness: ready --- diff --git a/docs/stories/runtime-v2/04-termios.md b/docs/stories/runtime-v2/04-termios.md index 8a9ffb4..7ef220b 100644 --- a/docs/stories/runtime-v2/04-termios.md +++ b/docs/stories/runtime-v2/04-termios.md @@ -1,7 +1,7 @@ --- track: runtime-v2 iteration: "4" -status: pending +status: done readiness: ready --- diff --git a/docs/stories/runtime-v2/05-fd-passing.md b/docs/stories/runtime-v2/05-fd-passing.md index 50f1856..8a6cac5 100644 --- a/docs/stories/runtime-v2/05-fd-passing.md +++ b/docs/stories/runtime-v2/05-fd-passing.md @@ -1,7 +1,7 @@ --- track: runtime-v2 iteration: "5" -status: pending +status: done readiness: ready --- diff --git a/docs/superpowers/plans/2026-09-01-runtime-v2.md b/docs/superpowers/plans/2026-09-01-runtime-v2.md new file mode 100644 index 0000000..30f5e9c --- /dev/null +++ b/docs/superpowers/plans/2026-09-01-runtime-v2.md @@ -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. diff --git a/docs/superpowers/specs/2026-09-01-runtime-v2-design.md b/docs/superpowers/specs/2026-09-01-runtime-v2-design.md index 9736f7c..2dd21bc 100644 --- a/docs/superpowers/specs/2026-09-01-runtime-v2-design.md +++ b/docs/superpowers/specs/2026-09-01-runtime-v2-design.md @@ -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 diff --git a/runtime/src/CODE-LOGIC.md b/runtime/src/CODE-LOGIC.md index 3e20f48..72f8a35 100644 --- a/runtime/src/CODE-LOGIC.md +++ b/runtime/src/CODE-LOGIC.md @@ -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