Skip to content

fix(signals): a derivation's untracked read of a held store value is born held (A29 at the store's untracked paths) - #3520

Closed
ryansolid wants to merge 1 commit into
fix/store-backing-stale-replayfrom
fix/store-untracked-born-held
Closed

ryansolid wants to merge 1 commit into
fix/store-backing-stale-replayfrom
fix/store-untracked-born-held

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Stacked on #3517 (← #3516#3515). Step 4 of DESIGN-CONSOLIDATION move 3b. Also commits the design doc.

The bug (S5)

Core read() serves a staged value to a deriving reader and enters its transaction in the same arm — an untracked read inside a memo is still that memo's read (context persists under untrack), so a mainline memo over a held signal is born held (A29) and publishes nothing until the action commits.

The store's untracked paths — nodeValue, pendingBackingVisible, readSource's adoption-hold view — restated the value half and dropped the entry. A mainline memo's or user effect's untrack(() => s.n) published the action's unrevealed write to the screen.

shape memo → render effect, on next signal
store key, node / no node / reconcile adoption [1] at creation [] until commit, then [1]

Same for createEffect. Fifteen witness rows, all store shapes now match the signal.

The fix

The three store sites call enterStagedRead on the arm that serves the staged value. enterStagedRead accepts a null node for a backing (the fold's / adoption's transaction is passed). The two store tunnels (latest() for store keys, authoritativeServe for truth authors) keep serving without entering, unchanged.

Discovery: the matrix gets the reader kind that sees this

visibility-oracle-posture gains memoUntracked / effectUntracked (an untracked read inside a derivation): 621 → 851 cells. The 5-reader subset is byte-identical to next; the new rows for the held plain-store state go from 1 | 1 | 1 (published, no entanglement) to exactly the signal's HELD | 1≠ ENTANGLED | …. This is the kind that would have caught S4 too.

The new rows also show two more store/signal divergences, recorded at their current values, not ruled (S6, S7 in posture-store-parity, with both sides pinned):

Both need a ruling before either side moves.

The design doc

packages/signals/docs/DESIGN-CONSOLIDATION.md — dropped from next by c7ec862's squash accident, restored from design/consolidation with a §0 ledger of moves 3a/3b (what merged, what's in this stack, what each step found, bytes). The code comments in #3515 now cite a file that exists.

Gate

check result
signals suite 2995 pass (+14 pins: S5 ×10, S6 ×2, S7 ×2)
visibility oracles identical to next
posture matrix 5-reader subset identical; only the new memoUntracked rows for the held store state moved, to the signal's values
fuzzer seed 3289 × 1000 994 / 0 / 6 — same as next
solid-js / web 623 pass / same 12 pre-existing server-diagnostics failures as origin/next
size +1 B minified core, +130 B minified store; two brotli caps ratcheted (+54 / +12 B)

Made with Cursor

…born held (A29 at the store's untracked paths)

Step 4 of DESIGN-CONSOLIDATION move 3b. Core read() serves a staged value to
a deriving reader and enters its transaction in the same arm — an untracked
read inside a memo is still that memo's read (context persists under
untrack), so a mainline memo over a held signal is born held. The store's
untracked paths — nodeValue, pendingBackingVisible, readSource's adoption-hold
view — restated the value half and dropped the entry: a mainline memo's or
user effect's untrack(() => s.n) PUBLISHED the action's unrevealed write.

They now call enterStagedRead on the arm that serves the staged value;
enterStagedRead takes a null node for a backing (the fold's transaction is
passed). The two store tunnels (latest() for store keys, authoritativeServe)
keep serving without entering, as before.

Discovery: the posture matrix gains memoUntracked / effectUntracked reader
kinds (621 -> 851 cells) — the kind that sees S4 and S5, and that shows two
more store/signal divergences recorded, not ruled, as S6 (staged-ambient
write read by a derivation created inside a foreign action) and S7
(optimistic override with the only reader gated away).

Commits docs/DESIGN-CONSOLIDATION.md (dropped from next by c7ec862's squash
accident) with a ledger of moves 3a/3b so far; the code citations now point
at it.

Gate: suite green (+14 pins); oracles identical; the 5-reader subset of the
matrix identical to next; +1 B minified core, +130 B minified store; two
brotli caps ratcheted.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
@changeset-bot

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e30fcc7

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

ryansolid added a commit that referenced this pull request Sep 17, 2026
…unobserved (S7; store follows signal)

Ruling 2026-09-17: store rules follow signal rules. Of the two divergences
the 7-reader posture matrix recorded in #3520, this ships S7 and defers S6.

S7 — the slot hook released a property node the moment its last subscriber
left, with the optimistic override on it (overrides live on nodes over a
clone the setter discards): s.n read 0 while the action was live, where an
optimistic signal keeps its override whether or not anything reads it. A
node carrying an override or a staged write now defers its release to the
flush that resolves it (deferSlotRelease feeds the scheduler's
transientStoreNodes sweep — present since the monorepo absorb with exactly
these checks, and unfed). +55 B minified store. Pinned: override visible
after gating away; node released at settle.

S6 — A28 at the backing (a mainline setStore invisible to a derivation
created inside a foreign action until the carrying flush) is ruled the same
way but DEFERRED: done at the store twin it cost +402 B minified (a node
born in the unflushed window must stage the write, and every arm existed
twice — core's and the twin's). Pinned at the store's current value with
the ruling attached; it closes when the store's value selection shares
core's serve (DESIGN-CONSOLIDATION move 3b step 6). Ledger updated.

Gate: suite green; oracles identical; matrix: only the S7 cells moved, to
the signal's values; solid/web unchanged from next; no size caps exceeded.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
ryansolid added a commit that referenced this pull request Sep 17, 2026
…unobserved (S7; store follows signal)

Ruling 2026-09-17: store rules follow signal rules. Of the two divergences
the 7-reader posture matrix recorded in #3520, this ships S7 and defers S6.

S7 — the slot hook released a property node the moment its last subscriber
left, with the optimistic override on it (overrides live on nodes over a
clone the setter discards): s.n read 0 while the action was live, where an
optimistic signal keeps its override whether or not anything reads it. A
node carrying an override or a staged write now defers its release to the
flush that resolves it (deferSlotRelease feeds the scheduler's
transientStoreNodes sweep — present since the monorepo absorb with exactly
these checks, and unfed). +55 B minified store. Pinned: override visible
after gating away; node released at settle.

S6 — A28 at the backing (a mainline setStore invisible to a derivation
created inside a foreign action until the carrying flush) is ruled the same
way but DEFERRED: done at the store twin it cost +402 B minified (a node
born in the unflushed window must stage the write, and every arm existed
twice — core's and the twin's). Pinned at the store's current value with
the ruling attached; it closes when the store's value selection shares
core's serve (DESIGN-CONSOLIDATION move 3b step 6). Ledger updated.

Gate: suite green; oracles identical; matrix: only the S7 cells moved, to
the signal's values; solid/web unchanged from next; no size caps exceeded.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
@ryansolid

Copy link
Copy Markdown
Member Author

Superseded by #3523, which now targets next directly and carries this PR's commit (rebased) along with the rest of the stack. Closing so the move-3b work is one review rather than five.

Claude via Cursor

@ryansolid ryansolid closed this Sep 17, 2026
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.

1 participant