- delete all RateLimitCounter access from limiter.wo: query, increment,
delete-then-insert, and the swallowing catch (e) nil -- the pool is now
the only writer, so its serialization guarantee actually holds
- Limiter gains pool/limit/trust_proxy fields; before() calls pool_count
and acts on the Verdict; make_limiter takes a pool
- key selection: req.principal first, else trust_proxy ? client_ip(req)
: net.peer(req.conn); delete the dead req.ctx["verified_proxy"] branch
- 429 on a spent window (Retry-After, X-RateLimit-*); 503 + Retry-After
on a caught actor trap (saturated pool), request never let through
- add Limiter.after(), registered alongside before() as both Mw and Aw
(Cors's own shape) so the allowed path's X-RateLimit-* headers reach
the response, not just req.ctx
- scripts/web-app-accept.sh: three new gate legs -- threshold (N pass,
N+1th 429), SIGTERM+restart (still limited from the WAL), and N
genuinely-parallel curl clients on one key with an exact-count assertion
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a653dd0aa64711c43461126d32b4632cc8f64c7a)
- keypool.wo:78,89 passed msg.window (µs) straight into pool_pack's
remaining_ms (ms) parameter on the first-hit and post-prune-reset
paths; the third call site already divided by 1000 and was correct
- fix: pool_pack(1, msg.window / 1000) at both sites — a 60s window
no longer reports reset_at ~16.7h away
- count/allowed were unaffected (computed independently); this only
hit the client-visible reset instant, on the two most common cases
(new key, window rollover)
- extended gate leg 16 to assert reset_at falls within a 5s band of
time.now() + window_ms, not just on count — verified the assertion
itself by reverting the fix, confirming leg 16 failed with the
exact defect shape, then restoring it and confirming green
- woc docs/examples/porch/ exits 0; web-app-accept.sh: 47 checks,
0 failures
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 153fd295d37905dd083823536c6781843699e455)
- add docs/examples/porch/middleware/keypool.wo: one PoolMsg (kind 1 =
count, kind 2 = begin placeholder for task 4), a Verdict class, a
fixed-size actor pool with a byte-sum-mod-N selector
- KeyActor.receive implements kind 1: reads the row, writes the new
count via field assignment (writes through, never delete+insert),
prunes a fully-elapsed window's row instead of resetting it
- window arithmetic on time.ticks(); reset instant sent back is built
from time.now() only
- call's reply must be a copyable scalar (WO-E226), so the count and
remaining window time are packed into one Int by the actor and
unpacked into Verdict by pool_count — the packing stays inside this
file, callers only ever see Verdict
- gate leg in scripts/web-app-accept.sh: a flat copy of porch (manifest
stripped) with a driver dropped beside keypool.wo asserts two
sequential counts return 1 then 2; verified failing (E403, make_pool
undeclared) before this file existed, passing after
- woc docs/examples/porch/ exits 0; full web-app-accept.sh: 47 checks,
0 failures
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 676e651d808ef2c3619f88c3808adc372cd0be6c)
- Add digest field to store sha256(method|path|body) separately from key
- Enables detection of "same key, different body" in future tasks
- Update idempotent.wo insert to compute and store digest value
- Typechecker passes: exit 0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3a9bddcd1e3c11f5b371ce54cafc373685ca08b6)
- docs/examples/porch/ did not typecheck: WO-E403 "cannot resolve the
receiver's type for the call to `encode`" on json.encode
- every other example that calls json.encode/decode imports it; this
file did not, so the whole porch library was uncompilable on dev
- woc docs/examples/porch/ now exits 0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 5c3544d524fa98dbb7a363600cd2eeb6dd1badac)
- replays a stored response for a repeated Idempotency-Key: before()
checks the key, after() stores status/body/content-type on a 2xx/3xx
- key is "idem:<header>:<value>", optionally plus a sha256 digest of
method|path|body when include_body is set
- 409 while a key is in flight (stored within the last 10s), lazy TTL
expiry on access, default 24h
- replay allowlists content-type only — never Set-Cookie or Date
- backed by IdempotencyKey from Phase A (519d411)
Written by a parallel session and committed here as-is because its
branch was consolidated away. NOT verified: it calls json.decode and
json.encode without a `use json` import, which every other example that
uses json has. Left unedited rather than fixed blind.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit aee79264095eea3b2c38c92789c44b31f1c9ef8a)
- resolved 9g's forks EMPIRICALLY against the running compiler:
- count(<query>) and len(<query>) already work (fork 2 collapses to
zero code)
- skillhost's correlated NOT EXISTS is a backlink emptiness in
writeonce (`where len(x.children) == 0`), using only 9b machinery
(fork 1) — verified on a self-referential ?ref/backlink table
=> corpus #1 forces NO new grammar; per the method ("add only what a
corpus uses"), exists/not-exists was NOT built
- docs/examples/skill-catalog: mirrors skillhost's `skills` table
(name @unique, description/location/root, parent ?ref Skill, children
backlink) and translates all five of its SQL statements 1:1
(insert+dup-trap, get-by-name, list, roots via backlink-emptiness,
count); scripts/skill-catalog-accept.sh 7/0, WAL-durable, dup trap
persists across restart
- fixture run/db-query-corpus (count(query) + backlink NOT EXISTS);
just skill-catalog module; target/ gitignored
- general exists/not-exists left unbuilt and recorded as "enters when a
corpus forces a non-relation correlation"
- gates: oop-e2e 80/0, woc-test 566/0, skill-catalog 7/0; story + board
record the finding
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4c82461634d45f11eca1a252702031c03d999c7f)
- docs/examples/subprocess: line-oriented TCP service, one Handler actor
per request — ping/run/slow/deadline/cap/long exercise the whole
bounded surface from .wo, traps caught with try/catch in the language
- scripts/subprocess-accept.sh + `just subprocess`: 12 checks, 0 failures
first run — deadline and cap messages verbatim, ping answered in 2 ms
while a sleep-2 child was parked, SIGTERM exit 0 with the sleep-30
child verifiably gone (pid checked from outside)
- service logs to /tmp/subprocess.log, banner-separated per run
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- extracted to github.com/shoneyJ/writeonce-site with `git subtree
split`, so the site keeps its own 9 commits of history rather than
landing there as a flattened snapshot
- .gitmodules gains the third entry, alongside reference/writeonce-app
and reference/writeonce-api; path is unchanged, so every doc and
script that names docs/examples/site still resolves
- site-accept.sh fails early and says `git submodule update --init`
when the directory is empty. Without it a clone lacking submodules
copies an empty app and fails later as a build error naming nothing
- releasing.md: the steps that edit install/view.wo now say that edit
is a commit in the site repo plus a pointer bump here — editing and
committing only in this repo would record nothing
- gate re-run against the submodule: site-accept 23 checks, 0 failures
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4b5634801d5379890bad24c8129cfb23ab6b98df)
- new chapter 7, "Storage modes: durable and resident", covering what
master gains with the databasev2 cherry-pick: durable: false for a
RAM-only table, resident: keys for a table that outgrows RAM
- states the parts a reader would otherwise hit as surprises: a
keys-resident table is REFUSED at startup without WO_DATA, an update
appends a delta rather than rewriting the row, and the chain is
bounded at 16 links so a hot row does not degrade reads or replay
- quotes the measured 2.55x smaller resident set, not an estimate
- actors/deps/serving shift to ord 8/9/10; seeding is ord-driven so an
existing WO_DATA keeps its rows and only a fresh boot reseeds
- home card says a table can be RAM-only or outgrow RAM
- two gate legs pin the new chapter; site-accept 23 checks, 0 failures
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b503c0db50aa737dd06ef6d17151c654a42c59b)
- new `residency` leg in db-bench.py driving docs/examples/residency-bench:
two tables identical except the annotation, control cap + binding cap
- ITS OWN PROGRAM, not a db-bench mode: declaring a resident: keys table
is a WHOLE-PROGRAM constraint, so the no-WO_DATA refusal fires for
every mode in the module. Putting those classes in db-bench's shared
types made growth/ceiling/randread — which run without WO_DATA —
refuse to start. Caught by running the leg, not by reading it
- gates the RATIOS, waives the absolutes: ops/sec under a cap is swap
and disk I/O and belongs to the box. Same split randread makes
- rss_ratio 2.55 floor 2.0 tol 10% (structural, like bytes_per_row);
overcap_vs_swap_x 1.53 floor 1.0; in_ram_cost_x 4.23 ceiling 8.0;
all_collapse_x 105.4 floor 2.0
- all_collapse_x exists because the leg's FIRST run silently measured
nothing: at QUICK's 40k rows a 48 MiB cap binds neither mode, so the
"over-cap" half was not over cap. The cap now scales with N and the
leg asserts it binds
- verified the gate bites: rss_ratio 1.4, overcap_vs_swap_x 0.6 and
in_ram_cost_x 12.0 are all rejected
- task 7 closed: both criteria moved to Met with how each was verified
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a310496664982c372f51b43113465eb8ad9e9fb5)
- hreadall/hreadkeys: the same resident A/B as wread_*, but ~2 KB per
row so a GB of data is reachable in a few hundred thousand inserts
- the insert path is fsync-bound at roughly 2 000 rows/s, so row COUNT
is the expensive axis and row SIZE is nearly free — 20k rows already
produce 38 MB
- NOT RUN: the GB-scale measurement was called off. These modes are
committed working and typechecking so the leg can be run later
without rebuilding it, not because a result exists
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit abc276ac39dd45a1052b6ae24d25aedb9eea3e1e)
- two tables identical except the annotation, 200k rows, 40k reads in
one key order, WAL on ext4 (not /tmp, which is tmpfs here and would
have put the log in RAM), rootless cgroup v2 cap
- WIDE shape, 2.55x smaller resident set: 34.4 MB vs 87.5 MB. That is
the real win and the thing the mode was built for
- under a 48 MB cap (between the two resident sets): keys 19635 ops/s
vs all 12854 — only 1.53x faster than letting the kernel swap
- degradation is far gentler though: all collapses 105x from its own
uncapped throughput, keys 16x
- costs 4.2x read throughput when memory is not tight, and writes are
markedly slower — the keys fill did not finish in 2 min where the
resident fill plus 40k reads did. No design doc had costed writes
- THE UNANTICIPATED FINDING: cgroup limits charge the PAGE CACHE, so
moving rows to a file does not escape a container memory limit. WAL
37 MB + RSS 34 MB cannot both live under a 48 MB cap, so every pread
reaches disk. The premise "the page cache will hold the hot rows"
fails in exactly the deployment this targets
- first attempt used Int-only rows and showed parity; recorded, because
drop_payload frees a field's VALUE and an Int's value is its inline
slot word, so that shape cannot benefit and would have condemned the
feature for the wrong reason
- verdict: keep it, to fit ~2.5x more data in given RAM — not to make
an over-capacity table fast
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7cba9b1174b0bf581314b3147e25cc49e6f49464)
- loader.c: delete the INCOMPLETE-update BAIL; durable:false +
resident:keys stays refused (nowhere to read from)
- table.c: root-cause fix for the Text-index gap — a keys-resident
borrow now holds ENGINE values, matching wo_row_ptr's contract
(table.h's "no VM pointer" doctrine), not a VM-decoded row. Fixes
idx_hash/idx_cols_equal/wo_idx_probe AND db.c's GET_FIELD/PROBE
arms with one change; reproduced pre-fix as an ASan
heap-buffer-overflow
- docs/examples/residency: Product is genuinely resident:keys;
residency-accept.sh's refusal leg replaced by proving the program
runs and stock survives a restart (11/0)
- test_wal.c: oracle test drives resident:all and resident:keys
through the same update sequence and asserts identical rows;
Text-indexed-update test catches the representation bug; five
pre-existing tests corrected to the fixed contract (4746/0)
- story, README, status board, CODE-LOGIC.md updated; three known
limitations documented: mid-drain stale reads, O(N^2) replay in
chain length, compaction blind to per-row chain length
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b87c68f950f01aa5e572fbb86a0f374adc83d813)
- the motivating workload was an audit log, which is append-only and so
argues for nothing. A catalogue is the real case: stable ids, and
stock moving on every order while name/price/sku sit still
- Product (durable, resident: all) and Cart (durable: false), with
place_order decrementing stock through a write-through field assign
- run `order` twice and stock goes 10 -> 7 -> 4: a level below the
seeded 10 can only mean an earlier order's UPDATE replayed. That is
the stronger claim — not just that inserts survive, but that a field
change does
- caught by running it three times: my first assertion required
before == 10, which only holds on a fresh seed and failed on the
third run even though the data was correct
- gate gains a leg for the update-replay claim; residency 12/0
- the commented resident: keys block now argues the DESIGN too: only
stock changes per sale, so appending the whole row would rewrite
every field to move one integer on a shop's hottest write path
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit d4104dcc5e3a460459dbf2d24043ce25b113bcdf)
- docs/examples/residency: one program, two tables filled by the same
loop, differing only in the annotation. Run twice against one WO_DATA
and orders replay while sessions do not
- the example checks its own claim (exits 1 if a durable:false table
survives, or a durable:true one fails to replay) rather than narrating
it in a print
- resident: keys is written out as a commented block with the loader's
exact refusal, so the frontier is visible in the example rather than
only in a story. It documents WHERE the refusal happens: woc compiles
it and emits a .wob; wovm exits 2, because the annotation is a
load-time property
- residency-accept gains two legs: the example runs and its restart
claim holds, and the refusal message the README quotes is checked so
doc and code cannot drift apart
- the gate writes the example's output to /tmp/residency.log,
banner-separated, for tail -F
- README commands verified verbatim; they needed mkdir -p because wovm
will not create WO_DATA
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c9c7e03e62c3918cb65ef7994d1e33a0c5337b71)
- loader's resident:keys refusal said "rows are still fully resident"
and "until tasks 5c/5d land". Both false since f606fc9. Corrected to
name the real blocker: UPDATE needs read-modify-append
- databasev2 00-story: the sequence graph drew 2->3->4, which reads as
3 needing 2 and 4 needing 3. Both backwards, and it still drew the
2->5->6 path the 2026-08-27 amendment retired. Redrawn stating only
real dependencies, with 4 and 3 shown as composing rather than
ordered, and the execution order that actually happened
- databasev2 03: the hazard and its Outstanding entry both claimed
nothing fails "because iteration 2's storage half is unimplemented".
Marked discharged, and recorded that the hazard named only half the
danger — the bitmap walk would have dropped keys rows outright
- databasev2 06: pending -> hold (largely superseded, revisit only on
a measurement); dated its 5c/5d references
- porch 01: rewritten to the settled shape. readiness ready, status
in-progress, phases B and C marked superseded with why
- porch 01 claimed time.after "is still a reserved builtin id". False —
builtin 90, implemented. That claim is what made the iteration look
cheaper than it is
- porch README gains honest ledger rows for both features (partial,
being rebuilt), not shipped
- skill-catalog README pointed at a story path that moved tracks;
linkcheck now 0 broken
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b3d8c403e1d19ac27ec966de85cb293e0765795c)
The branch was 17 ahead / 25 behind with 11 conflicting files, and drifting
further: db.c had been rewritten twice on master since (group commit, then
compaction). Resolved rather than rebased so both histories stay legible.
Conflicts, and how each was settled:
- db.c: BOTH semantics kept. Master's fatal path and compaction check now sit
behind the branch's `table_is_durable` predicate, in all three inline arms —
a volatile table reaches neither the barrier nor the compaction check
- db-bench sample: every mode from both sides (growth, growth-verify, randread,
replayseed, wmix) and ONE `boot` mode, which both sides had added
independently
- db-bench.py: all six legs kept. Both sides had also grown the same
WAL-size helper under different names; collapsed into one
- perf-targets: the branch's §5 (RAM ceiling) then master's §6/§7 — master's
numbering had already assumed a §5 it did not have
- story frontmatter: master's `status` (the landing truth) plus the branch's
`readiness` axis. 03 would have read `done` + `refine`, which is a
contradiction — it was brainstormed and landed on master, so `ready`
- board: both standup blocks newest-first; master's chain rows (a superset);
the branch's databasev2 1-2 rows with master's 3-4. Fixed a stray `|` in
master's row 3
- baseline: master's, then REGENERATED from a full campaign — 143 metrics,
132 checks, 0 failures with both sides' legs present
TWO HALF-EXPOSED FEATURES FIXED, because the merge rule is that master gets
no feature that is honoured in name only:
- `resident: keys` PARSED, set a .wob flag, and did nothing: rows stayed fully
resident. A developer could declare a 120 GB table keys-resident, watch it
compile, and be OOM-killed. The loader now REFUSES it with a message naming
what to write instead, until tasks 5c/5d land. The compiler still parses it
and its AST golden still passes, so the grammar work stays tested
- `durable: false` was honoured ONLY on the inline path. wo_db_exec_req had no
guard at all, so a volatile table written from an actor on a worker shard
would still be logged — precisely porch's session-table case, and precisely
what iteration 2 exists to provide. All three request-path arms now carry the
same predicate. Found by reading the merged code, not by a test: the obvious
probe runs main() on the primary and therefore only exercises the inline path
Verified on the merged tree: wovm-test 0, woc-test 0, oop-e2e 122/0,
residency-accept 8/0, db-bench 132/0, linkcheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
databasev2 3, task 6. Documentation, plus three gate-tolerance
corrections that are justified rather than silent.
- 04-db-binding.md: the NORMATIVE rule — compaction may run only where
nothing is staged (a correctness requirement, not scheduling), recovery
is unchanged, and a failed compaction is a missed optimisation rather
than a durability event
- database/src/CODE-LOGIC.md: why one file and not snapshot-plus-tail
(Postgres CANNOT compact — page deltas; ours are full row images, so a
compacted log IS a store), why rename is the whole crash-safety story,
why the dump flushes but does NOT fsync when it does, why the
replacement is preallocated, and where the trigger is checked
- README: the checkpoint knobs, the extended walstats line, the boot mode
- story -> status: done, with criteria split met/outstanding
- board: standup entry in the six-question shape, both rows rewritten
THE OBLIGATION IS AT THE COMPACTOR, not only in a spec: compaction moves
every record, so it invalidates every WAL offset iteration 2's
`resident: keys` stores, and the loop that knows each record's new
position must rebuild that map. Nothing fails today because that storage
half is unimplemented — it would fail later, looking like corruption.
Board claim corrected before it shipped: I wrote that the concurrency
chain is "complete". It is not — chain 5 stays in-progress because
databasev2 4's part B was never done and its premise was invalidated by
part A. Every link has landed its PLANNED work; that is a different
statement.
Gate tolerances, each with the measurement that justifies it:
- ckpt.pause_us_max is no longer gated relatively. The raw pause scales
with the live set and this workload's live set is not fixed (wmix's
hist_dump inserts a row per latency bucket), so gating it gates the
box. Added ckpt.pause_us_per_mb — the engine's own rate, gated for
real, and the metric that would have caught the 8x dump regression —
with the absolute 50ms budget still guarding the raw pause
- ram.*.msgrate 15% -> 70%. PRE-EXISTING, and measured: 10.7M-17.9M
msgs/sec across ten full runs, several predating this work — a 1.67x
spread against a 15% gate
- durable.sN.*.p99us 100% -> 300%, with more evidence than the first
widening: mixread 1043/2318/4147us, mixwrite 1623/4446us on the same
build. Floors stay the real guard and are not slack
Battery: wovm-test 36 suites 0 fail, woc-test, oop-e2e 119/0,
db-bench 117 checks 0 failures, linkcheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
databasev2 3, task 5.
Full campaign, same workload twice, differing only in whether
checkpointing may fire:
- WAL used 1962358 -> 907094 bytes (2.16x reclaimed)
- boot 114 -> 64 ms (1.78x), median of 3
- stop-the-world pause max 2651us against a STATED 50ms budget
The budget is asserted, not assumed: 50ms is a stall a serving process
can absorb without a client seeing a timeout, and the leg fails if it is
exceeded. The pause is O(live rows) — at ~181 MB/s a 1GB live set implies
~5.5s, which is the number an incremental design must be bought against.
The spec deliberately did not buy it in advance.
FOUND BY MEASURING: the dump was 8x slower than it needed to be. It
flushed through wo_wal_commit, which fdatasyncs, so it paid one barrier
per 256 records. Intermediate durability there is worthless — the temp is
not authoritative until the rename and is fsynced once immediately before
it. With a single final barrier:
- ~107KB live: 23948us -> 2903us
- ~500KB live: 36361us -> 7526us
- ~1.98MB live: 107649us -> 13212us
- marginal ~22 MB/s -> ~181 MB/s, sync-bound to bandwidth-bound
Correctness re-proven after that change: wovm-test 36 suites 0 fail,
test_wal 760 pass including the 40-round kill-during-compaction battery.
Two measurement defects of my own, fixed rather than reported:
- boot measured through the driver's run() helper reported 251ms both
with and without checkpointing — run() samples RSS on a 250ms poll, so
every timing floors at the quantum. Measured directly instead, median
of 3
- ckpt.reclaim_x was recorded as lower-is-better by the default detector,
which would have PASSED "reclaimed nothing" and FAILED an improvement:
the feature's central claim, gated backwards. Now higher-is-better,
gated at 15% while the wall-clock metrics stay wide — waiving them all
would have left the leg ungated, part A's task 4 mistake
- sample gains a `boot` mode that does nothing, so boot time is boot time
- walstats now reports compactions, pause max/total and compacted bytes
- baseline refreshed from the FULL campaign (N=20000, crash_reps=3), and
a fresh full run passes 116 checks 0 failures
- gate bites: reclaim_x doctored to 1.0 -> FAIL on exactly that metric
One flake seen and checked, not papered over: durable.sN.query.ops_sec
failed once at 53% below baseline. It is a read-only metric that touches
no WAL code, and a re-run passed 116/0 with the box at load 1.85.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
databasev2 4 part A, task 6. Mostly documentation, plus one real fix the
full battery caught.
THE FIX. The drain held EVERY DB reply until the barrier — including
reads, which stage nothing and have no stake in durability. That parked
readers behind an fsync for no reason: durable.sN.mixread.p99 rose from
~1043us to 4057us. Only a statement that actually staged a record now has
its reply held. Caught by the gate, not by review.
THE TRADE, recorded rather than smoothed over. What remains is inherent: a
barrier blocks the owner shard LONGER (more records per fsync) though LESS
OFTEN, so anything queued behind one waits. Three full runs of the same
build gave durable.sN.mixread.p99 of 1043 / 2318 / 4147us and wmix.p99 of
8758 / 20000us — a 2-4x spread with the box near idle. So part A buys ~3x
write throughput at the cost of a longer, noisier tail on the owner shard,
and that is the strongest argument for part B (submit and keep serving).
- durable.sN.*.p99us tolerance widened to 100% WITH the reason in the
code: a 2-4x-variable tail gated at 50% gates the disk, not the engine.
The floor is the real guard and is not slack — mixread's (4172us) came
within 25us of tripping on the worst run. Baseline refreshed; a fresh
full run then passed 106 checks 0 failures
EXIT STATUS MOVED 3 -> 74 (sysexits EX_IOERR). 3 and 4 are already used by
SAMPLES for their own meanings — db-bench's own `verify` exits 3 on a
checksum mismatch, and it is the gate that exercises durability, so a
durability abort exiting 3 would have been indistinguishable from the
mismatch it should help diagnose. The low range belongs to programs.
Docs:
- story: progress, the payoff measured two ways, the cost side, criteria
split met/outstanding, and a "part B — its premise changed" section:
it was justified by "close the 66x gap", but that gap is two problems
and only the concurrent one was a batching problem
- board: standup entry in the six-question shape; both databasev2 4 rows
rewritten. They had said "close the 66x gap" — recorded as MIS-STATED
rather than quietly renumbered
- 00-wob-format.md and 04-db-binding.md: the normative failure contract
("a failed WAL commit traps WO_T_IO after un-applying the row") was
false; corrected, along with the tick-scoped group commit that never
happened
- database/src/CODE-LOGIC.md: where the barrier runs and why there, why
replies are held, why the inline path is asymmetric, the one failure
rule, and how to measure it
- db-bench README: the wmix mode, the env knobs, and the tmpfs warning
Battery: wovm-test 36 suites 0 fail, woc-test, oop-e2e 119/0,
db-bench 106/0, linkcheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
databasev2 4 part A, task 4. Scope extended with developer approval: the
plan authorised touching the sample only for observability, but no
existing leg has enough concurrent durable writes to exercise group
commit at all, so the payoff was unevaluable either way.
The finding that forced it:
- `mix` writes on one op in ten with C=4 (all_mode calls mix_mode(n/10,
4); Mixer writes on i % 10 == 9), so the quick run performs 20 writes
total. Measured mean batch 1.01 over 3112 barriers, peak 3
- that is a property of the WORKLOAD, not the mechanism: peak 3 of a
possible 4 shows batches form whenever writes actually coincide
- `wmix N C` added: every op a durable write, C at once. Updates rather
than inserts, so it is comparable to mixwrite and the row count stays
flat. Histogram kind 2 — a replayed store still holds the seeding run's
kind-0/1 Hist rows and merging those would report someone else's
latencies
- WO_WAL_STATS=1 prints one line at exit: batches, records, peak_batch,
peak_staged. Opt-in, because it would otherwise pollute every durable
program's output. Counters live in wo_wal; no builtin, the numbers are
diagnostic and not part of the language
Measured, and it scales with concurrency exactly as designed:
- C = 4 / 16 / 64 -> mean batch 1.13 / 1.76 / 5.35, peak 3 / 10 / 39
- the gate's own legs: durable.s1 5412 records over 5412 barriers (mean
1.0, peak 1 — the inline path, one barrier per statement BY DESIGN),
durable.sN 7757 over 2296 (mean 3.38, peak 28) at 2x the throughput
- peak staged 1372 B settles the no-cap decision with a number: the batch
is tiny, so the upstream mailbox bound is sufficient
- mean_batch/peak_batch are higher-is-better (the default detector would
have called bigger batches worse)
- only the batch SHAPE metrics are waived to 100%; wmix throughput and
latency keep real tolerances (15% s1, 50% sN) — a blanket waiver would
have left the entire new leg ungated
- the live assertion `mean > 1.0` on the sN leg is what catches inertness
- gate bites: sN wmix ops_sec -60% -> FAIL on exactly that metric, 1 of 86
Verified: db-bench-quick 89 checks 0 failures; baseline refreshed (86
metrics).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Iteration 24 closes, absorbing 31 and 34. No code in this commit.
- stories 24, 31, 34 -> `status: done`, each with a landing banner. 24's
records the gate numbers and BOTH disclosed deviations: monitor takes
three arguments (the caller may be `main`, which has no mailbox) and a
v1 `call` reply is a typed scalar (which is what let the agreement be
checked at compile time, WO-E226). 31's notes it landed INSIDE 24 and
that a fifth mechanism it never anticipated came out of proving the
gate — the drain guarantee (40). 34's names the gap it did NOT close:
still no RNG, so CSRF/sessions stay blocked
- board: in-progress row cleared, marker doc deleted (convention), the
standup entry in the six-question shape, chain note — next link is
databasev2 4 (io_uring group-commit, chain 5)
- graph: PUBSUB2 (pub/sub + WebSockets, "rejected until here") -> done
- porch ledger: a WebSocket/pub-sub row added; the cancellation row now
says what it actually waits on rather than repeating "the arc"; the
README's "no WebSockets/SSE" limitation was stale — WebSockets are
supported, SSE and chunked encoding are not
- CODE-LOGIC: runtime/src gains the actor-lifecycle section (call, death,
the cap counter's sender/home-thread split, the monitor walk, the timer
list), the drain guarantee, and the digest section; docs/examples/chat
gains its own — actor topology, WHY two actors per connection, fd
ownership, and the shutdown choreography
Battery after the doc edits: wovm-test 36 suites 0 fail, woc-test exit 0,
oop-e2e 119/0, chat 11/0, web-app 46/0, linkcheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gate defects, all measured:
- fd check was core-count dependent: `fds_before + 8` read LAZY per-shard
init as a leak. Shards init on first fiber, each taking one io_uring +
one eventfd, capped at nproc; on 20 cores the first wave legitimately
adds 18. Measured 26 -> 44 after 20 clients, still 44 after 40 more.
Replaced with the invariant the check is for: a second wave must not
raise the count. Core-count independent, and catches a slow leak that
any fixed slack would hide
- a failed leg ORPHANED its server: drain inherited $SRV from the soak
leg, so its python died on int("") and the soak server was never
killed — its listener then broke the next run's soak on the same port.
drain now starts its own server; cleanup kills every server a run
started, matched on the run's unique temp dir
- two legs the plan requires were missing: WO_SHARDS=1 (the single-shard
control) and WO_MAILBOX=8 (drop-slow-member backpressure). Both added,
both green. The mailbox leg shrinks the slow client's SO_RCVBUF so it
needs no sleeps
- chat adopted the porch naming (use porch/..., [deps] key) after the
rename landed on master
Decoupling the legs exposed a REAL drain bug, traced and documented in
docs/2026-08-27-chat-drain-finding.md, NOT fixed here:
- on a FRESH server the SIGTERM drain is flaky: 5 of 16 runs left a
client at EOF with no close frame and no diagnostic
- traced: main -> Registry -> Room -> Writer. Registry runs (diag
confirms), the Room NEVER processes its shutdown message, so the
Writer's close branch never runs. Clients that do get a frame are
saved by their own Reader seeing env.stopping()
- ruled out: the spin budget (a 1s wall-clock deadline still failed 2 of
12 — reverted, it fixed nothing and cost 1s per shutdown),
dummy_writer() spawning during shutdown, and write failure
- the fix is an engine guarantee — a send issued before the stop flag is
delivered — which belongs to the actor lifecycle, not a spin count
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the last gap in databasev2 1; gives databasev2 3 its "before".
- `boot` mode: does NOTHING. WO_DATA replay runs before main, so a mode
with no work measures replay plus a fixed startup
- `replayseed N M`: N inserts + M updates — same live rows, longer log
- `replay` leg: empty-store startup floor measured and SUBTRACTED, then
two shapes timed, median of 3 boots each
- premise check: updates must actually append WAL records, else the two
shapes are one measurement and the penalty means nothing
- WAL bytes = non-zero prefix, never file size (fallocate'd to 1 MiB)
- per-record cost stored in NANOseconds: as us it rounded 5.5 and 5.3 to
6 and 5, too coarse for the number a checkpoint exists to improve
- 148 checks, 0 failures; gate bites on a doctored ns_per_record
Measured — same 20 000 live rows, different history:
- 20 000 records: 980 035 B WAL, 110 ms replay, 5.5 us/record
- 40 000 records: 1 960 035 B WAL, 211 ms replay, 5.3 us/record
- 1.9x boot cost for an IDENTICAL dataset; per-record cost flat, so
replay is linear in records not rows
- extrapolated: 10M records ~55 s of boot, 100M ~9 min
- databasev2 3 correction: it planned to use "22's aged-store replay
numbers", which never existed — 22 proved restart correctness, never
timed it
- databasev2 3 hazard recorded: compaction rewrites the log and moves
every record, so it invalidates every `resident: keys` offset — an
arbitrary byte in a rewritten file, not stale-but-readable
- databasev2 1 -> status: done
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- `randread N R` in the sample: fill N rows, read R across the WHOLE range
- Weyl order `i*2654435761 mod n` — no RNG in the language, none needed;
both legs read the SAME key order so residency is the only variable
- `randread` driver leg: control (256 MiB, does not bind) vs over-cap
(6 MiB + swap), sizes kept modest — quick resolves it in ~5s
- gates the RATIO, not the absolutes: over-cap reads/sec belongs to the
box's swap device, the factor between two runs belongs to the engine
- reads must all resolve (hits == R) or the leg fails; a collapse measured
over unresolved reads is noise
- 133 checks, 0 failures; gate bites on a doctored collapse_x
Measured — this closes the gap the swap leg left:
- resident 1 851 166 reads/sec, p50 0us p99 1us
- over-cap 6 771 reads/sec, p50 128us p99 487us
- 273x throughput, ~480x p99, all 20 000 reads resolving in both
- so the two access patterns sit ~270x apart under identical pressure:
append-mostly insert ~1%, random read 273x
- departure is a STEP not a curve (1us -> 487us, nothing between), which
is why p99_departure_decile finds no knee — there is none
- caveat recorded, NOT inherited: this is demand-paged anonymous memory
through swap (4 KiB/fault, no readahead). `resident: keys` preads via
the page cache — should be better, but databasev2 2 task 7 must measure
its own read path. New criterion added there
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- `Wide` text-heavy reference shape beside Int-only `Item`
- `growth N int|text`: per-decile RSS read from own /proc/self/status
- `growth-verify`: survivor of a crash must be a contiguous intact prefix
- four footprint legs under a rootless cgroup v2 cap, swap on/off
- `ceiling` leg: die at the cap, then replay must come back intact
- footprint read as median-of-marginals; doublings a separate metric
- 121 checks, 0 failures; footprint gated ±10%, kill-timing ±100%
Measured, and it inverted two of the iteration's own predictions:
- footprint 96.5-100 B/row Int vs 320.6-324 B/row text = 3.3x, NOT the
"order of magnitude" three docs asserted
- table storage has NO checked ceiling: SIGKILL signal 9, not a catchable
WO_T_OOM. overcommit lets malloc succeed; kernel kills on page touch
- swap is NOT latency collapse: 900k rows 148s capped-with-swap vs 150s
uncapped. Append-mostly never re-touches cold pages
- ack-after-fsync survives an OOM kill: ~40k rows, no holes, no corruption
- iteration 2's budget dependency is REMOVED not satisfied — there is no
"swap onset" to derive a fraction from
- fix: subprocess returncode -9 was labelled a "checked refusal"; 137 is
the shell spelling of the same signal
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/stories/databasev2/, numbered from 1. Six PENDING database iterations
moved from the language track and renumbered, keeping the old id in
`was_language_iteration:` so a search for "iteration 32" still finds it:
32 -> 3 WAL checkpoint, 23 -> 4 io_uring commit, 33 -> 7 single-file store,
27 -> 8 query grammar, 20 -> 9 cross-program, 21 -> 10 keypair auth.
Done work (9, 9b, 22) stays as v1 history; language 18 left whole
- the problem, read off the engine not guessed: rows are malloc'd slabs with
addresses stable forever, NO eviction/spill/paging anywhere in database/src,
the WAL never checkpoints so boot replays all history, and durability is one
process-global WO_DATA so no table can say it matters more than another.
An allocation failure IS a clean catchable WO_T_OOM — but swap thrash
arrives first and carries no error signal at all, which is the real hazard
- four new iterations:
1 measure the ceiling FIRST (curve not cliff; the three exits; kill -9 at
exhaustion) — every later default should follow from a number
2 `@table(mode: ram | durable | cold)` — the grammar ask. Small surface
(Ast.table_cfg gains a key, the parser already rejects unknown args), big
semantics: `durable` defaults so nothing changes silently, and the
compiler refuses a durable row holding a `ref` into a ram table
5 bounded tables + refuse/evict/back-pressure, shedding BEFORE the OS acts
6 cold tiering — mostly forks, incl. whether the language surfaces the
fault cost and whether @unique on cold is refused outright. A paged
B-tree stays rejected: if tiering needs one, reject tiering
- 39 links repointed, link TEXT renumbered to track-local ids; arc gains one
pointer row replacing the six moved; board + board-views cover three tracks
- linkcheck 0 broken / 0 anchors; no code blocks in any story
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/stories/porch/ — a TRACK folder, not a status folder: status still
lives only in frontmatter. Adds `track: porch` so a query over
docs/stories/ can tell a porch 3 from a language 3
- 00-story.md carries the sequence, the dependency graph, and a table of
what the track explicitly does NOT own (binding -> 29, cache -> 18,
proxy -> 38, metrics -> 30, TLS/templates -> doctrine)
- eight iterations, each with phases, per-phase tasks, Given/When/Then
criteria, out-of-scope and the forks a spec must settle:
1 store-backed middleware (limiter + idempotency — needs nothing new,
first on purpose so the store pattern is proven cheaply)
2 randomness + cookies (phase A is language-track: a CSPRNG builtin;
`Resp.headers` being a map cannot emit two Set-Cookie lines)
3 sessions 4 CSRF 5 routing/response ergonomics (independent)
6 streaming core (the seam 7 and 8 wait on; chunked-request refusal
must survive) 7 SSE + compression 8 static + lifecycle hooks
- language iteration 39 -> status: hold, retitled superseded, with a row
mapping each of its goals to the porch iteration that took it. Kept, not
deleted: the Fiber study cites it and its randomness argument is what
this track is built on
- board gains a porch section; board-views gains porch and both-track
Dataview queries; porch README and the Fiber study §7 point at the track
- no code blocks in any story (plans carry concept and actions in words);
linkcheck 0 broken / 0 anchors
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/writeonce-serve -> docs/examples/porch (git mv, history kept);
`[deps]` key and import are now `porch` / `porch/http` / `porch/router`
- name history preserved on the library README, not rewritten into dated
records: writeonce-framework -> writeonce-serve (08-25) -> porch (08-26).
Stories, specs, plans and the audit reports keep the older name by the
repo's own convention; only live docs and every path link were rewritten
- left alone deliberately: `internal/serve.wo`, `pub fn serve`, `serve_conn`,
`app.serve(...)` — those are functions, not the module name
- web-app/wo.toml comment corrected: it claimed hyphens are not identifier
characters and named a key this file never used. lexer.ml's `is_ident_cont`
DOES accept `-` (an internal dash is part of the identifier, which is why
binary minus needs spaces), so a hyphenated key would be legal too
- site now teaches the name: package card, the two-deps chapter and the
handlers-are-classes chapter say `porch`; site-accept asserted the old
/packages/serve route and caught the rename, as a gate should
- gates: web-app 46/0, site 21/0, deps-accept 8/0, oop-e2e 116/0,
linkcheck 0 broken / 0 anchors; porch typechecks entry-less as kind=library
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- README: shipped concurrency/HTTP/WebSockets sat in the roadmap as "not yet
available"; "no package manager" contradicted [deps]; the deps example
would not have compiled (the key IS the module name)
- runtime/README: leads with wovm, wo-rt.c demoted to a historical section;
dropped 2 nonexistent recipes, crates/rt, @gc refcounting, 13 suites -> 18
- employee + log-watcher READMEs claimed "does not compile"; both are gates
- error catalog: +10 emitted codes incl WO-E250, the only diagnostic the
shipped query surface raises; recorded why the sweep rotted
- language-surface: group-by parses, then the typechecker refuses it
- 00-code-review + 00-link-audit re-run; history kept, not rewritten
- 48 dead Rust-era exploration links de-linked rather than re-pointed (their
prose names the retired plan by number); successor map -> discarded.md
- 08-project-structure: compiler/plan/ never existed; corpus has 9 dirs, 5 empty
- releasing.md: dropped a --draft step the workflow never had
- new docs/00-doc-audit.md: findings + disposition, incl one row where the
audit was wrong and the doc it accused was right
- status folders removed: 34 stories flat, status only in frontmatter; 252
links recomputed from resolved paths; board/board-views/structure retaught
- story 24 -> in-progress, since frontmatter is now the only truth
- new iteration 38: fs mutation verbs + net.connect, the two capability
families no iteration owned
- new iteration 39: gofiber/fiber v3.5.0 parity study. The ledger called
CSRF/sessions unblocked by iteration 34's HMAC, but the runtime has no
source of randomness at all
- linkcheck skips .dev/.superpowers: 0 broken paths, 0 bad anchors
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- "View source" and the nav GitHub link pointed at the user profile
(github.com/shoneyj); both now point at github.com/shoneyJ/writeonce
- README: what actually has to reach the host — the self-contained
binary plus dist/ (served by /dl) and data/ (WO_DATA) — and the four
environment variables, with SITE_HOST left UNSET behind a proxy so
the process binds loopback
- says plainly that dist/ must hold the PUBLISHED release assets: the
build is not byte-reproducible, so a local tarball would not match
the published .sha256 and the mirror would disagree with GitHub
Prepared and verified locally: docs/examples/site/dist/ holds the real
v0.1.0 assets (digest matches the release) and target/site serves them
byte-identically. Both directories are gitignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The published v0.1.0 binaries need less than the page claimed:
woc imports up to GLIBC_2.35, wovm up to GLIBC_2.34. The page said
2.38, which was measured on a dev workstation (glibc 2.39) before CI
existed — and understating support turns working platforms away.
- supported systems: glibc 2.35+, covering Ubuntu 22.04+, Debian 12+,
Fedora 36+
- RHEL 9 (2.34) runs wovm but not woc: build elsewhere, copy the
self-contained binary
- say plainly that the floor is set by the machine that BUILT the
release, which is why CI pins ubuntu-22.04
- site-accept follows the new string
This is the pinned-runner decision paying off: 2.38 -> 2.35.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- rename the two libraries: writeonce-framework -> writeonce-serve
(`use serve`), wo-html -> writeonce-view (`use view`). Names say the
ROLE now; every sample, script, gate and live doc follows
- stories/specs/plans keep the old names: they are dated records, and
both library READMEs carry a "renamed 2026-08-25" note
- serve/http/files.wo: StaticFiles { dir, max_bytes } — traversal
refused not normalised, extension content types, attachment
disposition for archives. Lifted out of the shop, which had said in
a comment that it belonged in the framework
- shop drops its private copy and mounts the framework's
- site: /dl/*path over $WO_DIST (default ./dist), 16 MiB ceiling
- /install gains supported systems — Linux x86-64, glibc >= 2.38,
not musl — read off `file` and the binaries' GLIBC_ symbol
versions, not off a wish list; plus GitHub release as primary,
/dl as mirror, and the sha256 verify step
- site-accept: 17 -> 21 checks (supported systems, gzip download with
a binary-safe probe, checksum, /dl traversal 404)
Verified on 192.168.0.165: the real 960,820-byte tarball downloads
as application/gzip and its sha256 matches the published digest.
Gates: oop-accept MET, site 21/0, web-app 46/0, fibers 10/0,
db-actor 8/0; shop rebuilt and its /assets served by the framework.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- layout/logo.wo: the mark as inline SVG — dark tile, two-stroke "W"
(white then accent blue). One source: nav brand + /favicon.svg
- favicon/controller.wo: GET /favicon.svg, image/svg+xml, day cache
- install/: GET /install — toolchain tarball, PATH, verify, first
project, build/run, adding a dep. Copy from the real install README
- packages/: GET /packages + /packages/:name — catalogue with the
[deps] line, what each library gives you, and a usage snippet.
Index cards are child components (multi Component)
- wo-html: page_head(title, head, body) and a `head` slot on Layout —
a favicon link or meta tag had nowhere else to go; page() passes ""
- header: Install/Tutorial/Packages/GitHub, brand shows the mark
- main.wo: SITE_HOST picks the interface (loopback default), bound
address printed at startup
- site-accept: 11 -> 17 checks (install, packages x2, 404, favicon,
inline logo)
Verified on 192.168.0.165:8080 — every route, favicon bytes, and the
mark rasterised at 256px and 32px.
Gates: oop-accept MET, site 17/0, web-app 46/0, fibers 10/0,
db-actor 8/0; shop rebuilt clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- lexer: backtick raw text literal — content verbatim, no escape
processing, common source margin removed at lex time; `${ }` raw and
`{{ }}` auto-escaping holes
- `{{ e }}` desugars to `esc(${e})` in parser.ml — a Call on the `esc`
in scope, so types/owner/emit/.wob/VM are untouched
- WO-E004 unterminated raw literal; WO-E005 newline inside "..." —
closes a hole where a missing quote silently ate the rest of the file
- wo-html: `Component` interface, `render_all`, `Layout`, README
- framework: `ok_html` joins ok_text/ok_json in http/types.wo
- site + shop restructured to one-feature-one-module MVC (view +
controller per directory, model at the root, bootstrap-only main)
- removed the filler `pad: Int` convention — verified unnecessary for
plain classes, interface dispatch, containers and actors
- corrected recorded claims: gap #1 blocks neither the build nor the
layout; a class crosses module lines, only a free fn is scoped
- docs/guides/language-surface.md — the full grammar inventory
- story 37 landed and moved to done/
Gates: oop-accept MET, oop-e2e 116/0, woc-test 556/0, site 11/0,
web-app 46/0, fibers 10/0, db-actor 8/0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- render() bodies: one HTML line per 'h = h ..' statement, single-
quoted attributes, ${} holes, esc() on data — el() chains gone
- discovered + recorded gap #3: no multi-line expressions or literals
(leading/trailing .. and paren grouping all reject at NEWLINE) —
exactly the tax story 37's raw literal deletes
- 37-target comment blocks dropped (bodies now self-explanatory; the
README states the delta); rebuilt + full buy-flow re-smoked (178.0
total, stock 12->10, 409, traversal 404)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- separate view.html files dropped; every render() now carries its
'-- 37 target:' literal above the hand-lowered body — the pair is
the DX referendum in one file
- story 37 re-pointed: raw multi-line literal + {{ }} auto-escaped
typed holes + {!! !!} raw slots; structural control stays if/for;
w:if/w:for and .html files demoted to later; forks revised
- rebuild verified on untouched toolchain
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- view.html per feature + layout app/header/footer.html: the markup-
first form woc will compile ({{}} auto-escaped, w:if/w:for,
{!! !!} slots, w:component sections); inert today, verified not to
disturb the build
- README: pair is the DX referendum — .html is the target feel,
view.wo is today's cost; doctrine line reworded (templates compile
or don't exist)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/examples/chat: registry (call consumer) / room / reader+writer
actor pair per connection over ws_accept + wsframe; presence,
broadcast, cross-room isolation, mailbox-full = drop-from-room;
reader tail sends hardened (a full writer no longer orphans the fd)
- RUNTIME SEMANTICS CHANGE (the drain): SIGTERM no longer kills parked
fibers from outside — the plane WAKES them and each wait RESOLVES
(deadline'd waits answer their timeout result, sleeps return early,
plain waits answer WO_SYS_STOPPED and unwind THAT fiber alone; main's
STOPPED still ends the program). Workers keep adopting their inboxes
after stop until eng_shutdown. This is what lets a program drain:
chat's close frames now reach clients (byte-verified 0x88), then
main returns and the reap runs
- also: SIGPIPE ignored process-wide (EPIPE trap instead of death);
two-phase engine teardown (real drops while arenas+routing live,
settle passes for routed frees) — fixes the registry-map leak and
the drain UAF ASan found
- gate scripts/chat-accept.sh + just chat: handshake independently
verified, functional matrix on BOTH backends, 1k-hot-room soak
(1000/1000 in ~35ms), drain close-frames, SIGTERM exit 0, ASan leg
clean. OPEN: soak-fds check (18 fds settle slower than the window)
+ full battery after the semantics change — NOT yet run
- committed for manual testing at the user's request
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- parse.wo: ANY Transfer-Encoding header is 400-and-close (RFC 9112
§6.1) — silently treating chunked as body-less was the smuggling
door the dup-CL fix left open
- net.listen/listen_unix backlog 64 -> 1024: the soak's connect bursts
overflowed the kernel accept queue and BLACK-HOLED clients (three-way
handshake done, server never sees the conn — 35-70 stuck per run,
fully reproduced then gone at 1024; kernel clamps via somaxconn)
- web-app gate grows to 46 checks: TE-reject; the 1k soak — 500 real
conns all served + 500 idle conns all evicted, server fds home
(45 -> 45), RSS 24MB, healthy after
- battery green (site restart + fibers-TSan legs flaked under parallel
battery load, both clean serially — the standing flake pair)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- runtime ids 91-95: net.read_dl/accept_dl/write_dl (per-call deadline,
nil/false = the EXPECTED timeout; ms<=0 = old behavior bit for bit),
net.listen_unix (unlink-before-bind, O_NONBLOCK on the listener —
probe-found: accept4's flag covers accepted sockets only), net.peer
- plane: one-op-per-park stays law — deadlines ride one per-shard
TIMEOUT tick (sentinel user_data) + post-CQE expiry sweep +
POLL_REMOVE tombstone; epoll's deadline scan grew the fd-park case;
fibers POOL instead of freeing mid-run (stale-CQE UAF); plain parks
zero park_deadline (no stale sleep deadlines)
- probe: all five seams verified on BOTH WO_IO backends (timeout
timing exact, peer round-trip, unix rebind)
- framework: parse_request grows first_ms/read_ms; serve_conn — the
keep-alive loop with deadlines where parked idle conns are LEGAL
(close-when-idle RETIRED); App.handle_conn exposes it; plain serve()
unchanged for simple apps
- web-app: app-owned accept_dl loop + ConnWorker actor per connection
(each builds its own App; cross-shard placement rides the DB actor);
WA_IDLE_MS knob; gate grows to 41 checks — two slow requests served
in PARALLEL, stalled client evicted at the idle deadline, slow-loris
torn at the read deadline (400)
- docs: story 35 -> done with banner; SQE/CQE design spec LANDED (was
the review doc); ledger rows (timeouts/unix/keep-alive/peer), graph
(NETSEAM cleared, KEEPAL done), builtin-surface rows, runtime
CODE-LOGIC section, board entry
- battery 13/13 fresh-built
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/examples/wo-html: library dep — esc(), element builders,
Tailwind-style utility sheet as one static string, page() shell;
self-contained responses, no CDN/JS/build step
- docs/examples/site: the language tutorial served by the language —
9 seeded chapters in a @table (hello/values+bitwise/containers/
classes/optionals+traps/tables/actors/deps/serving), server-rendered
via wo-html, seed-if-empty so WAL restarts keep admin edits
- routes: / index, /ch/:slug (styled 404), /health, POST /admin/ch/
:slug (bearer handler-side — mechanism framework's, policy app's;
form-encoded title/body update by assignment, 302 back)
- chapter code samples use the lexer's \$ escape to show ${...}
literally; .. never straddles newlines (accumulator style)
- gate: scripts/site-accept.sh + just site — TWO file:// dep remotes,
11 checks incl. authed-edit-survives-restart; /health polling, no
boot-race sleep
- README: run + nginx sketch for writeonce.de; CODE-LOGIC beside code
- full battery 13/13 (site gate included)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>