Skip to content

fix(signals): hold conditional reveals on existing async work - #3305

Merged
ryansolid merged 2 commits into
solidjs:nextfrom
brenelz:fix/conditional-async-reveal-pending
Sep 8, 2026
Merged

ryansolid merged 2 commits into
solidjs:nextfrom
brenelz:fix/conditional-async-reveal-pending

Conversation

@brenelz

@brenelz brenelz commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Hold staged writes when a render reader first observes an unresolved async flight from an earlier flush. This keeps show committed as false with isPending(show) === true until the details can reveal with it, without retroactively holding the already committed value update.
  • Check transition ownership rather than reader initialization: newly created child readers must also hold the reveal, while readers already parked in a transition must not open a second one. Fresh/reset loading boundaries still consume pending and display their fallback.
  • Add coverage to the existing async-semantics and DOM loading suites, plus an @solidjs/signals patch changeset. No standalone scratch tests.

Reproduction
https://s.olid.uk/id/1xogoHinT9uZhdrpGNy7LQ

The show-line indicator in the playground uses isPending(value); the DOM regression uses isPending(show). The runtime bug remains independently of that typo: show() ? details() : "hidden" discovers the existing flight only after the reveal write.

Verification

  • Signals suite: 1,502 passed, 1 skipped.
  • Solid suite: 585 passed.
  • Web suites: 704 client, 750 server, and 165 hydration tests passed; 2 server tests skipped.
  • DOM loading suite with JSX_COMPILER=babel: 21 passed.
  • Rebuilt runtimes before downstream tests; bundle-size limits, Prettier, and git diff --check passed.

@changeset-bot

changeset-bot Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 98d457f

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

This PR includes changesets to release 11 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

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

@codspeed

codspeed Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 142 untouched benchmarks


Comparing brenelz:fix/conditional-async-reveal-pending (98d457f) with next (6f48a80)1

Open in CodSpeed

Footnotes

  1. No successful run was found on next (98d457f) during the generation of this report, so 6f48a80 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@ryansolid
ryansolid force-pushed the fix/conditional-async-reveal-pending branch from 29cacdb to 6ea30eb Compare September 8, 2026 05:14
…lError branch

