Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
82a50b8
Record #1620's measurement: the first live commit's excess is the ful…
tamashi095 Oct 15, 2026
0d89e1f
Merge codex/controls-batch-14a into codex/controls-batch-14bk
tamashi095 Oct 15, 2026
17a4dea
#1620: root ruling C1 (in-place admission of added parameter entries)
tamashi095 Oct 15, 2026
5fb2f7b
Build the effect control owner's candidate at begin (#1624)
tamashi095 Oct 15, 2026
c60b78b
Size the per-node live effect window by its live cells (#1621)
tamashi095 Oct 15, 2026
2831fe9
Bound the first live commit of a large session: in-place admission of…
tamashi095 Oct 15, 2026
4c38674
Generate the SDK's RESPONSE_GET codes and drop its own checks (#1623)
tamashi095 Oct 15, 2026
8ff0cce
Carry, then retarget a strip input section across a plan swap (#1630)
tamashi095 Oct 15, 2026
38af5bc
Merge codex/controls-batch-14bk into codex/controls-batch-14b
tamashi095 Oct 15, 2026
23d7c52
Merge codex/controls-batch-14bw into codex/controls-batch-14b
tamashi095 Oct 15, 2026
3c49a8b
Batch 14b review fold, part B: #1620, #1624, #1630
tamashi095 Oct 15, 2026
81aa9ef
Batch 14b review fold, part A: #1621, #1623
tamashi095 Oct 15, 2026
d7e3121
Merge origin/main (229ac1127) into codex/controls-batch-14b
tamashi095 Oct 15, 2026
10496f1
#1620: the session policy's preflight anchor follows check_caps(estim…
tamashi095 Oct 15, 2026
1fe8b5a
Batch 14b fold verification: a later write after adoption is not a se…
tamashi095 Oct 15, 2026
538537d
#1630 carry marks: regenerate the graph and builtins fixtures and re-…
tamashi095 Oct 15, 2026
5f51ba9
Merge the batch 14b fold verification into codex/controls-batch-14b
tamashi095 Oct 15, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,126 @@ AudioWorklet's message handler on a 64- or 256-track session.
## Size

Half a day, one sonnet-high implementer.

## Decision record: #1620 (batch 14b) -- measurement, stopped before coding

Anchor correction: the heap gate lives at `tools/parameter-metadata/tests/control_command_heap.rs`
(the batch 9 CI fold moved it to use `bench_support::alloc`), not under `hosts/host-web/tests/`.

Measured on `8b3ae13ec` (debug, `verify_stages(false)`), the nine-edit transaction of that test:

| tracks | first call: allocations / requested / peak / left | steady call peak | first-call excess | session canonical text |
|---|---|---|---|---|
| 1 | 834 / 129,249 / 77,347 / 1,078 | 72,588 | 4,759 | 5,474 |
| 64 | 1,848 / 549,392 / 197,929 / 1,078 | 72,588 | 125,341 | 96,887 |
| 256 | 4,922 / 1,866,704 / 663,179 / 1,078 | 72,588 | 590,591 | 375,479 |

**What the excess is.** Not lazily built state that later calls reuse. Traced with a temporary
backtrace hook on every allocation of 4 KiB or more (reverted): the first call leaves the in-place
path (#1613) and takes the full preparation. `SessionStore::prepare_in_place`
(`crates/protocol/src/model.rs`) returns `None` because `session::owners_counts_equal` is false: the
fixture's track holds a sparse `eq-simd1` parameter list (IDs 1 to 6, ID 4 per lane), and the edits
write IDs 65, 66, 67, 81, 82, 83 and ID 4 on `both`, which adds parameter entries. A count change is
not a change of values to the in-place path, so the store clones the whole model, runs
`compile_session` (a second model clone, the validation maps, the stable sorts) and writes the
whole canonical text. At 256 tracks the peak is that text's `String` doubling to 524,288 bytes
(final 376,405) plus about 66 KB of model clones and sort buffers; at 64 tracks the text reaches
131,072. The 1,078 bytes left are the committed model's added parameter entries. From the second
call on, the entries exist, the counts are equal and every call is in place (72,588 at every size).

**Why neither option fits.** Option A (build the reused state at boot) and Option B (lazy, one exact
allocation) both assume reusable state; there is none. The cost is the full compile path, taken by
any live edit that first writes a parameter the sparse model does not hold, on any revision, not
only the first. The candidate fixes are outside this spec's authorized paths and need a ruling:
(C1) let the in-place path accept a `SetControl` that adds a parameter entry (adjust
`owners_counts_equal` and the in-place resource estimate in `protocol`/`session` so an added entry
is charged exactly); or (C2) densify every effect's parameter list at preparation (changes the
committed model and its canonical snapshot, a contract change). Per the coordinator's ruling the
work stopped here; no code changed.

## Root ruling (2026-10-15): C1

- **Design.** The in-place path admits a `SetControl` that adds a parameter entry, and charges the
added entry exactly. The committed model and the canonical snapshot equal the full path's for
the same transaction. The rejected alternative (C2, fill every parameter list at preparation)
would change the committed model and its canonical snapshot.
- **Authorized paths** gain `crates/session/src/in_place.rs` and `crates/protocol/src/model.rs`
(the in-place admission and its resource estimate), and the heap gate's real home,
`tools/parameter-metadata/tests/control_command_heap.rs`.
- **Gates.** (1) A differential gate: for edits that add parameter entries, the in-place commit's
model and canonical snapshot equal the full path's, bit for bit. (2) The first-call peak at 64
and 256 tracks is measured and recorded, with a ceiling near the steady peak. (3) The first
call's allocation count does not depend on the track count.

### Implementation (batch 14b)

**What shipped (C1).** `session::owners_counts_equal` is replaced by `session::owners_growth`
(`crates/session/src/in_place.rs`): `Some(OwnersGrowth)` when the edited owners hold as many
console entries, inserts and VCA members as the committed ones and no effect's parameter list is
shorter, with the entries added and the longest grown list; `None` for any other change of a count
(a shorter list included, so every item the estimate takes a maximum over only grows). The new
`ResourceEstimate::after_owners_edit` (`crates/session/src/estimate.rs`) charges the growth exactly
as `estimate_session_resources` and `with_canonical_bytes` do: per added entry one
`size_of::<EffectParam>()` in the model and one structural item (1,024 bytes, now the named
constant `CANONICAL_BYTES_PER_ITEM`) in the canonical bound; the single largest allocation is the
largest of the committed one, the new canonical bound and the grown lists. It runs the compile's
own `check_caps` (now `pub(crate)`, its unused model argument dropped) and the platform and
canonical-bound checks on the result. `SessionStore::prepare_in_place`
(`crates/protocol/src/model.rs`) uses both and passes the store's caps: a growth that does not fit
returns `None` and the full path gives its typed refusal before any commit (no ack before a drop).
The entries land where the full path puts them: `controls`' parameter write already keeps the list
in canonical `(ID, channel)` order, the order `compile_session` sorts into.

Outside the listed paths (mechanical): the in-place admission comment in
`crates/control-plane/src/control.rs` (the in-place transaction may now add parameter entries, never
a count `check_counts` reads); `crates/session/src/estimate.rs` (`ResourceEstimate::after_owners_edit`
and the named `CANONICAL_BYTES_PER_ITEM`), `crates/session/src/compile.rs` (`check_caps` made
`pub(crate)`, its unused model argument dropped) and `crates/session/src/lib.rs` (the exports);
`crates/session/tests/in_place.rs` moved to the new function; the differential gate lives in
`crates/host-core/tests/in_place_commit.rs`, the home of the #1613 `Pair` harness that compares the
two paths.

**Gates.**

1. Differential: `in_place_commit::edits_that_add_parameter_entries_commit_in_place_as_the_full_path_does`
(console EQ above IDs 1 to 6 on one lane and both; an empty insert EQ list gaining an entry, then
one below and one between; a split of a `both` entry; one transaction over seven owners of tracks
and the submix, console and inserts; an abandoned one). Every transaction is prepared and
classified in place and the stores (model, canonical snapshot, estimate, revision) equal the
full path's after each. Refusal: `an_added_entry_over_the_caps_is_refused_before_any_commit`
(a store whose compiled-model or single-allocation cap is the committed figure: a fader commits
in place, an added entry is refused by both entries with the same `Validation` error and the
store is unchanged). Session unit: `owners_with_values_the_compiler_refuses_are_not_valid_and_counts_are_compared`.
2. `tools/parameter-metadata/tests/control_command_heap.rs` (debug): the first call is gated at
every size, `FIRST_PEAK_CEILING` 74,000 and `FIRST_REQUESTED_CEILING` 105,000 (was 78,000 peak
at one track only). Re-pinned for #1620 from these measurements, on this head before and after
the change:

| tracks | first call before: allocations / requested / peak | first call after | steady (unchanged) |
|---|---|---|---|
| 1 | 839 / 130,294 / 77,600 | 757 / 103,033 / 72,865 | 758 / 103,918 / 72,841 |
| 64 | 1,853 / 550,437 / 198,182 | 757 / 103,033 / 72,865 | the same |
| 256 | 4,927 / 1,867,749 / 663,179 | 757 / 103,033 / 72,865 | the same |

The steady ceilings (73,000 peak, 104,000 requested) were kept here; the batch 14b review fold
re-pinned all four (below).
3. The same test asserts the first call's allocation count equal at 1, 64 and 256 tracks.

**Review fold (batch 14b).** (1) The comments that still said the in-place form takes only existing
values now say it also adds parameter entries: `protocol/src/model.rs` (`TransactionForm::InPlace`,
`prepare_transaction_in_place`), `protocol/src/controller.rs`, `control-plane/src/control.rs`
(`commit_live`) and `host-core/tests/in_place_commit.rs`. (2) `control_command_heap.rs` drops the
`first.left <= 256` check: it had no unique catch (the first in-place commit frees the boot's
canonical text, and the live-bytes window stops at zero), so the left-bytes claim is removed from
gate 2 above; `steady.left <= 256` stays. (3) `OwnersGrowth`'s fields are private with `const`
getters (`parameters()`, `largest_grown_list()`) and it no longer derives `Default`: only
`owners_growth` builds one, so no caller outside `session` can hand the public
`ResourceEstimate::after_owners_edit` a growth smaller than the owners' real one; the session tests
compare the getters' pair. (4) The heap ceilings follow one slack rule (measured plus 2,000 bytes,
rounded up to the next 1,000): first call 75,000 peak / 106,000 requested, steady 75,000 / 106,000
(were 74,000 / 105,000 and 73,000 / 104,000); measured, debug, every size: first 757 / 103,033 /
72,865, steady 758 / 103,918 / 72,841 (the #1624 re-pin, recorded there). (5) Original gate 2
(`scripts/check-web-boot-budget.mjs`) was not run in attempt 1; run in this fold on a fresh
`build-web-audioworklet.sh --module-only` simd128 module (`765e340fa583b48c`, 4,096,024 B, built from this fold's working tree): `node
scripts/check-web-boot-budget.mjs` passes at its current pin ("web boot budget high-water gate
passed; mismatches: 0").
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,62 @@ banked effect does since #1611, so an unbanked EQ (no parameter cells) holds no
## Size

Half a day.

## Decision record: #1621 (batch 14b)

**Shipped.** `LiveControlEffect::new` (`crates/graph/src/runtime.rs`) sizes its staging window by
`control.parameter_cells()`: an unbanked EQ (prepared-target owner, no parameter cell) and a
channel-less lane hold no span storage; a compressor-shaped lane holds exactly its cells. The estimate
(`effect_control_resource`, `crates/graph-compiler/src/estimate.rs`) charges the same
`parameter_cells * size_of::<PreparedAutomationSpan>()`. Per-lane arithmetic is untouched.

**Departure (one, mechanical).** The bind-time check `EffectProcessBlock::check_automation_window`
demanded `window == automation_capacity`, which the cell-sized window cannot meet. The banked stage
already replaced that equality with "the lanes' cells never exceed the capacity" (#1611 D3). The same
rule now holds per node: `check_automation_window` (in `crates/effect-contract/src/lib.rs`) accepts a
window `<= automation_capacity`; the other half of the contract, that the window holds every cell the
lane reads (no drop, #1345 D4), stays `EffectControlLane::check_window` and still runs at bind. The
pairing-rule argument is unchanged: the hazard (#1012) was a window larger than the capacity, which
could stage a twin pair across the effect's `span_index < automation_capacity` cut-off; a window no
longer than the capacity cannot. Doc comments in `effect-contract/src/lib.rs` and `live.rs` say so.
Ack-before-drop: no. A cell-sized window holds one span per cell, `check_window` refuses a shorter
one at bind, and preparation raises `automation_capacity` to at least the descriptor's cells.

**Gates and tests.**
- Gate 1: `graph` `tests::a_per_node_window_is_its_lanes_cells_not_the_capacity` (EQ-shaped lane: 0;
compressor-shaped lane: 4 = its cells; gain fixture lane: 2; channel-less lane: 0; capacity 4,096).
`graph-compiler` `bypass_resources::a_per_node_live_window_does_not_grow_with_the_automation_capacity`
(real EQ, compressor and limiter inserts; allocator-measured retained bytes and the estimate are equal
at 128 and 4,096 spans). Both are red when the window returns to the capacity (checked by mutation).
- Gate 2: `graph-compiler` `effect_control_resource_uses_independent_queue_and_owner_arithmetic` follows
the new window formula (scalar-target window 0). Its `expected.largest > plain` assertion for the
scalar case was removed: it held only because the capacity-sized window was the largest owner
allocation; no per-node owner allocation outgrows the plain plan's largest any more, and the
exact-cap accept/refuse legs and the `largest` equality stay. The charge-equals-freed test
`a_bypass_or_live_controls_are_charged_at_least_what_they_retain`, `bypass_shunt_identity` and
`bypass_cohorts` stay green.
- Gate 3: `cargo test --locked` for graph, graph-compiler, rack, effect-contract, effect-compiler,
control-plane, controls, host-core (all features), host-web (all features), capi (all, incl.
`resource_lifecycle`) green; clippy `-D warnings` and fmt clean; `realtime-policy` ok;
`audit capi` `pcm_digest` `bf7dcedd1d92621f`; `check-browser-expected-resources.py` green.

**Measured numbers.** No pinned row or ceiling moved, so none was re-pinned. Before/after on the same
tree: `resource_lifecycle` nine-track EQ, soft-clip and browser-identity rows identical (EQ plan 218,401
observed, ceilings unchanged); the browser fixture's eight budget rows identical (for example
`graphIncrementalPlanBytes` 30,154). The one printed row that moved is the unpinned "routed submix"
plan row printed by `capi_retained_bytes_charge_every_byte_the_compile_retains`: observed 104,589 ->
98,781 and its estimate 196,180 -> 190,372 (-5,808 each, slack 91,591 unchanged; the per-node EQ/
compressor windows of that session, 128-span capacity).

**Review fold (batch 14b).** B1: `console-workload` `paired_spans`
`every_launch_effect_refuses_a_staging_window_that_is_not_its_capacity` still refused a window of
`capacity - 1`, which this issue made legal; it is now
`every_launch_effect_refuses_a_staging_window_longer_than_its_capacity`: it raises the direct
factory request to the live cells as preparation does (D4), accepts a window of the capacity and of
`EffectLaneShape::live_parameter_cells`, and refuses `capacity + 1`. The lower bound is the lane's
(`effect-contract` `live_window` `a_window_shorter_than_the_live_cells_is_refused_with_the_count_it_needs`).
Red when `check_window` returns `Ok(())` unconditionally (checked by mutation). Stale prose fixed in
`effect-contract/src/lib.rs` (`check_window` is the per-node bound, not "shared by both block types")
and `effect-compiler/tests/native_session.rs` (the window is the lane's cells, which the capacity
equals there). Gate 3 adds `cargo test --locked --release -p console-workload` (green; `paired_spans`
13 pass).
Loading
Loading