Skip to content

fix(signals): an async memo over a verdict memo asks each question once - #4397

Merged
ryansolid merged 14 commits into
nextfrom
claude/project-thread-x1ore2-lane-reask
Oct 29, 2026
Merged

ryansolid merged 14 commits into
nextfrom
claude/project-thread-x1ore2-lane-reask

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Summary

Before (next): with m0 = createMemo(() => ask("m0", 1 + latest(s) + s())) and m1 = createMemo(() => ask("m1", 1 + m0())), setS(2) under a held frame asked ["m0:4","m1:5","m0:5","m1:5","m1:5","m1:6"]. m1 asked m1:5 three times.

After: the same case asks ["m0:4","m1:5","m0:5","m1:6"]. m1:5 is the verdict lane's one answer over m0's display-ahead intermediate (F-6). The landing then asks each memo once (F-9: a re-ask of the same question is no new question).

m1 is a verdict reader only because it reads a verdict memo (D-25, #4050). It was re-derived whenever m0 went pending and was served the committed value it had already answered from (A10), so it asked the same question again. That extra flight held the verdict lane, and the held landing re-ran m1 a third time.

A grouped reader over a held write now also shows the lane's answer. [isPending(m1), m1()] showed 103 → 105, while a plain m1() reader shows 103 → 104 → 105. Value, pair and split readers now agree.

An async memo that calls latest()/isPending() itself is unchanged from next. It stays a verdict reader and shows the lane's answer ahead (D-25, F-6).

How

The verdict role (CONFIG_VERDICT from REACTIVE_PROBED) is unchanged from next. This PR adds D-25's other clause, a node whose fetch a hold is waiting on is that hold's work (#3884), for an async memo that is a verdict reader only through what it reads. The decision lives in the verdict engine behind one hook (holdWorkHook, installed by verdict.ts like observeFlightHook), called only from propagateStatus. Nothing new runs per pass in recompute.

  • CONFIG_ASYNC marks an async memo with no window of its own:
    • handleAsync sets it where a flight registers, unless the pass's REACTIVE_WINDOW is up;
    • markVerdictReader (a window), a sync answer or a thrown error clears it;
    • a pass left pending keeps the last answer's value.
  • At a pass's head, an inline check clears CONFIG_VERDICT for the pass in two cases. One is when the node is pending (isPending/latest on a superseded transition depend on whether the parked async memo also reads them #3884, as on next). The other is when it is both CONFIG_ASYNC and CONFIG_HELD: the write that re-ran it is its hold's, so it reads plainly. That second case covers only memos that never asked. D-25's "a verdict posture outlives the pass that asked" covers passes that called latest/isPending, and those clear CONFIG_ASYNC. Without the second case, five pins ask twice again.
  • When a source goes pending (propagateStatus), a CONFIG_ASYNC sub is not re-derived if it isn't pending, sits in a lane, or the source's pending is a refresh() re-ask. It waits on the source like any derivation (F-9). If it is pending outside a lane on a new question of the source's, it is still re-derived.
  • frameRead drops what a served node propagated (A15, lanes.ts laneServed behind laneServedHook). Lane work or a verdict reader served a held node's committed value stops waiting on that node's flight.
  • A lane's reveal leaves the stale-reader mark to the transaction (scheduler.ts reruns: if (!u._lane)), so the transaction's landing re-runs the reader.

Review fix (bb06dc7). REACTIVE_WINDOW was wiped at the pass tail, and both consumers run between passes. So the rule applied to every async verdict memo, including ones with their own window, and changed screens against next:

  • an async memo over ${isPending(m0)}:${latest(m0)} never showed the lane's true:2;
  • one over latest(m0) read through [isPending(m1), m1()] skipped l1.

The window is now folded into CONFIG_ASYNC, and both shapes are pinned as next shows them.

Perf fix (b9b9800). bb06dc7's holdWork call at the end of every pass cost update-partial 7-14% (CodSpeed -6.8%). CONFIG_ASYNC upkeep moved to handleAsync, markVerdictReader and the sync-answer branches. In 12 alternating local runs against next, update-partial was +0.3% and dynamic-tag +1.1%, both within noise.

Review fix (0d6ca5a). After b9b9800, a pass that threw a real error kept CONFIG_ASYNC. So refresh(m0) to the same erroring answer showed m1's pre-error value as final (F-1). An error now clears the bit, pinned held and unheld.

Pins

All pins are in packages/signals/tests/verdict-memo-flight-no-reask.test.ts: 20 pass and 1 is it.fails. Flights in the held-write, remount, extracted and error pins land when the test lands them. The 80 ms holder pins, the two "waits on it, pending" pins and twoVerdicts still sleep.

test rules
{latest,plain} {held,unheld}: an async memo over a verdict memo asks each question once F-9, F-6, A10, D-25
{m1(), [isPending(m1), m1()]}: a memo over a re-asking verdict memo waits on it, pending F-9, D-25
two verdict memos into one async memo: no question asked twice, grouped or split F-9, D-25, F-6
two verdict memos …: grouped asks the t lane's questions as split does (it.fails, #4398) D-25, D-26, F-6
held write / landing equal / refresh after it settles / refresh mid-hold F-6, A15, F-9, D-23 exc. 2
{value,pair,split}: an async memo over a latest() memo asks each question once, as inline D-25, D-22, F-6, F-9
remounted mid-flight (with and without a later write) D-26, F-6, F-9
own window: an async memo over isPending() and latest() … / … over latest() …, read with isPending() beside it D-25, F-6
error, then refresh(m0) to the same answer: {held,unheld}, the error stays F-1, D-25

"latest unheld" keeps next's shape [m0:4, m0:5, m1:6]. #4383 changes it to the held shape.

How did you test this change?

  • Pin file: 20 passed, 1 expected fail.
  • Own-window pins: both fail on 1e65362's source.
  • Error pins: both fail without 0d6ca5a's clear.
  • Suites (after merging next):
    • signals: 5,927 passed;
    • web: 1,556 passed;
    • @solidjs/web test-types clean;
    • rules-index --check clean.

Size

The PR doesn't re-record the ledger. It is re-recorded once on next after the batch lands, so the size check is expected to report the stale ledger. Each floor is +143 B minified (+40 to +55 B brotli) over next, and isPending/latest is about +403 B minified.

Size-Exception: the frozen live-server-components floor grows about +412 B minified over next for this correctness fix; the maintainer relaxed the size rule for it on 2026-10-29 (size matters only if hello world passes 20 KB brotli).

🤖 Generated with Claude Code

https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2

A memo that reads a verdict memo is a verdict reader (F-6, #4050), and
propagateStatus re-derived every verdict reader when its source went
pending. Re-derived, an async one was served the committed value it had
already derived from (A10) and asked that question again; that flight
held the verdict lane, and the source's held landing re-ran it as lane
work onto the same committed value, a third ask. Only the landing's
re-derive asked the new question.

Now an async memo that is a verdict reader only by reading a verdict
memo (it opened no isPending/latest window of its own: REACTIVE_WINDOW,
set per pass by markVerdictReader) drops CONFIG_VERDICT when that memo
goes pending and takes the plain pending mark: it is pending through the
flight (F-9) and the hold's work until it lands, reading plainly (D-25,
#3884), and asks once at the landing. Sync readers re-derive as before.

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 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: afec0a7

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 29, 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.48 KB +45 B (+0.6%) +134 B −22 B 7.44 KB ⚠️ over by 36 B, 42 B minified headroom
signals: + createStore 14.99 KB +35 B (+0.2%) +134 B −1246 B 15.04 KB ✅
signals: + isPending/latest 10.68 KB +118 B (+1.1%) +359 B +359 B 10.38 KB ❌ over by 303 B
signals: + until/refresh (truth readers, no optimism) 8.10 KB +44 B (+0.5%) +134 B −27 B 8.05 KB ⚠️ over by 52.99999999999909 B, 47 B minified headroom
app: render + one signal (the simple-app floor) 9.82 KB +33 B (+0.3%) +134 B −83 B 9.75 KB ⚠️ over by 67 B, 103 B minified headroom
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.77 KB +9 B (+0.0%) +134 B +30 B 18.49 KB ❌ over by 280 B lazy-page.js 0.04 KB
app: hydrating + every store primitive family 32.01 KB +38 B (+0.1%) +256 B +256 B 31.80 KB ❌ over by 214 B lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.49 KB +41 B (+0.3%) +134 B −7 B 13.40 KB ⚠️ over by 88 B, 27 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.23 KB +21 B (+0.1%) +134 B −15 B 15.14 KB ⚠️ over by 91 B, 35 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.83 KB +78 B (+0.3%) +134 B −14 B 29.70 KB ⚠️ over by 132 B, 34 B minified headroom lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.02 KB +42 B (+0.4%) +134 B −82 B 9.93 KB ⚠️ over by 91 B, 102 B minified headroom
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.32 KB −8 B (−0.0%) +134 B −332 B 26.27 KB ⚠️ over by 48 B, 352 B minified headroom stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.46 KB −5 B (−0.0%) +134 B −231 B 32.25 KB ⚠️ over by 212 B, 251 B minified headroom stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.75 KB 0 B 0 B 0 B 11.76 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.70 KB +99 B (+0.3%) +134 B +109 B 33.37 KB ❌ over by 335 B assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.04 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 38.35 KB +91 B (+0.2%) +361 B +361 B 37.75 KB ❌ over by 596 B 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.67 KB +88 B (+0.2%) +134 B +120 B 35.43 KB ❌ over by 245 B 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.26 KB +141 B (+0.3%) +435 B +435 B 41.56 KB ❌ over by 700 B eager (counted): web.js 22.64 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.06 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.11 KB −54 B (−0.1%) +134 B +109 B 40.81 KB ❌ over by 298 B 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.00 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.82 KB +98 B (+0.2%) +452 B +452 B 47.16 KB ❌ over by 660 B eager (counted): client.js 27.73 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 8.98 KB, wire.js 3.49 KB
server: floor (getRequestEvent + isServer) 1.27 KB 0 B 0 B −69 B 1.34 KB ✅
server: renderToString (the server-render floor) 22.79 KB 0 B 0 B 0 B 22.80 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • signals: core floor (createSignal/Memo/Effect/Root/flush): over brotli cap by 36 B; minified 20,115 B vs 20,137 B recorded with the cap (−22 B) — 42 B of the 20 B minified allowance left; +134 B minified over this PR's base
  • signals: + until/refresh (truth readers, no optimism): over brotli cap by 52.99999999999909 B; minified 21,919 B vs 21,946 B recorded with the cap (−27 B) — 47 B of the 20 B minified allowance left; +134 B minified over this PR's base
  • app: render + one signal (the simple-app floor): over brotli cap by 67 B; minified 26,946 B vs 27,029 B recorded with the cap (−83 B) — 103 B of the 20 B minified allowance left; +134 B minified over this PR's base
  • app: CSR with Show/For/Loading/Errored/lazy: over brotli cap by 88 B; minified 37,200 B vs 37,207 B recorded with the cap (−7 B) — 27 B of the 20 B minified allowance left; +134 B minified over this PR's base
  • app: CSR, observe tier (same app on the observe artifacts): over brotli cap by 91 B; minified 42,148 B vs 42,163 B recorded with the cap (−15 B) — 35 B of the 20 B minified allowance left; +134 B minified over this PR's base
  • app: CSR, observe tier + attribution engine enabled: over brotli cap by 132 B; minified 88,487 B vs 88,501 B recorded with the cap (−14 B) — 34 B of the 20 B minified allowance left; +134 B minified over this PR's base
  • app: compiled floor (one template, one text hole, one delegated click): over brotli cap by 91 B; minified 27,478 B vs 27,560 B recorded with the cap (−82 B) — 102 B of the 20 B minified allowance left; +134 B minified over this PR's base
  • app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store): over brotli cap by 48 B; minified 80,170 B vs 80,502 B recorded with the cap (−332 B) — 352 B of the 20 B minified allowance left; +134 B minified over this PR's base
  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 212 B; minified 100,813 B vs 101,044 B recorded with the cap (−231 B) — 251 B of the 20 B minified allowance left; +134 B minified over this PR's base

❌ Fails the gate

  • signals: + isPending/latest: over brotli cap by 303 B; minified 29,445 B vs 29,086 B recorded with the cap (+359 B) — 339 B past the 20 B minified allowance; +359 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: hydrating (no stores) with Show/For/Loading/Errored/lazy: over brotli cap by 280 B; minified 53,650 B vs 53,620 B recorded with the cap (+30 B) — 10 B past the 20 B minified allowance; +134 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: hydrating + every store primitive family: over brotli cap by 214 B; minified 97,682 B vs 97,426 B recorded with the cap (+256 B) — 236 B past the 20 B minified allowance; +256 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: base server components (hydrating + dynamic + frames + sf reference): over brotli cap by 335 B; minified 101,668 B vs 101,559 B recorded with the cap (+109 B) — 89 B past the 20 B minified allowance; +134 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: live server components (base + live/GET + action + isPending/latest): over brotli cap by 596 B; minified 116,062 B vs 115,701 B recorded with the cap (+361 B) — 341 B past the 20 B minified allowance; +361 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread): over brotli cap by 245 B; minified 108,038 B vs 107,918 B recorded with the cap (+120 B) — 100 B past the 20 B minified allowance; +134 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: compiled live server components (the compiled base page + live/GET + action + isPending/latest): over brotli cap by 700 B; minified 124,168 B vs 123,733 B recorded with the cap (+435 B) — 415 B past the 20 B minified allowance; +435 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 298 B; minified 125,496 B vs 125,387 B recorded with the cap (+109 B) — 89 B past the 20 B minified allowance; +134 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 660 B; minified 141,435 B vs 140,983 B recorded with the cap (+452 B) — 432 B past the 20 B minified allowance; +452 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)

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).

