Wave A: public-name check, WebAssembly determinism in CI, decode-path reporting, and pin by default #616

Merged
dcharlot merged 29 commits from wai/wave-a into main 2026-11-04 20:25:43 -05:00
Owner

What this is

Four pieces of reviewed work, put on one linear branch by cherry-picking, with no merge commits. Two small fixes follow them; the combined checks found both. Each fix is its own commit.

This stacks on #614 (test/timing-under-load, the CI timing fixes), which sits on main after #611. Until #614 merges, this PR's diff against main also shows #614's commits. Merge #614 first.

The four items

1. Public-text hygiene (wai/rd-hygiene)

  • scripts/check-public-names.sh now holds the names of the maintainers' own products in the script. A changelog line naming one of them had passed, because the private list never held them. A new --maintainer-products-only mode applies that list alone and reads no secret, so the repository checks run it on every pull request. The full check, with the private list, still runs on the push to main and still fails closed without the list.
  • A route-record changelog entry that named a product is redacted in place, with a dated marker.
  • sink-energy §4.6, Outside measurements of decode energy (informative), cites each measurement to its paper and table, with what each one measured and what its paper leaves unstated.

Verified: the public-text self-test, the --maintainer-products-only run and the full check with the private list. All three are clean.

2. WebAssembly determinism in CI (ci/wasm-gate)

  • Every workflow that runs WebAssembly now uses one runtime release, pinned by version and SHA-256. Every run turns on NaN canonicalisation and deterministic relaxed SIMD. scripts/check-wasmtime-pin.py fails any workflow that pins another release, installs the runtime another way, or runs it without both settings. The READMEs' commands pass the same settings.
  • The browser packages (both wai-web bundles and the package of record the site serves) are validated with relaxed SIMD and threads turned off (determinism-tiers §5).
  • Each leg of the two byte-equality matrices (wai-byte-equality.yml, and the byte-exact matrix in wai-conformance.yml) first measures the free space on its disks and writes a test file to each. A disk below 4 GiB, or one that refuses the write, fails the leg as an infrastructure failure, not as a divergence. If a later step fails, the disks are measured again.

Verified:

  • The pin check and its self-test both pass.
  • Both bundles were built and validated with relaxed SIMD and threads off, along with the package of record. Two planted modules, one using relaxed SIMD and one using a shared memory, were each refused for their own reason.
  • All four disk steps were taken from the workflows as committed. Each passed at the floor of 4 GiB, and each failed when the floor was set above the free space.

3. Decode-path close-off (wai/determinism-closeoff)

  • The neural dispatch now reports the capability it decoded through and the path that ran: sole, exact or float. The C ABI's wai_decoded_t carries the path and whether the fallback was used.
  • The reconstruction-binding check reads the path and the binding's descriptor. It never reports a float decode, or a different reconstruction, as a failed binding.
  • determinism-tiers §3 says that no tier lies between the two existing tiers. A session claim states the decode path, and three refusals follow: session_variant_not_exact, session_tier_exceeds_variant and session_stage_hash_not_exact. The claim corpus gains vectors for these, and the differential now reaches each new rule.
  • §1 and §7.1 record the published routes to a reproducible stage, and a research record of a catalogue prior is added with its own verifier.
  • A CI job now runs the exact wai.image.jpegai decoder on pinned, fetched inputs, and a skip counts as a failure.
  • The breaking changes are acknowledged in the CHANGELOG.

Verified:

  • The claim corpus regenerates byte for byte.
  • Every cargo-class corpus verifies.
  • claim_verify.py passes.
  • differential.py agrees with both builds over the sessions CI generates (seed 1).
  • The library tests pass.
  • Every step of the neural gates job passes.
  • The decode-path tests pass with WAI_REQUIRE_SOTA_VECTORS=1: the exact decoder on the fetched inputs, with no skip, and the cases that use committed inputs.
  • The FFI tests pass.
  • wai breaking --against main's tip reports every break as acknowledged.

4. Pin by default (wai/pin-by-default)

  • SPEC §3.1 now says an encoder SHOULD pin the parameter set a payload was coded against. A verifier reports an unpinned envelope (prior_unpinned) for information only. wai verify reads envelopes, and wai inspect and wai wrap note the finding.
  • A session's decoder entry names the set an unpinned decode read, with the tier none. The native recorder and the page's recorder both do this, and a new fixture holds both of them to it.
  • wai conform sink runs the native sink through the conformance runner. wai verify --conformant admits a result only for a run that passed.

That branch's real tip is carried. Its last commits were planted falsifications, each reverted by the next, with no net change, and they are not included.

Verified:

  • cli_dx verify_reports_an_unpinned_envelope passes.
  • The unpinned library tests pass under the conformance job's feature set.
  • wai_pin_vectors verify and pin_verify.py pass.
  • cli_breaking, cli_conform and cli_conform_sinks pass.
  • The conformance runs pass: the native sink through its module and through its C interface, and the score decoder. Each record is admitted. A planted failing sink is refused at rule 2.
  • wai ops export --check and actions.mjs --check pass.
  • wai_stage_measure verify and measure_verify.py pass.
  • The wasm32 staged-delivery and staged-measurement verifiers pass.

Two fixes found by the combined checks

These problems already existed on the branches above; combining them did not cause them. Each branch's own gates did not run the step that finds its problem.

  • wai: an honest session's base entry names the pin it verified, so its stage hash seals
    • The problem: the new stage-hash rule (determinism-tiers §3, rule 3) made staged::claim_tests::every_corpus_case_seals_an_honest_session fail. The test's replayed sink named no pin or tier for any base, so the honest sessions of the pinned cases were refused. This also fails on wai/determinism-closeoff alone.
    • The fix: the replay now names what a sink states. That is the pin it verified, with the tier the pin declares. For an unpinned decode, it is the set it read, with the tier none.
    • Checked: the test failed before the fix and passes after it. The unpinned case names the same set digest as the pin the pinned cases name over the same files.
  • CHANGELOG: the pin-by-default entry acknowledges the staged-measure goldens' break
    • The problem: wai breaking reported staged-measure-conformance/goldens.json as an unacknowledged wire break. The new fixture's evaluation was added, and the corpus-wide dedup totals moved.
    • The fix: the entry now acknowledges it.

Conflicts resolved

  • wai/CHANGELOG.md: every entry is kept once, newest at the top.
  • The decoder-entry table in jwp-receipts §2.4.12: the variant row comes from the decode-path work, and the pin and tier rows come from pin by default.
  • Studio's generated actions: regenerated from the merged operation catalogue.
  • Workflow files changed by two branches keep every step of both. This was checked step by step against all five sources, and each changed workflow was parsed as YAML.

Final review before push

A review of every commit message and the whole diff, for public history, led to these changes. The code tree is unchanged by the message edits.

  • Commit messages: no counts of vectors, files or runs, no internal tool names, and no lines about later work.
  • SPEC §3.1 states what an encoder SHOULD do and what a verifier reports today. A sentence about a later major revision is removed from §3.1, §8 and the CHANGELOG.
  • Dated research records are no longer rewritten in place. Each original passage is restored word for word, with a dated correction after it, as CONTRIBUTING requires.
  • The byte-equality matrix comments say the disk steps are bash and skip on Windows, without a to-do.
  • The CHANGELOG records that the public-name check now runs on every pull request.

Verified after these changes: the documentation checks, the catalogue-prior verifier, the workflow step check and YAML parse, wai breaking against #614's tip with every break acknowledged, the build, and the page recorder's set test.

Two tests that failed this pull request under load

Two checks failed on this pull request in code it does not change: the relay's DVR test on the linux-arm64 runner, and the watch-together example on the macOS runner (twice). Two commits on top fix them:

  • joule-moq-relay DVR: count an eviction under the index lock; the by-age test counts late groups
  • watch-together: the page makes its audio context before its join; a slot is set apart only when the machine ran nothing of the page

Each check still fails on the defect it was written to catch. No bound was widened and no retry was added.

1. The DVR window by age (joule-moq-relay)

What it asserted. The relay's store keeps groups for a 2 s window. a_window_by_age_empties_as_its_groups_pass_it publishes three groups on a media track and its receipts track, six groups in all, waits until the store holds none, and asserted groups_evicted == 6. It then publishes a fourth group and fetches from group 0: what aged out must come back as one unknown range, and every group after it whole.

What failed. On the linux-arm64 runner the assertion failed with 3 against 6. The run printed no other count.

The cause, as measured. The store takes a group only once it is whole: its objects and its receipts. A group that becomes whole after the window has already passed its number is late. The store counts it in groups_late and never stores it, by design. The relay reads each stream on its own task, so on a loaded host groups can become whole out of order. The test assumed an order the relay does not promise. On a build host other than the CI runner, forcing that order gives 3 groups stored and 3 late, and the old assertion fails with 3 against 6 in each run. The runner printed no groups_late, so the claim that its three missing evictions were late groups comes from this reproduction. It was not read on the runner.

A second defect turned up along the way. The store removed a group from its index under the index lock but counted the eviction after releasing it, so a reader that found the group gone could read the count one short. With 300 ms injected between the removal and the count, the in-order test failed with 5 against 6.

The fix.

  • The store now counts an eviction under the same lock as the removal.
  • The test asserts two things that hold in any arrival order: every group is either evicted or late (groups_evicted + groups_late == 6), and every stored group is counted when it leaves (groups_evicted == groups_written). Each prints all the store's counts when it fails.
  • A new test, a_window_by_age_passes_groups_taken_out_of_order, forces the loaded-host order and checks 3 stored and 3 late, then runs the same fetch checks.

Falsified on that build host:

  • The old assertion with the forced order fails with 3 against 6.
  • The count moved back outside the lock, with 300 ms injected, fails with 5 against 6.
  • The same 300 ms injected with the count under the lock passes.
  • Counting only one of a pair's two removals fails both tests.
  • A store that finds every group late fails both tests.

2. watch-together: a statement in every slot (co-watch §7.3)

What it asserted. Every slot wholly inside a page's presence holds a presentation statement from that page. Presence runs from the page's join to the slot before the last statement in the log, and for Ben's first stretch it ends at his deliberate hold.

What failed. The check failed in two runs on the macOS runner, at a host load average of about 50 and then about 30. Both times the first page in the browser, Ada's, made its first statement after its join, at 3.83 s and then 3.44 s on the room clock. It then made none in the slots from 4 s and 5 s, and in the second run from 6 s as well. During that time it was still entering the room and had not yet followed the log. It followed the log from 7.49 s and 8.49 s, which is 3.7 s and 5.0 s after its first statement. Ben's page took 1.2 s and 1.0 s for the same step. Ada's page was silent for at least 2.17 s and then at least 3.56 s. Nothing was refused, and the room never took the page for gone. What held the page on the runner was not measured.

What was measured.

  • On the CI host, while CI jobs ran (load average 30 to 39). The browser was launched as the test launches it: headless, the same flags, at nice 10. On a blank page, the first audio context in a fresh browser held its page's thread for 172 to 192 ms (3 runs), for the constructor call alone. A second page's took 3.8 to 4.0 ms. Neither page's thread had a gap over 60 ms in the next 1.5 s. That is a tenth or less of Ada's silences, so the audio context alone does not explain them. The rest of the page's entry was not measured on that host: its audio worklet, its relay session, its WebAssembly, its first draws, its camera and microphone, and an audio context made while that work runs.
  • On a laptop, in 25 runs. The first page's audio context took 94 to 128 ms and the second page's 0.8 to 1.4 ms.
  • On a laptop, on main, two injected faults. Each leaves the slots just after the first page's first statement empty, as in CI. One holds the audio-context call for 2.5 s with a busy wait. The other stops the first page's renderer process for 2.5 s from its join (SIGSTOP). Neither was measured on the CI host.

The fix.

  1. The audio context before the join. The page makes the composition's audio context before it joins the co-watch log, through Compositor.openAudio() in enter(), so that hold is no longer inside the stretch in which the page must state. By the blank-page measurement on the CI host, that is about 0.2 s. The rest of the entry still runs after the join.

  2. A failed entry's compositor is closed. When an entry fails, the next entry closes its compositor (Compositor.close()). Before, a second entry left two audio contexts open and two mix timers running.

  3. The statement loop records its stalls. Each gap of more than half a slot between two of the loop's passes is recorded on the room clock, with how long its wait and its signing took.

  4. A narrower excuse in the example's check. An empty slot is set apart only if the machine ran nothing of the page in the slot's second half, which means all three of these hold:

    • a stall of the loop spans that half;
    • a heartbeat worker the test starts in each page, ticking every 20 ms, did not tick in it either;
    • the stall is the loop's wait and its signing, plus at most one pass of the loop's own work.

    Any other empty slot fails, and the check says why:

    • no stall: a statement was lost;
    • the heartbeat ticking: the page's own work held its thread, or the host ran the worker but not that thread;
    • more than a pass of the loop's own work.

    Pass or fail, the check prints each page's audio-context time, the loop's stalls and the heartbeat's silences.

