Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Document host exhaustion test seam and make telemetry fixture deterministic

**Status:** Sol brief approved by root; matching GitHub issue misofm/engine#409. This is a candid merge-blocker correction for PR #398 / qualification run `33935625430`; it is not issue #393 attempt 4. Issue #393 already exhausted its three attempts and received Astra PASS at `bed7634c`. Its host product result remains closed.

## Smallest closable slice

Make the two required CI checks deterministic and truthful without changing browser-host behavior:

1. Document the existing private safe-integer test selector `MISO_ENGINE_WEB_HOST_MAX_SAFE_TEST` in the enforced environment vocabulary. It was introduced in reviewed #393 commit `beeb8557a`, is set only by `scripts/test-web-audioworklet.sh`, and selects the transformed-host exhaustion branch in `scripts/test-web-audioworklet.mjs`. No variable is renamed and no second selector is added.
2. Replace the telemetry fixture's use of the Node process's real `performance.now()` with a test-local deterministic clock installed only while the telemetry processors are constructed and exercised. Keep exact assertions for zero misses and add a positive case with exactly one injected over-budget block.

The second failure predates #393: `scripts/test-web-audioworklet.mjs:1502` and its `deadlineMisses === 0` assertion are from #137 baseline `1f8d3f0df`. Node 22.23.2 observed one scheduler-sensitive miss. Record that as a real-clock test-fixture defect, not a Node product regression established by evidence.

## Exact correction

### Vocabulary

Add one row beside the existing host test-module selector in `docs/ENGINE_ENV_VOCABULARY.md`:

`MISO_ENGINE_WEB_HOST_MAX_SAFE_TEST` — hermetic host allocator test selector; when `1`, the existing test runs the transformed private counter at `MAX_SAFE_INTEGER - 1`, proves the final safe ID once, then proves repeatable local exhaustion with no post, wrap or reuse.

The existing bidirectional checker and its self-test already discriminate undocumented and unused rows. Do not exempt a path, weaken `check-env-vocabulary.sh`, rename the environment variable, add an alias, or edit the #393 implementation/spec merely to hide the failure.

### Deterministic telemetry clock

In `scripts/test-web-audioworklet.mjs`, keep the existing worklet source and `makeProcessor()` path. Add a small local clock fixture that returns monotonic start/end samples for each render block. Temporarily replace `globalThis.performance` (preserving/restoring its original value in `finally`) before constructing the processor, so the existing `renderClock()` probe and `telemetryMessage.resolutionMs` use the same injected clock naturally. Do not assign `processor.clock` after construction or patch the imported worklet.

Run two exact 128-block windows through the normal `process()` path:

- a no-miss window whose per-block elapsed value is a fixed positive duration safely below the 64-frame/48-kHz budget; require one telemetry frame and `deadlineMisses === 0`;
- a fresh processor/window with that same duration except for exactly one elapsed value above budget; require one telemetry frame and `deadlineMisses === 1`.

Retain the existing assertions for block/window count, sequence, budget range, positive reported resolution, `belowResolution`, finite/nonnegative CPU fields, lease release, and no clock reads/messages after release. The injected clock should count reads so the fixture also proves two reads per leased rendered block and no reads after release. Restore the real global clock even if an assertion fails.

The exact one-miss case is the red discriminator: a correction that clamps, ignores or loosens deadline misses must fail. Do not replace `=== 0` with a range, retry the test, increase the budget, skip on Node 22, sleep, mock render output, or change telemetry/product arithmetic.

## Exact allowed paths

- `.github/ISSUE_SPECS/409-document-host-exhaustion-test-seam-and-make-telemetry-fixture-deterministic.md` — new stateless tooling successor and evidence
- `docs/ENGINE_ENV_VOCABULARY.md`
- `scripts/test-web-audioworklet.mjs`

