From ab8ef64cd93850d453fe4d595d0ec90ab1a9b591 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Tue, 1 Sep 2026 22:48:17 +0200 Subject: [PATCH] =?UTF-8?q?docs(rt2):=20runtime-v2=20track=20=E2=80=94=20t?= =?UTF-8?q?he=20runtime=20beyond=20sockets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - five stories under docs/stories/runtime-v2/: 1 streaming subprocess (42's follow-up; five forks incl. the mailbox-cap collision), 2 PTY, 3 signals-as-events (signalfd lean), 4 termios adoption, 5 SCM_RIGHTS fd passing; 00-story states the arc — the plane learned sockets in 8/11/35, files in 6, this adds processes/terminals/signals - build order 1 -> 2 -> 3; 4 and 5 startable alone; all readiness: refine, brainstormed on demand - board: Five tracks; "▸ runtime-v2" pending section; wmux section now points at it; graph section 6 nodes carry runtime-v2 numbers + links - wmux stories re-reference the track; prefix `rt2` claimed - linkcheck: 0 broken Co-Authored-By: Claude Fable 5 (cherry picked from commit e0451cb4889bb3d03429c0276d03c090269a5ced) --- docs/00-dependency-graph.md | 50 +++++ docs/00-git-commit-history.md | 4 + docs/stories/00-status.md | 195 +++++++++++++++++- docs/stories/runtime-v2/00-story.md | 49 +++++ .../runtime-v2/01-streaming-subprocess.md | 69 +++++++ docs/stories/runtime-v2/02-pty.md | 47 +++++ .../runtime-v2/03-signals-as-events.md | 57 +++++ docs/stories/runtime-v2/04-termios.md | 47 +++++ docs/stories/runtime-v2/05-fd-passing.md | 56 +++++ docs/stories/wmux/00-story.md | 49 +++++ docs/stories/wmux/01-wmux.md | 108 ++++++++++ 11 files changed, 729 insertions(+), 2 deletions(-) create mode 100644 docs/stories/runtime-v2/00-story.md create mode 100644 docs/stories/runtime-v2/01-streaming-subprocess.md create mode 100644 docs/stories/runtime-v2/02-pty.md create mode 100644 docs/stories/runtime-v2/03-signals-as-events.md create mode 100644 docs/stories/runtime-v2/04-termios.md create mode 100644 docs/stories/runtime-v2/05-fd-passing.md create mode 100644 docs/stories/wmux/00-story.md create mode 100644 docs/stories/wmux/01-wmux.md diff --git a/docs/00-dependency-graph.md b/docs/00-dependency-graph.md index e722942..15ca21b 100644 --- a/docs/00-dependency-graph.md +++ b/docs/00-dependency-graph.md @@ -317,6 +317,56 @@ language-track work (the CSPRNG builtin) — the cross-track edge this graph exists to make visible. Streaming responses' runtime prerequisites (fibers, iteration 11) are already green in graph 2. +## 6. wmux — the multiplexer track (wmux 1) and its gap chain + +The tmux study's gaps, remapped as buildable edges now that iteration 42 +landed. Every yellow node is an iteration of the +[runtime-v2 track](stories/runtime-v2/00-story.md) ("the runtime beyond +sockets", its own folder since 2026-09-01), brainstormed on demand — +[1 streaming subprocess](stories/runtime-v2/01-streaming-subprocess.md) · +[2 PTY](stories/runtime-v2/02-pty.md) · +[3 signals as events](stories/runtime-v2/03-signals-as-events.md) · +[4 termios](stories/runtime-v2/04-termios.md) · +[5 fd passing](stories/runtime-v2/05-fd-passing.md). wmux — its own +track, first of the softwares built with writeonce — is the driving +workload that consumes them all — [wmux 1](stories/wmux/01-wmux.md). + +```mermaid +flowchart TD + classDef done fill:#1a7f37,color:#fff,stroke:none + classDef gap fill:#eac54f,color:#000,stroke:none + classDef product fill:#0969da,color:#fff,stroke:none + 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 + 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 + TMONO["time.mono returns (status clock, repaint pacing) — v2"]:::later + + I42w --> GSTREAM + GSTREAM --> GPTY + GPTY --> GSIG + GSTREAM --> GVTE + GPTY --> WMUX + GSIG --> WMUX + GTERMIOS --> WMUX + GFDPASS --> WMUX + GVTE --> WMUX + TINFO -.settled at brainstorm.-> WMUX + TMONO -.v2.-> WMUX +``` + +Sibling reuse, for the record: the alacritty study's Wayland stage D +reuses GFDPASS + GVTE; the zen CDP driver shares GSTREAM only; skillhost +(28) consumes GSTREAM's stdin transport. GTERMIOS and GFDPASS have no +incoming edges — startable any time, alone. + ## Maintenance rule When an iteration or slice lands, update its node's class here in the diff --git a/docs/00-git-commit-history.md b/docs/00-git-commit-history.md index e3b35a2..74d47cc 100644 --- a/docs/00-git-commit-history.md +++ b/docs/00-git-commit-history.md @@ -44,6 +44,9 @@ features cannot collide. | `lang41` | runtime: unadopted shard must not impersonate shard 0 | on `dev` (`9dca0b4`); independent of the residency stack, not picked | | `porch-store` | porch store tables, Limiter and Idempotent middleware (Phases A, B, C) | on `dev` (`519d411`, `5b1e82a`, `aee7926`). **In progress**: Phase C was uncommitted work from a parallel session, committed as-is, and calls `json.decode`/`json.encode` with no `use json` import | | `query-corpus` | databasev2 query-grammar corpus #1 | on `dev` (`4c82461`). Conclusion was "no new grammar needed" | +| `lang42` | iteration 42 — bounded subprocess: `proc.run` bounded + parked (pidfd, caps, ceiling, owner-bound reaping), `proc.run_dl`; carries the alacritty/tmux/zen parity studies and the porch dependency-graph section from the same sweep | ✅ on `master` 2026-09-01 | +| `wmux` | the wmux track (`docs/stories/wmux/`, iteration 1 was language 43) — the terminal multiplexer, first of the softwares built with writeonce; story + gap-chain remap first, code follows gap by gap | on `dev` 2026-09-01 | +| `rt2` | the runtime-v2 track (`docs/stories/runtime-v2/`) — the runtime beyond sockets: streaming subprocess, PTY, signals-as-events, termios, fd passing; five refine stories, wmux is the driving workload | on `dev` 2026-09-01 | ## Cherry-picks onto master @@ -52,6 +55,7 @@ produced. | Date | Prefix | Feature | `dev` → `master` | | --- | --- | --- | --- | +| 2026-09-01 | `lang42` | **iteration 42 — bounded subprocess**: `proc.run` parked (pidfd + epoll bundle, `_dl` retry mould) with deadline/output-cap/ceiling refusals by name and owner-bound reaping; `proc.run_dl` (id 96) states bounds per call; the pre-42 sequential-drain deadlock proven then dissolved. Includes the alacritty/tmux/zen-browser parity studies and the porch graph section. Zero conflicts. Verified on `master` after rebuild: 38 runtime suites 0 fail both dispatch flavors (`test_proc` 128/0, `test_wal` 5966/0), woc-test 557/0 (forced, not cached), subprocess-accept 12/0, site-accept 23/0 | `5b92e20` → `2f6d39d`, `75fbd30` → `b287bf7`, `821899b` → `afa16e5`, `c30507b` → `81c28d8`, `975959a` → `64542e5`, `a3b5dc3` → `ce98fa1`, `b147dd4` → `346f885`, `5dfbeda` → `49b0193` | | 2026-08-31 | `db2-migrate` + `site-deploy` | **databasev2 12 — schema migrations v1**: WO_WAL_SCHEMA head record, name-keyed boot diff, record-level transcode for add/delete, poisons that bite only with records; plus the redeploy runbook the close-out edits (dev-only until now). Zero conflicts. Verified on `master`: 36 suites 0 fail (`test_wal` 5966/0), woc-test clean, residency-accept 14/0, site-accept 23/0 | `930a715` → `b594717`, `072e007` → `8d9207d`, `ba8519f` → `570e0d6`, `63a063b` → `b1b7984`, `b69092a` → `4a70fc7`, `b21943a` → `ace5699`, `4bb6ece` → `4f1fda1` | | 2026-08-30 | `site-submodule` | **`docs/examples/site` becomes a submodule** — extracted to github.com/shoneyJ/writeonce-site with `git subtree split` (its own 9 commits of history, not a snapshot) | `4b56348` → `a5497a3`, `4eead89` → `565b894` | | 2026-08-30 | `db2-keys` + `db2-delta` + `db2-chains` + `site` | **databasev2 `resident: keys`, end to end** — storage, readers, deletes, updates as delta records, bounded delta chains, and the tutorial chapter documenting them | 37 commits, mapped one-to-one below | diff --git a/docs/stories/00-status.md b/docs/stories/00-status.md index 326e4c8..5effb25 100644 --- a/docs/stories/00-status.md +++ b/docs/stories/00-status.md @@ -10,10 +10,13 @@ The single place to learn where this project stands. Organised in six buckets: folders** — a doc stays where it was authored, and only its frontmatter, its banner and this board change. -**Three tracks** (2026-08-26): +**Five tracks** (2026-08-26; wmux and runtime-v2 added 2026-09-01): [`language-runtime-database/`](language-runtime-database/00-story.md) — the language and runtime; [`porch/`](porch/00-story.md) — the web framework written -in it; [`databasev2/`](databasev2/00-story.md) — the database beyond RAM. Each +in it; [`databasev2/`](databasev2/00-story.md) — the database beyond RAM; +[`runtime-v2/`](runtime-v2/00-story.md) — the runtime beyond sockets +(processes, terminals, signals); [`wmux/`](wmux/00-story.md) — the terminal +multiplexer, first of the *softwares built with writeonce*. Each numbers its iterations from 1, so a porch 3 is not a language 3; every non-language story carries `track:` in frontmatter, and moved ones keep `was_language_iteration:` so a search for the old number still finds them. Track @@ -67,6 +70,113 @@ behind this board; live Obsidian Dataview views: ## ▶ NEXT PLAN +### Landed 2026-09-01 — iteration 42, bounded subprocess (brainstorm to gate in one day) + +**Implemented last time (2026-09-01):** iteration +[42](language-runtime-database/42-bounded-subprocess.md) end to end — +`proc.run` reworked from shard-blocking to parked (pipe read ends + +pidfd behind one epoll fd, the `_dl` retry mould), bounds everywhere +(30 s / 1 MiB / 64 KiB defaults; per-shard ceiling 32; every violation +kills the child and traps `WO_T_IO` naming the bound), owner-bound +reaping (`fib_reap`/`wo_vm_destroy`/stop all sweep), and `proc.run_dl` +(id 96) stating bounds per call. New suite `runtime/test/test_proc.c` +(128 checks) and `docs/examples/subprocess` + `just subprocess` +(12 checks). [Spec](../superpowers/specs/2026-09-01-bounded-subprocess-design.md) +· [plan](../superpowers/plans/2026-09-01-bounded-subprocess.md). + +**Key findings (measured, not asserted):** the suspected drain deadlock +was REAL — a child writing 200 KB to stdout while holding stderr open +hung the old `proc.run` until the test's 5 s alarm (stdout silently +truncated at 8,192 bytes, exit code lost to SIGPIPE); the parked rework +answers the same child in 15 ms. A `ping` request was answered in 2 ms +while a `sleep 2` child was parked on the same shard. One thousand +sequential spawns left the fd table byte-flat. SIGTERM with a `sleep 30` +child live: clean exit 0, child pid verifiably gone from outside. + +**Learned:** a new sysio builtin id is THREE registrations, not one — +the wob.h enum, the loader's arity table, and builtin.c's dispatch +range; missing any of them surfaces as `unknown stdlib builtin` from a +perfectly valid image. And glibc 2.35 (the release build floor) has no +pidfd wrappers — raw `syscall(SYS_pidfd_open/…_send_signal)` or the +release build breaks. + +**Dependencies unblocked:** the streaming form (long-lived children, +output as mailbox messages) now has its registry/pidfd/cap machinery +built; the tmux/alacritty studies' stage A and the zen study's CDP +driver (stage C′) queue behind that plus their own named gaps +(PTY/termios/fd-passing; ws-client). Iteration 28's "bounded subprocess +first" ordering item is spent. + +**Next steps:** cherry-pick lang42 to master when declared ready; the +startable set otherwise unchanged. The exploration studies' next +builtin-sized item is the WebSocket client (zen C′). + +**`.dev/reference` used:** alacritty, tmux, zen-browser (the three +parity studies that promoted this gap to an iteration); the kernel's own +pidfd/epoll interfaces for the mechanics. + +### Landed 2026-08-30 — keys-resident delta updates DONE, loader refusal lifted + +**Implemented last time (2026-08-30):** the six-task +[keys-resident delta updates](../superpowers/plans/2026-08-30-keys-resident-delta-updates.md) +plan's final task — lifting the `runtime/src/loader.c` refusal of +`resident: keys` and proving update end to end. The refusal (databasev2 2's +Outstanding criterion) is now Met: a keys-resident row updates through a WAL +delta record, read-modify-**append**, folded back to a value by +`wo_wal_fold_row_at` on every read, replay and compaction. Proven four ways — +the fold itself (earlier tasks), group-commit staging with the id-map re-point +deferred to the post-barrier flush, replay/compaction folding delta chains the +same way reads do, and this task's oracle test +(`test_oracle_all_vs_keys_same_update_sequence`, `runtime/test/test_wal.c`) +driving the SAME update sequence against a `resident: all` table and a +`resident: keys` table and asserting byte-identical rows at every step. +`docs/examples/residency`'s `Product` table is genuinely `resident: keys` now; +`scripts/residency-accept.sh`'s gate leg inverted from "the annotation is +refused" to "the program runs and `place_order`'s stock decrement survives a +restart" (11 checks, 0 failures). + +**A second bug surfaced auditing the request path before lifting the +refusal** — the same audit class that caught `delete`'s memory corruption +in the prior session. `idx_hash`, `idx_cols_equal` and `wo_idx_probe` +(`database/src/table.c`) read a TEXT column's slot as an engine `db_text*`, +but a keys-resident borrow was handing back VM-decoded `wo_str*` — a +different struct layout, reproduced as a genuine ASan heap-buffer-overflow, +not merely wrong values. The same bug was independently present in `db.c`'s +`GET_FIELD` and `PROBE` arms (inline and request-path), unaudited until now +because nothing could reach a keys-resident row through them while the +annotation was refused. Fixed at the root: a keys-resident borrow now hands +back engine values, exactly `wo_row_ptr`'s contract for `resident: all` +(`table.h`'s own "a row stores NO VM pointer" doctrine) — no index function +needed to change, and `db.c` needed none either. Pinned by +`test_keys_resident_update_indexed_text`, which reproduces the overflow +against the pre-fix code; all five pre-existing tests that read a +keys-resident Text field directly were auditing the OLD (wrong) contract and +are corrected alongside it. `test_wal` 4746/0 throughout. + +**What did NOT land, by design — three limitations documented, not fixed:** +(1) mid-drain stale reads — a request reading a row in the same uncommitted +drain as an earlier request's in-flight update to it may see the last durable +value, not that write; (2) replay is O(N²) in a row's delta-chain length, +since each replayed delta re-folds the whole chain; (3) compaction triggers on +byte ratio only, with no per-row delta-count signal, so one hot row (a single +popular SKU — this feature's own motivating workload) can grow a long chain +without moving the aggregate ratio enough to checkpoint. Item 3 is the +sharper finding: the design's decision not to cap chain length rests on +compaction bounding it, and for a hot-row workload it does not. Recorded in +[the story](databasev2/02-table-storage-modes.md) and the example's README. + +**.dev / reference projects used:** none — internal-only, `table.c`/`wal.c`/ +`db.c` read directly to audit the request path and trace the representation +mismatch. + +**Dependencies unblocked:** none newly technical — databasev2 2's own tasks 6 +(the two runtime refusals: no-`WO_DATA`, the byte budget) and 7 (measure, gate, +close out) were already the next items and do not depend on this. + +**Next steps:** databasev2 2 tasks 6/7, as before. `database/src/CODE-LOGIC.md` +is current with the stage-here/commit-in-caller update contract and the +engine-representation fix. + ### Landed 2026-08-30 — porch 1 DONE, store-backed middleware closed out **porch 1 (store-backed middleware) is `status: done`.** Task 5 added the @@ -115,6 +225,52 @@ critical path (unaffected by this session); on the porch track, porch 2's brainstorm (CSPRNG builtin id 96+, then repeated response headers) is next whenever that track resumes. +### Landed 2026-08-30 — porch 1 (rate limiting), and a runtime crash found + +**Implemented last time (2026-08-30):** porch 1's rate limiter, and only that. +Counting moved out of the request's own fiber into a sharded actor pool, so two +concurrent requests can no longer lose an increment — proven by 30 genuinely +parallel clients, not two sequential ones. Counters are WAL-durable across a +SIGTERM restart, `trust_proxy` is off by default with a `net.peer` fallback, and +saturation fails closed. + +**The iteration was re-scoped mid-flight.** Idempotency was built, reviewed and +then reverted to [porch 9](porch/09-idempotent-replay.md), whole, in the tag +`archive/porch-idempotency`. Not a design failure — it passed its gates. It +provokes a C-runtime SIGSEGV in `wo_arena_alloc`/`wo_str_new` under concurrent +`call()`-parked callers, and its legs flaked between 0 and 6 failures run to +run. After the split: five consecutive runs at 56 checks, 0 failures. **A +feature whose test passes some of the time is not shipped**, and the limiter was +finished either way — it was being held hostage by a defect in code it does not +call. + +**The most valuable output is arguably the bug, not the feature.** +[Language 41](language-runtime-database/41-actor-arena-crash.md) records a +SIGSEGV in the arena allocator under concurrent actors, with the evidence that +localises it: every failure belonged to the path with 5x the allocation inside +`receive`, none to the light path driving the same pool; and it scales with +sequential insert+delete volume on one key (N=4/5 crashed, N=1-3 clean over 12+ +trials). Two smaller runtime defects came with it — `try/catch` cannot tell a +literal `Int 0` reply from a trap, and a `json.decode` value is corrupted when +embedded in a struct crossing a function-return boundary. + +**Corrections to our own record:** the earlier claim that `Pool` being traced +forces a one-slot re-wrap was WRONG — WO-E222 fires on the class, not on +`multi`, so an actor can hold `slots: multi PoolSlot`. It shipped in a README +before being caught by the whole-branch review. All fifteen execution decisions +are in +[the rulings log](../superpowers/plans/2026-08-29-porch-store-backed-middleware-rulings.md). + +**.dev / reference projects used:** the Fiber parity study for porch 1's scope. + +**Dependencies unblocked:** porch 2-4 inherit the store convention — serialize +through an actor, persist in a `@table`, never read-modify-write from a handler +fiber. + +**Next steps:** language 41, the arena crash. It outranks the remaining porch +work: anything built on actors is exposed until it is fixed, and porch 9 is +written and waiting on it. + ### Landed 2026-08-29 — databasev2 2 tasks 5c/5d, and a branch consolidation **Implemented last time (2026-08-29):** `resident: keys` storage and every read @@ -981,6 +1137,8 @@ the language arc as v1 history. | 8 | [Query grammar from corpora](databasev2/08-query-grammar-corpus.md) *(was 27)* | ⬜ whole-query `count`, `exists`; independent | | 9 | [Cross-program tables](databasev2/09-cross-program-tables.md) *(was 20)* | ⏸ hold — attach to a running program's database over local IPC | | 10 | [Keypair attach auth](databasev2/10-keypair-attach-auth.md) *(was 21)* | ⏸ hold — program identity as a keypair; needs 9 | +| 11 | [Bounded delta chains](databasev2/11-bounded-delta-chains.md) | ✅ **LANDED 2026-08-30.** A `resident: keys` row's delta chain is bounded in the UPDATE path, because the checkpoint is blind to per-row chain length — it thresholds on whole-log bytes, so one hot row can grow an unbounded chain inside a log that never trips compaction. The fold now reports hop count (free — the walk already visited every hop), and past `WO_DELTA_MAX_HOPS` (16) the update writes a full row image instead of a delta, resetting depth to 0. **Two things the tests corrected.** The flattened image is a `WO_WAL_UPDATE`, not an `INSERT`: the row's original INSERT is already in a live log, so a second one for the same id is a duplicate that replay correctly refuses as corruption — INSERT is right only for compaction, which builds a *fresh* log. And the **proportional ceiling was removed as dead code**: with the absolute term at 64 MiB, garbage large enough to reach a 256 MiB ceiling has already tripped it, so the branch was unreachable. Borrowing both constants from postgres was the wrong inference — PG needs two because it thresholds on *tuples* with its pair at opposite ends (base 50, max 1e8); this thresholds on *bytes*, where one constant does both jobs. Found by trying to write a test for the ceiling and finding no input could reach it. Four tests: depth stays bounded across 2K+2 updates, a flattened chain replays, a delta on an **indexed** column composes with flattening (checked at every step across the bound and after restart — found no product defect), and the policy's absolute term with its boundary. `test_wal` **5700 pass / 0 fail**; `wovm-test` and `woc-test` green. **One criterion is weaker than written:** the replay check asserts an expected value, not a `resident: all` oracle table. [spec](../superpowers/specs/2026-08-30-bounded-delta-chains-design.md) | +| 12 | [Schema migrations](databasev2/12-schema-migrations.md) | ✅ **LANDED 2026-08-31.** A `@table` class is the schema, the log is the database, and boot now compares them — before this, an added or deleted field turned a healthy `WO_DATA` into "corruption" and reordering declarations silently decoded rows into the wrong class. Landed: `WO_WAL_SCHEMA` head record (written LAZILY ahead of the first real record — an eager head broke `durable: false`'s documented zero-bytes contract by 75 bytes and the gate caught it), a name-keyed diff whose refusals are per-class POISONS that bite only when a record of the class is met, and a record-level TRANSCODE: cids remap by name including inside stored owned values, deleted values freed, added fields zero-filled, delta back-pointers rewritten through an offset map with deltas on deleted fields SPLICED out; temp+fsync+rename, compaction's crash discipline. **Two bugs the tests forced out:** a poisoned class skipped plan identity so the retype refusal fell through to generic "corruption" (the message this iteration exists to replace), and early `goto corrupt` freed uninitialized memory. End-to-end: `migrating \`Note\`: +flag` then `flag=0`; retype refuses naming `val`, exit 2, old binary still boots the refused log. 21 new tests, `test_wal` **5966/0**; wovm/woc/site/residency gates green. v2 holds rename (`@renamed_from`), retypes, and data/seed migrations. [spec](../superpowers/specs/2026-08-31-schema-migrations-design.md) | --- @@ -1016,6 +1174,38 @@ manifests. check mode, and the `internal/` dep boundary (WO-E108). Driver-only. ✅ **19** — landed 2026-08-20: Float + Bytes, `.wob` v5. +### ▸ runtime-v2 — the runtime beyond sockets + +New 2026-09-01. The I/O plane learned sockets in 8/11/35 and files in 6; +this track adds the missing third — **processes, terminals, signals** — +five builtin-sized seams, each `runtime/src/` work with a `types.ml` row +as its whole compiler cost (the iteration 42 precedent). Iteration 42 +(bounded subprocess, ✅ on `master` 2026-09-01) opened the arc from the +language track before it had a name. Build order 1 → 2 → 3; 4 and 5 +startable alone. All ⬜ `refine`; edges in +[dependency graph section 6](../00-dependency-graph.md). + +| # | Iteration | State | +| --- | --- | --- | +| 1 | [streaming subprocess](runtime-v2/01-streaming-subprocess.md) | ⬜ `refine` — 42's named follow-up: long-lived child, output as events, stdin, exit notice. Five forks (verb shape, push-vs-pull transport, the mailbox-cap collision, stdin backpressure, idle-deadline semantics). First up | +| 2 | [PTY](runtime-v2/02-pty.md) | ⬜ `refine` — openpty + controlling terminal + resize; `.dev/reference/tmux` `spawn.c`/`fdforkpty.c` is the reading | +| 3 | [signals as events](runtime-v2/03-signals-as-events.md) | ⬜ `refine` — SIGWINCH/SIGCHLD as mailbox messages; signalfd lean; the seam 42 deferred to its real consumer | +| 4 | [termios adoption](runtime-v2/04-termios.md) | ⬜ `refine`, **startable alone** — raw mode + guaranteed restore on the process's own tty | +| 5 | [fd passing](runtime-v2/05-fd-passing.md) | ⬜ `refine`, **startable alone** — SCM_RIGHTS over unix sockets; detach/attach's foundation | + +### ▸ wmux — the terminal multiplexer track + +New 2026-09-01, from [the tmux parity study](../plan/exploration/tmux/00-tmux-parity.md). +First of the *softwares built with writeonce* tracks: the product is an +end-user program, not a library. Its runtime prerequisites are the +[runtime-v2 track](runtime-v2/00-story.md) above — iteration 42 was the +first domino; runtime-v2 1–5 remain, streaming-subprocess first — plus +the VTE grid + unicode width work wmux 1 itself owns. + +| # | Iteration | State | +| --- | --- | --- | +| 1 | [wmux](wmux/01-wmux.md) *(was language 43)* | ⬜ `refine` — five forks recorded (terminfo, v1 surface without split panes, scrollback residency, command surface, streaming verb shape). The beyond-tmux leg: durable sessions replay layout + scrollback after a server RESTART | + ### Language track — sequenced, on the critical path | # | Item | Plan | @@ -1032,6 +1222,7 @@ check mode, and the `internal/` dep boundary (WO-E108). Driver-only. | 23 | io_uring group-commit write path — batched durability overlapped on shard threads, fsync fallback | **no spec yet** — brainstorm after iterations 8 + 22 | | 27 | Query grammar from real embedded-DB corpora — whole-query count + correlated exists, driven by the skillhost SQL catalogue; add only what a corpus uses | **no spec yet** — three forks; may collapse to "confirm len(query) + add exists" | | 14 | skillhost host workload — port skillhost (MCP host + confined script runner) to writeonce; drives the missing host capabilities into the open (bounded subprocess, stdin/stdout transport, fs metadata, FFI-vs-out-of-process) | **no spec yet** — gaps recorded in the iteration; each gap brainstormed on demand, bounded-subprocess first | +| 42 | [Bounded subprocess](language-runtime-database/42-bounded-subprocess.md) — `proc.run` bounded in place (deadline, output caps, per-shard ceiling, owner-bound reaping via pidfd, fiber parked) + `proc.run_dl`; streaming form deferred by name | ✅ **DONE 2026-09-01, on `master`** — [spec](../superpowers/specs/2026-09-01-bounded-subprocess-design.md) · [plan](../superpowers/plans/2026-09-01-bounded-subprocess.md); test_proc 128/0, `just subprocess` 12/0; see NEXT PLAN | | 17 | library projects + dependency privacy — `wo.toml` kind = "library" (checkable without entry, dual lib+bin) + Go-style `internal/` at the [deps] boundary; framework reorg demonstrates both | ✅ **landed 2026-08-20** — [spec](../superpowers/specs/2026-08-20-library-kind-internal-design.md) · [plan](../superpowers/plans/2026-08-20-library-kind-internal.md) | | 10 | HTTP service layer | [plan 6](../superpowers/plans/2026-08-01-http-service-layer.md) | | 11 | Fibers | vision §3, [blue-green exploration](../plan/exploration/blue-green-vm/00-vision.md) | diff --git a/docs/stories/runtime-v2/00-story.md b/docs/stories/runtime-v2/00-story.md new file mode 100644 index 0000000..372a4e4 --- /dev/null +++ b/docs/stories/runtime-v2/00-story.md @@ -0,0 +1,49 @@ +# Story — runtime-v2: the runtime beyond sockets + +The fifth track. Where [`databasev2/`](../databasev2/00-story.md) takes +the database beyond RAM, this track takes the I/O plane beyond sockets: +**processes, terminals and signals** — the third of the native surface +the runtime never learned. Iterations 8/11/35 taught it sockets, the +program-mode stdlib taught it files, and iteration +[42](../language-runtime-database/42-bounded-subprocess.md) (bounded +subprocess, ✅ on `master` 2026-09-01) opened this arc from inside the +language track before the arc had a name. + +Numbering restarts at 1 and is local to this track; frontmatter carries +`track: runtime-v2`. Status rules are the repo's, unchanged. + +## The problem, stated once + +A shard's plane multiplexes fds it created itself — listeners, accepted +sockets, and (since 42) the pipe/pidfd bundle of a one-shot child. That +is not enough for any program whose job is other programs: a long-lived +child's output has no path into an actor, a pseudo-terminal cannot be +allocated or resized, a signal cannot become a message, a tty the +process was GIVEN cannot be adopted into raw mode, and an fd cannot +cross a unix socket. Five seams, each builtin-sized, each in +`runtime/src/` with a `types.ml` table row as its entire compiler cost +(the 42 precedent — no `emit.ml` change). + +## The iterations + +| # | Iteration | What it adds | +| --- | --- | --- | +| 1 | [streaming subprocess](01-streaming-subprocess.md) | a long-lived child as a first-class peer: output in, stdin out, exit as a notice | +| 2 | [PTY](02-pty.md) | openpty, controlling terminal, resize ioctls — a child that believes it owns a terminal | +| 3 | [signals as events](03-signals-as-events.md) | SIGWINCH/SIGCHLD/… as mailbox messages — the seam 42 deferred to its real consumer | +| 4 | [termios adoption](04-termios.md) | the process's OWN tty into raw mode and back — adopting a terminal it was given | +| 5 | [fd passing](05-fd-passing.md) | SCM_RIGHTS over unix sockets — detach/attach's foundation, and the Wayland stage's later | + +Build order is 1 → 2 → 3 (each leans on the last); 4 and 5 have no +incoming edges and are startable alone. Edges live in +[dependency graph section 6](../../00-dependency-graph.md). + +## The driving workload + +[`wmux/`](../wmux/00-story.md) — the terminal multiplexer — consumes all +five; that track owns the product, this one owns the seams. Sibling +consumers per iteration are named in each story (skillhost 28, the zen +CDP driver, the alacritty Wayland stage). The doctrine carried over from +42: every resource a ceiling, every violation a refusal by name, no +zombie and no orphan ever, `.dev/reference/tmux` as the measured +reference. diff --git a/docs/stories/runtime-v2/01-streaming-subprocess.md b/docs/stories/runtime-v2/01-streaming-subprocess.md new file mode 100644 index 0000000..2ec66c6 --- /dev/null +++ b/docs/stories/runtime-v2/01-streaming-subprocess.md @@ -0,0 +1,69 @@ +--- +track: runtime-v2 +iteration: "1" +status: pending +readiness: refine +--- + +# runtime-v2 1 — streaming subprocess: a long-lived child as a peer + +> Part of [Story — runtime-v2: the runtime beyond sockets](00-story.md). +> Iteration [42](../language-runtime-database/42-bounded-subprocess.md)'s +> named follow-up, promoted to this track's opening slice. 42 built the +> machinery a streaming form reuses whole: the `wo_child` registry, the +> pidfd wait, the epoll bundle, the cap/refusal doctrine, the ownership +> sweeps (`fib_reap`/`wo_vm_destroy`/stop). +> +> **The problem.** `proc.run`/`proc.run_dl` are one-shot: nothing can +> talk to a child while it runs, and a child that legitimately runs for +> hours (a shell under wmux, a browser under the CDP driver, a script +> under skillhost) has no shape at all. Output must reach the owning +> actor as it happens, stdin must reach the child, and exit must arrive +> as an event — all without violating a single 42 guarantee. + +## Info — the forks (open; why this is `refine`) + +1. **Verb shape.** A new `proc.spawn(cmd, args, …)` returning a child + HANDLE, versus spawn-options on `proc.run_dl`. Sub-fork: the handle + as an actor address (the child looks like an actor — `send` to feed + stdin, `monitor` for exit, iteration 24 machinery free) versus an + opaque scalar with its own verb family. +2. **Output transport.** PUSH — stdout/stderr chunks delivered as + `Bytes` messages into the owner's mailbox (the fd-event pattern) — + versus PULL — a `read`-style verb the fiber parks on, the + `net.read_dl` mould. Push composes with actors; pull composes with + backpressure. +3. **The mailbox-cap collision** — the fork that decides whether push is + viable at all: a chatty child versus `wo_mailbox_cap`'s fail-fast + 1024. Drop chunks? Kill the child by name? Or stop reading the pipe + and let the KERNEL buffer be the backpressure (the lean — the child + blocks on a full pipe exactly as it would under a slow tmux). +4. **stdin and its mirror.** Child not reading, pipe full: park the + writing fiber with a deadline (the `net.write_dl` mould) versus + refuse at a byte cap. +5. **Bounds semantics shift.** Total-output cap and total deadline stop + meaning anything for a shell that runs for days — per-chunk caps and + an IDLE deadline replace them; exit notification as a monitor-style + death notice carrying the code, versus a blocking `wait` verb. + +Ownership does not fork: 42's rule stands — the owner unwinding kills +the child; engine stop kills them all; a zombie or an orphan is a bug. + +## Acceptance sketch (firmed at brainstorm) + +- A child that emits a line a second: each line reaches the owning + actor while the child lives — not after it exits. +- stdin round trip: feed a `cat`-like child, read the echo back. +- The chatty child against whatever fork 3 picked: the declared + behavior happens, by name, and the shard never stalls. +- Exit: the owner learns the code as an event; the registry slot is + released; fd count flat (the 42 measurement legs, streaming edition). +- Stop/unwind: identical guarantees to 42, proven with a LIVE stream. +- Gate legs land in `runtime/test/test_proc.c` beside 42's. + +## Consumers + +wmux 1 (the shell under every pane), skillhost 28 (stdin/stdout +transport was half its named gap), the zen study's CDP driver (spawn the +browser, then talk to it). The alacritty stage-A "headless PTY runner" +is this plus [runtime-v2 2](02-pty.md). diff --git a/docs/stories/runtime-v2/02-pty.md b/docs/stories/runtime-v2/02-pty.md new file mode 100644 index 0000000..0ca6030 --- /dev/null +++ b/docs/stories/runtime-v2/02-pty.md @@ -0,0 +1,47 @@ +--- +track: runtime-v2 +iteration: "2" +status: pending +readiness: refine +--- + +# runtime-v2 2 — PTY: a child that believes it owns a terminal + +> Part of [Story — runtime-v2: the runtime beyond sockets](00-story.md). +> After [1](01-streaming-subprocess.md): a streaming child whose stdio +> is a pseudo-terminal instead of pipes. A shell run over pipes disables +> its prompt, its line editing and its job control; a multiplexer is +> pointless without them. +> +> **The problem.** Nothing can allocate a PTY pair, place the child on +> the slave side as its controlling terminal, or resize it. The +> reference reading is `.dev/reference/tmux` `spawn.c` + +> `compat/fdforkpty.c` (~450 lines of C covering five platforms; Linux +> alone is far smaller). + +## Info — the forks (open) + +1. **Surface.** A `pty: true` option on iteration 1's spawn verb (the + lean — one spawn shape, two transports) versus a separate + `proc.spawn_pty`. +2. **Resize.** A verb on the handle (`resize(cols, rows)` → TIOCSWINSZ) + — needed the moment [3](03-signals-as-events.md) delivers SIGWINCH. + The fork is only whether it ships here or with 3. +3. **The master fd's transport** is settled by iteration 1's fork 2 — + whatever won there carries the PTY master identically. +4. **Encoding edge.** A PTY master delivers the child's output with tty + post-processing (ONLCR and friends): raw the master by default versus + expose termios knobs. Lean: raw, no knobs, until a consumer asks. + +## Acceptance sketch + +- A real shell spawned on a PTY prints a PROMPT (it never does over + pipes) — asserted on the bytes. +- Resize: the child (a script reading TIOCGWINSZ) observes the new size. +- `isatty` inside the child answers yes on all three fds. +- Kill/reap/stop legs identical to 1's, PTY edition; fd count flat. + +## Consumers + +wmux 1 (every pane), the alacritty study's stage A (the headless +expect-clone is exactly "spawn on a PTY, script it, assert"). diff --git a/docs/stories/runtime-v2/03-signals-as-events.md b/docs/stories/runtime-v2/03-signals-as-events.md new file mode 100644 index 0000000..887b075 --- /dev/null +++ b/docs/stories/runtime-v2/03-signals-as-events.md @@ -0,0 +1,57 @@ +--- +track: runtime-v2 +iteration: "3" +status: pending +readiness: refine +--- + +# runtime-v2 3 — signals as events: SIGWINCH into a mailbox + +> Part of [Story — runtime-v2: the runtime beyond sockets](00-story.md). +> The seam iteration +> [42](../language-runtime-database/42-bounded-subprocess.md) +> deliberately deferred "to its real consumer" — this is that consumer +> arriving. A terminal application's resize IS a signal (SIGWINCH), and +> nothing today can turn a signal into anything a program observes +> except the SIGTERM/SIGINT stop latch. +> +> **The problem.** Signals are process-global, delivered on an arbitrary +> thread, and allowed to do almost nothing — the exact opposite of a +> shard-owned mailbox message. The runtime already crossed this bridge +> once: the stop flag is a signal made safe by latching. This iteration +> generalizes that shape without handing user code a signal handler. + +## Info — the forks (open) + +1. **Registration surface.** `signal.on(SIGWINCH, addr, msg)` in the + `time.after` mould (the msg MOVES to the runtime, delivered on + arrival) versus a process-level subscription table in `wo.toml`. + Lean: the builtin — dynamic, one consumer today. +2. **Delivery mechanics.** `signalfd` on shard 0's plane (a signal + becomes an fd event — no async-signal-safety questions at all, the + kernel-primitive taste) versus a latch array swept like deadlines. + Lean: signalfd; the runtime is Linux-first and the plane already + multiplexes fds. +3. **Which signals are offerable.** SIGWINCH and SIGCHLD certainly; + SIGTERM/SIGINT stay the ENGINE's (the stop latch is load-bearing — + iteration 40's drain). The fork is whether user registration for the + stop signals is refused by name or layered before the latch. +4. **Coalescing.** Signals coalesce in the kernel; a mailbox message per + delivery can't promise one-per-resize. Disclose coalescing (lean — + it is what SIGWINCH consumers expect anyway) versus sequence-number + them. + +## Acceptance sketch + +- Resize the controlling terminal of a test child: the registered actor + receives the message; the grid-owning code calls + [2](02-pty.md)'s resize onward — the wmux wiring, proven in miniature. +- SIGCHLD registration does not disturb 42's pidfd machinery (they + coexist; the pidfd stays the reap path). +- SIGTERM still stops the engine with the full drain — the iteration 40 + battery unchanged. + +## Consumers + +wmux 1 (SIGWINCH fan-out to panes). Everything else can wait — this +iteration exists exactly once a real consumer does, per 42's deferral. diff --git a/docs/stories/runtime-v2/04-termios.md b/docs/stories/runtime-v2/04-termios.md new file mode 100644 index 0000000..dff0a64 --- /dev/null +++ b/docs/stories/runtime-v2/04-termios.md @@ -0,0 +1,47 @@ +--- +track: runtime-v2 +iteration: "4" +status: pending +readiness: refine +--- + +# runtime-v2 4 — termios adoption: raw mode on a terminal we were given + +> Part of [Story — runtime-v2: the runtime beyond sockets](00-story.md). +> No incoming edges — startable alone, any time. +> +> **The problem.** [2](02-pty.md) creates terminals for children; this +> adopts the one the PROCESS was given. A wmux client must put its own +> stdin into raw mode (no echo, no line buffering, keys arrive as they +> are typed) and — non-negotiably — restore it on every exit path, +> including a trap. A terminal left raw is the classic way a program +> makes a user's shell unusable. + +## Info — the forks (open) + +1. **Surface size.** Exactly two verbs — `term.raw()` returning a + restore token and `term.restore(token)` (the lean: wmux needs + nothing else; tmux itself uses little more than `cfmakeraw`) — + versus exposing termios flag knobs. YAGNI says two verbs and a + refusal for the rest. +2. **Restore guarantee.** Tie restoration to the unwind machinery (the + 42 ownership pattern: fiber dies, terminal restored — a runtime + obligation) versus caller's-problem-with-a-doc-note. Lean: runtime + obligation; "no orphan" has a terminal-state sibling: no wrecked tty. +3. **Scope.** stdin only, versus any tty fd (a client adopting a tty it + received via [5](05-fd-passing.md) — the wmux SERVER's need). This + fork decides whether the verb takes an fd argument now or grows one + later. + +## Acceptance sketch + +- Raw on, keystroke arrives unbuffered and unechoed (driven under a + [2](02-pty.md) PTY in the test — the two iterations prove each other). +- Restore: `tcgetattr` before equals after, on the normal path AND after + a deliberate trap while raw. +- The stop path restores too — SIGTERM while raw leaves a sane terminal. + +## Consumers + +wmux 1's client half. The alacritty stage-A harness benefits but does +not require it. diff --git a/docs/stories/runtime-v2/05-fd-passing.md b/docs/stories/runtime-v2/05-fd-passing.md new file mode 100644 index 0000000..4137662 --- /dev/null +++ b/docs/stories/runtime-v2/05-fd-passing.md @@ -0,0 +1,56 @@ +--- +track: runtime-v2 +iteration: "5" +status: pending +readiness: refine +--- + +# runtime-v2 5 — fd passing: SCM_RIGHTS over the unix socket + +> Part of [Story — runtime-v2: the runtime beyond sockets](00-story.md). +> No incoming edges — startable alone, any time. Promoted from the +> alacritty study's Wayland stage by the +> [tmux study](../../plan/exploration/tmux/00-tmux-parity.md): the +> multiplexer needs it FIRST — a wmux client hands its tty fd to the +> server over the unix socket, the server writes escape sequences +> directly to the client's terminal, and detach is just the client +> process dying. That handover IS the tmux architecture +> (`.dev/reference/tmux` `compat/imsg-buffer.c`, `proc.c`). +> +> **The problem.** `net.listen_unix` exists (iteration 35) but a byte +> stream is all it carries; an fd cannot cross it. `sendmsg` with +> SCM_RIGHTS ancillary data is the only mechanism, and it is runtime +> work by nature. + +## Info — the forks (open) + +1. **Surface.** `net.send_fd(conn, fd)` / `net.recv_fd(conn)` — two + verbs, fd travels alone (the lean; tmux sends its imsg header as + ordinary bytes beside it) — versus fd-attached-to-a-message framing + in the runtime. +2. **What arrives.** The received fd as an opaque scalar the existing + `net.read`/`net.write`/termios verbs accept (lean — every fd verb + already takes an Int-shaped conn) versus a new wrapped type. +3. **The unix-socket client side.** Iteration 38's `net.connect` covers + outbound TCP; the CLIENT half of a unix-socket connection may or may + not exist by then — this iteration carries `net.connect_unix` if 38 + has not landed it first. Verify at brainstorm, not assumed. +4. **Bounds.** One fd per message, refusal by name past it (lean), + versus SCM_RIGHTS' multi-fd arrays. YAGNI: no consumer sends two. + +## Acceptance sketch + +- A test parent and child (via [1](01-streaming-subprocess.md)) pass an + open pipe fd across a unix socket; bytes written on one side arrive on + the other through the RECEIVED fd. +- A tty fd crosses and [4](04-termios.md)'s verbs work on it — the wmux + handover in miniature. +- Refusals: passing on a non-unix socket, receiving where none was sent + — both by name, neither a hang. +- fd hygiene: the churn leg, passing edition — counts flat. + +## Consumers + +wmux 1 (detach/attach), the alacritty study's stage D (Wayland needs +SCM_RIGHTS for shm buffers), and — the board's porch 2 aside — nothing +else yet, which is exactly why the surface stays two verbs. diff --git a/docs/stories/wmux/00-story.md b/docs/stories/wmux/00-story.md new file mode 100644 index 0000000..4265832 --- /dev/null +++ b/docs/stories/wmux/00-story.md @@ -0,0 +1,49 @@ +# Story — `wmux`, the writeonce terminal multiplexer + +The fourth track, and the first whose product is an end-user *program* +rather than a library or the toolchain: wmux, a tmux alternative written +in `.wo`. Where [`porch/`](../porch/00-story.md) proves writeonce can +carry a web framework, this track proves it can carry a native +fd-and-process workload — PTYs, signals, raw terminals, fd passing — +the territory the +[alacritty](../../plan/exploration/alacritty/00-alacritty-parity.md), +[tmux](../../plan/exploration/tmux/00-tmux-parity.md) and +[zen-browser](../../plan/exploration/zen-browser/00-zen-browser-parity.md) +parity studies mapped on 2026-09-01. + +Numbering restarts at 1 and is local to this track. Frontmatter carries +`track: wmux`; iteration 1 was briefly language iteration 43 and keeps +`was_language_iteration:` so a search for the old number still finds it. +Status rules are the repo's, unchanged. + +## Why a separate track + +Same three reasons porch got one, plus a fourth: + +1. **Different substrate, different gates.** wmux is `.wo` source, proven + by its own gate (`just wmux` when it exists) and a replay corpus, not + by the conformance corpus. +2. **Different cadence.** Its runtime prerequisites — the + [`runtime-v2/`](../runtime-v2/00-story.md) track: streaming + subprocess, PTY, signals, termios, fd passing — are each + builtin-sized runtime slices; the product iterations on top are + `.wo` work. +3. **It is a product surface.** A working tmux alternative is the + strongest public claim the language can make about native workloads. +4. **The upstream split is explicit.** When a wmux iteration needs a new + builtin, that half is a [`runtime-v2/`](../runtime-v2/00-story.md) + iteration, called out by name — the way iteration 42 (bounded + subprocess) landed first in the language track before the arc had a + folder. The gap chain lives in + [dependency graph section 6](../../00-dependency-graph.md). + +## Where the sequence came from + +The tmux study, measured against a shallow clone in +`.dev/reference/tmux`: ~112k lines of C, two dependencies, a ~27k-line +multiplexer kernel, a ~12k-line command/format/options DSL that +dissolves into writeonce language features, and one structural trick — +the client passes its own tty fd to the server with SCM_RIGHTS, which is +all detach/attach is. wmux's beyond-tmux leg is native to this stack: +sessions, layout and scrollback in `durable: true` tables, replayed from +the WAL after a server **restart** — state tmux loses by design. diff --git a/docs/stories/wmux/01-wmux.md b/docs/stories/wmux/01-wmux.md new file mode 100644 index 0000000..12874cd --- /dev/null +++ b/docs/stories/wmux/01-wmux.md @@ -0,0 +1,108 @@ +--- +track: wmux +iteration: "1" +was_language_iteration: "43" +status: pending +readiness: refine +--- + +# wmux 1 — the terminal multiplexer, writeonce's tmux alternative + +> Part of [Story — wmux, the writeonce terminal multiplexer](00-story.md). +> The product the [tmux study](../../plan/exploration/tmux/00-tmux-parity.md) +> scoped and iteration +> [42](../language-runtime-database/42-bounded-subprocess.md) unblocked +> first — +> a terminal multiplexer in pure `.wo`, driving every remaining +> native-workload gap into the open the way log-watcher drove the +> language and chat drove the actors. +> +> **Why this workload.** tmux is ~112k lines of C with two dependencies, +> but its multiplexer kernel is ~27k and a third of the rest is a +> hand-rolled command/format/options DSL that dissolves into writeonce +> language features. Its architecture — one server owning sessions and +> PTYs, thin clients over a unix socket — is the actor tree writeonce +> already has, with the concurrency written down instead of locked. And +> wmux can give the one demo tmux cannot: sessions, layout and +> scrollback in `durable: true` tables, so **reattaching after a server +> restart replays everything from the WAL**. + +## The dependency remap (graph section 6 carries the edges) + +Iteration 42 (bounded subprocess) is DONE and was the first domino. The +runtime seams now live as the [`runtime-v2/`](../runtime-v2/00-story.md) +track, each brainstormed on demand — the order below is the build order: + +1. **[Streaming subprocess](../runtime-v2/01-streaming-subprocess.md)** + (runtime-v2 1) — 42's named follow-up: a long-lived child whose + output arrives as mailbox messages in the owning actor; stdin + transport. The registry/pidfd/cap machinery is already built. +2. **[PTY allocation](../runtime-v2/02-pty.md)** (runtime-v2 2) — + openpty, the child on the slave as its controlling terminal, resize + ioctls. Extends the same `wo_child` slot; `.dev/reference/tmux` + `spawn.c`/`compat/fdforkpty.c` are the reading. +3. **[Signals as events](../runtime-v2/03-signals-as-events.md)** + (runtime-v2 3) — SIGWINCH (and SIGCHLD beyond the pidfd path) as + mailbox messages; the seam iteration 42 deliberately deferred to its + real consumer. This is that consumer. +4. **[termios adoption](../runtime-v2/04-termios.md)** (runtime-v2 4) — + the CLIENT puts its own tty into raw mode and restores it on exit. + Creating terminals is gap 2; adopting one you were given is this. + Startable alone. +5. **[SCM_RIGHTS fd passing](../runtime-v2/05-fd-passing.md)** + (runtime-v2 5) — the client hands its tty fd to the server over the + unix socket; detach/attach is built on it. Startable alone. +6. **VTE grid in pure `.wo`** — the escape parser and cell grid, pinned + by replaying recorded sessions against the reference `input.c`/ + `grid.c` behaviour. Needs **unicode width tables** in the stdlib. + This one is wmux's own, not runtime work. +7. **wmux itself** — server (session/window/pane actors, durable + tables), client (thin: raw mode, fd handover, restore), the gate. + +Not on the critical path, recorded: `time.mono`'s return (status-line +clock, repaint pacing — v2), and the terminfo fork below. + +## Info — the forks (open; why this is `refine`) + +- **Terminfo.** Answer "what does the client's terminal speak" by + parsing the compiled terminfo database in pure `.wo` (a documented + binary format — a parser, not a linked library) versus emitting a + fixed xterm-256color profile and refusing exotic terminals by name. + The tmux study leans fixed-first with a named refusal; confirm at + brainstorm. +- **v1 surface.** Sessions + one window each, no split panes (tmux's + `layout.c` is 2,022 lines of split-tree bookkeeping) versus splits in + v1. Lean: no splits — detach/attach + durability IS the product's + proof; splits are v2. +- **Scrollback residency.** Scrollback rows in a `resident: keys` table + (the 120 GB-audit-table machinery, databasev2 2) versus resident with + a row cap. Real fork: scrollback is append-mostly and read-rarely — + keys-resident's exact profile. +- **Command surface.** tmux's 63 `cmd-*.c` + yacc grammar versus wmux + taking commands as... writeonce source? a tiny line protocol? The DSL + third of tmux should dissolve, not be rebuilt — decide what into. +- **Verb shape of the streaming form** (gap 1's own brainstorm): one + `proc.spawn` returning an actor-addressable handle, versus spawn + options on `proc.run_dl`. + +## Acceptance sketch (firmed when the last gap lands) + +- Given a wmux server with one session running a shell, when the client + detaches and its process is killed, then the shell keeps running and a + NEW client attaches from a different terminal with the screen intact. +- Given a server stopped with SIGTERM and restarted against the same + `WO_DATA`, when a client reattaches, then sessions, layout and + scrollback are replayed from the WAL — the beyond-tmux leg. +- Given SIGTERM with live shells, then every child is gone before exit + (iteration 40 + 42's guarantee, now with PTYs). +- Given a `vttest`/asciinema replay corpus, then the grid matches the + reference implementation's final state (stage B's pin, kept green). +- Gate: `scripts/wmux-accept.sh` + `just wmux`, log to `/tmp/wmux.log`. + +## Consumers and siblings + +The alacritty study's stages A–C become wmux's gaps 1–6 verbatim; its +stage D (Wayland shm terminal) reuses gaps 5 and 6. The zen study's CDP +driver shares gap 1 only. skillhost (28) consumes gap 1's stdin +transport. Every remaining item in 28's old fan-out except fs-metadata +and FFI-vs-out-of-process now has wmux as its driving workload.