Skip to content

fix(signals): a held frame's leaf is the hold's own work, not its verdict reader (E2) - #4473

Open
ryansolid wants to merge 7 commits into
nextfrom
claude/project-thread-x1ore2-memo-form-tear
Open

ryansolid wants to merge 7 commits into
nextfrom
claude/project-thread-x1ore2-memo-form-tear

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Before: a branch that a held pass mounted, such as <Show when={y() > 0}>, shows only at the landing. Its leaves are members of a frame the hold built (CONFIG_HELD_FRAME). When the hold's next step re-ran one of those leaves, what happened depended on how the leaf was written:

  • Inline latest(y): route seated the leaf as the hold's verdict reader, so its run went out at the verdict lane's reveal, ahead of its own frame.
  • Through a memo, or split leaves: the leaf waited for the landing (L7'). A later run with nothing staged entered the verdict lane through enterLane instead, so this form didn't even agree with itself.

This happened with a single hold as well as merged holds, both before and after #4455. Inline gave [10, 20], while a memo inside or outside the branch gave [10]. Grouping changed the result, which #4284 forbids (pair E2).

After: every form waits for the landing and shows the same steps.

The inline form was the wrong one:

  • D-25: "unless the pass is the hold's own work: born in its world … a probe does not rescue them". fix(signals): a verdict reader born into a hold reads it whichever flush runs it (#4426) #4454 applied the same clause to these readers' reads.
  • D-27: "a joined pass's children are the hold's: its value commits with the frame".
  • Making the memo and split forms display ahead instead broke latest-first-later-throw-4275, because a child's run went out before its held parent's.

How:

  • route (verdict.ts) and the verdict arm of enterLane (lanes.ts) leave a pass as the frame's when it is CONFIG_HELD_FRAME and bornInto the hold. This is scoped to HELD_FRAME. Widening it to every born-held pass changed effect order in ispending-memo-gate-3766.
  • The seat table carries the same clause (__TEST__ only).

#4455's comparison removed. #4455 made laneRead's D-28 seat reset compare holds instead of verdict-lane objects, so that a merged transaction's lane counted as the same hold's. With the held-frame clause above, no shape depends on that comparison any more. laneRead is back to the pre-#4455 test (seated && (never || p !== l)), the seat table's #4427 known-difference entry is gone, and so are its comments. (Next has since retired #4428's entry too; the merge keeps both removals.) Review found every probe byte-identical on head with this revert, and both #4427 cases and the parity pin pass. verdict-seat-merged-lane-4427 now expects the frame's 1/10 until B lands (2/20 final). A mainline-mount contrast case (when={latest(y) > 0}) still shows 1/20.

Size vs next @ d0fc285, measured locally (linux), brotli / minified. The cost lands only where the verdict engine ships:

scenario delta cap
hello world and every floor except live SC 0 / 0 unchanged
page: live server components (frozen floor) +46 / +15 unchanged at 38.91 KB (38,895 B, under its cap); record unchanged
signals: + isPending/latest +4 / +15 unchanged at 11.19 KB; minified re-recorded
page: compiled live server components +18 / +41 42.83 → 42.85 KB
page: live + router −23 / +32 unchanged; minified re-recorded
app: hydrating + every store primitive +6 / −11 unchanged

The live floor's +46 B brotli is layout from the same +15 B of minified code. No floor changes, so there is no Size-Exception.

Tests:

  • New packages/signals/tests/held-frame-verdict-parity.test.ts covers a single hold (inline, memo inside, memo outside) and merged holds (inline, memo, split). The single-hold inline case fails on next.
  • Suites after merging next @ d0fc285: signals 6,060 passed and 17 expected fail; web 1,567 passed and 7 expected fail.
  • Async-gen differential against next (before the laneRead revert): 2,000 cases × 3 seeds, all pairs. 0 cases differ, and the seat shadow shows 0 / 0 / 0.

🤖 Generated with Claude Code

https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2

…dict reader (E2)

A render effect in a frame a held pass built (CONFIG_HELD_FRAME, e.g. the
branch `<Show when={y() > 0}>` opens under an action holding `y`) re-run
by the hold's next step showed two ways depending on memo wrapping:

- inline `latest(y)`: `route` seated it as the hold's verdict reader, so
  its run went out at the verdict lane's reveal, ahead of its frame;
- `createMemo(() => latest(y))` read plainly: `laneRead`'s L7' arm served
  the memo's staging as the hold's own value and the run waited for the
  landing (and a later run, with nothing staged, entered the verdict lane
  through `enterLane`).

`[10, 20]` beside `[10]` for one hold; with #4455, the two-hold review
shape (`[latest(y), m()]` inline vs a memo of it, or split leaves) the
same. A member of the hold's held frame is born in its world: it reads
plainly and a probe does not rescue it (D-25, as #4426/#4454 for its
reads), and its runs are the frame's, which commits with the hold (D-27's
joined pass, A29 ruling A). `route` and `enterLane`'s verdict arm now
leave it the frame's when the lane's hold is the one it was born into;
every form waits for the landing (the branch is not on screen before it).
The reader-seat table carries the same clause (test-only).

#4455's pin (`verdict-seat-merged-lane-4427`) asserted the inline leaf
running ahead of its held frame (`1/20` at A's step); it now expects the
frame's `1/10` until B's landing, and gains the mainline-mount contrast
(`<Show when={latest(y) > 0}>`), which still shows `1/20`.

Lives in verdict.ts / lanes.ts only: 0 B on every floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…pages

Measured locally (linux) over next @ 131ceb8. signals: + isPending/latest
11,125 B brotli / 30,704 B minified (+63 B), cap 11.12 -> 11.14 KB;
page: compiled live server components 42,810 / 125,600 (+82 B), cap
42.73 -> 42.82 KB. Caps at measured + 10 B rounded up to 0.01 KB; recorded
minified = measured. page: live server components (frozen floor) +63 B
minified, under its brotli cap (38,825 vs 38.85 KB): not raised. Every
floor without the verdict engine is 0 B. (app: hydrating (no stores) is
over its cap on next as well, unchanged here.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
@changeset-bot

changeset-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b580b19

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 15 packages
Name Type
@solidjs/signals Patch
test-integration Patch
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
solid-js Patch
@solidjs/universal Patch
board-example Patch
canvas-example Patch
server-dbmon-example Patch
todos-server-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.42 KB 0 B 0 B +15 B 7.43 KB ✅
signals: + createStore 14.96 KB 0 B 0 B +6 B 14.97 KB ✅
signals: + isPending/latest 11.11 KB 0 B +15 B 0 B 11.12 KB ✅
signals: + until/refresh (truth readers, no optimism) 8.05 KB 0 B 0 B +2 B 8.06 KB ✅
app: render + one signal (the simple-app floor) 9.78 KB 0 B 0 B +15 B 9.78 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.73 KB 0 B 0 B +15 B 18.74 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 32.43 KB +58 B (+0.2%) −11 B −5 B 32.48 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.43 KB 0 B 0 B +2 B 13.47 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.19 KB 0 B 0 B +2 B 15.20 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.76 KB 0 B 0 B +2 B 29.78 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.97 KB 0 B 0 B +2 B 9.98 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.33 KB 0 B 0 B +6 B 26.33 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.52 KB 0 B 0 B 0 B 32.54 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.75 KB 0 B 0 B 0 B 11.77 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.67 KB 0 B 0 B +2 B 33.77 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.03 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 38.81 KB +33 B (+0.1%) +15 B +30 B 38.85 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.03 KB, wire.js 3.47 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.70 KB 0 B 0 B +2 B 35.72 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 9.01 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 42.80 KB +91 B (+0.2%) +34 B 0 B 42.82 KB ✅ eager (counted): web.js 22.81 KB; assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.07 KB, sc-comments.js 0.19 KB, trace.js 9.07 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.23 KB 0 B 0 B 0 B 41.25 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, server.js 1.03 KB, serverForms.js 3.57 KB, trace.js 9.03 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 48.42 KB 0 B +32 B +47 B 48.44 KB ✅ eager (counted): client.js 27.92 KB; assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, server.js 1.03 KB, serverForms.js 3.30 KB, trace.js 9.00 KB, wire.js 3.49 KB
server: floor (getRequestEvent + isServer) 1.27 KB 0 B 0 B 0 B 1.29 KB ✅
server: renderToString (the server-render floor) 22.79 KB 0 B 0 B 0 B 22.80 KB ✅

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

@coveralls

coveralls commented Oct 30, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 41088674143

Coverage remained the same at 78.1%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1362
Covered Lines: 1125
Line Coverage: 82.6%
Relevant Branches: 1090
Covered Branches: 790
Branch Coverage: 72.48%
Branches in Coverage %: Yes
Coverage Strength: 29.94 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 8.59%

2 warnings

⚠️ 4 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ 1 benchmark with a significant change ran on different hardware

Its base and head results were measured on machines with a different CPU, core count or memory, so the change might not come from the code. Run base and head on the same runner type.

Learn more about Macro Runners

See affected benchmarks

⚡ 1 improved benchmark
✅ 199 untouched benchmarks
⏩ 5 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ memo + sync render effect only (reference) 34.7 ms 32 ms +8.59%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/project-thread-x1ore2-memo-form-tear (4ec50e3) with next (ab3d2ad)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

… held-frame clause covers it

laneRead's D-28 seat reset goes back to comparing verdict lanes by identity.
With the held-frame clause, no shape depends on the hold comparison, and the
seat table's #4427 known entry no longer matches anything. -48 B minified on
the verdict pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
seat.ts: next retired #4428's known entry and this branch retired #4427's;
both go. Size ledger taken from next and re-measured after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…tch PR)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants