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:
parent
185251c404
commit
ab8ef64cd9
11 changed files with 729 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
49
docs/stories/runtime-v2/00-story.md
Normal file
49
docs/stories/runtime-v2/00-story.md
Normal 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.
|
||||
69
docs/stories/runtime-v2/01-streaming-subprocess.md
Normal file
69
docs/stories/runtime-v2/01-streaming-subprocess.md
Normal 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).
|
||||
47
docs/stories/runtime-v2/02-pty.md
Normal file
47
docs/stories/runtime-v2/02-pty.md
Normal 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").
|
||||
57
docs/stories/runtime-v2/03-signals-as-events.md
Normal file
57
docs/stories/runtime-v2/03-signals-as-events.md
Normal 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.
|
||||
47
docs/stories/runtime-v2/04-termios.md
Normal file
47
docs/stories/runtime-v2/04-termios.md
Normal 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.
|
||||
56
docs/stories/runtime-v2/05-fd-passing.md
Normal file
56
docs/stories/runtime-v2/05-fd-passing.md
Normal 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.
|
||||
49
docs/stories/wmux/00-story.md
Normal file
49
docs/stories/wmux/00-story.md
Normal 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.
|
||||
108
docs/stories/wmux/01-wmux.md
Normal file
108
docs/stories/wmux/01-wmux.md
Normal 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.
|
||||
Loading…
Reference in a new issue