fix(signals): nodes created mainline during a hold are born held (A29 creation-time form) - #3451
Merged
Merged
Conversation
🦋 Changeset detectedLatest commit: 4869d98 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 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 |
… creation-time form) A memo or effect created from mainline code while a transaction holds a value it reads — a component mounting on a click while an action is in flight — was served the staged value (A29) and then recompute's creation arms committed it anyway (`create ||` direct-commit; a create pass is never held), publishing the held value into the mainline frame beside pre-existing readers showing the committed one. The oracle's two A29/A18(c) violation cells. And enterStagedRead entered the transaction with initTransition from creation code, outside any flush, leaving activeTransition and the batch pointed at it for the rest of the synchronous block: an unrelated write made after the mount was swallowed into the action (y never published). Now, outside a flush, enterStagedRead records the transaction (stagedEntry) instead of entering; recompute stages the pass's node INTO it — stamped, pushed to its pending nodes, STATUS_UNINITIALIZED kept — and for an effect adds it to the transaction's gated subs and skips the synchronous first run (effect()); the commit initializes the node and replays the effect. read() holds readers of a node with a staged value and no committed one: a stale reader enters instead of showing undefined, an untracked reader throws NotReady (A19 exception 1). Inside a flush nothing changes (#3408's pin). Verdict pulls (GlobalQueue._verdictPull — the latest() shadow is created before it is marked optimistic) and optimistic-posture nodes keep the entering path; supersededRead's staged truth is a staged read too. Ruling (2026-09-14): 1(a) — a node born of the held world has no committed value until the commit; 2 — the entry is the pass's, never the block's. Pins: tests/born-held.test.ts; the oracle cells flip from violation to rule. Size: +316 B minified core floor (23,365 → 23,681), brotli +105 core / +126 store / +146 verdict; caps ratcheted with notes. Signals 1895, solid 618, web 783 green. Co-authored-by: Cursor <cursoragent@cursor.com>
…re the node's first commit (A18 d) computePendingState gated its held-value branch on !STATUS_UNINITIALIZED before checking for an override, so an optimistic node whose first landing was held (a downstream reveal that never landed) and was then superseded under its override read false while the arrival differed — the oracle's last violation cell. The uninitialized suppression is A19 exception (1), "no observable value exists to be non-final"; a displayed override is an observable value, so the override check now comes first and the suppression applies only without one. Pinned by tests/superseded-before-first-commit.test.ts and the oracle cell (flipped from violation to rule; the state is renamed for its actual condition — the node never committed, not "downstream uninitialized"). A18 (d) amended in place. Verdict module only: no core-floor cost. Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid
force-pushed
the
fix/born-held
branch
from
September 15, 2026 08:15
f938bd3 to
8a5d25e
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>
Coverage Report for CI Build 34946380801Coverage remained the same at 71.842%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Was stacked on #3449; now rebased onto
next(the three #3449 commits dropped). Fixes the two A29/A18(c) violation cells the oracle found, plus a swallowed-write bug the fix exposed.The bug
A memo or effect created from mainline code while a transaction holds a value it reads — a component mounting on a click while an action is in flight:
Two causes:
recompute's creation arms: acreatepass always direct-commits (create ||) and is never held. The fresh memo was correctly served the staged value (A29) andenterStagedReadentered the transaction — then the create arm committed the value as the memo's mainline truth anyway. Same for a superseded node: the fresh memo published the truth while a fresh direct effect beside it showed the override.enterStagedReadentered viainitTransitionfrom creation code, outside a flush. Nothing restoresactiveTransitionthere (the flush loop does inside a flush; action steps end with a flush), so the rest of the synchronous block — including the unrelatedsetY— became the action's work. The analogous reveal path (a creation-time read of a pending async node) does not do this.The rule (maintainer, 2026-09-14)
NotReady(A19 exception 1).The fix
Outside a flush,
enterStagedReadrecords the transaction (stagedEntry) instead of entering.recomputestages the pass's node into it:_transitionstamped, pushed to its_pendingNodes,STATUS_UNINITIALIZEDkept; an effect is added to the transaction's_gatedSubsand skips its synchronous first run (effect());commitPendingNodeinitializes the node and the replay runs the effect.read()holds readers of a node with a staged value and no committed one (stale readers enter instead of showingundefined). Inside a flush nothing changes — #3408's pin is untouched. Verdict pulls (GlobalQueue._verdictPull; thelatest()shadow is created before it is marked optimistic, so the config bit alone cannot tell) and optimistic-posture nodes keep the entering path.supersededRead's staged truth is a staged read too.A mainline mount therefore shows the committed frame for direct bindings and holds derived ones until the commit reveals both.
Verification
tests/born-held.test.ts: fresh memo + effect held / fresh direct effect committed / commit reveals all; unrelated write after the mount stays mainline; untracked read of a born-held memo throws until commit; in-flush branch flip unchanged (2.0.0-rc.8 conditional memo reveals a held signal value early #3408).violationcells flip torule. 90/90.@solidjs/signals174 files / 1895 ·solid-js618 ·@solidjs/web783 — green.Size
+316 B minified on the in-package core floor (23,365 → 23,681); brotli +105 B core floor, +126 +createStore, +146 +isPending/latest. Conscious bump, notes in
treeshake.test.tsand.size-limit.js. Golfed once (flag as aGlobalQueuestatic instead of an exported setter; supersession entry moved intosupersededReadoutside the floor; droppedcurrentTransition/_valueTransitionin the staging arm — the pass runs outside a flush, no merges happen). What remains is the rule: a new node state needs detection, staging, read semantics, commit-time init, and the effect skip.Co-authored-by: Cursor cursoragent@cursor.com
Made with Cursor
Also: the last oracle violation (second commit)
isPending()under a displayed override now reads the arrival even before the node's first commit. The oracle state I had labeled "downstream never initialized" was really "the node itself never committed" — its first landing was held by a reveal that never landed — andcomputePendingStateapplied A19 exception (1) ("uninitialized is loading, not pending") before checking for an override. A displayed override is an observable value, so the override check comes first. Verdict module only, no core-floor cost. Pinned bytests/superseded-before-first-commit.test.ts; A18 (d) amended.Oracle: 90 cells, zero violations. A pre-existing, shape-sensitive
NotReadyErrorescapingflush()was observed while probing (reproduces onnext) but could not be pinned stably; not this PR's.Rebase onto
next(2026-09-15)notifyStatus/recomputeseams): in-package core floor 23,419 → 23,753 minified (+334); cap 23,750 → 23,800. Brotli againstnext: core floor 8708 → 8820 (+112), +createStore 15658 → 15743 (+85), +isPending/latest 11084 → 11213 (+129) — inside the caps the PR set. The six app scenarios had no headroom left (hydratingsat at 18899/18900) and now exceed by 46–123 B on the same core bytes; caps bumped +0.15–0.20 KB with notes.tscerror surfaced (CI never ran here — the old base wasn'tnext): TS narrowed the module-levelstagedEntrytonullat the pass's reset and kept that across the compute call, sobornHeldtypednulland the born-held arm wasnever. Re-widened with a cast at the read; no emitted code.RULES-INDEX.mdregenerated;--checkcurrent.Verified after rebase: signals 1905, solid 618, web client 786 / server 914, size-limit all scenarios pass.