Skip to content

Commit cbc3d20

Browse files
ryansolidClaude via Cursor
andcommitted
fix(signals): seat a pass per hold at its head (#4417, #4544, #4574) — step 4, stopped at size
Hold identity for the probe memory: `probesHeld(c, toward)` resolves the remembered nodes to the holds that hold them now (`holdOf`/`holdOfTx`, through `_fam.node` for a derived store's leaf, a `FLUSH` sentinel for a staging the flush has not made a hold for yet) and compares holds, so a posture earned against T does not apply toward a later separate hold and lapses at T's landing (Ruling 1, D-25 as amended). At the head a held node seated as the flush's work whose probes name nothing any live hold holds drops CONFIG_VERDICT before `joinPassTx` re-keys the stagings (Ruling 2). Ports salvage 8ccab0f (the in-place provisional re-derive adopting its readers): #4417 `[1,2,2]` ×4 pass, both signals and web. Pins: `verdict-memo-held-branch-4544.test.ts` (+ web spec) — plain row passes; the probe rows and the timing edge's lane side are `it.fails` with the conflicts written up; S-31 web spec in both forms. S-46 web plain row stays `test.fails` (finding). Stopped before the PR: `+ isPending/latest` is +670 B br over base (read side +307, salvage +278, head +85) and the targeted generator replay shows 33 ids differing from base (15 from the read side incl. two liveness failures, 18 F-2 empty slots from the salvage, none from the head layer). Details in documentation/plans/seat-per-hold.md §8 status. Co-authored-by: Claude via Cursor <[email protected]>
1 parent 5417006 commit cbc3d20

16 files changed

Lines changed: 892 additions & 73 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/signals": patch
3+
---
4+
5+
A verdict reader's one re-derive after a parked flush (D-2) runs in place: it compares against the staging it read, and when unchanged the readers that derived from that staging are adopted into the hold's verdict lane and revealed at its seam instead of re-running. A keyed `<Show>` over `latest()` now mounts its child once across the park (#4417); a frame derivation that read a held write re-derives as lane work (the lane sees the screen, D-37).

‎.changeset/seat-per-hold-head.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/signals": patch
3+
---
4+
5+
A pass's verdict-reader posture is toward the hold it was earned against, not toward every hold at once: the nodes a probe asked about are remembered per node and resolved to the holds that hold them at use (a lane's node to the hold its lane is under, a derived store's leaf through its family), so a later separate hold sees the pass as whatever its reads make it, and the posture lapses when its hold lands. At the head, a held node seated as the flush's work whose probes name nothing any live hold holds is that hold's work, not its verdict reader (#4417, #4544, #4574).

‎documentation/plans/seat-per-hold.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ stored on the pass as `passWork: Transaction | null` (one — D-4 merges two) an
189189

190190
**Release at landing.** As built: none to perform — `probesHeld` resolves the remembered nodes to their holds at use, and a landed hold holds nothing. (Sketch: `land` (scheduler.ts:1259–1288) walks `T._reruns` and removes `T` from each `_seats`.)
191191

192+
**Hold identity (as built, step 4, 2026-11-01).** `probesHeld(c, toward)` compares _holds_, not levels: `holdOf(p)` resolves a remembered node to the hold that holds it now — `holdOfTx(p._x._transaction)` for a held or override node (`resolveTx`, then up through `_lane` parents to the real hold), through `_fam.node` first for a derived store's leaf (D-30), or, for a node staged this flush or in flight and not yet held, the flush's hold to be (`flushTransaction`, or a `FLUSH` sentinel when the flush has not made one yet) — and `toward` is resolved the same way (a node, a transaction, `null` for the flush's own proposal, `undefined` for "any live real hold"). Ruling 1 (per hold, conditional on isolation): toward a later, separate hold the posture earned against T does not apply; a D-4 merge makes the two one hold and the posture carries. The head clear (Ruling 2, today's order): a `CONFIG_HELD` node seated as `tx`'s work at the head whose probe memory names nothing any live hold holds drops `CONFIG_VERDICT` _before_ `joinPassTx` re-keys this flush's stagings to `tx` — a pass that probes T and merely stages for T is T's verdict reader; only a fetch T waits on (#3884) or a node born into T (D-27) stays T's work against its own probe.
193+
192194
**Byte class (estimates, not measured).** Core floor: **0 B** — `_seats`, `passWork`/`passVerdict` live behind `observeFlightHook`/`laneStageHook`; without them `frameRead` folds to today's `t === flushTransaction || bornInto`. `+ isPending/latest`: +60–120 B br (the array slot, `markVerdictReader(c, t)`, head fold, `land` release) **minus** the removed per-site checks (`frameRead`'s `!verdict`, `stagedScreen`'s arm, `enterLane`'s mask, `route`'s guard) — net plausibly +40–80 B br; the maintainer's stop line on the option-2 probe was +60 B, so this is at the line. Live page: same order (+80–150 B br). The S1 rebuild's +1061 br came from the seat _stack_ and its restores; this design has one array per probed node and two pass locals.
193195

194196
**Relationship to #3905.** No fork: one held value per node (`_pendingValue` or `_x._lane`), one seat per (pass, hold). A lane node dirtied by T's flush (timing edge) publishes a T staging while its lane value stays displayed — two slots, but the lane slot is the _screen's_ for that node (D-37), not a second held value. What still needs #3905: a node that must hold two different _held_ values for two unmerged holds (D-4 makes them merge, so none today); a verdict reader of U whose U-lane value must differ from its T staging while _both_ are held — that is the merge case.
@@ -209,4 +211,17 @@ Does not fix: the pending-read posture hand-off (#4504, by design); `on`'s fallb
209211
4. **Engine, head**: fold the held re-pass arm and `route` onto the head decision; port `8ccab0f71`'s provisional re-derive. _Gate_: #4417 `[1,2,2]`, S-46 web row flips to passing, #4544 Shapes 1/4/timing per §4; 50k differential against `d1d6d51c0` with the 19-case signature re-classified.
210212
5. **Measure** `scripts/size` on all floors (caps `scripts/size/floor-caps.json`: core floor 7.43 KB, live page 39.10 KB); report against the maintainer's budget before merge.
211213

214+
**Status after step 4 (2026-11-01, branch `fix/seat-per-hold`).** Built: steps 2–4 (shadow, read side, head with the `8ccab0f71` provisional re-derive and the hold identity above). Gates met: #4417 `[1,2,2]` ×4 (signals and web) pass; the 49 isolated rows keep today's screen; full signals, web (client/server/hydrate) and solid suites green; the async-gen regression gate (seed 3289, 1500 cases) reports no regression and no improvement. **The targeted replay does not pass**: of the 1217 saved ids (seeds 7 and 3289, the `_b*`/`_cb*` lists) 33 differ from `39f951b84` (`/tmp/gen/seat-tracediff.txt`), and bisecting the branch's three layers (read side `5417006de` / `+ 8ccab0f71` salvage / `+ head`) puts them in two groups, neither forced by §2:
215+
216+
- **Read side (committed `5417006de`) — 15 ids, two of them liveness failures** (7 #6390, 3289 #30951: `[ROOT-L]` forever; the others 7 #21333 #25272 #29429 #43308 #44018, 3289 #840 #22719 #24851 #25679 #29047 #35194 #43096 #47870). Reduced (`/tmp/gen/seat-liveness-repro.test.ts`, signals level, passes on base): memos created _outside any root_ (the harness's un-owned form) `m0 = async(s1)`, `m1 = async(s1)`, `m2 = async(m1)` where `m2` rejects, and under a `Loading` a render effect reading `[isPending(m2), m0()]`. When `m2` rejects the effect re-runs, its read of `m0` finds it stale and recomputes it (both base and head re-ask `m0`); base serves the committed `3` through `observeFlight` (node-level `CONFIG_VERDICT`), head's `probesHeld(c, m0)` says the effect has no posture toward `m0`'s flight (its memory names `m2`, errored, held by nothing; an ownerless flight has no transaction, so `holdOf(m0)` is the `FLUSH` sentinel) and lets it throw `NotReady` — rule-faithful so far (Ruling 1) — but when `m0` lands the effect is never re-run: `propagateStatus` (async.ts:1099–1108) still consults the node-level bit and treats it as a verdict reader to mark, not re-derive. The §3 split (node bit for `propagateStatus`, per-hold seat for reads) is inconsistent for a plain pending read made by a node that carries the bit. Not fixed.
217+
- **Salvage `8ccab0f71` — 18 ids, F-2 empty slots** (7 #1596 #1866 #4423 #13985 #21348 #24789 #30013 #41189 #45702, 3289 #1831 #8668 #12357 #12695 #17777 #24610 #30977 #31700 #36682; a few also stop breaking an E3 law base broke): under `<Show when={isPending(m0)}>` a leaf's `m0()` text hole (or attribute) stays empty where base shows the value, e.g. `[s0=1 m0=async(s0 !%4=3)] <Loading><Show when=isPending(m0)><span>m0()</span></Show></Loading> :: S4 t S2 s0=3 m S3 s0=0 S3` → `<span></span>` from step 5 on (base `vm0:2`). The in-place re-derive adopts the readers that derived from the staging; here the adopted `insert` effect is not re-run at the seam. Not fixed; the salvage's own two cases (`adoption-3289-464-91501-411.spec.ts`) still pass. The head layer (this PR's per-hold seat and head clear) moves none of the 33 on its own.
218+
219+
Three findings from the gates themselves, each pinned `it.fails` at the ruled screen rather than fixed:
220+
221+
- **S-46 web plain row** (`display-ahead-plain-write-content-s46.spec.tsx`): §4's mechanism ("`Show`'s render effect that attaches the branch is created by the same pass and reads the born-held node") does not match the real structure. The attaching effect is `insert`'s, created at mount, reading `Show`'s value memo P — T's verdict reader, published display-ahead; the hole's `insert` effect inside the branch _is_ born held (correct), but nothing seats P's output as T's, so the branch still attaches with its hole empty. The only mechanism found (dropped `04c218537`: a born-held uncaught render effect marks its creator pass joined) looped on generator 91501 #32043. Needs a mechanism ruling.
222+
- **#4544 Shape 1 probe rows** (`verdict-memo-held-branch-4544.test.ts`, class (c) rule conflict): on the `plan` write's flush the join makes `plan` T-held; `chrome`'s D-22 provisional probe re-derives at the seam as T's verdict reader; `params` (T-held, inheriting `[plan]` by #4050) is seated T's verdict reader at the seam re-pass — Ruling 2 as pinned by `latest-first-one-pass-4402` covers inherited postures — and is served the committed `session`; the born-held branch asks `signedOutOnly` and T never lands (`0 signed out`). §4's path (row 3 "work beats verdict" for `chrome`) was narrowed away by §5 to keep Shape 2.
223+
- **Timing edge, the lane's side** (same file, last row): `params` is U's verdict-lane node; the pass T's write dirties is seated as U's lane work at the head (`setPassLane`: `CONFIG_OVERRIDE` without `CONFIG_GUESS`) and stages into the one lane slot, so U's readers see `signedIn: true` before T lands. D-37 as amended wants T's staging beside the lane's value — the two-slot seat named under "Relationship to #3905", not built here. Base behaves the same.
224+
225+
Size (step 5, measured against `39f951b84`): `+ isPending/latest` +670 B br / +2085 B min (verdict.js +884, lanes.js +683, core.js +365, scheduler.js +141 minified); live page +717 B br / +2160 B min; hydrating + every store family +202 B br / +860 B min; core floor +22 B br / +70 B min. Over the +150 B br line — stopped before the PR; the breakdown is in the step report.
226+
212227
Not verified: any size number (no build run); the D-30 store rows' `_fam` resolution; the exact 50-file list of the probe branch's flips (commit message says 15 files, not enumerated); whether `heldBy` (lanes.ts:1020–1047) needs `passWork` for the `stagedReaders` judging — I believe it does not (it inspects `_pendingSources`), but did not trace every caller.

0 commit comments

Comments
 (0)