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:
parent
f52ee83ff4
commit
6e997759e5
11 changed files with 365 additions and 22 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
track: runtime-v2
|
||||
iteration: "1"
|
||||
status: pending
|
||||
status: done
|
||||
readiness: ready
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
track: runtime-v2
|
||||
iteration: "2"
|
||||
status: pending
|
||||
status: done
|
||||
readiness: ready
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
track: runtime-v2
|
||||
iteration: "3"
|
||||
status: pending
|
||||
status: done
|
||||
readiness: ready
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
track: runtime-v2
|
||||
iteration: "4"
|
||||
status: pending
|
||||
status: done
|
||||
readiness: ready
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
track: runtime-v2
|
||||
iteration: "5"
|
||||
status: pending
|
||||
status: done
|
||||
readiness: ready
|
||||
---
|
||||
|
||||
|
|
|
|||
235
docs/superpowers/plans/2026-09-01-runtime-v2.md
Normal file
235
docs/superpowers/plans/2026-09-01-runtime-v2.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue