Repository navigation
fix(signals): a first answer landing in the tick a write re-asks it is answered, then re-asked (D-22, F-9) - #4459
Conversation
…s answered, then re-asked (D-22, F-9) An async memo's first answer resolves, and in the same synchronous turn a plain write re-asks it; the promise reaction runs after the write, so the landing and the re-ask are processed in one flush. The runtime treated the answer as never produced: - `asyncWrite` dropped it outright when the write's notification had marked the node dirty (a stale-answer guard meant for re-asks). - Otherwise it landed staged, the re-ask left the node pending and uninitialized, and `isPending` gave it no verdict (`false` from the flush's probe, never re-run), while an identity memo or a derived store over it never derived from it (`latest` of either threw: [ROOT-L]). Now a first answer always lands; `isPending` of a node answered and re-asked in one flush is the re-ask's verdict (true); and a memo with no value of its own that reads it derives from the answer once, then re-runs on the missed-wake latch and waits on the re-ask holding it (a verdict engine hook, so the hello-world floor pays only the call). Amends D-22's 2026-10-28 clause per the 2026-10-30 ruling: a plain write re-asking the memo carries the answer like an action's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…s answered, then re-asked (D-22, F-9) An async memo's first answer resolves, and in the same synchronous turn a plain write re-asks it; the promise reaction runs after the write, so the landing and the re-ask are processed in one flush. The runtime treated the answer as never produced: - `asyncWrite` dropped it outright when the write's notification had marked the node dirty (a stale-answer guard meant for re-asks). - Otherwise it landed staged, the re-ask left the node pending and uninitialized, and `isPending` gave it no verdict (`false` from the flush's probe, never re-run), while an identity memo or a derived store over it never derived from it (`latest` of either threw: [ROOT-L]). Now a first answer always lands; `isPending` of a node answered and re-asked in one flush is the re-ask's verdict (true); and a memo with no value of its own that reads it derives from the answer once, then re-runs on the missed-wake latch and waits on the re-ask holding it (in `read`'s pending path, behind the verdict engine's hook so the floor folds it). Amends D-22's 2026-10-28 clause per the 2026-10-30 ruling: a plain write re-asking the memo carries the answer like an action's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…-x1ore2-reask-landing-answered
…ask's flush The reader-seat table (#4438) predicted NotReady (row D3) for a memo with no value reading a first answer staged in the flush that re-asks it; under D-22 (amended 2026-10-30) the memo derives from it once. Row D3' predicts the staging (P), and `read` records the slot it serves. __TEST__ only: dist is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
isPending/latest +37 B brotli (+151 B minified) and the compiled live page: the first-answer serve in `read`'s pending path and the `isPending` verdict for it. The observe-attribution and compiled scenarios carry the core half (+10 B minified on every floor; the rest is brotli layout). Caps at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…st answer 38.76 KB -> 38.79 KB, recorded minified 117,227 -> 117,379 B, measured at 38,778 B brotli. Size-Exception in the PR body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: 80a6417 The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 packages
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 |
…wered Ledger conflicts in scripts/size/scenarios.js resolved to next's entries, then re-measured over next @ 5f07d04f: isPending/latest 11.15 KB (30,730), compiled live 42.78 KB (125,611); frozen floors simple-app 9.80 KB (26,762), hydrating 18.78 KB (53,608), base SC 33.68 KB (101,628), live SC 38.82 KB (117,464). Size-Exception in the PR body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Size (brotli, eager entry chunk)
|
Coverage Report for CI Build 41109677117Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage remained the same at 78.1%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will degrade performance by 8.84%2 warnings
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
|
CodSpeed's two regressions are noise, not this change:
The 5 "improvements" are the same runner swing in the other direction. Generated by Claude Code |
…-x1ore2-reask-landing-answered # Conflicts: # scripts/size/floor-caps.json # scripts/size/scenarios.js
…-x1ore2-reask-landing-answered
…-x1ore2-reask-landing-answered # Conflicts: # scripts/size/scenarios.js
…eader waiting on it, off lanes, at any depth read()'s D-22 branch served the staged first answer to a reader that did not wait on it (a first pass: the seat table's D3 row predicts NR) and to a verdict lane's pass (V6: D-37, a lane sees the screen plus its own guesses). It now requires passLane === null and that the reader wait on the source a read of the node parks on (_error.source): the node itself one level down, the async it derives from at any depth, so identity(identity(m)) reads as m does (E2 at depth 2). The seat's D3' row says the same (!inSeat, the parked-on source, no staging of its own). handleAsync's answered-then-reasked guard is the verdict engine's (holdWorkHook): plain-only bundles keep the plain return. D-23 exception 1: an answered-then-reasked node is not a first load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…-x1ore2-reask-landing-answered 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
isPending/latest, compiled live SC and live + router grow with the verdict-engine branch (+159..+161 B minified); no frozen floor moves: plain-only bundles are unchanged in minified bytes. 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
|
I don't think CodSpeed's −6.6% on Generated by Claude Code |
…-x1ore2-reask-landing-answered
…flush, out of line read()'s D-22 arm served a reader whose pass then threw anyway (a read beside the served one parked on the same source): the throw re-parked it on that source, the missed-wake latch re-ran it, and the re-run qualified again — a synchronous loop within one flush (async-gen 91501 #749, minimized: a boundary's children memo over two Shows that both park on the re-asked memo). The arm now stamps the served pass's clock on the reader (`_x._served`) and serves a reader once a flush; the re-run throws and waits. The seat's D3' row says the same. The arm moves out of read() into `servesStaged`, called behind `observeFlightHook` on the pending-throw path, so read() itself grows by a call (bytecode 1318 -> 1170; next 1139). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…its landing released servesStaged served a reader with no value only while it still waited on the source. When the memo's input is read through latest() or a sync memo, the write's re-ask reaches the memo after its landing ran, and the landing had already released the identity memo's wait: it ran with no value, waiting on nothing, and threw, so latest(identity(m)) and a derived store stayed on [ROOT-L] while latest(m) showed the answer (async-gen E2/E3 3289 #310, 91501 #1993, #2609). D-22 as amended says a derivation with no value of its own reads like the memo at any depth; which the flush ran first is not the reader's. A reader that is not pending is served too (one served pass a flush, as before); one pending on another source is not. The seat's D3' row says the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…-x1ore2-reask-landing-answered # Conflicts: # scripts/size/floor-caps.json # scripts/size/scenarios.js
…30c670 Live server components 38.85 -> 38.96 KB (117,747; Size-Exception in the PR body), isPending/latest 11.17 -> 11.26 KB (31,002), compiled live 42.79 -> 42.85 KB (125,887); live + router re-records minified (143,150). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…-x1ore2-reask-landing-answered # Conflicts: # packages/signals/docs/RULES-DERIVED.md # scripts/size/floor-caps.json # scripts/size/scenarios.js
…0fc285 Takes next's ledger (recorded after #4471/#4479) and re-records only the scenarios this PR moves: isPending/latest 11.19 -> 11.28 KB (31,053), live server components 38.91 -> 38.96 KB (117,793; Size-Exception in the PR body), compiled live 42.83 -> 42.87 KB (125,938), live + router 48.51 -> 48.58 KB (143,201). 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
…4475, #4478, #4459) (#4501) Next @ 643a7a7 sits over four caps after the batch merged back to back (each fix PR carried no ledger edits, per the batch process). Re-records, measured over next @ 643a7a7 (linux): - signals: + isPending/latest: 11.19 -> 11.29 KB, minified 30,840 -> 31,101 - page: live server components (frozen floor): 38.91 -> 38.98 KB, minified 117,566 -> 117,858 - page: compiled live server components: 42.87 -> 42.95 KB, minified 125,724 -> 126,018 - page: live + router: 48.51 -> 48.58 KB, minified 142,987 -> 143,269 and the recorded minified only (caps unchanged) where the batch drifted more than 20 B: hydrating + every store (98,463 -> 98,535), base server components (frozen, 101,638 -> 101,675), compiled base server components (108,007 -> 108,048), base + router (125,470 -> 125,503). Hello world is unchanged at 9,973 B (cap 9.98 KB). Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 Co-authored-by: Claude <noreply@anthropic.com>
…des the write's flush (#4495) (#4499) * fix(signals): a first answer landing in the tick a write re-asks it is answered, then re-asked (D-22, F-9) An async memo's first answer resolves, and in the same synchronous turn a plain write re-asks it; the promise reaction runs after the write, so the landing and the re-ask are processed in one flush. The runtime treated the answer as never produced: - `asyncWrite` dropped it outright when the write's notification had marked the node dirty (a stale-answer guard meant for re-asks). - Otherwise it landed staged, the re-ask left the node pending and uninitialized, and `isPending` gave it no verdict (`false` from the flush's probe, never re-run), while an identity memo or a derived store over it never derived from it (`latest` of either threw: [ROOT-L]). Now a first answer always lands; `isPending` of a node answered and re-asked in one flush is the re-ask's verdict (true); and a memo with no value of its own that reads it derives from the answer once, then re-runs on the missed-wake latch and waits on the re-ask holding it (a verdict engine hook, so the hello-world floor pays only the call). Amends D-22's 2026-10-28 clause per the 2026-10-30 ruling: a plain write re-asking the memo carries the answer like an action's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(signals): a first answer landing in the tick a write re-asks it is answered, then re-asked (D-22, F-9) An async memo's first answer resolves, and in the same synchronous turn a plain write re-asks it; the promise reaction runs after the write, so the landing and the re-ask are processed in one flush. The runtime treated the answer as never produced: - `asyncWrite` dropped it outright when the write's notification had marked the node dirty (a stale-answer guard meant for re-asks). - Otherwise it landed staged, the re-ask left the node pending and uninitialized, and `isPending` gave it no verdict (`false` from the flush's probe, never re-run), while an identity memo or a derived store over it never derived from it (`latest` of either threw: [ROOT-L]). Now a first answer always lands; `isPending` of a node answered and re-asked in one flush is the re-ask's verdict (true); and a memo with no value of its own that reads it derives from the answer once, then re-runs on the missed-wake latch and waits on the re-ask holding it (in `read`'s pending path, behind the verdict engine's hook so the floor folds it). Amends D-22's 2026-10-28 clause per the 2026-10-30 ruling: a plain write re-asking the memo carries the answer like an action's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * test(signals): the seat shadow seats a first answer served in its re-ask's flush The reader-seat table (#4438) predicted NotReady (row D3) for a memo with no value reading a first answer staged in the flush that re-asks it; under D-22 (amended 2026-10-30) the memo derives from it once. Row D3' predicts the staging (P), and `read` records the slot it serves. __TEST__ only: dist is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): raise caps for the same-tick first answer isPending/latest +37 B brotli (+151 B minified) and the compiled live page: the first-answer serve in `read`'s pending path and the `isPending` verdict for it. The observe-attribution and compiled scenarios carry the core half (+10 B minified on every floor; the rest is brotli layout). Caps at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): raise the live page's frozen floor for the same-tick first answer 38.76 KB -> 38.79 KB, recorded minified 117,227 -> 117,379 B, measured at 38,778 B brotli. Size-Exception in the PR body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(signals): serve an answered-then-reasked first answer only to a reader waiting on it, off lanes, at any depth read()'s D-22 branch served the staged first answer to a reader that did not wait on it (a first pass: the seat table's D3 row predicts NR) and to a verdict lane's pass (V6: D-37, a lane sees the screen plus its own guesses). It now requires passLane === null and that the reader wait on the source a read of the node parks on (_error.source): the node itself one level down, the async it derives from at any depth, so identity(identity(m)) reads as m does (E2 at depth 2). The seat's D3' row says the same (!inSeat, the parked-on source, no staging of its own). handleAsync's answered-then-reasked guard is the verdict engine's (holdWorkHook): plain-only bundles keep the plain return. D-23 exception 1: an answered-then-reasked node is not a first load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(signals): type the parked-on source read in read() and the D3' row Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): re-record #4459's caps over next @ 131ceb8 isPending/latest, compiled live SC and live + router grow with the verdict-engine branch (+159..+161 B minified); no frozen floor moves: plain-only bundles are unchanged in minified bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): re-record the live page floor's minified for #4459 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(signals): serve an answered-then-reasked first answer one pass a flush, out of line read()'s D-22 arm served a reader whose pass then threw anyway (a read beside the served one parked on the same source): the throw re-parked it on that source, the missed-wake latch re-ran it, and the re-run qualified again — a synchronous loop within one flush (async-gen 91501 #749, minimized: a boundary's children memo over two Shows that both park on the re-asked memo). The arm now stamps the served pass's clock on the reader (`_x._served`) and serves a reader once a flush; the re-run throws and waits. The seat's D3' row says the same. The arm moves out of read() into `servesStaged`, called behind `observeFlightHook` on the pending-throw path, so read() itself grows by a call (bytecode 1318 -> 1170; next 1139). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(signals): an answered-then-reasked first answer reaches a reader its landing released servesStaged served a reader with no value only while it still waited on the source. When the memo's input is read through latest() or a sync memo, the write's re-ask reaches the memo after its landing ran, and the landing had already released the identity memo's wait: it ran with no value, waiting on nothing, and threw, so latest(identity(m)) and a derived store stayed on [ROOT-L] while latest(m) showed the answer (async-gen E2/E3 3289 #310, 91501 #1993, #2609). D-22 as amended says a derivation with no value of its own reads like the memo at any depth; which the flush ran first is not the reader's. A reader that is not pending is served too (one served pass a flush, as before); one pending on another source is not. The seat's D3' row says the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): re-measure the answered-then-reasked serve over next @ 230c670 Live server components 38.85 -> 38.96 KB (117,747; Size-Exception in the PR body), isPending/latest 11.17 -> 11.26 KB (31,002), compiled live 42.79 -> 42.85 KB (125,887); live + router re-records minified (143,150). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): re-measure the answered-then-reasked serve over next @ d0fc285 Takes next's ledger (recorded after #4471/#4479) and re-records only the scenarios this PR moves: isPending/latest 11.19 -> 11.28 KB (31,053), live server components 38.91 -> 38.96 KB (117,793; Size-Exception in the PR body), compiled live 42.83 -> 42.87 KB (125,938), live + router 48.51 -> 48.58 KB (143,201). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(signals): every answer resolved in the tick a write re-asks it rides the write's flush (#4495) Two async memos m1, m2 answer, then a write re-asks both, in one tick. m1's landing flushed synchronously (`asyncWrite`), and that flush carried the queued write: it re-asked m2 before m2's answer's reaction ran, so the answer arrived superseded (`_inFlight !== result`) and was dropped. A memo over both then had no value and `latest` of it threw (D-22 says 13). A landing that arrives while a write's flush is queued (a staged value in `pendingNodes`, captured before the landing stages its own) no longer flushes: the write's flush carries it with every answer whose reaction is queued ahead of it (F-10), each answered, then re-asked (D-22, F-9). Only `latest` and `isPending` tell the difference, so it is gated on the verdict engine (`holdWorkHook`); a stream's next pull still waits on its landing's flush. Size: isPending/latest 11.28 -> 11.30 KB (31,087 B), compiled live server components 42.87 -> 42.90 KB (125,978 B); hello world unchanged. 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>
…pagation is committed and pending (R2 ruled) (#4571) * test(signals,web): pin #4569 at both screens — a first load's answer to an upstream-superseded question a0 = async(s); a2 = async(await a0()); <Loading>{isPending(() => a2()) ? pending : settled}</Loading>. a0:0 lands, a2 asks a2:11; setS(1) re-asks a0 and marks a2 pending by propagation while a2:11 is up and a2 has no value; a2:11 lands. Today / R2 (#3373, #4459: one question per node): the landing initializes a2, which stays pending on a0; the boundary reveals with pending — loading ×3, pending, pending, settled. Pinned as it. R1 (reporter, F-9 'a first load is loading, not pending'): the answer is to a superseded world; a2 stays loading until the chain settles — loading ×5, settled. Pinned as it.fails. Also pinned: the 3-deep chain and the no-boundary root (same decision), and two undisputed controls — a plain read of a2 under the boundary, alone or beside the probe, already gives R1's frames (the reader waits on the pending node); only the probe-alone content reveals. Tests only; no src changes. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com> * test(signals): pin #4569 — a first load answered while pending by propagation is committed and pending (R2 ruled) Ruled 2026-10-31: a node's question changes only when the node itself re-runs (#3373). a2:11, landing while a2 is pending by propagation from a0's re-ask and has no committed value, initializes a2: committed and pending on a0. Content that reads only its verdict reveals with pending (S-40); content that reads a2 holds until the chain settles (F-2, no screen to keep). The asymmetry is the rule. Frames: loading ×3, pending, pending, settled. The rejected reading (loading ×5, settled) is documented, not pinned: it needs a supersession by propagation no rule has and a new resting state, treats an uninitialized node more strictly than a committed one, and contradicts #4459/#4495's p:true first reveals. Docs: D-23 clause + Collapses ref; F-9 'What it does not say' line (on creation is the node's, not a boundary's first frame); S-40 verdict form with the frames and the plain-read control. No src change. Closes #4569 Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Before: an async memo's first answer resolves, and in the same synchronous turn a plain write re-asks the memo. The promise reaction runs after the write, so the landing and the re-ask are processed in one flush, and the runtime treated the answer as never produced:
isPendinggave it no verdict: it readfalsefrom the flush's probe, and the node never re-ran.latestof either threw and the screen showed[ROOT-L]. Plainlatest(m0)showed the answer.The async generator found these on rc.20: pairs E2/E3 for
[s0=2 m0=async(s0)] <span split[latest(m0)]>with stepst S6 s0=0 t, and the F-9isPendingcase.After: a first answer always lands. A node that is answered and re-asked in one flush gets the re-ask's verdict from
isPending, which istrue. A memo with no value of its own that is waiting on that answer derives from it once, then re-runs on the missed-wake latch and waits on the re-ask. This holds at any depth:identity(identity(m))reads likem. Plain, identity-memo and derived-store reads now show the same screen.That includes a reader whose input goes through
latest()or a sync memo, where the landing's settle walk had already released the reader before the re-ask reached it. These were the E2 residual cases: generator 3289 #310 and 91501 #1993/#2609.The 2026-10-30 ruling (Q1 = A) amends D-22's 2026-10-28 clause: a plain write that re-asks the memo carries the answer the same way an action's write does.
RULES-DERIVED.mdis updated, next to #4490's #4474 ruling in D-22, which stands as written. D-23 exception 1 now says that a node answered and then re-asked in the same flush is not a first load.Same-tick screen vs the two-tick orders. Readers mounted in the tick where the answer lands and the write re-asks show
l:2 p:trueon their first pass, thenl:3. That is the answer-first order's screen without its intermediatep:falseframe. In the write-first order the answer arrives after the re-ask, so it is stale, and the fallback stays untill:3. D-22 as amended rules the same-tick case: "an async memo's first answer that lands in the same tick as a write to its input that a hold carries — an action's, or a plain write re-asking it — is held with it (D-4), andlatestof it returns that answer while the plain read still throws (F-9); aLoadingwhose only read islatestof it shows the answer now (S-40)." D-23 exception 1 rules the verdict: such a node "is not a first load …isPendingof it istrue". No rule compares the same-tick screen with the write-first order.How
handleAsynckeeps a first answer even when the node is dirty. The keep is gated onholdWorkHook, because onlylatestandisPendingcan tell such a node apart from one with no value. Plain-only bundles keep the old guard and pay 0 B.readwould throw on a pending node, a cold helperservesStageddecides whether to serve the staged first answer instead.readcalls it behindobserveFlightHookon the pending-throw path, and its own bytecode drops from 1,318 to 1,170 (next: 1,139). It serves only when:elitself, or the asyncelderives from at any depth);_x._servedrecords the clock). Without that, a boundary's children memo that reads twoShows parking on the same re-asked memo was served, threw on the second read, parked again, and was served again, without end.isPending(verdict.ts) treats a node answered and re-asked in one flush as pending.servesStaged. The table is__TEST__only, so dist is unchanged.Size vs next @ d0fc285 (after #4471, #4479 and #4490), measured locally (linux), brotli / minified:
page: live server components(frozen floor)signals: + isPending/latestpage: compiled live server componentspage: live + routerAll four are over the 20 B flag. The once-per-flush guard and the cold helper account for about +58 B minified of the total. The released-reader case adds +13.
Size-Exception: page: live server components 38.91 KB -> 38.96 KB (recorded minified 117,566 -> 117,793 B, +227 B): an answered-then-reasked first answer lands and is served once a flush, out of line, including to a reader its landing released; correctness (D-22 as amended 2026-10-30, F-9). The code sits behind the verdict engine's hooks, so the core and app floors do not move in minified size.
How did you test this change?
New
packages/signals/tests/answered-then-reasked-same-tick.test.tscovers the landing, theisPendingverdict, and identity-memo (one and two levels deep) and derived-store reads.New
packages/signals/tests/answered-then-reasked-released-reader.test.tscovers the released-reader shapes; all three fail without the fix.New
packages/web/test/async-gen/same-tick-first-answer-reask.spec.tsande2-released-reader.spec.tsreplay the minimized generator scenarios.New
packages/web/test/async-gen/reask-served-once-a-flush.spec.tsreplays the hang in four variants (six[ROOT-L]screens). It hangs without the once-per-flush guard.Suites after merging next @ d0fc285:
The async generator with the seat shadow ran on seeds 3289, 7 and 91501, 3,000 cases each, pairs E1/E2/E3/E9, with a timeout as the hang guard. There were no hangs and no new mismatch classes. The E2 residual cases now pass. One non-settle screen changed: seed 7 DEV build seems to have O(n^2) in registerGraph name resolution for createSignal in onMount when using signals from props #1322 now shows
latest(m1)of an identity memo the same aslatest(m0)(parity).Two sources answered and re-asked in one tick (
c = m1() + m2()) still lose the second answer; that is 2.0: two async sources answered and re-asked in one tick: the second answer is dropped, latest() of their memo throws #4495, which throws on next too.Found by the async-gen rerun on rc.20 (group 5).
🤖 Generated with Claude Code
https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2