docs(rt2): runtime-v2 track — the runtime beyond sockets

- 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 <noreply@anthropic.com>
(cherry picked from commit e0451cb4889bb3d03429c0276d03c090269a5ced)
This commit is contained in:
shoney.arickathil 2026-09-01 22:48:17 +02:00
parent 185251c404
commit ab8ef64cd9
11 changed files with 729 additions and 2 deletions

View file

@ -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

View file

@ -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 |

View file

@ -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) |

View file

@ -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.

View file

@ -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).

View file

@ -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").

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.