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
|
classDef later fill:#6e7781,color:#fff,stroke:none
|
||||||
|
|
||||||
I42w["42 bounded subprocess ✅ 2026-09-01"]:::done
|
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
|
GSTREAM["runtime-v2 1 ✅ 2026-09-02 streaming subprocess: Child fds driven by the net verbs, wait_dl, signal"]:::done
|
||||||
GPTY["runtime-v2 2 PTY: openpty, controlling terminal, resize ioctls"]:::gap
|
GPTY["runtime-v2 2 ✅ 2026-09-02 PTY: spawn_pty + resize"]:::done
|
||||||
GSIG["runtime-v2 3 signals as events: SIGWINCH (and SIGCHLD beyond pidfd) as mailbox messages"]:::gap
|
GSIG["runtime-v2 3 ✅ 2026-09-02 signals as events: signal.on delivers Signal records"]:::done
|
||||||
GTERMIOS["runtime-v2 4 termios adoption: the CLIENT's own tty raw + restored"]:::gap
|
GTERMIOS["runtime-v2 4 ✅ 2026-09-02 termios: raw/restore, restore a runtime obligation"]:::done
|
||||||
GFDPASS["runtime-v2 5 SCM_RIGHTS fd passing over the unix socket (detach/attach's foundation)"]:::gap
|
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
|
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
|
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
|
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
|
TMONO -.v2.-> WMUX
|
||||||
```
|
```
|
||||||
|
|
||||||
**Remapped by the 2026-09-01 track brainstorm** (pull transport: a child
|
**The track landed whole on 2026-09-02** — every runtime edge into wmux
|
||||||
is fds, the net verbs drive them): the old 1→2→3 chain broke — signals
|
is green; what remains for wmux 1 is its own `.wo` work (the VTE grid +
|
||||||
never needed PTY, only the resize pairing, and the VTE grid needs no
|
unicode width node) and its brainstorm's terminfo fork. Sibling reuse:
|
||||||
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
|
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
|
## Maintenance rule
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,56 @@ behind this board; live Obsidian Dataview views:
|
||||||
|
|
||||||
## ▶ NEXT PLAN
|
## ▶ 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)
|
### Landed 2026-09-01 — iteration 42, bounded subprocess (brainstorm to gate in one day)
|
||||||
|
|
||||||
**Implemented last time (2026-09-01):** iteration
|
**Implemented last time (2026-09-01):** iteration
|
||||||
|
|
@ -1192,11 +1242,11 @@ starts. Edges in [dependency graph section 6](../00-dependency-graph.md).
|
||||||
|
|
||||||
| # | Iteration | State |
|
| # | 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 |
|
| 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) | ⬜ ready, after 1 — `proc.spawn_pty(cmd, args, cols, rows)` (master raw, in==out), `proc.resize`; `-lutil` link check flagged |
|
| 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) | ⬜ 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 |
|
| 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) | ⬜ ready, **startable alone** — `term.raw(fd)`/`term.restore(fd)`; restore is a runtime obligation (unwind/stop), no wrecked tty ever |
|
| 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) | ⬜ ready, **startable alone** — `net.send_fd`/`net.recv_fd` (one fd, SCM_RIGHTS) + `net.connect_unix` (38 pending, verified) |
|
| 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
|
### ▸ 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 |
|
| 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 |
|
| 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),
|
([spec](../../superpowers/specs/2026-09-01-runtime-v2-design.md),
|
||||||
2026-09-01), which also settled the build order: only 1 → 2 is chained
|
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,
|
(spawn_pty extends spawn's plumbing); **3, 4 and 5 are startable alone,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
track: runtime-v2
|
track: runtime-v2
|
||||||
iteration: "1"
|
iteration: "1"
|
||||||
status: pending
|
status: done
|
||||||
readiness: ready
|
readiness: ready
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
track: runtime-v2
|
track: runtime-v2
|
||||||
iteration: "2"
|
iteration: "2"
|
||||||
status: pending
|
status: done
|
||||||
readiness: ready
|
readiness: ready
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
track: runtime-v2
|
track: runtime-v2
|
||||||
iteration: "3"
|
iteration: "3"
|
||||||
status: pending
|
status: done
|
||||||
readiness: ready
|
readiness: ready
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
track: runtime-v2
|
track: runtime-v2
|
||||||
iteration: "4"
|
iteration: "4"
|
||||||
status: pending
|
status: done
|
||||||
readiness: ready
|
readiness: ready
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
track: runtime-v2
|
track: runtime-v2
|
||||||
iteration: "5"
|
iteration: "5"
|
||||||
status: pending
|
status: done
|
||||||
readiness: ready
|
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).
|
own `.wo` work, also standalone (a replay corpus needs no subprocess).
|
||||||
wmux 1 consumes all five plus the grid.
|
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
|
## Out of scope, by name
|
||||||
|
|
||||||
- PUSH delivery of child output — rejected above, revisit only with a
|
- 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
|
caps, ceiling, thousand-spawn fd flatness, stop/unwind), and
|
||||||
`scripts/subprocess-accept.sh` proves the language-level half.
|
`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)
|
## Class metadata and json (`.wob` v2)
|
||||||
|
|
||||||
The class table carries, per field, its name constant, the class it refers to
|
The class table carries, per field, its name constant, the class it refers to
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue