Repository navigation
fix(signals): latest() opens no transaction for its verdict; its reader watches the seam (#4365) - #4393
Conversation
…ad of undefined (#4362, #4363, #4376) A memo whose compute reads latest(s) is re-asked as the write's verdict lane work, so it becomes a lane derivation. While its first pass is in flight, nothing is in the lane, and laneValueOf/display fell back to the committed value, which an uninitialized node does not have: - latest(m)/isPending(m) served undefined and probed it pending (#4363); - the lane's end committed that undefined as the node's answer and cleared STATUS_UNINITIALIZED with the flight still up, which tripped the flush loop guard for [latest(m), isPending(m)] (#4362); - in hydration, m2 = fetchC(1 + latest(m1)) asked fetchC(NaN) instead of adopting the streamed answer (#4376). laneValueOf/display now return NOT_PENDING for such a node, so every read throws NotReady (A7; A19 exc. 1) and the lane's end leaves the node in flight. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Re-measured the store scenario's ledger over next @ 6b792a9. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…orld (#4365) A memo or render effect that reads `latest(a)` beside `b()` computed a torn pass on a batched write. The `latest()` read of a's staging opened the flush's transaction for its verdict (verdict.ts `provisionalVerdict`). The plain `b()` read after it then took that transaction for a hold (`stagedScreen`): it served b's committed value and made the reader a stale reader of the transaction. Nothing held the flush, so that torn pass was one of the flush's frames. Through a memo, a render effect ran `[2,1]` before `[2,2]`. A throw from the pass stuck, because the landing skips errored readers. A pass seated in the verdict lane only by its own `latest()` read (REACTIVE_PROVISIONAL) now reads a staging as the frame does: it leaves the lane and is recorded in `stagedReaders`. If the frame commits, the pass was the frame's one pass. If the flush parks, the lane seam re-derives it on the committed world as display-ahead work. A reader whose own stale pass threw now re-derives at the landing. `stagedScreen` moves into lanes.ts behind a hook, so the core floor does not carry it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…very (async-gen #119, #1238) - verdictValue: read the lane value before routing the reader into the verdict lane, so a node born in the lane with nothing in it yet is read as any uninitialized node (NotReady) without moving its reader (#1238). - reruns: a stale reader whose error is its own (thrown by its pass over the committed value it was served) re-derives at the landing; only an error from an errored source is skipped (#3945). With #4363 no longer committing `undefined`, async-gen case 119 reached this stuck error. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…ments - scripts/size: ledger lines for `signals: + isPending/latest`, the compiled live page and live + router (+46/+46/+47 B minified over next); this PR's store-app line re-measured (97,019 B). - async-gen baseline: 15 recorded failures no longer reproduce (10 undefined-value, 4 final-screen, 1 unhandled-rejection). - RULES-INDEX regenerated; changeset notes the error recovery. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…, not in reruns The verdict reader's own error from a served committed value (async-gen #119) now re-derives from `dissolveLane` (lane engine only) instead of a widened `reruns` condition, so the floors pay nothing (0 B; the lane scenarios +20 B over the previous form). `reruns` is next's again. Ledger re-measured; async-gen baseline unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…er watches the seam (#4365) Rework #4365 to the ruled shape (R5): in provisionalVerdict, latest() no longer calls joinFuture(null). Like a probe read before anything has joined, it goes through watchVerdict. Its pass is the frame's. If the flush parks, it re-derives once in the verdict lane: it is marked unanswered, so the seam does not take the transaction it joined as its answer. A flush that has already joined a transaction (Vl1), and reads outside a flush (Vl3), are unchanged. This drops the stagedScreen variant (REACTIVE_PROVISIONAL, the stagedScreen hook move and the laneSeams change). It also drops this branch's `reruns` change: #4383 handles a self-errored stale reader in lanes.ts dissolveLane, and that branch merges next. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…stale reader fix Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
… at a park (R-5, #4365) Under design ruling R-5, latest() opens no transaction for its verdict. The keyed condition's first pass is the frame's, and Show mounts A there. The parked flush re-runs the latest() reader once in the verdict lane, and Show mounts A again. The lane's mount is the one shown (the early fallback, still asserted); the held frame's mount is discarded at the lane's reveal. mounts goes [1,2,2] -> [1,3,3] for the static and callback forms over latest(count). The committed-key and `on` rows are unchanged. This re-pin waits on Ryan's ruling. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: 9c43740 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 |
…rror too The re-enqueue of a self-errored stale reader at the lane's end sat inside the non-effect branch, so a render effect whose display-ahead pass threw over a held write's screen value kept its error after the landing (found by the #4365 thread). It now applies to both. Byte-neutral (0 B minified in every scenario); floors 0 B. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Size (brotli, eager entry chunk)
|
Merging this PR will degrade performance by 7.84%2 warnings
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
|
… a park (D-2, A10) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
|
Two checks are red on 03a82df:
Generated by Claude Code |
A memo that reads a verdict memo is a verdict reader (F-6, #4050), and propagateStatus re-derived every verdict reader when its source went pending. Re-derived, an async one was served the committed value it had already derived from (A10) and asked that question again; that flight held the verdict lane, and the source's held landing re-ran it as lane work onto the same committed value, a third ask. Only the landing's re-derive asked the new question. Now an async memo that is a verdict reader only by reading a verdict memo (it opened no isPending/latest window of its own: REACTIVE_WINDOW, set per pass by markVerdictReader) drops CONFIG_VERDICT when that memo goes pending and takes the plain pending mark: it is pending through the flight (F-9) and the hold's work until it lands, reading plainly (D-25, #3884), and asks once at the landing. Sync readers re-derive as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…-x1ore2-4362 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…action re-derives once if the flush parks (D-22)
In R5, latest() opens no transaction. A render effect that is a verdict
reader (`v:${s()} a:${A()} l:${latest(s)}`) read s's staging before the
frame had a transaction, so it stayed the frame's pass. Its held run
then showed `l:0` until the landing. stagedRead now also lists such an
effect for the seam, which re-derives it once as a verdict reader when
the flush parks, as D-22 and D-25 require: it shows `v:0 a:0 l:1`.
This is limited to effects. A memo is not a leaf, and with async memos
the re-run asked its question again. With every memo, the async-gen
case e1-3289-1831 ended on [ROOT-E].
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…at carve (F-6, D-26) With #4397 merged, m1 is a verdict reader between its flights (D-25) and m0's lane pass re-derives it into the s lane early, so the t transaction lands while m1:1122 is in flight. Telling that pass apart as the hold's work needs a per-pass check; it goes to the carve. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
The grouped-vs-split ask order raced its 10 ms flights against a 12 ms sleep on a slow runner (m0b:20 before m1:5). The t write now comes after the s write's first flights land, while m1's question is up. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
The recompute clear runs only with the lane engine, and ext() no longer seeds the field, so builds without lanes drop both (-39 B minified on every floor). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…e row (#4411) * test(signals): pin #4402 and #4398 as fixed by #4393; #4399 as a carve row Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * test(signals): drive the hold pins on fake timers The change-during-hold pin raced the flights under coverage on CI ([7, 10] for [5, 8, 10]); the script's order now decides it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --------- Co-authored-by: Claude <[email protected]>
The differential proves the table subsumes it: with the mechanism gone the signals, solid and web suites (incl. #4397's and #4393's pins) stay green, the 2k × 3 generator screens match next except the two known closed cases, and the shadow reports 0 classes. The stale-reader mark the same rows fire (ST → _reruns) re-derives the reader at the landing, and holdWork keeps the #4397 wait; the pending-source drop had nothing left to change. The oracle drops the mark with it. + isPending/latest −104 B min; live page −106. Co-authored-by: Claude via Cursor <[email protected]> Co-authored-by: Cursor <[email protected]>
The #4400 case writes s, waits 3 ms, and writes again while a 10 ms ask is in flight. On CI's coverage run the real gap outlived the ask, which landed before the second write and asked `m1:23` (seen on #4455 and #4460). The gaps are part of each scenario, so the file now advances fake timers instead of sleeping. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…er it lands (#4460) * fix(signals): don't retain the transaction a latest() read opened after it lands `pendingVerdict` (#4453) remembered the flush's transaction a `latest` read of an unheld flight opened in a module-level `latestJoined`, compared only against `flushTransaction` and never cleared: after the seam it kept the last landed transaction (its `_nodes`, `_queues`, `_reruns`) alive until the next such read overwrote it. The mark now lives on the transaction itself (`Transaction.latest`), so it ends with it. Same answer for every probe: a transaction stays the flush's from its opening to the seam, a merge keeps the flush's own, and one merged away is no longer anyone's flush transaction, as before. The property is not `_`-prefixed (as `folded`), so the mangler's assignment is untouched; only bundles importing `latest`/`isPending` change, and they shrink (isPending/latest -13 B brotli, -10 B minified). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * test(signals): the #4393/#4400 two-reader specs run on fake timers The #4400 case writes s, waits 3 ms, and writes again while a 10 ms ask is in flight. On CI's coverage run the real gap outlived the ask, which landed before the second write and asked `m1:23` (seen on #4455 and #4460). The gaps are part of each scenario, so the file now advances fake timers instead of sleeping. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --------- Co-authored-by: Claude <[email protected]>
…ns no frame posture (measurement, not for merge) Encoding: REACTIVE_PROBED_HELD / CONFIG_PROBED_HELD (1 << 30, same-bit promotion as REACTIVE_PROBED → CONFIG_VERDICT) set by a probe that finds a held, staged, in-flight or lane's node (verdict.ts `probedHeld`, from `verdictRead`/`watchVerdict`/`pendingVerdict`), inherited through the #4050 arm (core.ts `read`, config at once unless `creatorReads`), and checked where CONFIG_VERDICT alone decided frame/screen reading: `recompute`'s head join, `frameRead`, both `stagedScreen` callers, the pending-node `observeFlight` arm, store.ts's staging arms, lanes.ts `enterLane`/`laneRead`, scheduler.ts zombies, boundaries.ts `on`, and the shadow seat table. Findings (signals suite: 50 pins flip across 15 files; Shape 1 of #4544 does not flip): - D-25 "either read order" is carried by the posture a nothing-found probe leaves: `[h(), isPending(h)]` on the pass where `h` first goes pending or held reads `h` plainly before the probe, and only the earlier probe's posture serves the screen. Under the ruling that pass has no posture and throws/joins (verdict-contract, #4050 "memo [h, inline probe]", #3028, #4393, #4402, #4491, #4504, D-30 store rows, …). A sound encoding needs per-dependency memory ("this pass probes x"), not a node bit. - Once `params` joins T, the `plan` write is held by T (L2); `chrome`'s seam re-derive probes a T-held node and is T's verdict reader; `params` inherits the posture (#4050) / enters T's verdict lane (#4399) and is served the committed `session` again. A "head-joined pass is the hold's work" gate flips Shape 1 but adds #4399/#4408 pins (74 failures). Size (local, base d1d6d51): core floor +1 B br / +3 B min; hello world −5 B br / +3 B min; + isPending/latest +45 B br / +200 B min; compiled live page +75 B br / +250 B min; live + router +151 B br / +254 B min. Co-authored-by: Cursor <[email protected]>
… no caller) Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com>
* refactor(signals): remove the dead latest-opened hold arm (#4393 left no caller) Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com> * chore: drop the scratch coverage configs committed by mistake Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com> --------- Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com>
Summary
Fixes #4365
Before: a memo or render effect that read
latest(a)besideb()computed a torn pass on a batched write.latest()read of a's staging opened the flush's transaction for its verdict (provisionalVerdict,joinFuture(null)).b()read after it then took that manufactured transaction for a hold and was served b's committed value.[2,1]before[2,2]. If the torn pass threw, the error stuck.After: D-22 (clarified 2026-10-28): inside a flush,
latest()of a staging that isn't held yet is the frame's read. It serves the staging and opens no hold, and a plain read beside it in the same pass reads the staging too (F-10). If the flush parks, the pass re-derives once as a verdict reader (D-2, A10).How
provisionalVerdict(verdict.ts): the!probingarm that calledjoinFuture(null)is gone. Alatest()reader goes throughwatchVerdictlike a probe and is marked unanswered, so it re-derives once if the seam parks. Once the flush has joined a transaction, readers route into its verdict lane as before. fix(signals): latest() read inline and through a memo show the same screen #4401'slatestOpenedis dropped with the arm.latest()of a flight (verdict.tspendingVerdict): it joins the verdict lane at once (joinFuture(null)when not probing), as a plain read of a flight does. Both seats serve the committed value (D-22), so the seam has nothing to decide. Without this,true:1andl1were asked twice (fix(signals): an async memo over a verdict memo asks each question once #4397's own-window pins).stagedRead): the pass is added tostagedReaders, so the seam re-derives it once if the flush parks. This keeps fix(signals): latest() read inline and through a memo show the same screen #4401's parity screens (v:0 a:0 l:1, inline and memo). It applies to effects only: including verdict memos made async memos ask twice.laneSeams→adoptParked): when the frame's flight asked the landing's question, the landing adopts it instead of asking again (F-9). When the R5 reader is a sync memo andtis blocked, the frame flights of its async readers are parked too. This fixesm1:220asked twice whenlatest()is extracted into a memo (D-25, [2.0 rc.15 + next] Held navigation never publishes delayed pending feedback #4050).laneSeamslane handling: a verdict lane that a re-derivedlatest()reader joins doesn't reveal at that seam. A parent whose lane reveals with readers to re-derive lands one round later (revealing), so the lane's answer shows first (F-6).heldBycounts the lanes undert.Nothing new runs per pass in
recomputewithout the lane engine.Pins
packages/signals/tests/latest-beside-staged-read-4365.test.ts: the torn pass and the sticking throw (memo and effect readers), both read orders, parked flushes, and display-ahead for a separate async reader. The torn-pass cases fail on next.packages/signals/tests/latest-two-readers-one-ask-4393.test.ts: each question asked once, and anisPendingsibling not changing whatv${m0()}shows (D-23, F-6).verdict-memo-flight-no-reask.test.ts(fix(signals): an async memo over a verdict memo asks each question once #4397): order-only re-pins for R5's frame pass (m0:5first), each with a comment. Every fix(signals): an async memo over a verdict memo asks each question once #4397 "asked once" pin still holds.it.fails: "m1's lane pass is not re-derived over a staging that lands with it (beside)", citing F-6 (amended 2026-10-28) and D-26. With fix(signals): an async memo over a verdict memo asks each question once #4397 merged,m1is a verdict reader between its flights (D-25), and m0's lane pass re-derives it into the s lane early. The t transaction then lands whilem1:1122is in flight. Telling that pass apart as the hold's work needs a per-pass check, so it is a row for the reader-seat carve. It fails on next too.<Show when={latest(count)}>mounts its child twice at a park ([1,3,3]). The second mount is the lane's and is the one shown (boundary-not-born-held-3540, webloading-on-keyed-boundary-3540).How did you test this change?
At 9c43740, merged with next 9baaba3 (#4383):
latest-memo-inline-parityand every async-gen spec;tsc --noEmitclean on src;Size
The PR doesn't re-record the ledger; it is re-recorded once on
nextafter the batch lands. No floor cap moves. The R5 parked-flight clear inrecomputeruns only with the lane engine, andext()no longer seeds the field, so the floors are back to about +0 (-39 B minified against the previous head on every floor, +11 B on the lane builds).🤖 Generated with Claude Code
https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2