Repository navigation
fix(signals): a verdict reader born into a hold joins it at its re-pass (#4467) - #4471
Conversation
…ss (#4467) A memo that read an action's held `x` is born held. When it mounts a `Loading` and calls the function the fallback returned, `latest(x)` runs in the memo's own pass and leaves CONFIG_VERDICT on it (D-25: the posture outlives the pass). At the content's landing the memo re-runs, still born into the hold and never committed, but recompute's held re-pass arm skipped the join for any verdict reader. The flush had no transaction, so the boundary the re-run mounted was not the hold's: it caught its own born-held tree (`freshHook(el, !mount && flushTransaction)`) and showed the fallback. The output then re-derived to the landed content, the memo re-ran, mounted another boundary, and so on without end. A pass born into its hold's world is the hold's own work and reads plainly whatever it asked before (D-25, D-27 #4341; `frameRead` already exempts it through `bornInto`). The held re-pass now joins it too, so the boundary it mounts is the hold's and appears at its commit (D-13): the screen goes from the held frame straight to "1:C". The check sits behind `observeFlightHook`, so bundles without the verdict engine are unchanged. Size: +29 B minified on the isPending/latest scenarios, 0 B elsewhere. page: compiled live server components re-recorded 42.73 -> 42.76 KB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: ce3e818 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 |
Size (brotli, eager entry chunk)
|
Coverage Report for CI Build 41096930237Warning 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 improve performance by 9.27%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
…-x1ore2-fallback-fn-latest # Conflicts: # scripts/size/scenarios.js
Live server components 38.85 -> 38.91 KB (117,544; Size-Exception in the PR body), compiled live 42.79 -> 42.83 KB (125,684); isPending/latest and live + router re-record minified with caps unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…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
…s answered, then re-asked (D-22, F-9) (#4459) * 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 --------- 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>
Before: a memo that joined an action's hold (born held) can mount a
Loadingand call the fallback's returned function in its own pass. If that function readlatest(x), the fallback was rebuilt without end until "Potential Infinite Loop Detected". A memo oflatest(x)called that way looped the same way. Fallbacks that return a string were fine.After: the fallback builds once, and every form settles on
1:Cwhen the content lands.Why it looped:
latest(x)in the mount's own pass, which leaves the mount a verdict reader (D-25: the posture outlives the pass).Loadingcreates its tree with no transaction.How: in that arm (
core.tsrecompute), a verdict reader that isbornIntothe hold (never committed), and is not a render effect, boundary tree or boundary output, now joins like any other held pass. This follows D-25: the hold's own work, "a pass born in its world, reads plainly whatever it asked before". It also follows D-13: a boundary mounted by a joined pass appears at the commit with no fallback.frameReadalready exempts these passes the same way. The added condition sits behindobserveFlightHook, so bundles without the verdict engine fold it away. It is only evaluated inside theCONFIG_HELDbranch, so ordinary passes do no extra work.Size vs next @ 749b0d7 (after #4456), measured locally (linux), brotli / minified:
signals: + isPending/latestpage: live server components(frozen floor)page: compiled live server componentspage: live + routerThe live floor's +49 B and compiled live's +40 B brotli are over the 20 B flag. That is layout noise on +30 B of minified code: over next @ 131ceb8 the same change measured −12 and +27.
Size-Exception: the live server-components page carries the D-25/D-13 join for a verdict reader born into a hold, which ends #4467's fallback rebuild loop (+30 B minified, +49 B brotli). Cap 38.85 → 38.91 KB; recorded minified 117,514 → 117,544. Correctness.
Tests:
New
packages/signals/tests/fallback-fn-latest-runaway-4467.test.ts, four fallback forms. Two of them fail on next, and all four pass here.Suite results after merging next @ 749b0d7:
Fixes #4467
🤖 Generated with Claude Code
https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2