What this does not show. It is not shown that the example now passes on the CI host. If what held Ada's page there was the page's own work on its thread, or the host running the page's other threads but not that one, the check still fails, with its heartbeat ticking. Its printout does not tell those two cases apart. It does show whether the page's thread alone or the whole page was held, and for how long.

Falsified on a laptop, with each fault put into a scratch copy of the room client:

  • The audio context moved back after the join, with the call held 2.5 s, fails the check: the empty slots fall in a stall of the loop while the heartbeat ticks.
  • A 2.5 s busy wait just after the join, or 4 s after it, fails the same way.
  • Statements dropped for one whole slot fail it, with no stall of the loop.
  • A 1.5 s await in the loop's own work fails it. The same await inside the loop's wait fails it on the heartbeat alone.
  • The page's renderer process stopped for 2.5 s passes. The slots inside the stop are set apart: the heartbeat was silent and the loop stalled over the same stretch, to within 25 ms.
  • In the unit tests, measuring the wait on a different clock from the stall, recording no stall, recording a stall only past a whole slot, or leaving the wait out of the record each fails a stall test.

Checked on this branch's tip

The two commits sit on this pull request's head as they were checked on main. The seven files they change are byte for byte the files checked there, and the branch merges onto main with no conflict. The tip, this pull request's changes included, was checked off the CI hosts:

  • DVR. cargo test -p joule-moq-relay --test dvr passes all 10 tests on a Linux build host (Rust 1.98.0, this branch's toolchain), both by-age tests among them.
  • Room client unit tests. node --test over web/room/*.test.mjs and web/moq/*.test.mjs passes 77 of 77, and cowatch.test.mjs, run as the conformance workflow runs it, passes 15 of 15.
  • The browser job, as the WAI examples workflow runs it. It ran on a laptop, at nice 10, in the host's browser. build-web.sh made the room client from this tree, on the Linux build host. build.sh built and packed the archive from this tree. The readiness check passed from the unpacked archive in an empty directory: wai hello, wai verify, and wai dev --check with 7 of 7 checks.
  • The examples. run-examples.sh browser passed video-room, watch-together, messaging, remote-desktop and package-and-deliver, and watch-together alone passed 2 more runs: 3 of 3 in all, each with 12 of 12 checks. In each watch-together run:
    • Ada's audio context was made before her join, in 95 to 102 ms.
    • Ben's took about 1 ms.
    • Ada's loop had no stall.
    • Ben's deliberate hold showed as one stall of 3.6 to 4.1 s, with his heartbeat never silent.
    • No empty slot was set apart.

None of this ran on the CI hosts. Whether the watch-together example now passes on the macOS runner under load is for this pull request's next CI run to show.

Repository checks run locally

Section references, SPEC references, tests in CI, orphan sources, comparative prose and vanity metrics all pass, and so does the public-name check with the private list.

🤖 Generated with Claude Code

## What this is Four pieces of reviewed work, put on one linear branch by cherry-picking, with no merge commits. Two small fixes follow them; the combined checks found both. Each fix is its own commit. This stacks on #614 (`test/timing-under-load`, the CI timing fixes), which sits on main after #611. Until #614 merges, this PR's diff against main also shows #614's commits. Merge #614 first. ## The four items ### 1. Public-text hygiene (`wai/rd-hygiene`) - `scripts/check-public-names.sh` now holds the names of the maintainers' own products in the script. A changelog line naming one of them had passed, because the private list never held them. A new `--maintainer-products-only` mode applies that list alone and reads no secret, so the repository checks run it on every pull request. The full check, with the private list, still runs on the push to main and still fails closed without the list. - A route-record changelog entry that named a product is redacted in place, with a dated marker. - `sink-energy` §4.6, *Outside measurements of decode energy* (informative), cites each measurement to its paper and table, with what each one measured and what its paper leaves unstated. **Verified:** the public-text self-test, the `--maintainer-products-only` run and the full check with the private list. All three are clean. ### 2. WebAssembly determinism in CI (`ci/wasm-gate`) - Every workflow that runs WebAssembly now uses one runtime release, pinned by version and SHA-256. Every run turns on NaN canonicalisation and deterministic relaxed SIMD. `scripts/check-wasmtime-pin.py` fails any workflow that pins another release, installs the runtime another way, or runs it without both settings. The READMEs' commands pass the same settings. - The browser packages (both `wai-web` bundles and the package of record the site serves) are validated with relaxed SIMD and threads turned off (`determinism-tiers` §5). - Each leg of the two byte-equality matrices (`wai-byte-equality.yml`, and the `byte-exact` matrix in `wai-conformance.yml`) first measures the free space on its disks and writes a test file to each. A disk below 4 GiB, or one that refuses the write, fails the leg as an infrastructure failure, not as a divergence. If a later step fails, the disks are measured again. **Verified:** - The pin check and its self-test both pass. - Both bundles were built and validated with relaxed SIMD and threads off, along with the package of record. Two planted modules, one using relaxed SIMD and one using a shared memory, were each refused for their own reason. - All four disk steps were taken from the workflows as committed. Each passed at the floor of 4 GiB, and each failed when the floor was set above the free space. ### 3. Decode-path close-off (`wai/determinism-closeoff`) - The neural dispatch now reports the capability it decoded through and the path that ran: sole, exact or float. The C ABI's `wai_decoded_t` carries the path and whether the fallback was used. - The reconstruction-binding check reads the path and the binding's descriptor. It never reports a float decode, or a different reconstruction, as a failed binding. - `determinism-tiers` §3 says that no tier lies between the two existing tiers. A session claim states the decode path, and three refusals follow: `session_variant_not_exact`, `session_tier_exceeds_variant` and `session_stage_hash_not_exact`. The claim corpus gains vectors for these, and the differential now reaches each new rule. - §1 and §7.1 record the published routes to a reproducible stage, and a research record of a catalogue prior is added with its own verifier. - A CI job now runs the exact `wai.image.jpegai` decoder on pinned, fetched inputs, and a skip counts as a failure. - The breaking changes are acknowledged in the CHANGELOG. **Verified:** - The claim corpus regenerates byte for byte. - Every cargo-class corpus verifies. - `claim_verify.py` passes. - `differential.py` agrees with both builds over the sessions CI generates (seed 1). - The library tests pass. - Every step of the neural gates job passes. - The decode-path tests pass with `WAI_REQUIRE_SOTA_VECTORS=1`: the exact decoder on the fetched inputs, with no skip, and the cases that use committed inputs. - The FFI tests pass. - `wai breaking --against` main's tip reports every break as acknowledged. ### 4. Pin by default (`wai/pin-by-default`) - SPEC §3.1 now says an encoder SHOULD pin the parameter set a payload was coded against. A verifier reports an unpinned envelope (`prior_unpinned`) for information only. `wai verify` reads envelopes, and `wai inspect` and `wai wrap` note the finding. - A session's decoder entry names the set an unpinned decode read, with the tier `none`. The native recorder and the page's recorder both do this, and a new fixture holds both of them to it. - `wai conform sink` runs the native sink through the conformance runner. `wai verify --conformant` admits a result only for a run that passed. That branch's real tip is carried. Its last commits were planted falsifications, each reverted by the next, with no net change, and they are not included. **Verified:** - `cli_dx verify_reports_an_unpinned_envelope` passes. - The unpinned library tests pass under the conformance job's feature set. - `wai_pin_vectors verify` and `pin_verify.py` pass. - `cli_breaking`, `cli_conform` and `cli_conform_sinks` pass. - The conformance runs pass: the native sink through its module and through its C interface, and the score decoder. Each record is admitted. A planted failing sink is refused at rule 2. - `wai ops export --check` and `actions.mjs --check` pass. - `wai_stage_measure verify` and `measure_verify.py` pass. - The wasm32 staged-delivery and staged-measurement verifiers pass. ## Two fixes found by the combined checks These problems already existed on the branches above; combining them did not cause them. Each branch's own gates did not run the step that finds its problem. - **`wai: an honest session's base entry names the pin it verified, so its stage hash seals`** - The problem: the new stage-hash rule (`determinism-tiers` §3, rule 3) made `staged::claim_tests::every_corpus_case_seals_an_honest_session` fail. The test's replayed sink named no pin or tier for any base, so the honest sessions of the pinned cases were refused. This also fails on `wai/determinism-closeoff` alone. - The fix: the replay now names what a sink states. That is the pin it verified, with the tier the pin declares. For an unpinned decode, it is the set it read, with the tier `none`. - Checked: the test failed before the fix and passes after it. The unpinned case names the same set digest as the pin the pinned cases name over the same files. - **`CHANGELOG: the pin-by-default entry acknowledges the staged-measure goldens' break`** - The problem: `wai breaking` reported `staged-measure-conformance/goldens.json` as an unacknowledged wire break. The new fixture's evaluation was added, and the corpus-wide dedup totals moved. - The fix: the entry now acknowledges it. ## Conflicts resolved - `wai/CHANGELOG.md`: every entry is kept once, newest at the top. - The decoder-entry table in `jwp-receipts` §2.4.12: the `variant` row comes from the decode-path work, and the `pin` and `tier` rows come from pin by default. - Studio's generated actions: regenerated from the merged operation catalogue. - Workflow files changed by two branches keep every step of both. This was checked step by step against all five sources, and each changed workflow was parsed as YAML. ## Final review before push A review of every commit message and the whole diff, for public history, led to these changes. The code tree is unchanged by the message edits. - Commit messages: no counts of vectors, files or runs, no internal tool names, and no lines about later work. - SPEC §3.1 states what an encoder SHOULD do and what a verifier reports today. A sentence about a later major revision is removed from §3.1, §8 and the CHANGELOG. - Dated research records are no longer rewritten in place. Each original passage is restored word for word, with a dated correction after it, as CONTRIBUTING requires. - The byte-equality matrix comments say the disk steps are bash and skip on Windows, without a to-do. - The CHANGELOG records that the public-name check now runs on every pull request. **Verified after these changes:** the documentation checks, the catalogue-prior verifier, the workflow step check and YAML parse, `wai breaking` against #614's tip with every break acknowledged, the build, and the page recorder's set test. ## Two tests that failed this pull request under load Two checks failed on this pull request in code it does not change: the relay's DVR test on the linux-arm64 runner, and the watch-together example on the macOS runner (twice). Two commits on top fix them: - `joule-moq-relay DVR: count an eviction under the index lock; the by-age test counts late groups` - `watch-together: the page makes its audio context before its join; a slot is set apart only when the machine ran nothing of the page` Each check still fails on the defect it was written to catch. No bound was widened and no retry was added. ### 1. The DVR window by age (`joule-moq-relay`) **What it asserted.** The relay's store keeps groups for a 2 s window. `a_window_by_age_empties_as_its_groups_pass_it` publishes three groups on a media track and its receipts track, six groups in all, waits until the store holds none, and asserted `groups_evicted == 6`. It then publishes a fourth group and fetches from group 0: what aged out must come back as one unknown range, and every group after it whole. **What failed.** On the linux-arm64 runner the assertion failed with 3 against 6. The run printed no other count. **The cause, as measured.** The store takes a group only once it is whole: its objects and its receipts. A group that becomes whole after the window has already passed its number is late. The store counts it in `groups_late` and never stores it, by design. The relay reads each stream on its own task, so on a loaded host groups can become whole out of order. The test assumed an order the relay does not promise. On a build host other than the CI runner, forcing that order gives 3 groups stored and 3 late, and the old assertion fails with 3 against 6 in each run. The runner printed no `groups_late`, so the claim that its three missing evictions were late groups comes from this reproduction. It was not read on the runner. A second defect turned up along the way. The store removed a group from its index under the index lock but counted the eviction after releasing it, so a reader that found the group gone could read the count one short. With 300 ms injected between the removal and the count, the in-order test failed with 5 against 6. **The fix.** - The store now counts an eviction under the same lock as the removal. - The test asserts two things that hold in any arrival order: every group is either evicted or late (`groups_evicted + groups_late == 6`), and every stored group is counted when it leaves (`groups_evicted == groups_written`). Each prints all the store's counts when it fails. - A new test, `a_window_by_age_passes_groups_taken_out_of_order`, forces the loaded-host order and checks 3 stored and 3 late, then runs the same fetch checks. **Falsified** on that build host: - The old assertion with the forced order fails with 3 against 6. - The count moved back outside the lock, with 300 ms injected, fails with 5 against 6. - The same 300 ms injected with the count under the lock passes. - Counting only one of a pair's two removals fails both tests. - A store that finds every group late fails both tests. ### 2. watch-together: a statement in every slot (co-watch §7.3) **What it asserted.** Every slot wholly inside a page's presence holds a presentation statement from that page. Presence runs from the page's join to the slot before the last statement in the log, and for Ben's first stretch it ends at his deliberate hold. **What failed.** The check failed in two runs on the macOS runner, at a host load average of about 50 and then about 30. Both times the first page in the browser, Ada's, made its first statement after its join, at 3.83 s and then 3.44 s on the room clock. It then made none in the slots from 4 s and 5 s, and in the second run from 6 s as well. During that time it was still entering the room and had not yet followed the log. It followed the log from 7.49 s and 8.49 s, which is 3.7 s and 5.0 s after its first statement. Ben's page took 1.2 s and 1.0 s for the same step. Ada's page was silent for at least 2.17 s and then at least 3.56 s. Nothing was refused, and the room never took the page for gone. What held the page on the runner was not measured. **What was measured.** - **On the CI host, while CI jobs ran (load average 30 to 39).** The browser was launched as the test launches it: headless, the same flags, at nice 10. On a blank page, the first audio context in a fresh browser held its page's thread for 172 to 192 ms (3 runs), for the constructor call alone. A second page's took 3.8 to 4.0 ms. Neither page's thread had a gap over 60 ms in the next 1.5 s. That is a tenth or less of Ada's silences, so the audio context alone does not explain them. The rest of the page's entry was not measured on that host: its audio worklet, its relay session, its WebAssembly, its first draws, its camera and microphone, and an audio context made while that work runs. - **On a laptop, in 25 runs.** The first page's audio context took 94 to 128 ms and the second page's 0.8 to 1.4 ms. - **On a laptop, on main, two injected faults.** Each leaves the slots just after the first page's first statement empty, as in CI. One holds the audio-context call for 2.5 s with a busy wait. The other stops the first page's renderer process for 2.5 s from its join (SIGSTOP). Neither was measured on the CI host. **The fix.** 1. **The audio context before the join.** The page makes the composition's audio context before it joins the co-watch log, through `Compositor.openAudio()` in `enter()`, so that hold is no longer inside the stretch in which the page must state. By the blank-page measurement on the CI host, that is about 0.2 s. The rest of the entry still runs after the join. 2. **A failed entry's compositor is closed.** When an entry fails, the next entry closes its compositor (`Compositor.close()`). Before, a second entry left two audio contexts open and two mix timers running. 3. **The statement loop records its stalls.** Each gap of more than half a slot between two of the loop's passes is recorded on the room clock, with how long its wait and its signing took. 4. **A narrower excuse in the example's check.** An empty slot is set apart only if the machine ran nothing of the page in the slot's second half, which means all three of these hold: - a stall of the loop spans that half; - a heartbeat worker the test starts in each page, ticking every 20 ms, did not tick in it either; - the stall is the loop's wait and its signing, plus at most one pass of the loop's own work. Any other empty slot fails, and the check says why: - no stall: a statement was lost; - the heartbeat ticking: the page's own work held its thread, or the host ran the worker but not that thread; - more than a pass of the loop's own work. Pass or fail, the check prints each page's audio-context time, the loop's stalls and the heartbeat's silences. **What this does not show.** It is not shown that the example now passes on the CI host. If what held Ada's page there was the page's own work on its thread, or the host running the page's other threads but not that one, the check still fails, with its heartbeat ticking. Its printout does not tell those two cases apart. It does show whether the page's thread alone or the whole page was held, and for how long. **Falsified** on a laptop, with each fault put into a scratch copy of the room client: - The audio context moved back after the join, with the call held 2.5 s, fails the check: the empty slots fall in a stall of the loop while the heartbeat ticks. - A 2.5 s busy wait just after the join, or 4 s after it, fails the same way. - Statements dropped for one whole slot fail it, with no stall of the loop. - A 1.5 s await in the loop's own work fails it. The same await inside the loop's wait fails it on the heartbeat alone. - The page's renderer process stopped for 2.5 s passes. The slots inside the stop are set apart: the heartbeat was silent and the loop stalled over the same stretch, to within 25 ms. - In the unit tests, measuring the wait on a different clock from the stall, recording no stall, recording a stall only past a whole slot, or leaving the wait out of the record each fails a stall test. ### Checked on this branch's tip The two commits sit on this pull request's head as they were checked on main. The seven files they change are byte for byte the files checked there, and the branch merges onto main with no conflict. The tip, this pull request's changes included, was checked off the CI hosts: - **DVR.** `cargo test -p joule-moq-relay --test dvr` passes all 10 tests on a Linux build host (Rust 1.98.0, this branch's toolchain), both by-age tests among them. - **Room client unit tests.** `node --test` over `web/room/*.test.mjs` and `web/moq/*.test.mjs` passes 77 of 77, and `cowatch.test.mjs`, run as the conformance workflow runs it, passes 15 of 15. - **The browser job, as the WAI examples workflow runs it.** It ran on a laptop, at nice 10, in the host's browser. `build-web.sh` made the room client from this tree, on the Linux build host. `build.sh` built and packed the archive from this tree. The readiness check passed from the unpacked archive in an empty directory: `wai hello`, `wai verify`, and `wai dev --check` with 7 of 7 checks. - **The examples.** `run-examples.sh browser` passed video-room, watch-together, messaging, remote-desktop and package-and-deliver, and watch-together alone passed 2 more runs: 3 of 3 in all, each with 12 of 12 checks. In each watch-together run: - Ada's audio context was made before her join, in 95 to 102 ms. - Ben's took about 1 ms. - Ada's loop had no stall. - Ben's deliberate hold showed as one stall of 3.6 to 4.1 s, with his heartbeat never silent. - No empty slot was set apart. None of this ran on the CI hosts. Whether the watch-together example now passes on the macOS runner under load is for this pull request's next CI run to show. ## Repository checks run locally Section references, SPEC references, tests in CI, orphan sources, comparative prose and vanity metrics all pass, and so does the public-name check with the private list. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Step 5 of `a_gpu_span_is_a_sole_users_rail_or_an_exact_zero_or_no_figure`
burns this process's GPU across a second child's exit and asserted

    matches!(refused, Err(GpuClientVanished { pid, .. }) if pid == second_pid)

It failed on the macOS energy job four times since 2026-10-11 (jobs 21067,
36264, 75996, 76205), each time naming another process's pid. The meter was
right each time; the assertion assumed the child's client is the only one that
can vanish in that ~0.3 s span, and the build host is shared: Playwright's
headless browsers, other test processes and mediaanalysisd open, close and reset
GPU clients all the time (the same run's 1.5 s burn had already retried twice
for two other pids).

`apportion` checks this process's own clients first, then the others, each
pass walking the start snapshot in order and naming the first client that is
gone or went back. The registry lists clients oldest first, so any older client
of another process's that vanished in the same span is named before the child's.
Every such client is a true witness: its time over the span is lost, so
`all_ns`, and the 99% sole-user test, cannot be decided.

The step now proves what it is for, whatever else the host does:

- the refusal is `GpuClientVanished`, never a figure;
- the client it names was at the start under that pid and is gone or went back
  at the end (the check `until_no_client_vanished` already made, now one helper);
- it is exactly the one the rule picks: this process's own vanished client
  first, else the first vanished in the start snapshot;
- every client of the second child's is gone from the end snapshot;
- with the host's other vanished clients taken out of the real start snapshot,
  the child's exit alone still refuses the span, naming the child; so the check
  cannot pass only because unrelated churn refused it.

When another client was named it says which. The step is not retried: there is
nothing to wait out, and a retry would hide a regression in the rule.

The tie-break gets a unit test of its own over built snapshots (two other
processes' clients vanish: the first at the start is named, in either order,
gone or gone back alike; this process's own vanished client is named even when
it comes last), and `GpuClientVanished`'s doc states it. The meter is unchanged.

Verified on the build host (load 95-160): clippy -D warnings on
jouleclaw-energy with `--features ioreport` and without; the CI step
(`cargo test -p jouleclaw-energy --features ioreport`, the liveness line
printed) passed; process_gpu passed 5 of 5 on the host as it is and 6 of 6
beside a churner opening an idle GPU client for 1.5 s every 2.5 s, which failed
the old assertion 2 of 6. In one of those six the refusal named a churner's
client, as in CI, and every check above held. Falsified: dropping a vanished
other-process client instead of refusing fails step 5 (it gave GpuShared) and
two unit tests; walking the start snapshot last-first fails the new unit test
alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
watch-together failed on the macOS examples job five times (jobs 65458, 71887,
72455, 76030, 76114), each with "both play on Ada's control: not within 60000 ms
(last: false)". It was not slow. Each of those pages' consoles shows Ada joining
the watch room, a 409, a second join, another 409, and "the room refused an
event: time_reversed"; from then on her page applied nothing.

The room service ends a viewer that posts nothing for 4 s (SILENT_US), and the
page posted nothing between its co-watch join and the end of `begin()`: the
compositor, the audio worklet and the relay session. On a loaded host that took
4.4-8.2 s in every failure against 1.2-3.7 s in every passing run. Ada's first
post then got `not_present`; `flush()` entered again, and the clock signed her
new `join` at a later t than the event she had signed before it, which `flush()`
then sent again: refused as `time_reversed` (watch.rs `post`), the page stopped,
and nothing clears a stop. When that event was her play control, the room never
carried it at all. So any stall past 4 s, at any point, stopped a page for good.

Two changes in the page:

- cowatch.js: re-entering works. The page keeps where the room's copy of its
  chain ends (each outbox entry has its seq and digest), and after the new join
  signs every control the room had not carried again from there, at or after
  the join: at its instant if the policy's lead still allows it, else at the
  earliest it does. A presentation statement signed before then describes an
  instant the page was not present for and is dropped; the page states anew
  within half a slot. The page counts its re-entries and what it signed again
  and dropped.
- app.js: the statements start at the join (co-watch 7.3: a present player
  states at least once a slot), not after `begin()`. That removes the usual way
  into the silence; it cannot prevent one, since a thread held by a loaded
  machine runs no timer. The log is still followed after `begin()`, which reads
  the grants a control's author is checked against.

The example's test now makes that silence happen instead of hoping the host
does: `roomTest.watchHoldUntilGone` holds Ada's page's thread from the moment it
enters until the room's log carries her `leave` (read with blocking requests),
while Ben's page enters, then signs her play in the same task, before her page
can enter again. Both pages must still apply it at its instant within 250 ms,
and Ben's pause, at one position; her page must have entered again with the
play signed again; and at the end no page's event was refused and none stopped,
so each viewer's chain was carried whole. A wait now ends at once when a page
stops, and says each page's controls, playing, error and stop instead of "last:
false". cowatch.test.mjs adds the same path against a service that applies
watch.rs's rules: a pause signed while the page was gone is carried after its
second join, the stale statement is not, and the chain goes on.

Verified on the build host, the room client and the archive built as the job
builds them: cowatch.test.mjs 8 of 8; watch-together through run-examples.sh 5
of 5 at load 130-150 and 3 of 3 beside 96 busy loops (load 240-260), the
setting that failed it 1 in 3 before. In one loaded run the room had already
taken Ada for gone before the hold began, as in CI, and her page entered again
and went on. Falsified: without the re-signing, the unit test fails and the
example fails in 9 s with Ada stopped on time_reversed and no control carried.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of 822bb2ac5. `GpuClientVanished`'s doc said that, of several vanished clients, the
meter names this process's own first, else the first in the start snapshot, "which the I/O
Registry lists oldest first". The meter did not make that so: it walked the snapshot in the
order the registry read gave it, which is attach order only within one accelerator's children,
and the new unit test pinned that order (reversed, it expected client 20 named over the older
10). Nothing tested "oldest".

`spent` now checks the start's clients in the order of their registry entry IDs, which the
kernel gives entries in the order it makes them and never reuses while the host is up: the
oldest vanished client is named, wherever the snapshot lists it. `GpuClient::id`'s doc says so.
The unit test expects client 10 for the snapshot as built, reversed and shuffled, and this
process's own oldest vanished client (31 of 31 and 32) wherever they sit. Step 5 of
`a_gpu_span_is_a_sole_users_rail_or_an_exact_zero_or_no_figure` picks the expected client the
same way.

Step 5 also reads the registry once with `ioreg`, independently of the counter, after the
counter's end snapshot, and asserts that no client the end snapshot lacks is still listed: an
entry once destroyed never comes back and its ID is not reused, so such a client would be one
the counter's read left out, not one that closed. When the refusal names another process's
client, it prints every client that vanished as (pid, id, start ns, end ns or gone), so a
failure log tells host churn from a fault in the read.

Verified on the build host (load 55-65): clippy -D warnings on jouleclaw-energy with
`--features ioreport` and without; the CI step (`cargo test -p jouleclaw-energy --features
ioreport -- --nocapture`, the liveness line printed) passed 5 of 5. Falsified: without the sort
by ID, the unit test fails on the reversed snapshot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of b01681570. Under the state lock, `ended()` marks a silent viewer `leaving`;
`run_watch` then signs the window's due skew statements, each through the clock's lock with its
rate wait and relay write, and only after them the viewer's `leave`. A post of the viewer's in
that window is refused `not_present`, and its page enters again: `watch_join` saw it not present
and asked the clock for a `join`, which could take the clock's lock before the queued `leave`.
The `leave` was then signed after the new `join` and ended the presence it began, the page's
post signed again was refused `not_present` once more, and the page stopped for good: the
"never" this branch set out to remove. The clock's chain also held a `join` of a viewer already
present, which live-rooms §7.2 refuses (`already_present`), so the room no longer verified.

The order is now decided under the clock's lock, from the room's state read under it:

- `Viewer::leaving` holds the reason of a `leave` asked of the clock and not yet signed;
- `Service::watch_leave` signs that `leave` only if it is still pending: a viewer that entered
  again in between is not ended by it. `run_watch` signs each `leave` through it;
- `Service::watch_enter` (`POST /watch/join`) signs a pending `leave` first, then the `join`;
  a viewer present and not leaving is answered `again`, as before.

`emit_items_after` is split into taking the lock and `sign_items`, which both use with the
lock held. The README and http.rs's table say what a join does now.

tests/room_service.rs: a_watch_viewer_taken_for_gone_enters_again_after_its_leave runs the
service with the clock but not its watch loop, so the test decides when the `leave` is signed.
The room takes the viewer for gone; its post is refused `not_present`; it enters again before
the `leave` is signed; the room's own `leave` then signs nothing, and the refused position,
signed again, is carried. Then the other order: the `leave` signed (once), then the `join`
alone. The clock's lifecycle of the viewer is join, leave, join, leave, join, the viewer's
chain is whole, and the room verifies (co-watch §11, which runs live-rooms §9).

Verified on the build host (load 55-65): `cargo test --locked --all-targets` in
joule-moq-room passed; room_service's two watch tests and watch.rs's unit test passed 5 of 5.
Falsified: signing the `join` without the pending `leave` first fails the new test, and so does
signing the room's `leave` without checking it is still pending.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of b01681570, the page's side.

- A page taken for gone again right after it entered again (a second stall, or a service that
  signed a queued `leave` after its `join`) stopped for good: one re-entry was allowed between
  carried posts. It now enters again up to three times with nothing of it carried between, then
  stops. A join that throws (the service answers 503 when its clock cannot sign) no longer
  leaves the page marked as entered with a stale outbox, which stopped it at the next post: the
  page logs it and the next post tries again, as after any other failed post. Each post refused
  `not_present` is counted (`stats.gone`), apart from `rejoins`.
- `resign()` moved a control signed while the page was gone to the earliest instant the lead
  allows even past another viewer's control carried meanwhile; controls apply in the order of
  their instants (co-watch §5.4), so the earlier press overrode the later one. Such a control is
  now dropped (logged, counted in `dropped`) when another viewer's control the page holds lies
  at or after its instant as pressed and at or before the instant it would move to; otherwise it
  is signed again as before.
- Signing again a position whose event the room refused is not equivocation: the room holds no
  event it refuses. Co-watch §10 now says so, and `resign()`'s doc.
- A presentation statement whose signing straddled a re-entry was carried with an `at` from
  before the new join; it is dropped and the page states anew at once.
- `run()` starts at the join (b01681570); if `begin()` then fails, the page now stops its
  co-watch log instead of stating a presentation it does not keep as a viewer that applies no
  control.
- The snapshot gives the page's principal, its outbox, and `followed_at`, the room clock when it
  began to follow the log; `roomTest.watchLog()` reads the room's log with each record verified,
  and `watchHoldUntilGone` says when the hold began.

cowatch.test.mjs adds, against its watch.rs-like service: a page taken for gone again right
after it entered again enters once more and its pause is carried; a join that fails once is
tried again and the page goes on; a page taken for gone at every entry stops after three, its
refusal counted; and a control pressed while gone is dropped where it would move past another
viewer's, signed again where it keeps its place.

Verified on the build host (load 60-75): cowatch.test.mjs, 12 tests, passed 5 of 5.
Falsified: allowing one re-entry, rethrowing a failed join, and removing the drop each fail the
test written for it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of b01681570, the example's test. It had replaced the example's own run with the hold:
Ada's play always went through take-for-gone and re-entry, never the path a present viewer's
control takes. And it passed defects the old test failed: it let either page re-enter any
number of times uncounted (only Ada's `rejoins >= 1` was read, Ben's never), a `not_present`
refusal went nowhere, and "the room refused no page's event" held while the test had Ada's
events refused. Moving app.js's statements back after `begin()` passed (Ada was held either
way; a Ben slow past 4 s just re-entered), as did a page that stopped stating, or a room whose
silence limit shrank. And it checked the play against the instant Ada first signed it for, not
the one the room carried after re-signing moved it.

Now:

1. As before, both pages enter; Ada plays and Ben pauses, each while present; both apply each
   control at its instant within a quarter second of the room and pause at one position.
2. Ben's page is held until the room's log carries his `leave`; Ada seeks back to 2 s
   meanwhile; Ben's play is signed as the hold ends, before his page can enter again. The
   test reads the room's log until Ben's play is carried after his second `join`, takes the
   instant the room carried (at least that join's `t` plus the lead, and no earlier than first
   asked), and both pages must play from the seek at it within the bound, then pause at one
   position again when Ada pauses 3 s later.
3. The room's log, read once and every record verified (`roomTest.watchLog`): exactly one
   `leave`, Ben's, after his hold began; Ada joined once, Ben twice, the second after the
   `leave`; every slot wholly within each stretch of each page's presence holds a statement of
   it (co-watch §7.3), but Ben's hold; and each page's first statement comes before the instant
   it began to follow the log, which it does only once it has its relay session. That last is
   ordering, not timing: with the statements started after `begin()` it cannot hold.
4. Each page's own counts: Ada never taken for gone and never entering again, Ben taken for
   gone once (`stats.gone`), entering again once with his play signed again; no other event of
   either refused, and no page stopped.

A wait still ends at once when a page stops and says each page's counts.

Verified on the build host, the room client and the archive built as wai-examples.yml builds
them (load 62-73): watch-together through run-examples.sh passed 5 of 5, every check held.
Falsified, each built and run the same way: statements started after `begin()` again fails the
first-statement check; a page that stops stating once it has entered again fails the slot and
`leave` checks; a room silence limit of 400 ms fails the `leave` count and the pages' counts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`ProcessSpanMeter::finish_showing_gpu` is `finish`, and the span's two GPU snapshots when both
were taken; `finish` now calls it and drops them. It is `#[doc(hidden)]`, not part of the
metering API, as `probe_gpu_liveness_uncached` is not. tests/process_gpu.rs needs it to check
what the GPU counter made of the span meter's own snapshots against a registry read of its own;
it had picked a span out of six by the text of the reason. The GPU half is unchanged:
`gpu_figure` shares the two snapshots when both were taken, and is otherwise the start's failure,
then the end's, as `finish` had it.

span.rs's unit test a_spans_gpu_figure_comes_with_the_snapshots_it_was_shared_over: the share is
over the two snapshots and they come back with it; a failed start, a failed end, and both, are
each that failure, the start's first, with nothing shared and nothing handed back.

Verified on the build host: clippy -D warnings on jouleclaw-energy with `--features ioreport`
and without; span.rs's unit tests pass. Falsified: the end's failure ahead of the start's fails
the new test (`Err(NotAdvanced)` for a span whose start was `NotCurrent`), and so does handing
back the start snapshot twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The macOS energy job's process_gpu test gave up when a GPU client vanished in six spans in a
row: its checks needed a span in which no client of the shared host vanished, and on a host
whose other sessions open and close GPU clients every second there may be none. Reproduced on
the build host beside a churner of short-lived GPU clients of our own: at one every 0.75 s, 4 of
4 runs gave up; at one every 1.5 s each run took 2-4 tries. Two other failures had the same
root: the oracle's sum over every client gave up when one went back inside its window (job
27254, the `.expect` on `oracle_times`), and fell below the counter's when a client came and went
inside the span (job 48857, the every-client assertion).

No span now needs to be free of churn. Each span of the counter's or the span meter's is
bracketed by `ioreg` listings of the registry, read independently of the counter: before it,
around its work and after it. `classify` sorts every client against them. A registry entry once
destroyed never comes back and its ID is not reused, so:

- a client two reads list is in every read between them, under one pid; anything else is a
  contradiction and fails the test: a client the end snapshot lacks while `ioreg` lists it after
  (step 5's own check, now on every span), one the start lacks while `ioreg` listed it before and
  after, one both snapshots hold that `ioreg` did not list around the work, one gone inside the
  work and back in the end snapshot;
- a client's time goes down only where a queue of it closed, which the listings show as a fall,
  so each counter reading lies between the listings either side of it, or on one side of a fall
  across it; a reading the registry does not explain voids the span;
- a client the counter saw vanish is corroborated if `ioreg` listed it under the same pid (gone)
  or showed its fall inside the span (gone back); one `ioreg` never listed voids the span;
- a client only `ioreg` listed inside the span, or a fall it showed where the counter saw an
  advance, is GPU time the counter's snapshots cannot hold, counted as hidden, a lower bound;
- this process's own clients must be clean, or the span is void.

A void span is taken again after 0.5, 1, 2, 4, 8, 8 ... s, and none the registry vouches for in
90 s fails as `process-gpu INCONCLUSIVE`, with each reason's count: not a verdict, and not a
pass. A refusal must be the registry's: a corroborated vanished client refuses the span, naming
the client the meter's rule picks (`expected_refusal`, factored out of step 5), and with none the
span is not refused. The checks then run on the view, the snapshots with the vanished clients
taken out of both, which can only raise this process's share.

`agrees_with_oracle` checks client by client. This process's GPU time equals `ioreg`'s exactly,
as before. The clients whose time went down nowhere advanced at most the oracle's advance plus
what the gap can hold (their lower bound is now the sorting's, per client). The meter's
every-client time is the sum over the view's clients. The rail is checked against the test's own
IOReport read on every span whose snapshots both read it current, shared ones included (it had
been on sole-user spans only), and a figure's rail is the snapshots' own advance. A figure must
be a sole user's share of the time the counter saw plus the time hidden from it. `oracle_times`
and its give-up are gone.

Steps 4c and 6 take the span meter's span through `finish_showing_gpu` (the previous commit) and
check that it is the GPU counter's figure, or the CPU alone with the counter's reason, over the
meter's own snapshots, where the test had picked a span out of six by the text of its reason. A
corroborated refusal is accepted there as the CPU alone, `HwShunt`, `Partial`, with CPU energy.
Step 5's span-meter span gives the counter's reason over its own snapshots. The idle spans of
steps 3 and 4a, and 4c's CPU-only span, also assert that `ioreg` saw no GPU time of this
process's inside them. After the liveness askers, the test waits, at most 3 s and never failing,
for this process's own clients to settle.

the_span_classifier_sorts_every_client drives `classify` over six built reads: clean; gone and
gone back, with the registry vouching and without; made and destroyed inside the span (48857's
shape); a fall inside the work that the counter saw as an advance (27254's); falls across each
snapshot inside the span and outside it; under- and over-reads; this process's own client not
clean; each contradiction and a pid mismatch; `expected_refusal`'s order; and the 99% gate with
hidden time.

standards-build.yml: the jouleclaw-energy step also greps `process-gpu: oracle-checked:`, which
step 2 prints once its burn was checked against the oracle. MEASURED_ENERGY.md says what the test
checks.

Verified on the build host, at this commit's jouleclaw files: clippy -D warnings on
jouleclaw-energy with `--features ioreport` and without; the macOS energy job's jouleclaw-energy
step as standards-build.yml runs it (under `bash -eo pipefail`, both greps), 5 of 5, three of
them beside a churner of short-lived GPU clients of our own every 0.75 s; and the process_gpu
test 16 of 16, 4 quiet and 4 beside a churner every 3, 1.5 and 0.75 s, with no void span, no
INCONCLUSIVE and no contradiction, every vanished client corroborated on the first attempt. A
read-only walk of the registry the way the counter walks it, every 5 ms for 60 s beside a churner
every 0.2 s (268 clients made and destroyed), found no walk lacking a client the walks either
side of it held and no child iterator invalid at the end of a walk (phantom_walks 0,
invalid_at_end 0, of 7266 walks): a contradiction is a fault in a read, not churn.

Falsified, each failing as named: a start or end walk dropping a client, or giving it another
pid, and a phantom client in both snapshots (contradictions); the counter skipping another
process's vanished client, or giving a figure across a gone one ("the registry vouches for ...
but the counter gave Ok"); refusing a client that did not vanish, or naming another than the
rule's; this process's time and every client's time each 1 ns off; the end snapshot misstating
when it read; the rail under-read by 10%, and a figure's rail not the snapshots' advance; the
sole-user gate at 1%; the span meter's figure, or its reason, not the counter's over its own
snapshots; GPU work in an idle span; the CI grep's line renamed; and the classifier's stretch
rule, fit rule, fall placement, the per-pid oracle, the refusal order and the hidden-time gate,
each broken in turn. Two persistent faults end INCONCLUSIVE, as designed: (c) a 10% under-read of
another process's client in every snapshot, "process-gpu INCONCLUSIVE: 1.5 s burn: no span in 92
s (12 spans) could be checked against the oracle: a counter reading out of the registry's order
×12; ...", and (d) a phantom client in every start snapshot, "... a vanished client the registry
never listed ×12; ...".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The macOS browser e2e job failed its cowatch scenario five times with "cowatch: both play from
Ada's control: not within 60000 ms (last: false)" (jobs 67776, 70090, 73183, 76976, 77089). It
is the take-for-gone chain b01681570 records for watch-together, with the stall in Ada's own
entering: `begin()` posted nothing for longer than the room's 4 s silence limit on the loaded
host, the room took her for gone, and the old page, entering again, sent an event signed before
its new join, refused `time_reversed`, and stopped. This branch's page recovers (b01681570,
4cc9a3e1b), but the scenario still failed: its check that the service carried every event the
pages posted required the service to have refused nothing, and the service counts each
`not_present` a page taken for gone gets before it enters again. On the build host under load,
8 of 11 runs of the branch head failed on exactly that check, with {"not_present":1} or
{"not_present":2}, every page having entered again and played.

The check now takes the design at work for what it is. The service's refusals must all be
`not_present`, as many as the pages count themselves taken for gone (`gone`); each page must have
entered again as often (`rejoins`, at most three), had no other event refused (`post_refused`),
and not stopped. The pages' counts are read either side of the service's until the two readings
agree. That check and the closing one print each page's gone, rejoins, resigned and dropped.

Each control's instant is now read from the room's log, as the room carried it: a page that
entered again signs its controls again, no earlier than its new join plus the lead, so the
instant can be later than the one first signed. The waits for each control use it, and the
play's line reports it, with the instant first signed when it moved. Before, a seek moved that
way could pass its wait before it applied, and the position check after it then failed.

The scenario does not hold a page on purpose: the watch-together example does that every run,
and this scenario stays the room as people use it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The check ff08b646c added read the pages' counts of being taken for gone either side of the
service's count of `not_present` refusals, and took the first reading in which the two page
readings agreed. The service counts a refusal before the page it answers can count it, so a
refusal landing between the first page reading and the service's could still be counted by the
service alone in a reading the check took: a page taken for gone at that moment would fail the
check although it entered again as designed. The readings are now taken again every 500 ms, for
at most 15 s, until the page readings agree and the service's count is theirs; the check takes
the last reading either way, so a real disagreement still fails with both counts printed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`classify` voided a span when a counter reading of one of this process's own clients lay outside
the `ioreg` listings either side of it, and the span was taken again. The reason for voiding holds
for another process's client: a queue of it can close and the client accrue again within one gap,
which looks like a misread. It does not hold for this process's own: it runs no GPU work between
the counter's reads and those listings, so a correct read equals them, or one side of a fall of
its own they show. Voiding there meant `assert_eq!(self_ns, o_me)` could no longer see a wrong
own reading in a snapshot: any one lands outside the listings and was voided first, so an
intermittent misread passed on the next attempt.

A reading of this process's own client the registry does not explain, with no fall of it shown
across that reading, is now a contradiction, which fails the test at once, as the exact
own-time check had before the registry sorting. Voids stay for own resets the registry shows:
gone, gone back, fallen inside the span, made and destroyed there, and a reading between the two
sides of a fall of its own across it (two falls in one gap).

the_span_classifier_sorts_every_client: an own under-read at the start by 1 ns, an own over-read
at the end by 1 ns, and a 10% own under-read are each a contradiction naming the read; an own
reading between the sides of a fall of its own is void.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`refusal_is_the_registrys` matches the counter's verdict on a span against the registry's
sorting of it: a refusal exactly when the registry vouches for a vanished client, naming the one
the meter's rule picks. On a quiet host no span has a vanished client, so its branches ran only
under churn, and an inverted condition in it would have gone unnoticed until then. It now returns
`Result<(), String>` (the spans' callers fail the test on `Err`, as before), and so does the
named-client check it uses, which step 5 still asserts through `assert_names_a_vanished_client`.

the_span_classifier_sorts_every_client drives it over built snapshots: with two vanished clients
the registry vouches for, a figure, `GpuShared`, a refusal naming the younger one and one naming a
client not at the start are each `Err`, and a refusal naming the oldest is `Ok`; with none, a
refusal is `Err` and a figure or `GpuShared` is `Ok`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`until_sorted` takes a span again while the registry cannot vouch for it, and a passing run said
nothing of how many were void first: an intermittent misread of another process's client, voided
and retried, left no trace in CI's logs. It now prints `process-gpu: <what>: vouched on attempt
<n>; voids: <kind ×count, ...>` (or `none`) for the span it returns, so a rising rate of void spans,
`OUT_OF_ORDER` above all, shows in the logs and can later be bounded. The INCONCLUSIVE failure
is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`agrees_with_oracle` checks the rail against the test's own IOReport read only when both
snapshots read it current and their rails match; otherwise it printed "rail not checked" and went
on, and a figure there would have passed with no check of its rail. The meter gives none then
(`NotCurrent`, the rails differing, or one going back, pinned by its unit tests), but the hardware
test did not hold it to that. That branch now asserts that neither the span as taken nor its view
without the vanished clients is a non-zero figure.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`agrees_with_oracle` fails a figure of the counter's that is not a sole user's share of the time
the counter saw plus the time `classify` shows was hidden from it (a client made and destroyed
inside the span, a fall where the counter saw an advance). The span meter's spans of steps 4c and
6 had no such gate: a `SpanSource::Process` figure at 99% of what the counter saw passed there even
with GPU time hidden from both snapshots, so the test proved a figure wrong in one place and let
it through in another. `checkable_meter_span` now applies the same gate to the GPU counter's
figure over the meter's own snapshots, which the span is checked to hold. The meter is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Step 4's burner child burned for at most 60 s, its wall time capped there whatever it was asked,
while steps 4b and 4c now each take spans again for up to 90 s (`BUDGET`) where the old test
allowed six quick tries. A long run of void spans could outlast the child; its exit then read as
a corroborated vanish, the view was no longer shared, and the test failed with messages that
look like a meter defect ("a shared GPU gave Ok(..)", "the child did not contend", "the child's
client is not in the start snapshot") instead of INCONCLUSIVE.

- The child burns for the seconds it is asked, of wall time, and step 4 asks for 2 × `BUDGET` +
  60 s, longer than 4a, 4b and 4c can take between them; step 5 still kills it.
- Right after 4b's and 4c's spans, the test fails `process-gpu INCONCLUSIVE: ... the contending
  child exited (<status>) while its spans were taken` if the child is gone.
- The child is held in `Burning`, which kills and reaps it on drop: std's `Child` does neither,
  and a failed check would otherwise leave a GPU burner on the shared host for the rest of its
  life. A child whose parent is gone (killed outright, dropping nothing) stops burning too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
MEASURED_ENERGY.md and the test's module doc claimed more than the test does. The registry is
listed before, around and after each span of GPU work the test checks against the oracle or
through the span meter (steps 2, 4b, 4c and 6), not before every span: the idle spans of step 3
and step 5's spans are not bracketed that way. The rail is checked against the test's own read
on the two spans checked against the oracle (the 1.5 s burn and the contended burn) when their
rail was current at both ends, not on every such span. And a client whose time went back is
corroborated by a fall the registry shows inside the span, not merely by being listed. Both now
say so, and that a reading of this process's own GPU time the registry contradicts fails at once.
Step 2's comment on the rail check points at `agrees_with_oracle` rather than every span.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
ff08b646c made the cowatch scenario read each control's instant as the room carried it, since a
page the room took for gone signs its controls again, no earlier than its new join plus the lead.
A carried instant other than the signed one was printed as moved, but never tied to that page
having entered again: a room that carried a control at the wrong instant, with no re-entry, would
have passed where the scenario had compared with the signed instant.

`control()` now reads the page's `rejoins` before it signs and after the control is carried, and
checks that the carried instant is the signed one unless that count rose. It reads `rejoins`
rather than `gone`: the page counts `gone` when its post is refused, before it enters again, so a
control signed while the page is already entering again is moved with `gone` unchanged since it
was signed; `rejoins` rises as the page signs again, after any control pushed before. Each of the
four controls adds its line, with both instants and the count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The cap on the rail's advance over the gap between the counter's window and the oracle's now
also runs on shared spans, and had been measured only beside the test's own burner child and an
empty-command-buffer churner. Measured on the build host beside three more burners of the test's
kernel (the test binary in its burner-child mode), so five clients burned at once and the GPU
drew about 49 W across each window: the power over the gap and republish allowance, the quantity
the cap bounds, was at most 19.2 W over three runs (15.4-19.2 W), against 12-17 W beside the child
alone; every run passed, every span shared or refused. That is well below 80% of the cap, so it
stays; the comment above the assertion records the measurement.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
losses_each_way_are_recovered_within_the_window failed twice on the
thor-linux-arm64 runner (runs 16749 and 17926) at `part.recovery.max <
300_000 && part.margin_min > 0`, with nothing dropped and every payload
delivered whole and in order. Under induced load on the build host the
receiving task fell up to 384 ms behind its socket, and two cases met that
signature: a recovery of 349 ms with 50 ms to spare (a recovery counts from
when the packet was found missing, the window from its timestamp, which a
late time base puts later), and a margin of 0 (a retransmission that reached
the socket 152 ms early was taken 1 ms after its own delivery time, before
the first packet after it was due, and delivered as live-contribution §4.1
allows). No recovered packet was taken past its drop point in any of the
instrumented runs. The bound measured the host, not the protocol.

The test now gives the link a window of a second on both ends and asserts
what the protocol guarantees: nothing dropped, every lost first send found
and recovered, every payload whole and in order, the part's account, and
margin_min within the window. Its assertions print the part.
a_listener_that_sends_and_a_caller_that_receives, which dropped packets
under the same load with a 250 ms window, gets a second as well.

The reproductions also showed an order defect in both receivers: a datagram
was taken before what had fallen due was resolved, so a retransmission taken
after its drop point with no timer tick between was delivered as recovered
with a margin of 0, and with a tick between was dropped and counted late.
The SRT and RIST receivers now deliver and drop what is due before they take
each packet. Unit tests drive each receiver on a clock of their own: in time
(margin = due - arrival), after its own delivery time but before the drop
point (SRT: delivered, margin 0), at or after the drop point with and
without a tick (dropped and late), and a sweep in which a tick before an
arrival never changes the outcome. Without the reordering four of them fail.

live-contribution §6.1 says when a packet arrives and what becomes of one
taken between its delivery time and its drop point, §6.2 that its margin
counts as 0, and §6.3 that an SRT recovery is not bounded by the window;
the CHANGELOG records it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The connection's loop sleeps until the receiver's next wake, and the
receiver put that 10 ms after the moment it was asked: after each datagram.
While datagrams came closer together than 10 ms, the wake moved on with
each one, and the timer ran first when the first packet held fell due. Until
then, a link's whole first window, it sent no full ACK and no periodic NAK,
so a packet whose first NAK or retransmission was lost was asked for again
only as the window ran out. With the link test's window of a second, under
a load of 32 busy loops on the build host, 2 runs in 30 dropped 4 packets;
3 of them were sent in the stream's first 350 ms.

The timer now runs an ACK period after it last ran. A unit test runs the
connection's loop on a clock of its own, with data every 2 ms and a NAK
lost: the timer runs every 10 ms from the start, the first full ACK goes at
10 ms, and the NAK goes again 150 ms after the first. With the wake counted
from the moment asked, the timer first runs at 300 ms and the test fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-contrib: the SRT link tests again require every recovered packet taken before its delivery time; a recovery is pinned to when its gap was found, and a held packet keeps its margin at the link's end
All checks were successful
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 15m14s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 14m8s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 14m16s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 35s
standards workspaces build + test / joulespec-proofs (joulespec/joulespec-rs, joulespec-proofs, true, macos) (pull_request) Successful in 1m24s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 6m53s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 49s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 7m47s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m18s
standards workspaces build + test / smart-byte-rs (smart-byte/smart-byte-rs, smart-byte-rs, linux) (pull_request) Successful in 7m17s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 10m8s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 8m32s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 13m53s
wai-quantum WebGPU member / member · GPU, Vulkan (thor) (pull_request) Successful in 24m45s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 14m21s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 26m3s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 13m44s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 7m1s
wai-quantum WebGPU member / member · GPU, Vulkan (jetson-hub) (pull_request) Successful in 23m54s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 18m31s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 3m56s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 20m12s
standards workspaces build + test / aac-rs (aac-conformance, aac/aac-rs, aac-rs, linux-arm64) (pull_request) Successful in 3m27s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 20m43s
standards workspaces build + test / joulecontract-rs (joulecontract/joulecontract-rs, joulecontract-rs, linux-arm64, true) (pull_request) Successful in 1m1s
standards workspaces build + test / map-rs (map-conformance, map/map-rs, map-rs, linux-arm64) (pull_request) Successful in 1m3s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 59s
standards workspaces build + test / openpay (openpay, openpay, linux-arm64) (pull_request) Successful in 3m30s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 6m32s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h31m25s
d1b4bd0c01
The loss test's last assertion had become `margin_min <= window`, which the
receiver's clamp and the verifier's rule already guarantee: the end-to-end test
checked no timing at all. It again asserts `margin_min > 0`, the only check
across the real link (the sender's NAK handling, the connection's loop, the
proxy) that every recovered packet was taken before its own delivery time. The
ordering defect fixed in 066d82edf delivered a retransmission taken after its
drop point as recovered with a margin of 0, with nothing dropped and every
payload in order: exactly CI's failure at the old line 148. That failure fits
the defect, a busy runner's lag, or both (with the starved timer fixed in
76264fae6 lengthening the first window's recoveries); the part was not printed,
so it cannot be told which, and the earlier commit's "the bound measured the
host, not the protocol" says more than the evidence does. The window stays a
second, which absorbs the receiving task's lag seen under load. The recovery
time stays unbounded there: it counts from when the gap was found, not from the
packet's timestamp (live-contribution §6.3).

What pins where a recovery counts from is now the receivers' unit tests: they
find each gap at 20 ms rather than at the clock's origin, so a recovery counted
from 0, or from the last tick, fails them. The loss test also asserts what does
not depend on the host: every recovered packet came back as a retransmission
(the proxy neither delays nor reorders), and every NAK names a sequence number.

The caller that receives takes its time base from the listener's CONCLUSION
reply, wiring no unit test reaches. Its lossy test now checks its window, its
account (nothing dropped, a margin above 0), and a new test holds one packet
for the caller's window as the listener's test does.

A recovered packet still held when a link ends, SRT or RIST, was recorded with
a margin of 0, which reads as one taken after its delivery time; it now keeps
the time it arrived before that. A unit test for each.

A receiver unit test feeds ACKACKs through the connection's loop on a clock of
its own and pins the periodic NAK after the RTT is measured: every
(RTT + 4 x RTTVar) / 2, 30 ms at a 60 ms round trip, and the 20 ms floor at
1 ms, until the drop point. The comment on the timer test now says the periodic
NAK counts from the link's start; the NAK sent at once does not restart it.

live-contribution: §5.3 and §6.8 said a recovered packet arrives on time, which
under §6.1 does not hold for SRT: one taken after its own delivery time, before
the first packet after it that has arrived is due, is delivered late, counted
recovered with a margin of 0, and the record does not count how many. They now
say so, and §6.8 that the record does not say when a packet delivered at the
first try was taken. §4.1 and §6.1 name the drop point as the delivery time of
the first packet after it that has arrived, since the next one can itself be
missing. The CHANGELOG entry says the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The entry for version 2 of the route record named the product whose proposal it
adopted, against CONTRIBUTING's public-text rule. The name is redacted in place
with a dated marker, as this changelog's history redacts names, and the sentence
keeps its disclosure: the proposal came from a product of WAI's maintainers
[name removed 2026-11-03].

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The name check read only the private list, which never held the maintainers' own
product names, so a changelog line naming one passed it. Those names are public
already, and the private list also serves checks of text about the products, which
names them where it should; so they are listed in the script, MAINTAINER_PRODUCTS,
in any case, joined or with one hyphen or underscore, the shorter one standing alone.
They are applied beside the private list, with the same opt-out, and an unset private
list still fails closed. The self-test gains a case for each spelling, a generic
phrase that is not a hit, an honoured opt-out, and the unset list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A hardware decoder against software decoders of the same format (arXiv 2402.09001,
Table IV), fewer frames chosen by the encoder (arXiv 2603.18305, Table V) and a lower
resolution or coarser quantiser chosen by the encoder (arXiv 2511.00707, Table 4), each
with what was measured and against what. They are informative: no record states them,
no rule reads them, and none is a figure of a WAI decoder. The section says how they bear
on the text: the two encoder choices cost frames or quality, SPEC §4's energy order
already ranks the platform's power-efficient decode first, and a hardware row of this
revision reads no media engine, so it and the first point are not like for like.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The private list is a secret, given only to the run on a push to main, so a pull
request that named one of the maintainers' products was caught only after it landed.
Those names need no secret: --maintainer-products-only applies MAINTAINER_PRODUCTS
alone and reads no private list, and the repository checks run it on every change.
The default still requires the private list and fails closed without it.

The script's header and CONTRIBUTING now say which products the script lists: those
the maintainers' public site names. A product not yet announced is named on no site,
so the private list holds it until the day it is announced, when it moves to the
script. The self-test gains cases for the new mode: a product is a hit, a clean file
is clean, the private list is not read, and a missing file fails closed. Two
paragraphs are rewrapped to their file's width.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Point 3 said its energy was read by the processor's own counter through a software
tool. The paper's tool reads the processor's counter and the graphics processor's,
and the paper states neither whether the idle energy is subtracted nor whether Table
4's savings are the measured or the predicted energy of the configurations chosen;
the point now says so. Point 2's classifier chooses per rate-factor setting, of which
Table V covers four, not per quantisation setting.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
WAI's WebAssembly legs ran two stock wasmtime versions: 49.0.2 in the
quantum and face install steps, and 38.0.3 in wai-ops.yml and wai-vector.yml
(wai-ops runs the wasm32-wasip2 decode determinism-tiers §5 cites). A stock
wasmtime leaves NaN canonicalisation off and lets each relaxed SIMD
instruction take the host's own behaviour, so the bits of a NaN a module reads
back, and the result of a relaxed instruction, were the engine's choice.

Every install step now pins wasmtime 50.0.1 by version and SHA-256 (the
x86_64 Linux release archive, 685466455dd4...98a5, the digest the release
lists), and every run passes

    -W nan-canonicalization=y -W relaxed-simd-deterministic=y

wasmtime's two determinism settings: every NaN a float instruction makes is
the canonical one, and each relaxed SIMD instruction has one behaviour on
every host. The flag names are 50.0.1's own, read from that release's
`wasmtime run -W help` on the build host. The two 38.0.3 steps gain the
architecture check and the digest the others already had.

Both settings took effect on the build host (arm64, the 50.0.1 release for
that platform). f32.add of a NaN with payload 0x200001 and 1.0 gives
0x7fe00001 without the flags and the canonical 0x7fc00000 with them. The
fixed relaxed behaviour is the engine's, not always the one WebAssembly's
deterministic profile prescribes: f32x4.relaxed_madd(1+2^-12, 1+2^-12,
-(1+2^-11)) gives the fused 2^-24 with and without the flag, where the
profile's choice (parameter 0) is the unfused fadd(fmul), which gives 0.
The workflows' comment says so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
determinism-tiers §5 says a WebAssembly decoder at decode-equivalence MUST
NOT use relaxed SIMD, and that the browser crate was validated without it by
hand, "which CI does not check". CI now checks it. wasm-tools 1.250.0, pinned
by version and SHA-256, validates the node bundle and the platform_decode
bundle the job builds, and the package of record the site serves
(reach/record/wai_web_bg.wasm), with --features=-relaxed-simd,-threads. A
relaxed SIMD instruction, a shared memory or an atomic in any of them fails
the job.

Before the packages, the step shows that it can fail. A planted module with
i8x16.relaxed_swizzle and one with a shared memory each validate with the
default features, and each is refused with them off for its own feature's
reason (the error text is matched).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's run 17676 at 2bef11f71 failed on both arm64 legs with "No space left
on device": linux-aarch64 in the checkout's `git fetch`, and macOS in the
toolchain's temporary files. Both runners live on one host, whose disk was
full. No fixture was decoded, yet the cross-architecture matrix read as red.

Each leg now begins, before the checkout, with a step named
"infrastructure: this runner's disk has room for the leg". It measures the
free space under the workspace, the runner's temporary directory, TMPDIR and
HOME. Below a floor of 4 GiB it fails with an error that begins
"infrastructure:" and says that no fixture was decoded. The leg itself writes
under 1 GiB (measured on the build host: about 270 MiB of working tree and a
67 MiB release target), and a cold toolchain install about 1 GiB more.

A disk can also fill while the leg runs, from the jobs that share it. A
second step runs only when a step after the check failed. It measures again,
and names the failure infrastructure when the disk is below the floor, or
says that the failure is not the disk.

Both steps are bash and skip on Windows, as the matrix comment says.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The install steps' comment said every WAI workflow that runs wasmtime pins one
version and passes both determinism settings, but nothing enforced it: a step
that ran a stock or differently pinned wasmtime would have merged green.
scripts/check-wasmtime-pin.py now fails, in the repository checks, any workflow
that

1. downloads a wasmtime release other than 50.0.1, or checks it against any
   SHA-256 but that archive's;
2. installs wasmtime another way (cargo install, the install script, an action);
3. runs wasmtime (run, serve, compile, or a module given to it) or names it as
   a CARGO_TARGET_*_RUNNER without -W nan-canonicalization=y and
   -W relaxed-simd-deterministic=y.

It also fails when it finds no download or no run, so a check that looked in
the wrong place cannot pass. On this tree it finds the downloads and runs it
should. Its self-test (scripts/check-wasmtime-pin-selftest.sh) passes a scratch
workflow shaped like these and fails each way of breaking it that the
self-test lists, matching the reason in the output.

The comment's relaxed_madd claim was measured on arm64 only. It now rests on
both architectures, measured on an arm64 macOS host and an x86_64 Linux host
with a small probe (wasmtime 50.0.1's release for the host, checked against
the digest the release lists):

    host Darwin arm64; wasmtime 50.0.1 (c307ec81d 2026-10-29)
    f32.add(bits 0x7fa00001, 1.0)                     [no flags]: 0x7fe00001
    f32x4.relaxed_madd(1+2^-12, 1+2^-12, -(1+2^-11)) [no flags]: 0x33800000
    f32.add(bits 0x7fa00001, 1.0)                     [both flags]: 0x7fc00000
    f32x4.relaxed_madd(1+2^-12, 1+2^-12, -(1+2^-11)) [both flags]: 0x33800000
    an unknown -W option is refused: error: failed to parse -W / --wasm option:
      unknown option: no-such-option

    host Linux x86_64; wasmtime 50.0.1 (c307ec81d 2026-10-29)
    (the same five lines, bit for bit)

0x33800000 is 2^-24, the fused result; the unfused fadd(fmul) gives 0. So the
NaN setting takes effect on both, and relaxed_madd is fused on both with and
without the relaxed setting. That matches 50.0.1's source (the
wasmtime-internal-cranelift crate, read from the registry): the translator
lowers F32x4/F64x2RelaxedMadd through fma_f32x4/fma_f64x2
(src/translate/code_translator.rs L2530-2539), which emit a native fma on a
host that has one, a fused library call under relaxed-simd-deterministic on a
host that has none, and the unfused fmul and fadd only on a host with no FMA
and the setting off (src/func_environ.rs L5907-5925). The setting is what
makes the result fused on every host. And since an unknown -W option stops
wasmtime, a run that exits 0 with the two flags has parsed them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
§5 said the browser crate's SIMD validation was done by hand, "which CI does
not check". Since "wai-web-wasm: the packages validate with relaxed SIMD and
threads off", wai-web-wasm.yml validates both bundles and
reach/record/wai_web_bg.wasm with relaxed SIMD and threads disabled. §5 now
says so, and says what CI still does not check: that they hold no other SIMD
instruction. The changelog gains the CI entry for this branch.

The commands that reproduce CI's wasmtime legs ran a stock wasmtime: the WAI
Quantum READMEs (English and Chinese), the bit_identity test's header, the WAI
README's edge example, and the conformance READMEs (quantum, qasm3, formats,
answers, intents). Each now passes -W nan-canonicalization=y
-W relaxed-simd-deterministic=y, as CI does. The determinism-cost run scripts
under wai/research keep their commands: they record runs already made.

The quantum docs check could not see that drift. For a `norun=ci:` block it
stripped every leading VAR=value before matching the command against the
workflow, and its pattern, \S*, did not read a quoted value. It now reads a
quoted or bare value, and when the workflow sets VAR (as an env: key or in a
script) the documented value must be one the workflow gives it, so
CARGO_TARGET_WASM32_WASIP2_RUNNER=wasmtime is refused against a workflow that
sets it to the flagged runner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The disk pre-check read df under the leg's four directories (the workspace,
the runner's temporary directory, TMPDIR and HOME). Three gaps remained.

1. On linux-aarch64 df reads the runner VM's disk, not the host's. Run in a
   job container started as the runner starts one (its image and its two
   cache mounts), the step's loop put all four directories on the VM's data
   disk (84 GiB free), while the host volume that holds the disk image had
   50 GiB. The image is a 200 GiB file that was already allocated on the host
   but for 0.7 GiB, so a full host volume could take back at most that much.
   The step now also writes and syncs 32 MiB once on each filesystem it
   measures (dd conv=fsync, which the macOS host's dd and the job image's
   both take), and names a write refused for space, an I/O error, a read-only
   filesystem or a quota infrastructure. That also catches a disk out of
   inodes, which df's block count does not show. Another failed write is a
   warning, not a verdict.
2. The second determinism matrix, wai-conformance.yml's byte-exact job, runs
   on the same three labels with no disk check. It now has both steps, with
   the same floor.
3. The floor had no reading behind it. The step's loop, run on 2026-11-04 in
   each label's job environment: linux-x86_64 201 GiB (one host) and 304 GiB
   (the other); linux-aarch64 84 GiB; macos 78 GiB under the workspace and
   the runner's temporary directory, 85 GiB under TMPDIR and HOME. The floor
   stays at 4 GiB: it fails a leg only on a host within a few GiB of full,
   and the macOS runner's internal disk, where HOME and TMPDIR sit, has gone
   from 7 GiB free to 118 MiB within hours, with its jobs failing under HOME.
   The env comment says so.

The class of the failure is still not in the status context: the forge
reports `wai cross-architecture byte-equality / <label>` as failed for a full
disk as for a divergence. The comment now says so, and why the check is not a
job of its own per label: the `linux` label has two hosts, so such a job
could measure another host's disk than the leg's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
§1 now records five published routes to a reproducible stage, each from the
paper section read. The explicit-parameter route keeps its entropy stage and
lets the reconstruction drift: its body says so, and its periodic I-frames
re-anchor the picture to one float decode (§3.2, Appendix B.8). Two integer
routes join the quantised decoder: a float model run in a 16-bit integer mode,
whose accumulators were observed not to overflow (§4.5), and an integer-trained
codec (abstract only). One more line reaches no tier: float synchronised on a
tested hardware class, whose streams decode only within the GPU groups its
tests found (Table IV, §III-B). §7.1 cites the two records of float decode
across devices beside WAI's record, and is not a comparison.

syfai-input §5 and the September sweep's row of arXiv 2608.00483, G7 and F5
are corrected against the bodies read on 2026-11-03.

research/catalogue-prior.md records a linear basis learned on openly
licensed films of one studio, measured against AV1: its readings, procedure,
decision rule and the time the rule was fixed. The rule refutes a linear or
spectral catalogue basis and exact recurrence across titles for a studio's
catalogue; a learned prior is not measured. tools/wai_catalogue_prior_verify.py
recomputes every BD-rate and verdict from the readings and checks every figure
the page quotes, in the conformance workflow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A reconstruction kept within a bound of the exact one between points at which
it is made exact again is measured on named devices and a named corpus, and a
sink cannot check it from what it received. So §3 says, normatively, that no
tier, class or claim states such a bound, and three rules follow: a decode of a
NeuralIntegerExact capability in reduced precision or float is not a decode of
it; a capability whose payload holds several frame types takes the class of its
weakest; and a clip digest, a stage hash or a binding over a sequence covers
only frames each decoded at decode-equivalence. SPEC §7 points to it.

A reconstruct-session decoder entry's `variant` now names the path that ran:
`exact`, `float`, or null for a capability with one path. Rule 13 refuses a
NeuralIntegerExact capability named with another path
(session_variant_not_exact); rule 14 refuses a float path under a pin of
decode-equivalence (session_tier_exceeds_variant); and against the stage
claims, a stage hash over a base an entry decoded at a weaker step, or over a
base or layer decoded by a path that is not exact, is refused
(session_stage_hash_not_exact).

claim-conformance gains a vector for each refusal and accepted ones for both
of wai.image.jpegai's paths, appended after the earlier vectors, which a test
pins by digest. claim_verify.py reads the same rules, and differential.py
generates sessions around them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai.image.jpegai decodes through its exact decoder or through a float decode,
and both returned the same result, so neither a receipt nor a binding check
could tell which had run. decode_envelope, decode_envelope_strict,
decode_multi, decode_multi_strict and decode_payload now return a Dispatched:
the output, the capability the payload decoded through (its own or the
fallback) and the path, DecodePath::Sole, Exact or Float. DecodePath::variant
gives the term a session claim's decoder entry states.

check_reconstruction_binding reads the path: over a decode-equivalence
capability or the exact decoder it reports whether the hash holds, and over
the float decode it reports the binding as not checked, never as failed, as
reconstruction-binding §2 requires. The notes in determinism-tiers §5 and
reconstruction-binding §2 that the dispatch did not report the path are gone.

tests/jpegai_decode_path.rs runs the float path and requires the result, the
session claim and the binding check to say so, and the exact decoder likewise.
Its JPEG-AI cases need inputs outside the repository and skip without them;
the case with one path runs in the neural gates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rule 14 refused only `float` under a pin of decode-equivalence, while rule 13
and the stage-hash check read any variant but null or exact as not exact. Rules
6 and 13 already refuse such a pin for every class the registry gives, so rule
14 reaches only capabilities with no class, and there `fp16` passed. The three
now share one predicate, DecoderEntry::exact_path: null, `exact`, or, for a
NeuralIntegerExact capability, the lowering its registration names. The last is
how opus-dred §7.2's present entry names wai.audio.opus.dred.int's integer
path, `dred-int/1`, which rule 13 would otherwise have refused; opus-dred says
a session claim of that decode may name it so.

The stage-hash refusal is per object: an entry naming the base at a weaker step
counts whichever units it presented, so the outcome does not depend on whether
the verifier holds the outputs. determinism-tiers §3 rule 3 now says so. The
vector session_stage_hash_not_exact moves its fallback decode inside the set,
at unit 2, so it is refused under either reading, and the earlier form stays as
session_stage_hash_not_exact_outside_the_set, the per-object case.

determinism-tiers §3 calls the closed vocabulary a pin's, says rule 3 governs
decoded media while generation-equivalence carries its own digest, and says
rule 2 is held over the registry class. The README says rule 6 enforces it, with
an inter-frame capability's vector, and rule 14 is its per-path complement.

Five vectors are appended after the earlier ones, which a test pins by
digest: a registered lowering accepted, another capability's lowering
refused, `fp16` under decode-equivalence refused, an inter-frame capability
over its class refused, and the per-object case.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
check_reconstruction_binding compared only the hash, so a binding of another
decoderId, format or size than the exact decoder computes was reported as
Differs. reconstruction-binding §2 forbids reporting a binding as failed when
the verifier decoded some other way than its descriptor says. Such a binding
is now BindingCheck::OtherReconstruction, with the path that ran; for the
exact decoder that is any decoderId but 2.

wai_neural_decode_ex dropped the path and the capability, so a native sink
could not state its decoder entry's variant. wai_decoded_t gains `path`
(WAI_DECODE_PATH_SOLE, _EXACT or _FLOAT) and `fallback`, 1 when a WAI1
envelope decoded through its fallback. include/wai.h is regenerated.

The neural gates counted the two JPEG-AI cases' skips as passes, so CI ran
only the one-path case. That step now selects its two committed-input cases by
name, one of them a binding over int_mlicpp checked under its descriptor. A new
job in the 24.04 image fetches the pinned jpegai/v1 bundle and runs the exact
decoder's case with WAI_REQUIRE_SOTA_VECTORS=1, requiring it by name and
refusing a skip. The float decode needs the reference-software assets, which
no job holds, so its case runs locally only; the test header and
determinism-tiers §5 say so. The C ABI step runs the path and fallback test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reason gains three variants and has no #[non_exhaustive], so the API check
counts them as a break, and the entry named none. It now acknowledges them,
the five neural dispatch functions' new return type (the neural feature is
outside the checked API, so the check does not report it), and wai_decoded_t's
two new fields. The entry also states the exact path, the per-object stage-hash
check, where each decode-path case runs, and that §1 records the five routes
while §7.1 cites the two float-decode records.

syfai-input's G11 and the September sweep's G9 said the result does not record
which path ran. Each gains a dated status note pointing to determinism-tiers §5
and neural::Dispatched::path.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
In the sessions CI's run of the differential generates (seed 1) neither
session_tier_exceeds_variant nor session_stage_hash_not_exact was the verdict
once. An entry naming a stage set's base almost never had the unit the session
names the set at, so session_first_unit_mismatch came first, and a path that
is not exact under a pin of decode-equivalence rarely met a capability with no
class. Entries naming the base now take that unit, as an honest sink states
it, four times in five, and the path defect moves its entry to another
capability more often, an unclassified one half the time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The previous change did not reach either rule: a planted regression of rule 14
to `float` alone still left the differential with nothing to report, since the
random sessions are refused by an earlier rule first. session_decode_paths now
sweeps one picture through one entry over five capabilities (classified at each
tier, or not at all), seven terms for the path, three pins and both kinds of
decoder, where nothing else decides, so rules 6, 13 and 14 each give the
verdict. session_stage_bounds, whose sessions pass every earlier rule, states a
stage hash a third of the time, and then half the time unpins the base's entry
or names another path on an entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
An encoder SHOULD pin the parameter set a payload was coded against wherever a
capability that decodes it is registered as decoding against one, whatever the
intent (SPEC §3.1, Pin by default).
An envelope that pins nothing there still conforms in 1.x; a verifier reports it
(prior_unpinned), informatively: Wai::check_unpinned, WaiMulti::check_unpinned and
codecs::pin_by_default. wai verify now reads the envelopes under each path, refusing
one whose pin breaks SPEC §3.1 and noting prior_unpinned; wai inspect and wai wrap note
it too. pin-conformance/unpinned.json holds the vectors, reached by wai_pin_vectors and
by pin_verify.py, which reads the classes from the published registry export.

A reconstruct-session decoder entry's pin (jwp-receipts §2.4.12) now names the set an
unpinned decode read, by the digest a pin over those bytes names, with the tier none.
The reference sink's base decode gives it (staged::BaseDecode::set, from
IntParams::read_pin), and stage_measure's sessions carry it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The browser recorder (wai-web's SessionHandle.decoderFor) still wrote pin and tier null
for every decode that verified no pin, so for one unpinned decode the page and the native
tool sealed different claims, against SPEC §3.1 rule 4 and jwp-receipts §2.4.12. Every
committed fixture pinned its base, which hid it. decoderFor now takes the set the decode
read, a trailing argument, apart from whether it verified a pin; StageBase gives both
(paramSet, pinned) from staged::decode_base_pinned. A decode that verified no pin is named
by the set it read, with the tier none, as stage_measure's recorder names it; a pinned
decode given a set other than its pin's is refused. measure_verify.py names the set as the
text does: the SHA-256 of a one-file capability's file, otherwise of the wai.prior.set
listing of the files read.

staged-measure-conformance/fixtures/unpinned is a title over the staged corpus's unpinned
wai.neural.int_hyper base (image_int_hyper_unpinned), decoded against the same files the
still's pin names, so its decoder entry names that digest; goldens.json regenerated
with wai_stage_measure. The parity test and verify_measure_wasm.mjs pass the verified-pin
flag apart from the triple's pin, so both recorders and both verifiers are compared on it,
and verify_staged_wasm.mjs holds StageBase.pinned and paramSet to every staged case.

jwp-receipts §2.4.12 says that an entry from a sink of the earlier text may carry null for
an unpinned decode that read a set, which a verifier cannot tell from one that read none.
SPEC §3.1 rule 2 says that an unpinned envelope conforms unless §7 requires the pin
(replicate_unpinned). DecoderEntry::pin says what it now names.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
conformance-results §4.1: a relying party that admits an implementation on a
conform-result/1 record requires the record accepted, its run passed (not_conformant), its
suite one whose corpus the party holds (suite_unknown), and its corpus digest that corpus's
(corpus_mismatch). wai verify --conformant [--root <wai-dir>] applies the rules against a
checkout; without it a record of a run that did not pass verifies as before. A sink kind:
wai conform sink --suite native-sink gives a sink each session of
live-conformance/native-sink/ with its answers withheld, and each configuration the corpus
states a session refuses; wai_native_sink_vectors answers [--c-abi] is the reference sink,
through its module or through the C interface the platform integrations link.

wai-breaking.yml's runner job runs the native sink both ways and the score decoder, each
record admitted against the checkout's corpus, and a planted sink that presents an object
whose digest does not match: its run must exit 1 with tampered-fragment failed, and
admission must refuse its record at rule 2. tests/cli_conform_sinks.rs holds the same, and
corpus_mismatch and suite_unknown; it has its [[test]] entry with its required features,
so check-tests-in-ci.py passes. The binaries are built with the test step's features, so
cargo reuses what that step built. The browser sink has no sink suite, and §7 says that
§4.1 has only this reference implementation, with no admission vectors.

The catalogue's receipt.verify (the new options, and the envelopes the pin-by-default commit
added) and conformance.run (the sink kind, the {case} placeholder) are exported again, and
Studio's actions regenerated from the export: actions.mjs --check had failed since that
commit. c_abi::take reads and frees no null buffer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
every_corpus_case_seals_an_honest_session replays each staged case as a sink
and records its session, but its decoder entries named no pin and no tier
for any base. Since determinism-tiers §3 rule 3, a stage hash over a
NeuralIntegerExact base is refused unless every entry naming the base is at
decode-equivalence, so the honest sessions of the pinned int_hyper,
int_codec and splat cases were refused with session_stage_hash_not_exact.

A base's entry now names what the sink's decode states (jwp-receipts
§2.4.12): the pin it verified, with the tier that pin declares; for an
unpinned decode that read a set, the digest a pin over the bytes read
names, with the tier none (SPEC §3.1, Pin by default, as
IntParams::read_pin names it); nothing for a decode that read no set.
Layer entries name nothing, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai breaking reports staged-measure-conformance/goldens.json as a wire break:
the unpinned fixture's evaluation was added, and the corpus-wide dedup
record, taken over every fixture's objects, counts its objects too, so its
totals moved. The entry named no break for it. It now acknowledges it, and
says that no other fixture's evaluation changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two passages followed a published route's limitation with a sentence about
WAI's integer networks: in §1, an integerised video codec's accumulators,
whose overflow was observed not to happen rather than bounded; in §7.1, a
requantisation that was not bit-exact across neural processors. Each
passage now describes the route alone. integer-operators §8 and §5.1
specify WAI's bounds and rounding.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
claim-conformance: an unpinned decode's entry names the set it read, and one vector keeps the earlier text's null
Some checks failed
wai-quantum views in agent hosts / views · a scripted host (pull_request) Successful in 7m30s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 9m42s
wai-quantum signal processor / processor and member · signal processor, emulated (pull_request) Successful in 15m43s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Failing after 5m42s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 14m44s
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Successful in 9m39s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 12m11s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 10m18s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 31m16s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 25m3s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 30m52s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 1m59s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m12s
wai-ops integer operators / linux-aarch64 (pull_request) Successful in 1m2s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 31m3s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m35s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 17s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 14s
wai-quantum HIP member / member · HIP absent (linux-arm64) (pull_request) Successful in 50s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 4m44s
wai-quantum npu-cores member / member · npu-cores absent (linux-arm64) (pull_request) Successful in 23s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 9m36s
wai-quantum tensor-engine member / member · tensor engine absent (linux-arm64) (pull_request) Successful in 26s
wai-quantum pulse and dynamics / the same bits · linux-arm64 (pull_request) Successful in 1m17s
wai-quantum extension points / the registry and the example · linux-arm64 (pull_request) Successful in 2m12s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (pull_request) Successful in 2m30s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m15s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 6m30s
wai-vector vector graphics / linux-aarch64 (pull_request) Successful in 2m21s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
081b71e9b4
Two decode-path vectors stated an unpinned NeuralIntegerExact decode with
pin and tier null, the form of the earlier text. Under SPEC §3.1, Pin by
default, rule 4 and jwp-receipts §2.4.12, a sink names the set it read, by
the digest a pin over it names, with the tier none, and null means a decode
that read no set or an entry from a sink of the earlier text.
session_variant_not_exact_unpinned and session_stage_hash_over_an_unpinned_base
now write the entry so, and each is still refused for its own reason: rule
13, and a stage hash over a base whose tier is not decode-equivalence. A
vector appended after them, session_stage_hash_over_an_unpinned_base_earlier_text,
keeps the null form, which a verifier cannot tell from a decode that read
no set, and is refused all the same. The vectors before the decode-path
rules are unchanged, as the test that pins them by digest requires.

differential.py reaches the tier none too: in its decode-path sweep, in the
mutation that puts a path under a pin, and where its stage-bounds sweep
unpins the base's entry, which now writes either form.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai-web-wasm: the native unit test count includes the decoder entry's set test
All checks were successful
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 10m48s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 6m49s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 13m1s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 11m54s
wai-vector vector graphics / independent checker (Python) (pull_request) Successful in 12m11s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 33m18s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 38m48s
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Successful in 5m24s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 6m18s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 5m51s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 2m8s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m32s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 39m58s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 6m0s
wai-ops integer operators / linux-aarch64 (pull_request) Successful in 2m3s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 2m34s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 40m14s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 52s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 37s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 12m10s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h8m39s
wai-quantum HIP member / member · HIP absent (linux-arm64) (pull_request) Successful in 1m11s
wai-quantum npu-cores member / member · npu-cores absent (linux-arm64) (pull_request) Successful in 54s
wai-quantum tensor-engine member / member · tensor engine absent (linux-arm64) (pull_request) Successful in 37s
wai-quantum pulse and dynamics / the same bits · linux-arm64 (pull_request) Successful in 2m2s
wai-quantum extension points / the registry and the example · linux-arm64 (pull_request) Successful in 3m2s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (pull_request) Successful in 3m0s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m50s
wai-vector vector graphics / linux-aarch64 (pull_request) Successful in 2m47s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 10m6s
2a48fffdb7
Pin by default added measure::tests::a_decoder_entry_names_the_set_its_decode_read,
so the native unit tests step's count check must expect it; the step otherwise
fails after every test has passed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under load the relay can take a group whole only after the age window has
passed its number, because its streams finish out of order. Such a group is
late: it is never stored, and it is counted in groups_late. The by-age test
expected six evictions whatever the order, and failed 3 against 6 when three
groups came late. It now checks that every group is evicted or late, and that
each group stored is counted as it goes. A second run makes the out-of-order
case happen on purpose (group 0 and group 2's receipts, then, once the window
has passed group 2, group 1 and group 2's objects) and checks three stored,
three late.

The store also counted an eviction after releasing the index lock, so a
reader that found the group gone could read the count one short (5 against 6
with a delay injected there). The count now moves under the same lock as the
removal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
watch-together: the page makes its audio context before its join; a slot is set apart only when the machine ran nothing of the page
All checks were successful
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 21m22s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 7m27s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 10m15s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 11m59s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 6m48s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 6m44s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 9m5s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 33m23s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 7m11s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 18m7s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 2m19s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m20s
wai-ops integer operators / linux-aarch64 (pull_request) Successful in 1m2s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m45s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 32m36s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 32s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 16s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 7m5s
wai-quantum npu-cores member / member · npu-cores absent (linux-arm64) (pull_request) Successful in 27s
wai-quantum HIP member / member · HIP absent (linux-arm64) (pull_request) Successful in 58s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 39m55s
wai-quantum pulse and dynamics / the same bits · linux-arm64 (pull_request) Successful in 1m24s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 40m34s
wai-quantum tensor-engine member / member · tensor engine absent (linux-arm64) (pull_request) Successful in 20s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (pull_request) Successful in 2m40s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m39s
wai-quantum extension points / the registry and the example · linux-arm64 (pull_request) Successful in 4m1s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 11m3s
wai-vector vector graphics / linux-aarch64 (pull_request) Successful in 4m17s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h10m6s
4e8e4fb098
The watch-together example failed twice on the macOS runner for #616, on a loaded host (load
average about 50, then about 30). Each time the first page in the browser, Ada's, signed its
first presentation statement after its join, at 3.83 s and then 3.44 s on the room clock, and
then none in the slots from 4 s and 5 s (and 6 s the second time), while it was still entering
(begin()): it followed the log from 7.49 s, then 8.49 s, 3.7 s and 5.0 s after its first
statement, where Ben's page took 1.2 s and 1.0 s. Ada's page was silent at least 2.17 s, then
at least 3.56 s, with nothing refused, and was never taken for gone. What held it on the runner
was not measured.

What was measured:
- On the CI host, with CI jobs running (load average 30 to 39), in a browser launched as the
  example's test launches it (headless, the same flags, at nice 10), on a blank page with
  nothing else in it: the first audio context in a fresh browser held its page's thread 172 to
  192 ms (3 runs), for the constructor call alone; a second page's took 3.8 to 4.0 ms; neither
  page's thread had a gap over 60 ms in the 1.5 s after. That is a tenth or less of Ada's
  silences. Moving the context before the join takes about 0.2 s, as measured there, out of the
  stretch in which the page must state; what held Ada's page for the rest is unexplained. The
  rest of begin() (its audio worklet, its relay session, its WebAssembly, its first draws, the
  camera and microphone) was not measured there, nor an audio context made beside that work.
- On a laptop, in 25 runs of the example: the first page's audio context 94 to 128 ms, the
  second's 0.8 to 1.4 ms.
- On a laptop, on main, two injected faults each leave empty the slots just after the first
  page's first statement, as in CI: the audio-context call held 2.5 s (a busy wait in it; the
  slots from 1 s and 2 s, 1 run), and the first page's renderer process stopped 2.5 s from its
  join (SIGSTOP; the slot from 2 s, 2 of 2 runs). Neither was measured on the CI host.

This change is not shown to make the example pass on the CI host. If what held Ada's page there
was the page's own work on its thread, or the host running the page's other threads and not
that one, the check below still fails (its heartbeat ticking), and what it prints does not tell
those two apart.

1. The page makes the composition's audio context before it enters the co-watch log (app.js
   enter(), the new Compositor.openAudio()), since from that join it states in every slot
   (co-watch §7.3), so that hold is not in the stretch in which it must state. The rest of the
   entry (begin()) still runs after the join. begin() takes the compositor the entry made; in
   any other room it makes one, as before. The snapshot carries when the context was made, on
   the room clock, and how long making it held the thread (cowatch.audio_open).

2. A compositor made for an entry that failed is closed when the next entry makes its own
   (Compositor.close(): its mix timer, its worklet node, its drawing, its frames and its audio
   context). Before, it stayed, its audio context open and its mix timer running.

3. The statement loop records its stalls (cowatch.js run(), snapshot().stalls): each gap over
   half a slot between two of its passes, `{ from, to }` on the page's estimate of the room
   clock, with how long its wait and its signing took in it, on that clock (`timer_us`,
   `signing_us`); the rest of a stall is the loop's own work. It states every half slot while
   it runs, so it leaves a slot empty only inside such a stall. Its wait is a method (`wait`),
   and roomTime(ms) gives the room clock at an instant of the page's clock. The snapshot
   carries the loop's wait (`pass_ms`) and the page's clock beside the room clock (`wall_ms`).
   The co-watch rule and §7.3 are unchanged.

4. The example's slot check (co-watch §7.3: a statement in every slot while present) sets an
   empty slot apart only if the machine ran nothing of the page in the slot's second half: a
   stall of the loop spans that half; a heartbeat worker that the test starts in each page,
   ticking every 20 ms, did not tick in it either (to within a pass at each end); and the stall
   is the loop's wait and its signing but for at most one pass of its own work. Otherwise the
   check fails and says why: no stall of the loop (a statement lost); the heartbeat ticking (the
   page's thread did not run its loop while the worker ran: the page's own work holding the
   thread, or the host running the worker and not the page's thread, which the check does not
   tell apart); or more than a pass of the loop's own work (its own code, an await outside its
   measures, or the thread taken off the processor inside it). On a host that runs the page's
   threads unevenly, the check can fail in these ways with no defect in the page. Pass or fail,
   it prints each page's audio-context time, its loop's stalls and its heartbeat's silences over
   100 ms. They show whether the page's thread or the whole page was held, not what held it.

Unit tests (cowatch.test.mjs) run the loop on a room clock the test moves: no stall and every
slot held while it runs; a thread held while the loop waited, recorded once with all of the
stall its wait, and the only empty slots those it spans; a gap just over half a slot recorded
and one of half a slot not; a stall in a pass's own work recorded with a wait of one pass, the
rest neither wait nor signing.

Verified on a laptop with this change on main (152aab5c7), in a browser launched as the
example's test launches it and niced:
- From an archive build.sh made, with the WebAssembly build-web.sh made from crates this change
  does not touch, run-examples.sh browser passes video-room, watch-together, messaging,
  remote-desktop and package-and-deliver in each of 2 runs, and watch-together alone 2 more.
- With this room client served beside an earlier archive's binaries (the same Rust but for the
  relay's DVR change in the commit before), watch-together passes 3 of 3, and the faults below
  ran on that set. In each passing run the first page's audio context was made before its join
  and Ada's loop had no stall. Ben's deliberate hold, a synchronous request, shows as a stall of
  3.8 to 4.2 s with his heartbeat never silent.
- The room client's unit tests pass under node --test (77).
- Falsified, each fault put into a scratch copy of the room client, 2 runs each unless said:
  the audio context made in begin() again and that call held 2.5 s (a busy wait), the check
  fails: no statement in the slots from 1 s and 2 s (Ada) and 4 s and 5 s (Ben), each in a
  stall of the loop with its heartbeat ticking, the context printed as made after the join in
  2.5 to 2.6 s. A 2.5 s busy wait just after the join, or 4 s after it, fails it the same way:
  the slots it spans, with the heartbeat ticking. Every statement of the third whole slot after
  each join dropped fails it (no stall of its loop). One 1.5 s await in the loop's own work
  fails it (heartbeat ticking, 1.5 s neither its wait nor its signing); the same await inside
  the loop's wait fails it on the heartbeat alone. The page's renderer process stopped 2.5 s
  (SIGSTOP) at Ada's join, or 2.5 s after it, or Ben's 1 s after his (1 run): it passes, the
  slots inside the stop set apart; the heartbeat was silent and the loop stalled over the stop,
  to within 25 ms. The stop at Ada's join and a 2.5 s busy wait 4 s after it (1 run): the
  stop's slot is set apart and the busy wait's fail.
- In the unit tests, the wait measured on another clock than the stall, no stall recorded, a
  stall recorded only past a whole slot, and the wait left out of the record each fail one or
  two of the stall tests.
- A video room: one page publishes; the other presses Watch, its first relay connection is
  refused (a fault the probe injects), and it presses Watch again. Then that page holds one
  audio context open (the first closed) and one mix timer, and mixes 50 blocks of 20 ms a
  second. On main it holds two open and two timers; with close() doing nothing, two open. The
  same in a watch room: one open with this change, two on main.
- Not shown: that the example passes on the CI host.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!616
No description provided.