Skip to content

fix(signals): a first answer landing in the tick a write re-asks it is answered, then re-asked (D-22, F-9) - #4459

Merged
ryansolid merged 24 commits into
nextfrom
claude/project-thread-x1ore2-reask-landing-answered
Oct 30, 2026
Merged

ryansolid merged 24 commits into
nextfrom
claude/project-thread-x1ore2-reask-landing-answered

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

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:

  • If the write's notification had already marked the node dirty, the answer was dropped outright. The stale-answer guard that did this was meant for re-asks.
  • Otherwise the answer landed staged, but the re-ask left the node pending and uninitialized. isPending gave it no verdict: it read false from the flush's probe, and the node never re-ran.
  • An identity memo or a derived store over the memo never derived from the answer, so latest of either threw and the screen showed [ROOT-L]. Plain latest(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 steps t S6 s0=0 t, and the F-9 isPending case.

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 is true. 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 like m. 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.md is 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:true on their first pass, then l:3. That is the answer-first order's screen without its intermediate p:false frame. In the write-first order the answer arrives after the re-ask, so it is stale, and the fallback stays until l: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), and latest of it returns that answer while the plain read still throws (F-9); a Loading whose only read is latest of it shows the answer now (S-40)." D-23 exception 1 rules the verdict: such a node "is not a first load … isPending of it is true". No rule compares the same-tick screen with the write-first order.

How

  • Landing. handleAsync keeps a first answer even when the node is dirty. The keep is gated on holdWorkHook, because only latest and isPending can tell such a node apart from one with no value. Plain-only bundles keep the old guard and pay 0 B.
  • Serving. When read would throw on a pending node, a cold helper servesStaged decides whether to serve the staged first answer instead. read calls it behind observeFlightHook on the pending-throw path, and its own bytecode drops from 1,318 to 1,170 (next: 1,139). It serves only when:
    • the reader is a memo with no value that waits on the source the read parks on (that source is el itself, or the async el derives from at any depth);
    • or the reader carries the missed-wake latch;
    • or the landing released the reader (no longer pending);
    • and the reader is not a render effect, lane work, a verdict lane's pass (D-37), a held node, or a staged error.
  • Once per flush. A reader is served at most once a flush (_x._served records the clock). Without that, a boundary's children memo that reads two Shows parking on the same re-asked memo was served, threw on the second read, parked again, and was served again, without end.
  • Verdict. isPending (verdict.ts) treats a node answered and re-asked in one flush as pending.
  • Seat table. The reader-seat table gains row D3', with the same conditions as servesStaged. The table is __TEST__ only, so dist is unchanged.

Size vs next @ d0fc285 (after #4471, #4479 and #4490), measured locally (linux), brotli / minified:

scenario delta cap
hello world (9,973 B), every other floor, every plain-only scenario 0 B minified unchanged
page: live server components (frozen floor) +101 / +227 38.91 → 38.96 KB; recorded minified 117,566 → 117,793 (Size-Exception)
signals: + isPending/latest +90 / +231 11.19 → 11.28 KB
page: compiled live server components +35 / +232 42.83 → 42.87 KB
page: live + router +70 / +232 48.51 → 48.58 KB

All 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.ts covers the landing, the isPending verdict, and identity-memo (one and two levels deep) and derived-store reads.

  • New packages/signals/tests/answered-then-reasked-released-reader.test.ts covers the released-reader shapes; all three fail without the fix.

  • New packages/web/test/async-gen/same-tick-first-answer-reask.spec.ts and e2-released-reader.spec.ts replay the minimized generator scenarios.

  • New packages/web/test/async-gen/reask-served-once-a-flush.spec.ts replays the hang in four variants (six [ROOT-L] screens). It hangs without the once-per-flush guard.

  • Suites after merging next @ d0fc285:

    suite passed expected fail
    signals 6,065 17
    web 1,573 7
  • 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 as latest(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

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

changeset-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 80a6417

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

…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
@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.41 KB −3 B (−0.0%) 0 B +15 B 7.43 KB ✅
signals: + createStore 14.96 KB +4 B (+0.0%) 0 B +6 B 14.97 KB ✅
signals: + isPending/latest 11.27 KB +85 B (+0.8%) +231 B +231 B 11.19 KB ❌ over by 80 B
signals: + until/refresh (truth readers, no optimism) 8.05 KB −6 B (−0.1%) 0 B +2 B 8.06 KB ✅
app: render + one signal (the simple-app floor) 9.79 KB +8 B (+0.1%) 0 B +15 B 9.78 KB ⚠️ over by 8 B, 5 B minified headroom
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.72 KB −34 B (−0.2%) 0 B 0 B 18.74 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 32.41 KB −51 B (−0.2%) 0 B 0 B 32.48 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.45 KB −5 B (−0.0%) 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 +6 B (+0.0%) 0 B +9 B 15.20 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.76 KB +13 B (+0.0%) 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 −1 B (−0.0%) 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.27 KB −66 B (−0.3%) 0 B +13 B 26.33 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.48 KB +26 B (+0.1%) 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.71 KB +34 B (+0.1%) 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.98 KB +68 B (+0.2%) +227 B +245 B 38.91 KB ❌ over by 67 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: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.66 KB −53 B (−0.1%) 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.00 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 42.92 KB +56 B (+0.1%) +232 B +232 B 42.87 KB ❌ over by 46 B eager (counted): web.js 22.95 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.08 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.27 KB +155 B (+0.4%) 0 B +7 B 41.25 KB ⚠️ over by 16 B, 13 B minified headroom 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.02 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 48.60 KB +110 B (+0.2%) +232 B +232 B 48.51 KB ❌ over by 91 B eager (counted): client.js 28.09 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: render + one signal (the simple-app floor): over brotli cap by 8 B; minified 26,754 B vs 26,739 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 16 B; minified 125,477 B vs 125,470 B recorded with the cap (+7 B) — 13 B of the 20 B minified allowance left; +0 B minified over this PR's base

❌ Fails the gate

  • signals: + isPending/latest: over brotli cap by 80 B; minified 31,071 B vs 30,840 B recorded with the cap (+231 B) — 211 B past the 20 B minified allowance; +231 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 67 B; minified 117,811 B vs 117,566 B recorded with the cap (+245 B) — 225 B past the 20 B minified allowance; +227 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 46 B; minified 125,956 B vs 125,724 B recorded with the cap (+232 B) — 212 B past the 20 B minified allowance; +232 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 91 B; minified 143,219 B vs 142,987 B recorded with the cap (+232 B) — 212 B past the 20 B minified allowance; +232 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).

@coveralls

coveralls commented Oct 30, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 41109677117

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.76 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 8.84%

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 regressed benchmarks
✅ 198 untouched benchmarks
⏩ 5 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ arrow in template 11.1 ms 12.4 ms -10.02%
❌ memo + sync render effect only (reference) 32 ms 34.7 ms -7.64%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/project-thread-x1ore2-reask-landing-answered (80a6417) with next (c8d6fd2)

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

Copy link
Copy Markdown
Member Author

CodSpeed's two regressions are noise, not this change:

  • arrow in template (−10.7%) is the known outlier on next's recorded baseline for that bench; every PR against this base shows it.
  • spread store record × 500 (−7.4%) never reaches the code this PR touches. The only runtime change is one guard in the async landing path (async.ts), and that bench renders a spread of a plain store record with no async at all. CodSpeed also flags that base and head ran on different hardware for 7 of the changed benches.

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

# 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
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

Copy link
Copy Markdown
Member Author

I don't think CodSpeed's −6.6% on spread store record × 500 comes from this PR. The new branch in read sits inside the path that runs only when the read node is pending, and the handleAsync change runs only when an async answer lands. This bench spreads a sync store record, so it reaches neither. The other four flagged benches moved the other way by 7–100%, which matches what #4463 and #4464 show against the same base (next 131ceb8): that base run was an outlier on several benches. I'm treating it as noise.


Generated by Claude Code

…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
@ryansolid
ryansolid merged commit 643a7a7 into next Oct 30, 2026
7 of 8 checks passed
ryansolid added a commit that referenced this pull request Oct 30, 2026
…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>
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>
ryansolid added a commit that referenced this pull request Nov 1, 2026
…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>
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