ryansolid pushed a commit that referenced this pull request Oct 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
@coveralls

coveralls commented Oct 29, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 40844824351

Coverage remained the same at 78.516%

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: 1355
Covered Lines: 1125
Line Coverage: 83.03%
Relevant Branches: 1084
Covered Branches: 790
Branch Coverage: 72.88%
Branches in Coverage %: Yes
Coverage Strength: 30.11 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 9.78%

2 warnings

⚠️ 3 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

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

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

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ memo + sync render effect only (reference) 34 ms 31 ms +9.78%

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-lane-reask (afec0a7) with next (5f22701)

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. ↩

…o verdict role

#4397 review: with two verdict sources (`m1 = ask(1 + m0() + m0b())` over
`m0 = ask(1 + latest(s) + s())` and `m0b = ask(10 * (latest(t) + t()))`),
the grouped `[isPending(m1), m1()]` reader asked `m1:5` a second time on
stale inputs after both sources landed. m1 lost CONFIG_VERDICT when its
first source went pending, but a pass that threw NotReady at that
source's read picked it back up (F-6, `read`). The settle walk then
re-ran m1 as a verdict reader, and the committed values a hold had moved
past were served to it (A10).

The demotion is now per pass, in `recompute`. An async memo (new
CONFIG_ASYNC: its last answering pass registered a flight) whose pass
opened no isPending/latest window of its own (REACTIVE_WINDOW) does not
become a verdict reader by reading a verdict memo. It waits on its
sources like any derivation (F-9), reads them plainly (D-25), and asks
each question once. The side-effecting clear in `propagateStatus` is
gone, since that path no longer sees such a memo as a verdict reader.

Pinned: two verdict memos into one async memo, grouped and split, with
no question asked twice and equal final screens. A `test.fails` pin
records that the grouped shape still skips the t lane's m1:15/m1:16
asks that split makes: whether a later write merges into an open
transaction or opens its own lane depends on grouping (#4398).

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

Copy link
Copy Markdown
Member Author

The size check fails here because the scenarios.js ledger is stale against next, and this head adds about +90 B minified across the client scenarios. That puts "+ isPending/latest", "hydrating + every store", "live server components" and "compiled live server components" over their recorded caps. This is expected under the batch-merge plan. The ledger and floor caps get re-recorded once on next after the batch lands, so this PR doesn't re-record them itself. Every other check is the real signal. Byte growth is listed in the review handoff.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

The CodSpeed regression on projection derive: write one NESTED field (reference) (−13%) is noise, not this change. That bench moves ±13–15% with module byte size alone: a V8 lazy sweep after the pre-run gc() either lands in its single measured run or misses it. A 200 B comment on next reproduced the same jump under callgrind with CodSpeed's flags (see #4383). This PR doesn't touch the store projection path.


Generated by Claude Code

A reader that groups isPending with the value over a held write
(`[isPending(m1), m1()]`) never showed m1's lane answer, though a plain
value reader did. Two things kept it out:

- A pass served a held node's committed value (frameRead, a stale reader
  of the hold) still kept the node's propagated pending sources, so when
  it went pending on another source it never woke when that source
  settled. frameRead now drops what the served node propagated onto it
  (A15): the landing re-derives it.