Same semantics, fewer bytes: the new check and the reporter registration
share one actualError test, read the module-level currentBatch instead of
this._batch, and the error fallback is `??` (every caller passes nothing or
this node's own _x._error, so a null falls back to the same null). Measured
against next: signals + createStore 14358 B brotli, under both the cap and
the pre-PR baseline; the PR as written was 18 B over on Linux.

Co-authored-by: Cursor <[email protected]>
@ryansolid
ryansolid force-pushed the fix/conditional-async-reveal-pending branch from 6ea30eb to 98d457f Compare September 8, 2026 05:14
@ryansolid

Copy link
Copy Markdown
Member

Ruling: this is a gap, not a semantics change — the same A15 rule (a non-action write whose render surfaces async is held with isPending on the written signal), applied when the reader discovers a flight instead of starting one. Today's fresh-start path opens the transition from handleAsync; nothing did so for a pre-existing, never-held flight, so show committed true while the content froze on "hidden" with nothing reporting pending.

I pushed one commit on top (98d457f8): identical semantics, the new check folded into the existing actualError branch, currentBatch instead of this._batch, and ?? for the error fallback (exact — every caller passes nothing or this node's own _x._error). That takes signals: + createStore from 18 B over the cap on Linux to 14.36 KB, under both the cap and the pre-PR baseline, so no ratchet was needed. I also checked the guards: the _pendingNodes.length gate is what keeps a first mount from opening a reporter-blocked transaction that would stash every effect in the flush, and !node._transition is load-bearing (three createProjection.async isPending tests fail without it). Both stay.

Merging — thanks for the clean reproduction and the boundary-shape tests.

— Claude via Cursor

@ryansolid
ryansolid merged commit b6b4804 into solidjs:next Sep 8, 2026
6 checks passed
ryansolid added a commit that referenced this pull request Sep 10, 2026
…ess of stamp (#3334)

Two halves of one tear. `read()`'s pending branch let a stale (render)
reader of another transaction carve out a node pending in a foreign
transaction and show its committed value — on the theory that the stamp
meant that transaction also held the node's inputs. It does not: the
stamp is pending-node bookkeeping, and the inputs may already be on
screen (committed #3305, lane-revealed #3334). A render reader landing on
a pending node now throws whichever transaction the node is stamped with
(A15: observed async settles as one unit).

And `handleAsync`'s settle-time re-entry entered the lane owner's
transaction for a lane-routed landing, folding every transaction waiting
on the flight into the owner's action — a reveal that discovered the
flight then waited on the action instead of on the flight. It enters the
waiter: the transaction whose blocker this landing clears
(`waitingTransition`).

Pinned in spec A15 (#3334 describe) and
tests/stale-read-uninitialized-cross-transition.test.ts (re-expected).
INTERNALS §3.

Co-authored-by: Claude via Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 10, 2026
…ess of stamp (#3334)

Two halves of one tear. `read()`'s pending branch let a stale (render)
reader of another transaction carve out a node pending in a foreign
transaction and show its committed value — on the theory that the stamp
meant that transaction also held the node's inputs. It does not: the
stamp is pending-node bookkeeping, and the inputs may already be on
screen (committed #3305, lane-revealed #3334). A render reader landing on
a pending node now throws whichever transaction the node is stamped with
(A15: observed async settles as one unit).

And `handleAsync`'s settle-time re-entry entered the lane owner's
transaction for a lane-routed landing, folding every transaction waiting
on the flight into the owner's action — a reveal that discovered the
flight then waited on the action instead of on the flight. It enters the
waiter: the transaction whose blocker this landing clears
(`waitingTransition`).

Pinned in spec A15 (#3334 describe) and
tests/stale-read-uninitialized-cross-transition.test.ts (re-expected).
INTERNALS §3.

Co-authored-by: Claude via Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 10, 2026
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331)

Review on #3347 (GabbeV). Two fixes and a re-ruling.

A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch
carve-out outright — a stale (render) reader of a node pending in another
transaction always held on the flight. That made a new reader of a memo
whose input write is ITSELF still held wait for the landing, though showing
the committed pair introduces no inconsistency (parallel transactions;
effects don't entangle; React 19.3 stopped entangling the same shape). The
carve-out returns, gated on input visibility: the reader shows committed,
does not entangle, and is recorded for the transaction's commit replay
(heldFromStale) — refused, holding instead, when the committed value would
tear against the frame: the flight's inputs were PUBLISHED while it was
pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed
branch when a commit leaves the node in the air, #3305; cleared by
notifyStatus when the node next enters pending from a settled state), the
node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather
than sticky; optimistic / latest, #3334), or the node is uninitialized. The
initialized-memo pin and its store twin return to "show v0, then v1".

Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs
that later recomputes UNDER the recording transaction sees its staged view
and is applied by the commit (ownership) — the stale recording published
the frame twice. recompute drops it at its start; a lane's committed-view
read re-records during the run, so the lane replay is untouched.

Provenance (#3331 follow-up): a same-value optimistic write by a newer
action took the fast path — entangling the transaction — without renewing
_overrideStamp, so the OLDER action's slow source superseded a value the
user had just re-confirmed: a corrective downstream refetch and a pending
flip for nothing. The fast path now renews the stamp to a newer origin.

Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never
catches up" and "conditional reader stays hidden" shapes, a replacement
flight over published inputs, a retired lane), the same-value provenance
twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3.
Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled
for the lane-authority stack with per-scenario notes.

Co-authored-by: Claude via Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 10, 2026
…ess of stamp (#3334)

Two halves of one tear. `read()`'s pending branch let a stale (render)
reader of another transaction carve out a node pending in a foreign
transaction and show its committed value — on the theory that the stamp
meant that transaction also held the node's inputs. It does not: the
stamp is pending-node bookkeeping, and the inputs may already be on
screen (committed #3305, lane-revealed #3334). A render reader landing on
a pending node now throws whichever transaction the node is stamped with
(A15: observed async settles as one unit).

And `handleAsync`'s settle-time re-entry entered the lane owner's
transaction for a lane-routed landing, folding every transaction waiting
on the flight into the owner's action — a reveal that discovered the
flight then waited on the action instead of on the flight. It enters the
waiter: the transaction whose blocker this landing clears
(`waitingTransition`).

Pinned in spec A15 (#3334 describe) and
tests/stale-read-uninitialized-cross-transition.test.ts (re-expected).
INTERNALS §3.

Co-authored-by: Claude via Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 10, 2026
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331)

Review on #3347 (GabbeV). Two fixes and a re-ruling.

A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch
carve-out outright — a stale (render) reader of a node pending in another
transaction always held on the flight. That made a new reader of a memo
whose input write is ITSELF still held wait for the landing, though showing
the committed pair introduces no inconsistency (parallel transactions;
effects don't entangle; React 19.3 stopped entangling the same shape). The
carve-out returns, gated on input visibility: the reader shows committed,
does not entangle, and is recorded for the transaction's commit replay
(heldFromStale) — refused, holding instead, when the committed value would
tear against the frame: the flight's inputs were PUBLISHED while it was
pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed
branch when a commit leaves the node in the air, #3305; cleared by
notifyStatus when the node next enters pending from a settled state), the
node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather
than sticky; optimistic / latest, #3334), or the node is uninitialized. The
initialized-memo pin and its store twin return to "show v0, then v1".

Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs
that later recomputes UNDER the recording transaction sees its staged view
and is applied by the commit (ownership) — the stale recording published
the frame twice. recompute drops it at its start; a lane's committed-view
read re-records during the run, so the lane replay is untouched.

Provenance (#3331 follow-up): a same-value optimistic write by a newer
action took the fast path — entangling the transaction — without renewing
_overrideStamp, so the OLDER action's slow source superseded a value the
user had just re-confirmed: a corrective downstream refetch and a pending
flip for nothing. The fast path now renews the stamp to a newer origin.

Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never
catches up" and "conditional reader stays hidden" shapes, a replacement
flight over published inputs, a retired lane), the same-value provenance
twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3.
Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled
for the lane-authority stack with per-scenario notes.

Co-authored-by: Claude via Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 11, 2026
…ess of stamp (#3334)

Two halves of one tear. `read()`'s pending branch let a stale (render)
reader of another transaction carve out a node pending in a foreign
transaction and show its committed value — on the theory that the stamp
meant that transaction also held the node's inputs. It does not: the
stamp is pending-node bookkeeping, and the inputs may already be on
screen (committed #3305, lane-revealed #3334). A render reader landing on
a pending node now throws whichever transaction the node is stamped with
(A15: observed async settles as one unit).

And `handleAsync`'s settle-time re-entry entered the lane owner's
transaction for a lane-routed landing, folding every transaction waiting
on the flight into the owner's action — a reveal that discovered the
flight then waited on the action instead of on the flight. It enters the
waiter: the transaction whose blocker this landing clears
(`waitingTransition`).

Pinned in spec A15 (#3334 describe) and
tests/stale-read-uninitialized-cross-transition.test.ts (re-expected).
INTERNALS §3.

Co-authored-by: Claude via Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 11, 2026
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331)

Review on #3347 (GabbeV). Two fixes and a re-ruling.

A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch
carve-out outright — a stale (render) reader of a node pending in another
transaction always held on the flight. That made a new reader of a memo
whose input write is ITSELF still held wait for the landing, though showing
the committed pair introduces no inconsistency (parallel transactions;
effects don't entangle; React 19.3 stopped entangling the same shape). The
carve-out returns, gated on input visibility: the reader shows committed,
does not entangle, and is recorded for the transaction's commit replay
(heldFromStale) — refused, holding instead, when the committed value would
tear against the frame: the flight's inputs were PUBLISHED while it was
pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed
branch when a commit leaves the node in the air, #3305; cleared by
notifyStatus when the node next enters pending from a settled state), the
node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather
than sticky; optimistic / latest, #3334), or the node is uninitialized. The
initialized-memo pin and its store twin return to "show v0, then v1".

Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs
that later recomputes UNDER the recording transaction sees its staged view
and is applied by the commit (ownership) — the stale recording published
the frame twice. recompute drops it at its start; a lane's committed-view
read re-records during the run, so the lane replay is untouched.

Provenance (#3331 follow-up): a same-value optimistic write by a newer
action took the fast path — entangling the transaction — without renewing
_overrideStamp, so the OLDER action's slow source superseded a value the
user had just re-confirmed: a corrective downstream refetch and a pending
flip for nothing. The fast path now renews the stamp to a newer origin.

Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never
catches up" and "conditional reader stays hidden" shapes, a replacement
flight over published inputs, a retired lane), the same-value provenance
twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3.
Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled
for the lane-authority stack with per-scenario notes.

Co-authored-by: Claude via Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 11, 2026
…ess of stamp (#3334)

Two halves of one tear. `read()`'s pending branch let a stale (render)
reader of another transaction carve out a node pending in a foreign
transaction and show its committed value — on the theory that the stamp
meant that transaction also held the node's inputs. It does not: the
stamp is pending-node bookkeeping, and the inputs may already be on
screen (committed #3305, lane-revealed #3334). A render reader landing on
a pending node now throws whichever transaction the node is stamped with
(A15: observed async settles as one unit).

And `handleAsync`'s settle-time re-entry entered the lane owner's
transaction for a lane-routed landing, folding every transaction waiting
on the flight into the owner's action — a reveal that discovered the
flight then waited on the action instead of on the flight. It enters the
waiter: the transaction whose blocker this landing clears
(`waitingTransition`).

Pinned in spec A15 (#3334 describe) and
tests/stale-read-uninitialized-cross-transition.test.ts (re-expected).
INTERNALS §3.

Co-authored-by: Claude via Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 11, 2026
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331)

Review on #3347 (GabbeV). Two fixes and a re-ruling.

A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch
carve-out outright — a stale (render) reader of a node pending in another
transaction always held on the flight. That made a new reader of a memo
whose input write is ITSELF still held wait for the landing, though showing
the committed pair introduces no inconsistency (parallel transactions;
effects don't entangle; React 19.3 stopped entangling the same shape). The
carve-out returns, gated on input visibility: the reader shows committed,
does not entangle, and is recorded for the transaction's commit replay
(heldFromStale) — refused, holding instead, when the committed value would
tear against the frame: the flight's inputs were PUBLISHED while it was
pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed
branch when a commit leaves the node in the air, #3305; cleared by
notifyStatus when the node next enters pending from a settled state), the
node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather
than sticky; optimistic / latest, #3334), or the node is uninitialized. The
initialized-memo pin and its store twin return to "show v0, then v1".

Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs
that later recomputes UNDER the recording transaction sees its staged view
and is applied by the commit (ownership) — the stale recording published
the frame twice. recompute drops it at its start; a lane's committed-view
read re-records during the run, so the lane replay is untouched.

Provenance (#3331 follow-up): a same-value optimistic write by a newer
action took the fast path — entangling the transaction — without renewing
_overrideStamp, so the OLDER action's slow source superseded a value the
user had just re-confirmed: a corrective downstream refetch and a pending
flip for nothing. The fast path now renews the stamp to a newer origin.

Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never
catches up" and "conditional reader stays hidden" shapes, a replacement
flight over published inputs, a retired lane), the same-value provenance
twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3.
Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled
for the lane-authority stack with per-scenario notes.

Co-authored-by: Claude via Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 11, 2026
…ess of stamp (#3334)

Two halves of one tear. `read()`'s pending branch let a stale (render)
reader of another transaction carve out a node pending in a foreign
transaction and show its committed value — on the theory that the stamp
meant that transaction also held the node's inputs. It does not: the
stamp is pending-node bookkeeping, and the inputs may already be on
screen (committed #3305, lane-revealed #3334). A render reader landing on
a pending node now throws whichever transaction the node is stamped with
(A15: observed async settles as one unit).

And `handleAsync`'s settle-time re-entry entered the lane owner's
transaction for a lane-routed landing, folding every transaction waiting
on the flight into the owner's action — a reveal that discovered the
flight then waited on the action instead of on the flight. It enters the
waiter: the transaction whose blocker this landing clears
(`waitingTransition`).

Pinned in spec A15 (#3334 describe) and
tests/stale-read-uninitialized-cross-transition.test.ts (re-expected).
INTERNALS §3.

Co-authored-by: Claude via Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 11, 2026
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331)

Review on #3347 (GabbeV). Two fixes and a re-ruling.

A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch
carve-out outright — a stale (render) reader of a node pending in another
transaction always held on the flight. That made a new reader of a memo
whose input write is ITSELF still held wait for the landing, though showing
the committed pair introduces no inconsistency (parallel transactions;
effects don't entangle; React 19.3 stopped entangling the same shape). The
carve-out returns, gated on input visibility: the reader shows committed,
does not entangle, and is recorded for the transaction's commit replay
(heldFromStale) — refused, holding instead, when the committed value would
tear against the frame: the flight's inputs were PUBLISHED while it was
pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed
branch when a commit leaves the node in the air, #3305; cleared by
notifyStatus when the node next enters pending from a settled state), the
node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather
than sticky; optimistic / latest, #3334), or the node is uninitialized. The
initialized-memo pin and its store twin return to "show v0, then v1".

Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs
that later recomputes UNDER the recording transaction sees its staged view
and is applied by the commit (ownership) — the stale recording published
the frame twice. recompute drops it at its start; a lane's committed-view
read re-records during the run, so the lane replay is untouched.

Provenance (#3331 follow-up): a same-value optimistic write by a newer
action took the fast path — entangling the transaction — without renewing
_overrideStamp, so the OLDER action's slow source superseded a value the
user had just re-confirmed: a corrective downstream refetch and a pending
flip for nothing. The fast path now renews the stamp to a newer origin.

Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never
catches up" and "conditional reader stays hidden" shapes, a replacement
flight over published inputs, a retired lane), the same-value provenance
twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3.
Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled
for the lane-authority stack with per-scenario notes.

Co-authored-by: Claude via Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 11, 2026
…ess of stamp (#3334)

Two halves of one tear. `read()`'s pending branch let a stale (render)
reader of another transaction carve out a node pending in a foreign
transaction and show its committed value — on the theory that the stamp
meant that transaction also held the node's inputs. It does not: the
stamp is pending-node bookkeeping, and the inputs may already be on
screen (committed #3305, lane-revealed #3334). A render reader landing on
a pending node now throws whichever transaction the node is stamped with
(A15: observed async settles as one unit).

And `handleAsync`'s settle-time re-entry entered the lane owner's
transaction for a lane-routed landing, folding every transaction waiting
on the flight into the owner's action — a reveal that discovered the
flight then waited on the action instead of on the flight. It enters the
waiter: the transaction whose blocker this landing clears
(`waitingTransition`).

Pinned in spec A15 (#3334 describe) and
tests/stale-read-uninitialized-cross-transition.test.ts (re-expected).
INTERNALS §3.

Co-authored-by: Claude via Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Sep 11, 2026
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331)

Review on #3347 (GabbeV). Two fixes and a re-ruling.

A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch
carve-out outright — a stale (render) reader of a node pending in another
transaction always held on the flight. That made a new reader of a memo
whose input write is ITSELF still held wait for the landing, though showing
the committed pair introduces no inconsistency (parallel transactions;
effects don't entangle; React 19.3 stopped entangling the same shape). The
carve-out returns, gated on input visibility: the reader shows committed,
does not entangle, and is recorded for the transaction's commit replay
(heldFromStale) — refused, holding instead, when the committed value would
tear against the frame: the flight's inputs were PUBLISHED while it was
pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed
branch when a commit leaves the node in the air, #3305; cleared by
notifyStatus when the node next enters pending from a settled state), the
node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather
than sticky; optimistic / latest, #3334), or the node is uninitialized. The
initialized-memo pin and its store twin return to "show v0, then v1".

Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs
that later recomputes UNDER the recording transaction sees its staged view
and is applied by the commit (ownership) — the stale recording published
the frame twice. recompute drops it at its start; a lane's committed-view
read re-records during the run, so the lane replay is untouched.

Provenance (#3331 follow-up): a same-value optimistic write by a newer
action took the fast path — entangling the transaction — without renewing
_overrideStamp, so the OLDER action's slow source superseded a value the
user had just re-confirmed: a corrective downstream refetch and a pending
flip for nothing. The fast path now renews the stamp to a newer origin.

Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never
catches up" and "conditional reader stays hidden" shapes, a replacement
flight over published inputs, a retired lane), the same-value provenance
twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3.
Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled
for the lane-authority stack with per-scenario notes.

Co-authored-by: Claude via Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Oct 5, 2026
…node in a held frame stays the lane's; a verdict reader revealing an orphan flight holds it (#3798)

* fix(signals): a lane's node inside a held frame stays the lane's (fuzzer F7a/F7b)

`holdFrame` holds a parked pass's frame with the transaction (ruling A).
It re-listed a lane's node found there — a verdict reader created in
the frame, broken out into the holder's verdict lane (display-ahead,
L2 rule 6), or a derivation of a guess — on the frame's transaction,
still carrying its lane value (CONFIG_OVERRIDE). At the landing
`dissolveLane` no longer found the node (its `_transaction` was the
transaction's) and `land` nulled the transaction: a lane node with no
lane, and the landing's re-derivation tripped over it (`laneRead` →
`txOf(null)` → TypeError, REACTIVITY_HALTED).

The shapes: a `Loading` whose content reads `latest(x)`, unmounted and
remounted while `x` is held by an action (F7a); a memo of
`isPending(() => x())` mounted in the tick of the write it probes, its
mount toggled off and on while the write is held (F7b). The remounting
gate's write joins the hold (A34 (1)), so its new frame is held with it
— and the verdict-lane nodes inside it were taken along.

`holdFrame` now skips a child listed on a lane, as the seam's own hold
loop already does for this flush's stagings: lane work never makes its
node transaction work (A15, #3698), and a lane pass's frame is the
lane's, revealed and retired at its seam. The lane's end (`dissolveLane`
at the parent's landing) commits the node as before. Every other
`holdNode` site (the seam loop, the store's three) already excluded
lane work; this was the one that did not.

F7a and F7b flip from `it.fails` to `it`. No other pin moved.

Co-authored-by: Claude via Cursor <[email protected]>

* fix(signals): a verdict reader revealing a flight nobody holds registers it with the frame (fuzzer F12)

A verdict reader (`latest`/`isPending` in its pass) that plainly reads a
pending node is served the committed value and `observeFlight`s it: the
frame holds, the reader is the holder's verdict lane's, and it re-derives
at the landing (REACTIVE_FRAME_READ — a blocker of the hold through the
flight's subscribers, `blockedBy`). That hold is on the flight's node: a
reader served committed never goes pending, so the transaction waits only
through the node it holds. A flight that went pending this flush is queued
for the seam to hold; one in the air from before and held by nobody was
not — the transaction opened with no entry, landed at its own seam, and
its `_reruns` re-derived the reader into the same observation, every
flush ("Potential Infinite Loop Detected").

The fuzzer's shape (F12): an action whose body ends with `setSource(1)`,
`setShow(true)` adopted in the action's tick (O1), a reader of
`[latest(source), node()]` over an async memo of `latest(source)`. At the
body's end the memo re-asks as the action's verdict lane's work (the
proposal) while the reader, a stale reader of the held `show`, shows
"hidden"; nothing displays the flight, the action lands (A29: a flight
nobody renders holds nothing), `dissolveLane` commits the memo beneath
its flight, and the landing's rerun reveals the orphan flight to the
verdict reader. Reduced: any memo re-asked unobserved (its inputs
commit, #3305) then revealed to a `latest()` reader did the same.

`observeFlight` now queues the flight's node for the seam when nobody
holds it, as if it had gone pending here (A15 first observer, #3458:
"the observation registers it, and the transaction waits on the flight
it now shows a reader of"); the seam applies its own hold rules (a
loading source is nobody's frame, A34 (2), `holdFrame`). The landing
then resumes that transaction through `setSignal`'s join and the reader
re-derives once. The display follows the existing rules: a reveal that
read a staging stays the frame's and waits with the flight (rule 3,
A15 reveal corollary — the reduced pin asserts `"hidden"` → `[1,1]`); a
verdict reader with nothing staged shows the screen ahead (rule 6).

F12 flips from `it.fails` to `it`; a reduced pin without the action or
lane is added beside it. No other pin moved.

Co-authored-by: Claude via Cursor <[email protected]>

* chore(size): raise three caps for the fuzzer F7/F12 fixes (Size-Exception)

Raises only the caps the two fixes push over, each set to measured + 10 B
rounded up to 0.01 KB, with dated ledger notes in scenarios.js:

- signals: + isPending/latest        9.45 -> 9.47 KB (9,456 B; was 6 B over)
- page: base server components      44.78 -> 44.84 KB (44,829 B; was 49 B over)
- page: live server components      48.45 -> 48.47 KB (48,454 B; was 4 B over)

Attributable delta is +33 B minified in signals (holdFrame lane gate +16,
observeFlight first-observer hold +17); the brotli deltas are layout over
the shifted stream (page base moves +67 B br on +16 B minified). All other
caps hold; no cap is loosened beyond measured + 10 B.

Size-Exception: fuzzer F7/F12 crash-class fixes — `+ isPending/latest` 9.45 -> 9.47 KB (9,456 B), `page: base` 44.78 -> 44.84 KB (44,829 B), `page: live` 48.45 -> 48.47 KB (48,454 B); +33 B minified in signals, brotli layout. Accepted by the maintainer (2026-10-05).
Co-authored-by: Claude via Cursor <[email protected]>

---------

Co-authored-by: Claude via Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Oct 29, 2026
…d published-inputs table rows, results in the design doc

The 50,000-case generator runs (seeds 3289, 91501, 7, every pair) found two
table rows wrong against a rule, fixed in seat.ts (test-only):

- L4 / L2 / V1' (D-41): a held node's staged error throws for a leaf or a
  born-into frame reader; the rows said P.
- V3 (F-9, #3305): a verdict reader's plain read of a node staged and in
  flight with INPUTS_PUBLISHED is the flight's (V4', NR), not the staging.

Design doc §4.2 records the differential results (suites, 2k, 50k) and §6
the two rows; RULES-INDEX regenerated for the new #3305 citation. Build
output unchanged (size harness: 15/15 bundling scenarios byte-identical).

Co-authored-by: Claude via Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
ryansolid added a commit that referenced this pull request Oct 30, 2026
…s a CI spec (S0+M1, byte-identical) (#4438)

* docs(plans): reader-seat rebuild — build the seat beside the deciders under __TEST__, differential, then swap

Co-authored-by: Cursor <[email protected]>

* test(signals): reader seat shadow — new seat/slot implementation under __TEST__ beside the existing deciders

core/seat.ts computes the reader seat (world/lane/ahead/posture) and the
slot table (U/F/L/D/V/Vl/Vp/A/B rows) beside today's deciders; under
__TEST__ every read and every recompute compares the two and records
disagreements (INV-seat-1..3) into a registry (SEAT_SHADOW_REPORT /
__SEAT_SHADOW_STRICT__). Nothing reaches a build: every statement is
behind __TEST__ (false in every rollup define); SIGNALS_TEST_DEFINE=true
builds a dev dist with it on for the solid/web/generator differential.

Design doc §3/§6/§7 updated with the harness as built, the table rows
found wrong against a rule (fixed in seat.ts), and the two-readings
items the differential leaves open.

Co-authored-by: Claude via Cursor <[email protected]>

* test(signals): reader seat shadow — 50k differential: staged-error and published-inputs table rows, results in the design doc

The 50,000-case generator runs (seeds 3289, 91501, 7, every pair) found two
table rows wrong against a rule, fixed in seat.ts (test-only):

- L4 / L2 / V1' (D-41): a held node's staged error throws for a leaf or a
  born-into frame reader; the rows said P.
- V3 (F-9, #3305): a verdict reader's plain read of a node staged and in
  flight with INPUTS_PUBLISHED is the flight's (V4', NR), not the staging.

Design doc §4.2 records the differential results (suites, 2k, 50k) and §6
the two rows; RULES-INDEX regenerated for the new #3305 citation. Build
output unchanged (size harness: 15/15 bundling scenarios byte-identical).

Co-authored-by: Claude via Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>

* docs(plans): reader-seat rebuild §4.2 — three-seed 50k class count, post-fix re-run, plain-build abort indices, untraced residue

The 50k row counted seed 3289's classes (133) against the three-seed
reads/passes/instances; the aggregate is 220 classes / 8,821 instances
before the two 50k table fixes and 209 / 8,706 re-run with the table as
committed (the 11 classes the fixes cover go, none appear). The infinite
loop abort on each seed is verified against a dist built without
SIGNALS_TEST_DEFINE, at the same case index on both builds (7: 1158,
3289: 3963, 91501: 5807). Classes no §6 finding or §7 item names
literally (623 instances / 43 classes) are listed and left untraced.

Co-authored-by: Claude via Cursor <[email protected]>

* docs(plans): reader-seat rebuild §7 — the six two-readings items ruled / chosen 2026-10-28

Co-authored-by: Cursor <[email protected]>

* docs(plans): reader-seat rebuild §7 F-vlane — written in D-25; #3884 is the hold's own work, not a persisted posture; swap pins corrected

Co-authored-by: Cursor <[email protected]>

* test(signals): reader seat S1 M1 — re-baseline the shadow against next @ 5e68925

Table fixes (design doc §6.1): V1'' staged error → E (D-41); the verdict seat
is the hold the pass's own node's verdict lane names (vhold); the branch world
(creatorsWorld) walks the owner chain of running passes (#4341); ahead only
from a lane or verdict seat; verdict reads leave marks and do not raise;
Vl4' (latest of a node staged and in flight serves the staging); D8 needs a
value (u f is NR); lane rows for a boundary's structure are a derivation's
(A15/#4283 — today is right, renderLeaf is EFFECT_RENDER); fallback's
read(b._fb) brackets the seat (shadowSeatSave/Restore, #4337); INV-seat-1
amended per D-27 (a leaf that read a born node or the hold's own staging is
in the hold's world).

shadowKnown allow-list of next bugs the swap fixes by construction, each
filed with a generator replay: #4426 (V1' P→C), #4427 (V6 Lv→P), #4428 (B2
born held via the owner-context creator), #4429 (probing leaks into a pulled
pass; Vl4 world). §7 F-transitive reopened: today is a third reading;
one-level and transitive each differ from it in disjoint born-held shapes.

Differential: signals 393 files / solid / web / visibility oracle / generator
2k × {7, 3289, 91501} — zero classes besides the known entries; zero screen
differences against next recordings. Build output byte-identical to next
(scripts/size: every scenario equal in brotli and minified).

Co-authored-by: Claude via Cursor <[email protected]>

* docs(plans): reader-seat rebuild §8 — S1 result and decision (2026-10-29): the swap is stopped, S0+M1 land alone as the spec

Appends §8 with the measured record from #4430: the four-column size table
and the per-module floor breakdown after the levers; the deletions proven,
not subsumed and undecidable; the 50k differential inventory with
milestone attribution (20 of one D-37 class at M3b, two next bugs closed,
two regressions — 3289 #39609 at M3a, 7 #45702 at M3b); the decision (the
table replaces the deciders, ≈ −540 B in core.js, but S1 is additive —
+294 B brotli on the core floor, +1,061 B on + isPending/latest — because
the growth lives in the seam/reveal/hold layer the table does not touch);
method lessons (2k is not evidence for a ruling; size is measured before
design); what is salvaged and what the next decision is. §5 marked
stopped; the brief's §4 is superseded by §8.

Co-authored-by: Claude via Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>

* docs(reader-seat): correct §8.4 overnight-growth unit (unmangled, not minified)

* docs(reader-seat): §8.6 — record the measured salvage results (#4417 adoption, lanes split)

* test(signals): give the heap-mark ratio tripwire a 30 s timeout — the __TEST__ shadow puts it past 5 s under coverage

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: Claude via Cursor <[email protected]>
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.

2 participants