Skip to content

fix(signals): a verdict reader born into a hold joins it at its re-pass (#4467) - #4471

Merged
ryansolid merged 3 commits into
nextfrom
claude/project-thread-x1ore2-fallback-fn-latest
Oct 30, 2026
Merged

ryansolid merged 3 commits into
nextfrom
claude/project-thread-x1ore2-fallback-fn-latest

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Before: a memo that joined an action's hold (born held) can mount a Loading and call the fallback's returned function in its own pass. If that function read latest(x), the fallback was rebuilt without end until "Potential Infinite Loop Detected". A memo of latest(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:C when the content lands.

Why it looped:

  1. Calling the returned function runs latest(x) in the mount's own pass, which leaves the mount a verdict reader (D-25: the posture outlives the pass).
  2. When the content lands, the mount re-runs while it is still born held. Recompute's held re-pass arm skips the join for any verdict reader, so the mount's Loading creates its tree with no transaction.
  3. The fresh boundary catches its own tree and returns the fallback.
  4. The output then re-derives, which re-runs the mount, which mounts another boundary.

How: in that arm (core.ts recompute), a verdict reader that is bornInto the 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. frameRead already exempts these passes the same way. The added condition sits behind observeFlightHook, so bundles without the verdict engine fold it away. It is only evaluated inside the CONFIG_HELD branch, so ordinary passes do no extra work.

Size vs next @ 749b0d7 (after #4456), measured locally (linux), brotli / minified:

scenario delta cap
hello world, core floor, every plain-only scenario 0 / 0 unchanged
signals: + isPending/latest +13 / +29 unchanged at 11.17 KB; minified re-recorded
page: live server components (frozen floor) +49 / +30 38.85 → 38.91 KB (Size-Exception)
page: compiled live server components +40 / +29 42.79 → 42.83 KB
page: live + router −36 / +29 unchanged at 48.51 KB; minified re-recorded

The 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:

    suite passed expected fail
    signals 6,047 11
    web 1,564 6

Fixes #4467


🤖 Generated with Claude Code

https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2

…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-bot

changeset-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ce3e818

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

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

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

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

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.42 KB 0 B 0 B +15 B 7.43 KB ✅
signals: + createStore 14.96 KB 0 B 0 B +6 B 14.97 KB ✅
signals: + isPending/latest 11.17 KB +13 B (+0.1%) +29 B 0 B 11.17 KB ✅
signals: + until/refresh (truth readers, no optimism) 8.05 KB 0 B 0 B +2 B 8.06 KB ✅
app: render + one signal (the simple-app floor) 9.78 KB 0 B 0 B +15 B 9.78 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.75 KB 0 B 0 B 0 B 18.74 KB ⚠️ over by 12 B, 20 B minified headroom lazy-page.js 0.04 KB
app: hydrating + every store primitive family 32.38 KB 0 B 0 B +13 B 32.48 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.45 KB 0 B 0 B +9 B 13.47 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.19 KB 0 B 0 B +9 B 15.20 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.75 KB 0 B 0 B +9 B 29.78 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.97 KB 0 B 0 B +2 B 9.98 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.33 KB 0 B 0 B +13 B 26.33 KB ⚠️ over by 3 B, 7 B minified headroom stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.45 KB 0 B 0 B +7 B 32.54 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.75 KB 0 B 0 B 0 B 11.77 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.67 KB 0 B 0 B +9 B 33.77 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.03 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 38.90 KB +49 B (+0.1%) +30 B 0 B 38.91 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.03 KB, wire.js 3.47 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.72 KB 0 B 0 B +9 B 35.72 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 9.01 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 42.82 KB +40 B (+0.1%) +29 B 0 B 42.83 KB ✅ eager (counted): web.js 22.85 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.05 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.11 KB 0 B 0 B +7 B 41.25 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, server.js 1.03 KB, serverForms.js 3.57 KB, trace.js 9.03 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 48.46 KB −36 B (−0.1%) +29 B 0 B 48.51 KB ✅ eager (counted): client.js 27.99 KB; assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, server.js 1.03 KB, serverForms.js 3.30 KB, trace.js 9.03 KB, wire.js 3.49 KB
server: floor (getRequestEvent + isServer) 1.27 KB 0 B 0 B 0 B 1.29 KB ✅
server: renderToString (the server-render floor) 22.79 KB 0 B 0 B 0 B 22.80 KB ✅

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

  • app: hydrating (no stores) with Show/For/Loading/Errored/lazy: over brotli cap by 12 B; minified 53,607 B vs 53,607 B recorded with the cap (+0 B) — 20 B of the 20 B minified allowance left; +0 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 3 B; minified 80,048 B vs 80,035 B recorded with the cap (+13 B) — 7 B of the 20 B minified allowance left; +0 B minified over this PR's base

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

@coveralls

coveralls commented Oct 30, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 41096930237

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage remained the same at 78.1%

Details

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

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

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

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 9.27%

2 warnings

⚠️ 4 benchmarks are dominated by system calls

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

Learn more about system calls

See affected benchmarks

⚠️ 2 benchmarks with a significant change ran on different hardware

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

Learn more about Macro Runners

See affected benchmarks

⚡ 2 improved benchmarks
✅ 198 untouched benchmarks
⏩ 5 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ memo + sync render effect only (reference) 34.8 ms 31.6 ms +10.25%
⚡ arrow in template 12.2 ms 11.3 ms +8.29%

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-fallback-fn-latest (ce3e818) with next (749b0d7)

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

…-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
@ryansolid
ryansolid merged commit 663ecb0 into next Oct 30, 2026
8 checks passed
ryansolid pushed a commit that referenced this pull request Oct 30, 2026
…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
ryansolid added a commit that referenced this pull request Oct 30, 2026
…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>
ryansolid added a commit that referenced this pull request Oct 30, 2026
…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>
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