- A lane's reveal (reruns on a lane) cleared REACTIVE_FRAME_READ, the
  mark the held transaction's landing needed to re-run the reader. Only
  the transaction's own reruns clear it now.

CONFIG_ASYNC is worked out once, at the end of each pass, from the pass's
`_inFlight` (registered a flight: async; answered sync: not; pending or
errored without a flight: the last answer's), so a plain sync recompute
does no extra work and handleAsync no longer touches it.

refresh(m0) mid-hold now shows the lane's answer for the refreshed
question in every reader shape. Pins for the held write, an equal
landing, and refresh after and mid-hold cover the value, pair and split
readers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
ryansolid pushed a commit that referenced this pull request Oct 29, 2026
…2' into claude/project-thread-x1ore2-4365

The held-shapes pins from #4397 ask the same questions here, each once,
and show the same values; with `latest()`'s pass the frame's (R5), the
held transaction asks `m0:5` at the write, before the lane's `m0:4`.

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

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

Review of #4397: the strict rule (an async memo with no `latest`/`isPending`
window of its own took no verdict role at all) overrode D-25 ("one that
reads a verdict reader's value is ... a verdict reader itself"; "a memo
computes under its own posture"). It caused two regressions against next:

1. Memo-extracted `latest()` (`v = createMemo(() => latest(s))`, `m1` async
   over `v()` and `s()`): m1's pass at the write read the transaction's
   staging and asked m1:220, the lane re-derived it (m1:120), and the
   landing asked m1:220 again. It now asks m1:120 then m1:220, once each,
   as the inline form does (D-22, F-9 exc. 2), showing v120 then v220 (F-6).
2. A reader disposed and remounted while the lane answer was in flight:
   `[isPending(m1), m1()]` in one effect showed true,4 -> true,5 -> false,6
   while `m1()` alone and split showed 4 -> 6. All shapes now show the
   screen, then the landing (F-6: grouped holds like a lone read; D-26).

The strict rule is gone. In its place, D-25's "a node whose fetch a hold
is waiting on is that hold's work" (#3884), kept by the verdict engine
(verdict.ts `holdWork`, behind `holdWorkHook`, like `observeFlightHook`):

- A verdict source going pending does not re-derive an async memo with no
  window of its own: none of its reads changed, and re-derived it was
  served the committed value it had already derived from and asked that
  question again (A10). It waits on the source like any derivation (F-9).
  Except when its own flight is up outside a lane on a new question of the
  source's: that re-run supersedes a flight asked of a value the source
  has moved past (a `refresh()` re-ask is the same question).
- A held re-run of such a memo reads plainly, as a pending one already did
  (#3884): the write that re-ran it is its hold's.
- Its async posture (CONFIG_ASYNC) is kept at each pass end in verdict.ts,
  not computed in core.

Kept from the PR, as review asked: the frame read's drop of a served
flight's pending (A15; now in lanes.ts `laneServed`, behind a hook, run
only for lane work and verdict readers, with no closure), and `reruns`
leaving the stale-reader mark to the transaction at a lane's reveal.

Pins (tests/verdict-memo-flight-no-reask.test.ts): the memo-extracted and
inline forms ask m1:120, m1:220 once and show v110, v120, v220 in every
reader shape (D-22, D-25, F-6); a reader remounted mid-flight shows the
screen, then the landing, grouped as alone or split (F-6, D-26). Both
fail on the previous head.

Size: the floors are +14 B minified over next (was +126). The verdict
scenarios carry the rule: + isPending/latest +339 B. Ledger lines and the
frozen live-server-components floor are re-recorded to the measured sizes
(Size-Exception: floor-caps.json 115,423 -> 115,646 B minified, measured
38,197 B brotli; the maintainer relaxed the size rule for this fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
CI ran the "remounted mid-flight, then a write" pin slower than local runs:
the landing's `m1:6` was asked before the remount in the grouped run and
after the write in the lone run, so comparing one shape's asks against
another's compared two timings, not two shapes. Run at a later remount
locally, every shape asks `m1:6` alike, and none diverges.

The pin now states what holds at any timing, per shape: no question asked
twice (F-9), the lane's `m1:5` never shown to a reader mounted mid-hold
(D-26), and the grouped `[isPending(m1), m1()]` ending on the landing like
the lone read (F-6). The no-write case still fails on the strict rule's
head, where the grouped remount showed `true,5`.

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

RULES-INDEX.md regenerated with scripts/rules-index.mjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
CI's coverage run (slower than a plain run) let m1's 10 ms lane flight land
before the pin's 2 ms sleep ended, so the later write came after the
landing and the run asked m1:220 where the pin expects m1:125. The pin
compared two timings, not two behaviours.

m1's flights now land only when the test lands them, so the later write
always comes while the write's flights are up. The pin's expectations are
unchanged (D-25, D-22, F-6, F-9), and it still fails on ee0b535's source in
all three reader shapes (the m1:220, m1:120, m1:220 triple ask).

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

Copy link
Copy Markdown
Member Author

Review at bf4d14f9e, against the rules as of next @ 24adaff59 (D-25 gained a paragraph today).

Not a gate for the other PRs. No open PR depends on this code. #4383 merged all nine of this branch's commits in (and then adjusted pin 1), so landing #4383 lands this; #4393 carries an older strict-rule version of three commits; #4401 is independent but conflicts on the same recompute-head line (different rule for clearing the verdict role on a pending node) — whichever lands second reconciles. Nothing should be held behind this PR.

One real defect. REACTIVE_WINDOW is not in the survivor mask at the pass tail (core.ts ~630–639), and both consumers of own — the pass head and propagateStatus — run between passes, where the bit is always 0. So own ≡ CONFIG_ASYNC: the rule applies to every async verdict memo, including ones that call latest/isPending themselves, not only those "with no window of their own". On the canonical F-6 shape that changes screens vs next, uncovered by the suite:

  • m1 = async(\${isPending(m0)}:${latest(m0)}`), m0async overs, setS(2): nextaskstrue:1, true:2, false:2and showstrue:2(the lane's answer); this branch askstrue:2, false:2and never showstrue:2`.
  • m1 = async(latest(m0)), reader [isPending(m1), m1()]: next asks l1, l2; this branch asks l2 only and shows true,l1 meanwhile.

The head arm own && CONFIG_HELD → read plainly is also wider than D-25's #3884 clause (a node whose fetch the hold waits on). A landed-but-held async memo is not that; D-25 now says it stays a verdict reader "whoever dirtied it". The changeset text still describes the reversed strict rule.

The pins are good. All 17 in verdict-memo-flight-no-reask.test.ts are justified by rule (F-6/F-9/D-25 for the asks; the 103/104/105 reader-shape change is the lane's revealed value, D-26; refresh mid-hold 4/5/6 by A19 exc. 2). Pin 1 "latest unheld" is already rewritten by #4383 to the held shape. Generator at 2k: no new findings.

Please:

  1. Drop the ledger re-records in ee0b53583 and c4426aff4 (caps and the frozen floor). Convention is one re-record on next after the batch lands, as the 02:32Z comment says.
  2. Split: keep the pin file (pin 1-unheld in fix(signals): latest() of a memo born in a lane throws NotReady, not undefined (#4362, #4363, #4376) #4383's shape; add the two own-window shapes above as it.fails), drop the mechanism (holdWork, REACTIVE_WINDOW, CONFIG_ASYNC, laneServed, the reruns lane carve-out). This layer is what the reader-seat rebuild (test(signals): reader seat shadow — new seat/slot implementation under __TEST__, differential against the existing deciders #4395) replaces; the pins are what carries over.
  3. If the maintainer wants the fix in now instead of the split: make REACTIVE_WINDOW survive to its consumers (or restate the rule honestly as "any async verdict memo" and justify or remove the own && HELD arm against D-25), fix the changeset text, and add the two own-window shapes as pins. Then it can land.

Do not refine the verdict-role heuristic further either way.

— Claude via Cursor

…rk (D-25)

Review of #4397: REACTIVE_WINDOW was wiped at the pass tail, and both of
holdWork's consumers (the pass head and propagateStatus) run between
passes, so `own` was CONFIG_ASYNC alone. The rule hit every async verdict
memo, including one that calls latest()/isPending() itself, and changed
screens against next on the canonical F-6 shape: an async memo over
`${isPending(m0)}:${latest(m0)}` never showed the lane's true:2, and one
over latest(m0) read through [isPending(m1), m1()] skipped l1 and showed
true,l1.

CONFIG_ASYNC is now set at the pass end, while the pass's REACTIVE_WINDOW
is still up, only for a pass that registered a flight and opened no window;
a pass that opened one clears it. A memo that asks is a verdict reader
whoever dirties it (D-25, ruled 2026-10-28), unchanged from next. The held
re-run arm applies only to memos that never asked; removing it reasks
m1:5/m1:104/m1:15 in five pins, so it stays, documented against D-25.

Pins: the two own-window shapes, as next shows them (D-25, F-6). The pin
file takes the hand-landed flights from the split prepared for review, so
no pin races a timer. Ledger re-records dropped: the size ledger is
re-recorded once on next after the batch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
CONFIG_ASYNC is now set where a flight registers (handleAsync) and
cleared by a verdict window (markVerdictReader) or a sync answer, instead
of a holdWork call at every pass's end. The pass-head check is inline.
holdWork only answers propagateStatus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
A pass that throws a real error answers with no flight of its own, so a
verdict source going pending re-derives the memo instead of holding it on
its pre-error value (F-1). Pinned, held and unheld.

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

# Conflicts:
#	packages/signals/docs/RULES-INDEX.md
#	packages/signals/src/core/core.ts
@ryansolid
ryansolid merged commit f94dcd5 into next Oct 29, 2026
7 of 8 checks passed
ryansolid pushed a commit that referenced this pull request Oct 29, 2026
Merging #4397 into #4393 asked three questions twice:

- An async memo over `latest()` of a flight (the own-window pins) took
  R5's frame pass and then a verdict-lane pass that asked the same
  question again. `latest` of a flight serves the committed value in
  either world (D-22), so the seam has nothing to decide. The reader
  joins the verdict lane at once, as a plain read of a flight makes it
  (`observeFlight`). `latest` of a staging stays the seam's (#4365).
- An async memo over a `latest()` memo (D-22 extracted): R5 parked only
  the reader's own flight. The memo's frame flight was superseded by its
  lane pass, and the landing asked `m1:220` again. The seam now parks
  the frame flights of a sync reader's async readers held by the frame,
  and the landing adopts them as it does inline (F-9).

The remount and extracted pins take R5's order: the frame's question
comes first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
ryansolid pushed a commit that referenced this pull request Oct 29, 2026
Conflicts resolved as follows:

- recompute's pass-head CONFIG_VERDICT clear takes next's form:
  laneStageHook && !isEffect && (pending || ASYNC|HELD).
- laneSeams takes next's signature, (t), which reads stagedReaders
  directly. It also takes next's `l` for an unheld staged reader and its
  bornInto/blocked(l) arm. It keeps #4393's heldBy lane arm, its R5
  parking and adoptParked, and the hook.
- provisionalVerdict keeps #4393's R5 shape: latest() opens no
  transaction. #4401's joinFuture/openedByLatest path breaks 25 R5 pins,
  so `latestOpened` is now never set.
- verdictValue keeps #4393's NOT_PENDING display path for a first load
  (#4363). #4401's first-load throw is not taken.
- verdict-memo-flight-no-reask keeps #4393's R5 expectations and adds
  next's "error, then refresh(m0)" pins.
- The async-gen baseline and RULES-INDEX are taken from next and then
  regenerated.

Still failing: latest-memo-inline-parity "inline, plain-first" and
"inline, latest-first" (and the web spec "inline"), which come from R5
against #4401, and latest-two-readers "beside".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
ryansolid pushed a commit that referenced this pull request Oct 29, 2026
…at carve (F-6, D-26)

With #4397 merged, m1 is a verdict reader between its flights (D-25) and
m0's lane pass re-derives it into the s lane early, so the t transaction
lands while m1:1122 is in flight. Telling that pass apart as the hold's
work needs a per-pass check; it goes to the carve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
ryansolid added a commit that referenced this pull request Oct 29, 2026
…undefined (#4362, #4363, #4376) (#4383)

* fix(signals): latest() of a memo born in a lane throws NotReady instead of undefined (#4362, #4363, #4376)

A memo whose compute reads latest(s) is re-asked as the write's verdict
lane work, so it becomes a lane derivation. While its first pass is in
flight, nothing is in the lane, and laneValueOf/display fell back to the
committed value, which an uninitialized node does not have:

- latest(m)/isPending(m) served undefined and probed it pending (#4363);
- the lane's end committed that undefined as the node's answer and
  cleared STATUS_UNINITIALIZED with the flight still up, which tripped
  the flush loop guard for [latest(m), isPending(m)] (#4362);
- in hydration, m2 = fetchC(1 + latest(m1)) asked fetchC(NaN) instead of
  adopting the streamed answer (#4376).

laneValueOf/display now return NOT_PENDING for such a node, so every
read throws NotReady (A7; A19 exc. 1) and the lane's end leaves the node
in flight.

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

* fix(signals): keep a verdict reader's lane routing and own-error recovery (async-gen #119, #1238)

- verdictValue: read the lane value before routing the reader into the
  verdict lane, so a node born in the lane with nothing in it yet is read
  as any uninitialized node (NotReady) without moving its reader (#1238).
- reruns: a stale reader whose error is its own (thrown by its pass over
  the committed value it was served) re-derives at the landing; only an
  error from an errored source is skipped (#3945). With #4363 no longer
  committing `undefined`, async-gen case 119 reached this stuck error.

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

* chore: re-measure sizes over next @ 40cde2c, record async-gen improvements

- scripts/size: ledger lines for `signals: + isPending/latest`, the
  compiled live page and live + router (+46/+46/+47 B minified over
  next); this PR's store-app line re-measured (97,019 B).
- async-gen baseline: 15 recorded failures no longer reproduce (10
  undefined-value, 4 final-screen, 1 unhandled-rejection).
- RULES-INDEX regenerated; changeset notes the error recovery.

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

* fix(signals): re-derive a self-errored stale reader at the lane's end, not in reruns

The verdict reader's own error from a served committed value (async-gen
#119) now re-derives from `dissolveLane` (lane engine only) instead of a
widened `reruns` condition, so the floors pay nothing (0 B; the lane
scenarios +20 B over the previous form). `reruns` is next's again.
Ledger re-measured; async-gen baseline unchanged.

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

* fix(signals): the lane's end re-derives a render effect's own stale error too

The re-enqueue of a self-errored stale reader at the lane's end sat inside
the non-effect branch, so a render effect whose display-ahead pass threw
over a held write's screen value kept its error after the landing (found
by the #4365 thread). It now applies to both. Byte-neutral (0 B minified
in every scenario); floors 0 B.

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

* fix(signals): an async memo over a verdict memo asks each question once

A memo that reads a verdict memo is a verdict reader (F-6, #4050), and
propagateStatus re-derived every verdict reader when its source went
pending. Re-derived, an async one was served the committed value it had
already derived from (A10) and asked that question again; that flight
held the verdict lane, and the source's held landing re-ran it as lane
work onto the same committed value, a third ask. Only the landing's
re-derive asked the new question.

Now an async memo that is a verdict reader only by reading a verdict
memo (it opened no isPending/latest window of its own: REACTIVE_WINDOW,
set per pass by markVerdictReader) drops CONFIG_VERDICT when that memo
goes pending and takes the plain pending mark: it is pending through the
flight (F-9) and the hold's work until it lands, reading plainly (D-25,
#3884), and asks once at the landing. Sync readers re-derive as before.

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

* test(signals): with #4383, isPending of the unresolved m1 holds its Loading, so the unheld latest case asks like the held one

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

* docs(signals): regenerate RULES-INDEX

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

* fix(signals): an async memo with no verdict window of its own takes no verdict role

#4397 review: with two verdict sources (`m1 = ask(1 + m0() + m0b())` over
`m0 = ask(1 + latest(s) + s())` and `m0b = ask(10 * (latest(t) + t()))`),
the grouped `[isPending(m1), m1()]` reader asked `m1:5` a second time on
stale inputs after both sources landed. m1 lost CONFIG_VERDICT when its
first source went pending, but a pass that threw NotReady at that
source's read picked it back up (F-6, `read`). The settle walk then
re-ran m1 as a verdict reader, and the committed values a hold had moved
past were served to it (A10).

The demotion is now per pass, in `recompute`. An async memo (new
CONFIG_ASYNC: its last answering pass registered a flight) whose pass
opened no isPending/latest window of its own (REACTIVE_WINDOW) does not
become a verdict reader by reading a verdict memo. It waits on its
sources like any derivation (F-9), reads them plainly (D-25), and asks
each question once. The side-effecting clear in `propagateStatus` is
gone, since that path no longer sees such a memo as a verdict reader.

Pinned: two verdict memos into one async memo, grouped and split, with
no question asked twice and equal final screens. A `test.fails` pin
records that the grouped shape still skips the t lane's m1:15/m1:16
asks that split makes: whether a later write merges into an open
transaction or opens its own lane depends on grouping (#4398).

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

* fix(signals): a grouped reader over a held write shows the lane's answer

A reader that groups isPending with the value over a held write
(`[isPending(m1), m1()]`) never showed m1's lane answer, though a plain
value reader did. Two things kept it out:

- A pass served a held node's committed value (frameRead, a stale reader
  of the hold) still kept the node's propagated pending sources, so when
  it went pending on another source it never woke when that source
  settled. frameRead now drops what the served node propagated onto it
  (A15): the landing re-derives it.
- A lane's reveal (reruns on a lane) cleared REACTIVE_FRAME_READ, the
  mark the held transaction's landing needed to re-run the reader. Only
  the transaction's own reruns clear it now.

CONFIG_ASYNC is worked out once, at the end of each pass, from the pass's
`_inFlight` (registered a flight: async; answered sync: not; pending or
errored without a flight: the last answer's), so a plain sync recompute
does no extra work and handleAsync no longer touches it.

refresh(m0) mid-hold now shows the lane's answer for the refreshed
question in every reader shape. Pins for the held write, an equal
landing, and refresh after and mid-hold cover the value, pair and split
readers.

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

* test(signals): name frameRead in the held-shapes pin comment

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

* chore(size): re-record this PR's four sizes over next @ 7ebd4a3

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

* fix(signals): an async memo over a verdict memo stays a verdict reader (D-25)

Review of #4397: the strict rule (an async memo with no `latest`/`isPending`
window of its own took no verdict role at all) overrode D-25 ("one that
reads a verdict reader's value is ... a verdict reader itself"; "a memo
computes under its own posture"). It caused two regressions against next:

1. Memo-extracted `latest()` (`v = createMemo(() => latest(s))`, `m1` async
   over `v()` and `s()`): m1's pass at the write read the transaction's
   staging and asked m1:220, the lane re-derived it (m1:120), and the
   landing asked m1:220 again. It now asks m1:120 then m1:220, once each,
   as the inline form does (D-22, F-9 exc. 2), showing v120 then v220 (F-6).
2. A reader disposed and remounted while the lane answer was in flight:
   `[isPending(m1), m1()]` in one effect showed true,4 -> true,5 -> false,6
   while `m1()` alone and split showed 4 -> 6. All shapes now show the
   screen, then the landing (F-6: grouped holds like a lone read; D-26).

The strict rule is gone. In its place, D-25's "a node whose fetch a hold
is waiting on is that hold's work" (#3884), kept by the verdict engine
(verdict.ts `holdWork`, behind `holdWorkHook`, like `observeFlightHook`):

- A verdict source going pending does not re-derive an async memo with no
  window of its own: none of its reads changed, and re-derived it was
  served the committed value it had already derived from and asked that
  question again (A10). It waits on the source like any derivation (F-9).
  Except when its own flight is up outside a lane on a new question of the
  source's: that re-run supersedes a flight asked of a value the source
  has moved past (a `refresh()` re-ask is the same question).
- A held re-run of such a memo reads plainly, as a pending one already did
  (#3884): the write that re-ran it is its hold's.
- Its async posture (CONFIG_ASYNC) is kept at each pass end in verdict.ts,
  not computed in core.

Kept from the PR, as review asked: the frame read's drop of a served
flight's pending (A15; now in lanes.ts `laneServed`, behind a hook, run
only for lane work and verdict readers, with no closure), and `reruns`
leaving the stale-reader mark to the transaction at a lane's reveal.

Pins (tests/verdict-memo-flight-no-reask.test.ts): the memo-extracted and
inline forms ask m1:120, m1:220 once and show v110, v120, v220 in every
reader shape (D-22, D-25, F-6); a reader remounted mid-flight shows the
screen, then the landing, grouped as alone or split (F-6, D-26). Both
fail on the previous head.

Size: the floors are +14 B minified over next (was +126). The verdict
scenarios carry the rule: + isPending/latest +339 B. Ledger lines and the
frozen live-server-components floor are re-recorded to the measured sizes
(Size-Exception: floor-caps.json 115,423 -> 115,646 B minified, measured
38,197 B brotli; the maintainer relaxed the size rule for this fix).

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

* test(signals): pin the remount shapes on what holds whatever the timing

CI ran the "remounted mid-flight, then a write" pin slower than local runs:
the landing's `m1:6` was asked before the remount in the grouped run and
after the write in the lone run, so comparing one shape's asks against
another's compared two timings, not two shapes. Run at a later remount
locally, every shape asks `m1:6` alike, and none diverges.

The pin now states what holds at any timing, per shape: no question asked
twice (F-9), the lane's `m1:5` never shown to a reader mounted mid-hold
(D-26), and the grouped `[isPending(m1), m1()]` ending on the landing like
the lone read (F-6). The no-write case still fails on the strict rule's
head, where the grouped remount showed `true,5`.

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

* test(signals): land the extracted pin's flights by hand, not by timer

CI's coverage run (slower than a plain run) let m1's 10 ms lane flight land
before the pin's 2 ms sleep ended, so the later write came after the
landing and the run asked m1:220 where the pin expects m1:125. The pin
compared two timings, not two behaviours.

m1's flights now land only when the test lands them, so the later write
always comes while the write's flights are up. The pin's expectations are
unchanged (D-25, D-22, F-6, F-9), and it still fails on ee0b535's source in
all three reader shapes (the m1:220, m1:120, m1:220 triple ask).

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

* fix(signals): only an async memo with no window of its own is hold work (D-25)

Review of #4397: REACTIVE_WINDOW was wiped at the pass tail, and both of
holdWork's consumers (the pass head and propagateStatus) run between
passes, so `own` was CONFIG_ASYNC alone. The rule hit every async verdict
memo, including one that calls latest()/isPending() itself, and changed
screens against next on the canonical F-6 shape: an async memo over
`${isPending(m0)}:${latest(m0)}` never showed the lane's true:2, and one
over latest(m0) read through [isPending(m1), m1()] skipped l1 and showed
true,l1.

CONFIG_ASYNC is now set at the pass end, while the pass's REACTIVE_WINDOW
is still up, only for a pass that registered a flight and opened no window;
a pass that opened one clears it. A memo that asks is a verdict reader
whoever dirties it (D-25, ruled 2026-10-28), unchanged from next. The held
re-run arm applies only to memos that never asked; removing it reasks
m1:5/m1:104/m1:15 in five pins, so it stays, documented against D-25.

Pins: the two own-window shapes, as next shows them (D-25, F-6). The pin
file takes the hand-landed flights from the split prepared for review, so
no pin races a timer. Ledger re-records dropped: the size ledger is
re-recorded once on next after the batch.

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

* perf(signals): keep D-25's async posture off recompute's hot path

CONFIG_ASYNC is now set where a flight registers (handleAsync) and
cleared by a verdict window (markVerdictReader) or a sync answer, instead
of a holdWork call at every pass's end. The pass-head check is inline.
holdWork only answers propagateStatus.

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

* fix(signals): an error clears D-25's async posture

A pass that throws a real error answers with no flight of its own, so a
verdict source going pending re-derives the memo instead of holding it on
its pre-error value (F-1). Pinned, held and unheld.

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

* fix(signals): a stale reader's own error re-derives at any landing, not only a lane's end

With #4383 the lane's end no longer commits `undefined` for a node that has
produced nothing (#4363), so a sync memo that errored before its first
value (async-gen 91501 #1158: m1 = m0 + 1 throws at m0 = 1) stays
uninitialized through the next lane's end. Its next pass, under the
transaction a `latest()` read opened, is "a pass with no value yet":
`stagedScreen` serves it the screen (m0 = 1, not the staged 4) and lists it
as a stale reader to run again at the landing. That pass throws its own
error, and `reruns` skipped every errored reader whose error source is
errored -- its own included -- so it never ran again and the root stayed on
[ROOT-E]. On next the memo had been initialized as `undefined`, so it read
the staging instead and never hit this.

`reruns` now re-derives a reader whose error is its own (A15; D-39: the
landing is the new answer), as #4383 first did (1e270cd). That also covers
the lane's node case, so the `dissolveLane` re-enqueue (3fc3ef1,
15b9419) goes: +11 B minified on the floors, -27 B on the lane scenarios.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
ryansolid added a commit that referenced this pull request Oct 29, 2026
…er watches the seam (#4365) (#4393)

* fix(signals): latest() of a memo born in a lane throws NotReady instead of undefined (#4362, #4363, #4376)

A memo whose compute reads latest(s) is re-asked as the write's verdict
lane work, so it becomes a lane derivation. While its first pass is in
flight, nothing is in the lane, and laneValueOf/display fell back to the
committed value, which an uninitialized node does not have:

- latest(m)/isPending(m) served undefined and probed it pending (#4363);
- the lane's end committed that undefined as the node's answer and
  cleared STATUS_UNINITIALIZED with the flight still up, which tripped
  the flush loop guard for [latest(m), isPending(m)] (#4362);
- in hydration, m2 = fetchC(1 + latest(m1)) asked fetchC(NaN) instead of
  adopting the streamed answer (#4376).

laneValueOf/display now return NOT_PENDING for such a node, so every
read throws NotReady (A7; A19 exc. 1) and the lane's end leaves the node
in flight.

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

* fix(signals): latest(a) beside b() computes one pass on the frame's world (#4365)

A memo or render effect that reads `latest(a)` beside `b()` computed a
torn pass on a batched write. The `latest()` read of a's staging opened the
flush's transaction for its verdict (verdict.ts `provisionalVerdict`).
The plain `b()` read after it then took that transaction for a hold
(`stagedScreen`): it served b's committed value and made the reader a
stale reader of the transaction. Nothing held the flush, so that torn pass
was one of the flush's frames. Through a memo, a render effect ran `[2,1]`
before `[2,2]`. A throw from the pass stuck, because the landing skips
errored readers.

A pass seated in the verdict lane only by its own `latest()` read
(REACTIVE_PROVISIONAL) now reads a staging as the frame does: it leaves
the lane and is recorded in `stagedReaders`. If the frame commits, the
pass was the frame's one pass. If the flush parks, the lane seam
re-derives it on the committed world as display-ahead work. A reader
whose own stale pass threw now re-derives at the landing.

`stagedScreen` moves into lanes.ts behind a hook, so the core floor does
not carry it.

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

* fix(signals): keep a verdict reader's lane routing and own-error recovery (async-gen #119, #1238)

- verdictValue: read the lane value before routing the reader into the
  verdict lane, so a node born in the lane with nothing in it yet is read
  as any uninitialized node (NotReady) without moving its reader (#1238).
- reruns: a stale reader whose error is its own (thrown by its pass over
  the committed value it was served) re-derives at the landing; only an
  error from an errored source is skipped (#3945). With #4363 no longer
  committing `undefined`, async-gen case 119 reached this stuck error.

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

* chore: re-measure sizes over next @ 40cde2c, record async-gen improvements

- scripts/size: ledger lines for `signals: + isPending/latest`, the
  compiled live page and live + router (+46/+46/+47 B minified over
  next); this PR's store-app line re-measured (97,019 B).
- async-gen baseline: 15 recorded failures no longer reproduce (10
  undefined-value, 4 final-screen, 1 unhandled-rejection).
- RULES-INDEX regenerated; changeset notes the error recovery.

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

* fix(signals): re-derive a self-errored stale reader at the lane's end, not in reruns

The verdict reader's own error from a served committed value (async-gen
#119) now re-derives from `dissolveLane` (lane engine only) instead of a
widened `reruns` condition, so the floors pay nothing (0 B; the lane
scenarios +20 B over the previous form). `reruns` is next's again.
Ledger re-measured; async-gen baseline unchanged.

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

* fix(signals): latest() opens no transaction for its verdict; its reader watches the seam (#4365)

Rework #4365 to the ruled shape (R5): in provisionalVerdict, latest()
no longer calls joinFuture(null). Like a probe read before anything has
joined, it goes through watchVerdict. Its pass is the frame's. If the
flush parks, it re-derives once in the verdict lane: it is marked
unanswered, so the seam does not take the transaction it joined as its
answer. A flush that has already joined a transaction (Vl1), and reads
outside a flush (Vl3), are unchanged.

This drops the stagedScreen variant (REACTIVE_PROVISIONAL, the
stagedScreen hook move and the laneSeams change). It also drops this
branch's `reruns` change: #4383 handles a self-errored stale reader in
lanes.ts dissolveLane, and that branch merges next.

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

* test(signals, web): re-pin the keyed Show over latest() to two mounts at a park (R-5, #4365)

Under design ruling R-5, latest() opens no transaction for its verdict.
The keyed condition's first pass is the frame's, and Show mounts A there.
The parked flush re-runs the latest() reader once in the verdict lane,
and Show mounts A again. The lane's mount is the one shown (the early
fallback, still asserted); the held frame's mount is discarded at the
lane's reveal. mounts goes [1,2,2] -> [1,3,3] for the static and
callback forms over latest(count). The committed-key and `on` rows are
unchanged. This re-pin waits on Ryan's ruling.

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

* fix(signals): the lane's end re-derives a render effect's own stale error too

The re-enqueue of a self-errored stale reader at the lane's end sat inside
the non-effect branch, so a render effect whose display-ahead pass threw
over a held write's screen value kept its error after the landing (found
by the #4365 thread). It now applies to both. Byte-neutral (0 B minified
in every scenario); floors 0 B.

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

* test(signals, web): note why the keyed Show over latest() mounts 3 at a park (D-2, A10)

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

* fix(signals): an async memo over a verdict memo asks each question once

A memo that reads a verdict memo is a verdict reader (F-6, #4050), and
propagateStatus re-derived every verdict reader when its source went
pending. Re-derived, an async one was served the committed value it had
already derived from (A10) and asked that question again; that flight
held the verdict lane, and the source's held landing re-ran it as lane
work onto the same committed value, a third ask. Only the landing's
re-derive asked the new question.

Now an async memo that is a verdict reader only by reading a verdict
memo (it opened no isPending/latest window of its own: REACTIVE_WINDOW,
set per pass by markVerdictReader) drops CONFIG_VERDICT when that memo
goes pending and takes the plain pending mark: it is pending through the
flight (F-9) and the hold's work until it lands, reading plainly (D-25,
#3884), and asks once at the landing. Sync readers re-derive as before.

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

* test(signals): with #4383, isPending of the unresolved m1 holds its Loading, so the unheld latest case asks like the held one

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

* docs(signals): regenerate RULES-INDEX

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

* fix(signals): a latest() reader's frame flight is adopted at the landing, not asked again (#4365)

Under R5 a latest() reader's first pass is the frame's: an async one
asks the frame's question there. When the flush parks, the seam
re-derives it in the verdict lane (F-6: the display-ahead view's own
async), and that re-derive superseded the frame's flight. The
transaction's landing then re-ran the reader as a stale reader and asked
the frame's question a second time on the same input.

The seam now parks the frame pass's flight (`_parked`) when it re-derives
an async unanswered reader outside a lane, and the landing's `reruns`
adopts it (`adoptFlight`: registered as if the re-derive had returned it,
through a promise of its own so the frame pass's listener cannot land it
twice) instead of re-running the reader. That happens whether the flight
landed before the transaction or is still in the air. Any later pass
drops the parked flight. Asks for m0 = ask(1 + latest(s) + s()) and
m1 = ask(1 + m0()) after setS(2): m0:5, m0:4, m1:5, m0:5, m1:6 become
m0:5, m0:4, m1:5, m1:6 (F-9: a re-ask of the same question is no new
question).

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

* test(web): #4375 hydration pins the asks over latest(s) under R5's adopted frame flight

Over `latest(s)` the write asks m0's frame question first (R5), the
verdict lane's intermediate next, and the landing adopts the frame's
flight: `m0:5, m0:4, m1:5, m1:6`, each question asked once. The control
keeps its `m0:5, m1:6` tail.

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

* fix(signals): an async memo with no verdict window of its own takes no verdict role

#4397 review: with two verdict sources (`m1 = ask(1 + m0() + m0b())` over
`m0 = ask(1 + latest(s) + s())` and `m0b = ask(10 * (latest(t) + t()))`),
the grouped `[isPending(m1), m1()]` reader asked `m1:5` a second time on
stale inputs after both sources landed. m1 lost CONFIG_VERDICT when its
first source went pending, but a pass that threw NotReady at that
source's read picked it back up (F-6, `read`). The settle walk then
re-ran m1 as a verdict reader, and the committed values a hold had moved
past were served to it (A10).

The demotion is now per pass, in `recompute`. An async memo (new
CONFIG_ASYNC: its last answering pass registered a flight) whose pass
opened no isPending/latest window of its own (REACTIVE_WINDOW) does not
become a verdict reader by reading a verdict memo. It waits on its
sources like any derivation (F-9), reads them plainly (D-25), and asks
each question once. The side-effecting clear in `propagateStatus` is
gone, since that path no longer sees such a memo as a verdict reader.

Pinned: two verdict memos into one async memo, grouped and split, with
no question asked twice and equal final screens. A `test.fails` pin
records that the grouped shape still skips the t lane's m1:15/m1:16
asks that split makes: whether a later write merges into an open
transaction or opens its own lane depends on grouping (#4398).

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

* fix(signals): two latest() readers under one async memo ask each question once

- A latest() reader whose frame lands at the seam that would re-derive it
  in the verdict lane keeps its flight: the pass was the landing's (it
  asked the frame question twice in one flush). The check moves from
  core's `_update` into lanes.ts `laneSeams`.
- Verdict-lane work that read a staging re-derives on the committed world
  only while the transaction holding the staging stays parked: judged on
  the flush's transaction, not the lane's parent, and not when the pass's
  own render effect is all that keeps it parked (`heldBy`). The re-derive
  asked a question already asked and showed it over inputs the landing
  replaced.
- The landing's adopt of a parked frame flight is a lane hook
  (`laneAdoptHook`): scheduler.ts no longer imports `adoptFlight`.
- #4398's grouped/split pin passes here: grouped and split ask the same
  questions with latest()'s pass the frame's.

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

* fix(signals): a grouped reader over a held write shows the lane's answer

A reader that groups isPending with the value over a held write
(`[isPending(m1), m1()]`) never showed m1's lane answer, though a plain
value reader did. Two things kept it out:

- A pass served a held node's committed value (frameRead, a stale reader
  of the hold) still kept the node's propagated pending sources, so when
  it went pending on another source it never woke when that source
  settled. frameRead now drops what the served node propagated onto it
  (A15): the landing re-derives it.
- A lane's reveal (reruns on a lane) cleared REACTIVE_FRAME_READ, the
  mark the held transaction's landing needed to re-run the reader. Only
  the transaction's own reruns clear it now.

CONFIG_ASYNC is worked out once, at the end of each pass, from the pass's
`_inFlight` (registered a flight: async; answered sync: not; pending or
errored without a flight: the last answer's), so a plain sync recompute
does no extra work and handleAsync no longer touches it.

refresh(m0) mid-hold now shows the lane's answer for the refreshed
question in every reader shape. Pins for the held write, an equal
landing, and refresh after and mid-hold cover the value, pair and split
readers.

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

* test(signals): name frameRead in the held-shapes pin comment

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

* chore(size): re-record this PR's four sizes over next @ 7ebd4a3

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

* fix(signals): a latest() reader's lane answer reaches the screen whatever the grouping

Three review cases on R5, decided from F-6 (display-ahead readers show
the held change now, beside the held screen, in their own lane; the lane
reveals at its own landing, ahead of the parent's) and D-22/D-23:

- An isPending(m0) effect beside `v${m0()}`, with an unrelated async
  keeping the flush parked: the verdict lane revealed at the write, before
  R5's lane pass of m0 joined it, so m0's reader read m0 outside the shown
  lane and waited for the unrelated async. The verdict lane a re-derived
  latest() reader joins no longer reveals at that seam (laneSeams).
- An async memo over latest(s) held only by its own flight: the lane
  asked m1:120 and its parent landed at the same seam, adopting the frame
  flight before the lane's reader re-ran, so only v220 showed. A parent
  whose lane reveals with readers to re-derive now lands a round later
  (`revealing`), so they show the lane's answer first: v120, then v220,
  inline and with latest(s) extracted into a memo (F-6; Ryan, 2026-10-29,
  after #4284).
- A sync latest() memo's `[isPending(m0), m0()]` reader under an
  unrelated hold showed the frame pass's `true,12` before the lane's
  `false,22`: the same early reveal. It now shows `false,22` directly, as
  next does (D-22: the pass re-derives once as a verdict reader; D-23).

`heldBy` now counts a lane under `t` that only `r`'s own flight keeps
blocked: with the parent landing a round later, a lane pass's flight was
the only thing parking it, and the seam re-derived the pass and asked
its question again.

Size: adoptFlight folds into adoptParked (-16 B) and adopts through
`parked.then()` (-4 B).

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

* docs(signals): changeset for the latest() lane-answer fixes

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

* fix(signals): an async memo over a verdict memo stays a verdict reader (D-25)

Review of #4397: the strict rule (an async memo with no `latest`/`isPending`
window of its own took no verdict role at all) overrode D-25 ("one that
reads a verdict reader's value is ... a verdict reader itself"; "a memo
computes under its own posture"). It caused two regressions against next:

1. Memo-extracted `latest()` (`v = createMemo(() => latest(s))`, `m1` async
   over `v()` and `s()`): m1's pass at the write read the transaction's
   staging and asked m1:220, the lane re-derived it (m1:120), and the
   landing asked m1:220 again. It now asks m1:120 then m1:220, once each,
   as the inline form does (D-22, F-9 exc. 2), showing v120 then v220 (F-6).
2. A reader disposed and remounted while the lane answer was in flight:
   `[isPending(m1), m1()]` in one effect showed true,4 -> true,5 -> false,6
   while `m1()` alone and split showed 4 -> 6. All shapes now show the
   screen, then the landing (F-6: grouped holds like a lone read; D-26).

The strict rule is gone. In its place, D-25's "a node whose fetch a hold
is waiting on is that hold's work" (#3884), kept by the verdict engine
(verdict.ts `holdWork`, behind `holdWorkHook`, like `observeFlightHook`):

- A verdict source going pending does not re-derive an async memo with no
  window of its own: none of its reads changed, and re-derived it was
  served the committed value it had already derived from and asked that
  question again (A10). It waits on the source like any derivation (F-9).
  Except when its own flight is up outside a lane on a new question of the
  source's: that re-run supersedes a flight asked of a value the source
  has moved past (a `refresh()` re-ask is the same question).
- A held re-run of such a memo reads plainly, as a pending one already did
  (#3884): the write that re-ran it is its hold's.
- Its async posture (CONFIG_ASYNC) is kept at each pass end in verdict.ts,
  not computed in core.

Kept from the PR, as review asked: the frame read's drop of a served
flight's pending (A15; now in lanes.ts `laneServed`, behind a hook, run
only for lane work and verdict readers, with no closure), and `reruns`
leaving the stale-reader mark to the transaction at a lane's reveal.

Pins (tests/verdict-memo-flight-no-reask.test.ts): the memo-extracted and
inline forms ask m1:120, m1:220 once and show v110, v120, v220 in every
reader shape (D-22, D-25, F-6); a reader remounted mid-flight shows the
screen, then the landing, grouped as alone or split (F-6, D-26). Both
fail on the previous head.

Size: the floors are +14 B minified over next (was +126). The verdict
scenarios carry the rule: + isPending/latest +339 B. Ledger lines and the
frozen live-server-components floor are re-recorded to the measured sizes
(Size-Exception: floor-caps.json 115,423 -> 115,646 B minified, measured
38,197 B brotli; the maintainer relaxed the size rule for this fix).

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

* test(signals): pin the remount shapes on what holds whatever the timing

CI ran the "remounted mid-flight, then a write" pin slower than local runs:
the landing's `m1:6` was asked before the remount in the grouped run and
after the write in the lone run, so comparing one shape's asks against
another's compared two timings, not two shapes. Run at a later remount
locally, every shape asks `m1:6` alike, and none diverges.

The pin now states what holds at any timing, per shape: no question asked
twice (F-9), the lane's `m1:5` never shown to a reader mounted mid-hold
(D-26), and the grouped `[isPending(m1), m1()]` ending on the landing like
the lone read (F-6). The no-write case still fails on the strict rule's
head, where the grouped remount showed `true,5`.

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

* test(signals): land the extracted pin's flights by hand, not by timer

CI's coverage run (slower than a plain run) let m1's 10 ms lane flight land
before the pin's 2 ms sleep ended, so the later write came after the
landing and the run asked m1:220 where the pin expects m1:125. The pin
compared two timings, not two behaviours.

m1's flights now land only when the test lands them, so the later write
always comes while the write's flights are up. The pin's expectations are
unchanged (D-25, D-22, F-6, F-9), and it still fails on ee0b535's source in
all three reader shapes (the m1:220, m1:120, m1:220 triple ask).

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

* fix(signals): only an async memo with no window of its own is hold work (D-25)

Review of #4397: REACTIVE_WINDOW was wiped at the pass tail, and both of
holdWork's consumers (the pass head and propagateStatus) run between
passes, so `own` was CONFIG_ASYNC alone. The rule hit every async verdict
memo, including one that calls latest()/isPending() itself, and changed
screens against next on the canonical F-6 shape: an async memo over
`${isPending(m0)}:${latest(m0)}` never showed the lane's true:2, and one
over latest(m0) read through [isPending(m1), m1()] skipped l1 and showed
true,l1.

CONFIG_ASYNC is now set at the pass end, while the pass's REACTIVE_WINDOW
is still up, only for a pass that registered a flight and opened no window;
a pass that opened one clears it. A memo that asks is a verdict reader
whoever dirties it (D-25, ruled 2026-10-28), unchanged from next. The held
re-run arm applies only to memos that never asked; removing it reasks
m1:5/m1:104/m1:15 in five pins, so it stays, documented against D-25.

Pins: the two own-window shapes, as next shows them (D-25, F-6). The pin
file takes the hand-landed flights from the split prepared for review, so
no pin races a timer. Ledger re-records dropped: the size ledger is
re-recorded once on next after the batch.

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

* perf(signals): keep D-25's async posture off recompute's hot path

CONFIG_ASYNC is now set where a flight registers (handleAsync) and
cleared by a verdict window (markVerdictReader) or a sync answer, instead
of a holdWork call at every pass's end. The pass-head check is inline.
holdWork only answers propagateStatus.

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

* fix(signals): R5 with #4397 asks each question once

Merging #4397 into #4393 asked three questions twice:

- An async memo over `latest()` of a flight (the own-window pins) took
  R5's frame pass and then a verdict-lane pass that asked the same
  question again. `latest` of a flight serves the committed value in
  either world (D-22), so the seam has nothing to decide. The reader
  joins the verdict lane at once, as a plain read of a flight makes it
  (`observeFlight`). `latest` of a staging stays the seam's (#4365).
- An async memo over a `latest()` memo (D-22 extracted): R5 parked only
  the reader's own flight. The memo's frame flight was superseded by its
  lane pass, and the landing asked `m1:220` again. The seam now parks
  the frame flights of a sync reader's async readers held by the frame,
  and the landing adopts them as it does inline (F-9).

The remount and extracted pins take R5's order: the frame's question
comes first.

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

* fix(signals): a verdict effect's staged read before the frame's transaction re-derives once if the flush parks (D-22)

In R5, latest() opens no transaction. A render effect that is a verdict
reader (`v:${s()} a:${A()} l:${latest(s)}`) read s's staging before the
frame had a transaction, so it stayed the frame's pass. Its held run
then showed `l:0` until the landing. stagedRead now also lists such an
effect for the seam, which re-derives it once as a verdict reader when
the flush parks, as D-22 and D-25 require: it shows `v:0 a:0 l:1`.

This is limited to effects. A memo is not a leaf, and with async memos
the re-run asked its question again. With every memo, the async-gen
case e1-3289-1831 ended on [ROOT-E].

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

* test(signals): pin #4393's beside shape as it.fails for the reader-seat carve (F-6, D-26)

With #4397 merged, m1 is a verdict reader between its flights (D-25) and
m0's lane pass re-derives it into the s lane early, so the t transaction
lands while m1:1122 is in flight. Telling that pass apart as the hold's
work needs a per-pass check; it goes to the carve.

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

* test(signals): land twoVerdicts' flights by hand

The grouped-vs-split ask order raced its 10 ms flights against a 12 ms
sleep on a slow runner (m0b:20 before m1:5). The t write now comes after
the s write's first flights land, while m1's question is up.

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

* perf(signals): keep R5's parked flight off the floors

The recompute clear runs only with the lane engine, and ext() no longer
seeds the field, so builds without lanes drop both (-39 B minified on
every floor).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
ryansolid added a commit that referenced this pull request Oct 30, 2026
The differential proves the table subsumes it: with the mechanism gone the
signals, solid and web suites (incl. #4397's and #4393's pins) stay green,
the 2k × 3 generator screens match next except the two known closed cases,
and the shadow reports 0 classes. The stale-reader mark the same rows fire
(ST → _reruns) re-derives the reader at the landing, and holdWork keeps the
#4397 wait; the pending-source drop had nothing left to change. The oracle
drops the mark with it. + isPending/latest −104 B min; live page −106.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Nov 1, 2026
…efresh (#4576, #4577) (#4585)

* fix(signals): a lane pass served past a refresh retires the pending it carried (#4576)

A derivation that is lane work (it reads an optimistic guess) and is served
the committed value of a refreshing store drops that refresh from its own
pending sources (`laneServed`, #4397). The render effect the refresh's
pending had propagated onto through it kept the mark: the refresh's landing
walk stopped at the derivation, which no longer carried the source, so the
effect stayed pending on a flight that had already landed. A filter change
held with it never committed once the second async memo answered last.

The drop now retires the source from the dependents that carried it through
the derivation (`settlePendingSource`, which keeps it where another path
still reaches it). A hold lasts only while an on-screen reader observes a
flight it waits on (D-11), so the change commits once every ask has
answered (F-2), settling as one with what shares the screen (D-9) and whole
on the way (F-1). Lanes engine only; no core-path change.

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

* fix(signals): lane work re-based over a parked frame is not re-derived on the committed world (#4577)

Re-derived at the seam, a re-based lane pass paired the frame's new rows
with the old filter, and the landing then showed that beside the new
filter (F-1, D-34 ¶3).

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

* fix(signals): a re-based lane reads its frame's world in every flush, through merges (#4577)

A second async change merged into the parked filter frame mid-hold; the
lane's later passes then read the screen and re-asked over the old
filter, and the landing committed that beside the new one (F-1, D-9).
This also covers #4576, so laneServed is back to its next form.

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

* chore: drop an accidentally committed node_modules symlink

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

* fix(signals): restore laneServed retiring the carried pending (#4576)

Without it a nested action's refresh answers were lost (the store ended
[2, 5] instead of [5]).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
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