No other tracked path is allowed. In particular, do not edit `hosts/host-web/**`, SDK source/declarations, `scripts/test-web-audioworklet.sh`, either environment-vocabulary checker/test, workflows, generated assets, Rust/Wasm/ABI files, issue #393, or issue #405. If the existing shell wrapper cannot pass after only these corrections, stop and amend this successor rather than broadening it during implementation.

## Gates and evidence

1. `bash scripts/check-env-vocabulary.sh` passes and reports the incremented documented-name count.
2. `bash scripts/test-env-vocabulary.sh` passes its existing undocumented-name, unused-row and deleted-row red cases unchanged.
3. On Node 22.23.2, `node scripts/test-web-audioworklet.mjs` passes once and records the deterministic `0`-miss and injected `1`-miss windows. One invocation is evidence; no retry loop or repeated-until-green run.
4. `bash scripts/test-web-audioworklet.sh` passes, including the existing transformed safe-integer host and unchecked-increment red mutation. The selector remains private to the harness.
5. Run the proportional qualification lint/test route that failed in run `33935625430`, then `git diff --check` and an exact-path audit. The diff must contain zero host/product/worklet/generated changes.

The issue evidence must say plainly that the real-clock zero-miss test was scheduler-sensitive and that the deterministic one-miss case preserves the behavioral assertion. It must not claim a Node 22 engine bug or new host qualification.

## Delivery and review

Root creates and synchronizes this separately numbered tooling issue before implementation. Luna implements one coherent correction checkpoint; a dedicated Astra review verifies the exact diff, runs the zero/one-miss discriminator and the unchanged safe-integer mutation, and confirms no product file changed. Because this successor is independent from closed #393, its attempt count starts at one. Merge PR #398 only after the required qualification context is green and the successor evidence is upstream; do not reopen or amend #393 as a disguised fourth attempt.

## Decision record

- 2026-09-05: Root approved this independent tooling correction in isolated /private/tmp/miso-dx-ci, branch codex/dx-ci-fixtures. It can run alongside SDK #405 without shared edits or broad workspace gates. Luna implements; dedicated Astra reviews. No product scope is added. Root checkpoints each coherent tranche before more edits.
- 2026-09-05 attempt 1 evidence: `docs/ENGINE_ENV_VOCABULARY.md` now documents the existing private `MISO_ENGINE_WEB_HOST_MAX_SAFE_TEST` selector, and `scripts/test-web-audioworklet.mjs` runs two fresh 128-block telemetry windows through `process()`: a fixed positive 0-miss window and a same-duration window with exactly one injected over-budget block. The local clock fixture proves 256 reads (two per leased block) and no reads or messages after release, while restoring the original `globalThis.performance` descriptor in `finally`.
- Focused gates pass on Node `v22.23.2`: `/private/tmp/node-v22.23.2-darwin-arm64/bin/node scripts/test-web-audioworklet.mjs`; `bash scripts/check-env-vocabulary.sh` reports 99 documented names; `PATH=/opt/homebrew/opt/gnu-sed/libexec/gnubin:$PATH bash scripts/test-env-vocabulary.sh` passes unchanged mutation coverage; and the full `PATH=/opt/homebrew/opt/gnu-sed/libexec/gnubin:$PATH bash scripts/test-web-audioworklet.sh` passes, including the safe-integer boundary and unchecked-increment red mutation.
- The prior real-clock zero-miss assertion was scheduler-sensitive; the deterministic one-miss case keeps the behavioral `deadlineMisses === 1` discriminator. This evidence does not claim a Node 22 engine bug or new host qualification. `git diff --check` passes and the exact diff contains only the issue spec, environment vocabulary, and test harness paths; no host/product/worklet/generated file changed.

## Dedicated Astra attempt 1 verdict — PASS (2026-09-05)

Astra independently reviewed `04bbf4e5` and verified exact three-path scope, focused Node22.23.2 suite, 99-name vocabulary and unchanged mutation suite, and full unchanged browser wrapper. Force-zero, count-every-block, and read-after-release mutations each fail. Original performance property descriptors restore on normal completion and callback exceptions. No product source changed. The full review is attached to PR #398. This tooling result fixes the demonstrated CI causes; combined PR qualification still requires its own green run and separate PCM review.
1 change: 1 addition & 0 deletions docs/ENGINE_ENV_VOCABULARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ Read by one subject each.
| `MISO_ENGINE_WEB_STRIP` | AudioWorklet build: the `wasm-strip` binary. |
| `MISO_ENGINE_WEB_WORKLET_TEST_MODULE` | Hermetic worklet test: override module path for the bootstrap-under-test (#132). |
| `MISO_ENGINE_WEB_HOST_TEST_MODULE` | Hermetic worklet test: override module path for the main-realm host under test, so a red mutation of the host runs the same suite (#151). |
| `MISO_ENGINE_WEB_HOST_MAX_SAFE_TEST` | Hermetic host allocator test selector; when `1`, the existing test runs the transformed private counter at `MAX_SAFE_INTEGER - 1`, proves the final safe ID once, then proves repeatable local exhaustion with no post, wrap or reuse. |
| `MISO_ENGINE_PRINT_HELPER_MANIFEST` | native PCM runner portability gate: helper manifest path. |
| `MISO_ENGINE_EFFECT_CONTRACT_V1_H` | the C include guard `check-effect-contract.sh` asserts. Not an environment variable. |
| `MISO_ENGINE_BENCH_POLICY_NEEDLE` | `check-bench-policy.sh`'s `sole_owner_or_delegate`: internal transport of the four-character backslash char-literal needle from bash to the `awk` subprocess through `ENVIRON`, chosen over `-v` because `-v` assignments go through awk's own C-style escape processing a second time. Set and read only inside that one function invocation; not user-facing. |
Expand Down
17 changes: 3 additions & 14 deletions hosts/host-web/qualification/qualification.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,8 @@ function corpusPlanes(description) {
return [left, right];
}

function corpusRequest(requestId, description) {
function corpusRequest(description) {
return {
requestId,
sourceId: "fixture-source",
generation: 1n,
startFrame: BigInt(description.startFrame),
Expand All @@ -158,7 +157,7 @@ async function renderCorpusSegment(createHost, sessionDocument, descriptions) {
if (host.backend !== "simd128") throw new Error("corpus worklet backend mismatch");
host.node.connect(context.destination);
for (const [index, description] of descriptions.entries()) {
const acknowledgement = await host.submitSource(corpusRequest(index + 1, description));
const acknowledgement = await host.submitSource(corpusRequest(description));
if (acknowledgement.result !== 0) throw new Error("corpus prefill rejected");
}
const rendered = await context.startRendering();
Expand Down Expand Up @@ -296,7 +295,6 @@ async function runConsoleQualification(createHost, sessionDocument) {
inputPeak = Math.max(inputPeak, Math.abs(planes[0][frame]), Math.abs(planes[1][frame]));
}
const acknowledgement = await host.submitSource({
requestId: block + 1,
sourceId: "console-source",
generation: 1n,
startFrame: BigInt(block * QUANTUM_FRAMES),
Expand All @@ -312,18 +310,15 @@ async function runConsoleQualification(createHost, sessionDocument) {
// taken after the prefill they will be observed over.
const map = await host.sessionMap();
const meterLease = await host.meters({
requestId: 10001,
enabled: true,
onFrame: (frame) => meterFrames.push(frame),
});
const telemetryLease = await host.telemetry({
requestId: 10002,
enabled: true,
onFrame: (frame) => telemetryFrames.push(frame),
});

const command = await host.command({
requestId: 20001,
commands: [{
kind: COMMAND_MATRIX,
rack: 255,
Expand Down Expand Up @@ -405,7 +400,6 @@ async function runObservationRun(createHost, sessionDocument, armed) {
const frames = [];
for (let block = 0; block < OBSERVATION_BLOCKS; block += 1) {
const acknowledgement = await host.submitSource({
requestId: block + 1,
sourceId: "console-source",
generation: 1n,
startFrame: BigInt(block * QUANTUM_FRAMES),
Expand All @@ -418,7 +412,6 @@ async function runObservationRun(createHost, sessionDocument, armed) {
}

const meterLease = await host.meters({
requestId: 30001,
enabled: true,
onFrame: (frame) => frames.push({
trackGrDb: Array.from(frame.trackGrDb),
Expand All @@ -436,11 +429,10 @@ async function runObservationRun(createHost, sessionDocument, armed) {
windowBlocks: Number(CONSOLE_METER_BLOCKS),
armed: true,
};
const subscribed = await host.observe({ requestId: 30002, subscriptions: [subscription] });
const subscribed = await host.observe({ subscriptions: [subscription] });
let unsubscribed = null;
if (!armed) {
unsubscribed = await host.observe({
requestId: 30003,
subscriptions: [{ ...subscription, armed: false }],
});
}
Expand Down Expand Up @@ -514,7 +506,6 @@ async function runStallQualification(createHost, sessionDocument) {
expected[0].set(planes[0], block * QUANTUM_FRAMES);
expected[1].set(planes[1], block * QUANTUM_FRAMES);
const acknowledgement = await host.submitSource({
requestId: block + 1,
sourceId: "stall-source",
generation: 1n,
startFrame: BigInt(block * QUANTUM_FRAMES),
Expand All @@ -527,7 +518,6 @@ async function runStallQualification(createHost, sessionDocument) {
}

const meterLease = await host.meters({
requestId: 30001,
enabled: true,
onFrame: (frame) => stallMeterFrames.push(frame),
});
Expand All @@ -536,7 +526,6 @@ async function runStallQualification(createHost, sessionDocument) {
// digest still applies, while the control path, its queue and the meter fold are all live
// across the stall.
const stallCommand = await host.command({
requestId: 30002,
commands: [{
kind: COMMAND_MATRIX,
rack: 255,
Expand Down
8 changes: 2 additions & 6 deletions hosts/host-web/web/miso-engine-v1-audio-worklet-host.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,6 @@ export interface MisoCommand {
}

export interface MisoCommandRequest {
requestId: number;
commands: MisoCommand[];
}

Expand Down Expand Up @@ -441,7 +440,6 @@ export interface MisoObservationSubscription {

/// One observation batch. Like a command batch, it is one transaction (issues 143, 151).
export interface MisoObservationRequest {
requestId: number;
/// At least one and at most `256` subscriptions, arming and disarming freely mixed.
subscriptions: MisoObservationSubscription[];
}
Expand Down Expand Up @@ -595,7 +593,6 @@ export interface MisoError {
}

export interface MisoSourceRequest {
requestId: number;
sourceId: string;
generation: bigint;
startFrame: bigint;
Expand All @@ -606,7 +603,6 @@ export interface MisoSourceRequest {
}

export interface MisoSeekRequest {
requestId: number;
sourceId: string;
generation: bigint;
sourceFrame: bigint;
Expand Down Expand Up @@ -636,11 +632,11 @@ export interface MisoAudioWorkletHost {
sessionMap(): Promise<MisoSessionMap>;
/// Take or release the decimated meter lease (issue 137 D2).
meters(
request: { requestId: number; enabled: boolean; onFrame: ((frame: MisoMeterFrame) => void) | null },
request: { enabled: boolean; onFrame: ((frame: MisoMeterFrame) => void) | null },
): Promise<MisoAck>;
/// Take or release the render-telemetry lease (issue 137 D3).
telemetry(
request: { requestId: number; enabled: boolean; onFrame: ((frame: MisoTelemetryFrame) => void) | null },
request: { enabled: boolean; onFrame: ((frame: MisoTelemetryFrame) => void) | null },
): Promise<MisoAck>;
dispose(): Promise<void>;
}
Expand Down
Loading
Loading