fix(signals): reporter liveness reads this pass's deps; a dropped dep retires the reporter (fuzzer #3446 P1, remaining forms) - #3495
Merged
Conversation
🦋 Changeset detectedLatest commit: 0e80ca7 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 |
Coverage Report for CI Build 35161441724Coverage remained the same at 71.46%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 16.74%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | projection derive: write one NESTED field (reference) |
2.1 ms | 1.8 ms | +16.74% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing fix/staged-reporter-deps (0e80ca7) with next (d741d1e)
… retires the reporter and wakes every parked transaction (fuzzer #3446 P1, remaining forms) Three corrections to how a parked transaction learns that a reporter stopped observing the flight it waits on (A15 / #3426; spec O3): 1. `reporterBlocksSource`'s deps scan is bounded at `_depsTail` — this pass's reads. A staged pass keeps its previous deps linked past the tail until the commit trims them (A30), so a reporter whose pass had stopped reading the source in the same flush as the write still looked live through the kept dep, and the hold it kept was the commit that would have trimmed the dep that kept it. Rule 2's predicate reading Rule 3's deferral (fuzzer case 21). 2. recompute's tail retires a reporter when its pass DROPPED a dep, not only when it recovered from pending: a reporter registered by the stale-reader carve-out (heldFromStale, an initialized source refetching) displays the committed value and is never pending (fuzzer case 79). 3. The retirement wakes every parked transaction (wakeParked), not the reporter's stamp — the transaction waiting on it registered it without stamping it (a later write's hold over a flight an earlier step observed). One idle pass per parked transaction; done ones return. Fuzzer, same campaign (seed 3289, 1000 cases): 994 pass / 0 fail / 6 policy, from 984 / 4 / 12. Same-flush pin flipped from it.fails. Posture matrix: the runner records INVARIANT_VIOLATIONs per cell instead of dying (new `invariant` column). With the parked-transaction leak gone, quiescence checks run again and surface a pre-existing INV-4 — a projection leaf's latest() shadow is stale on the flush right after its root is disposed mid-refetch (spec O5; pinned it.fails as S3 in posture-store-parity.test.ts; standalone repro on `next`). The two matrix cells that trigger it are excluded until it is fixed; every other cell's served value is unchanged. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…d it.fails in its own file (spec O5); size caps Surfaced by the O3 fix: with the parked-transaction leak gone, the posture matrix's quiescence checks run again and INV-4 fires on a pre-existing window — the synchronous flush right after disposing a projection store with a held refetch and a latest() companion on a leaf. Externally the leaf and its latest() agree; the stale pair is an internal companion owner, and the window closes a microtask later. Pinned in its own file because a live action in a sibling test masks the quiescence check the same way the leak did. Two brotli caps +50 B for the O3 fix (0 B minified in the floor). Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…both were swept in by a soft-reset squash from a pre-#3491 working tree Co-authored-by: Cursor <cursoragent@cursor.com>
Regenerate RULES-INDEX after the reporter-liveness docs update and reconcile the combined Brotli limits after rebasing over #3496. Co-authored-by: GPT-5.6 Sol via Cursor <cursoragent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid
force-pushed
the
fix/staged-reporter-deps
branch
from
September 16, 2026 23:14
0de7bdd to
0e80ca7
Compare
ryansolid
added a commit
to brenelz/solid
that referenced
this pull request
Sep 17, 2026
… latest() of a dead leaf creates none (spec O5, INV-4) Disposing a store whose async source was mid-refetch left the latest() shadow of a leaf orphaned: never derived (its compute read through the projection in flight, the backfilled override stood in), its override dropped at the settle, NotReady/uninitialized forever against a leaf whose committed value differed — INV-4 at the next quiescence. Externally coherent (latest() fell back to the committed value), but a companion outliving its source, which is what INV-9's rationale forbids. The disposal snap covered the disposed computed's own companions only; a projection's leaves hang off the firewall, not the child chain. Two parts: - disposeChildren(self) snaps the companions of the firewall's _companionChildren (the set markFirewallChildCompanions maintains, solidjs#3038); snapCompanionsToState retires a shadow whose owner's firewall is disposed — a disposed shadow reads as absent, a later read recreates it from the committed view. The isPending signal snaps as the owner's does. - latestRead of a leaf whose firewall is disposed serves the committed value and creates no shadow: a boundary's content re-running after the teardown recreated one that no teardown would ever retire. That recreated shadow was the posture matrix's actual trigger. Surfaced once solidjs#3495 stopped leaking parked transactions, which had kept `transitions.size > 0` and masked every __TEST__ quiescence check in the matrix. All 621 matrix cells run with zero invariant violations; the two excluded cells are back. Pin flipped from it.fails and extended with the post-disposal read. +104 B minified (owner.ts, core floor); five brotli caps +13…+43 B. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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.
Closes spec O3 fully. Fuzzer (#3446), same campaign as every run this week: 994 pass / 0 fail / 6 policy, from 984 / 4 / 12 after #3488.
The remaining forms
After #3488 the fuzzer's law P1 still failed on 4 cases (21, 79). Both are "a reporter stopped observing the flight and nobody noticed", in shapes #3488's wake site didn't cover:
Case 21 — gate and write in one flush. Probing showed the effect's compute does run in that flush and stages
"hidden". A30 keeps a staged pass's previous deps linked past_depsTailuntil the commit trims them — soreporterBlocksSource's deps scan, walking the whole list, found the kept dep on the memo and called the effect live. The hold kept the dep that kept the hold. Rule 2's predicate reading Rule 3's deferral.Case 79 — an initialized memo refetching. The reader shows the committed value through the stale-reader carve-out (
heldFromStale) and registers as a reporter without ever being pending — so #3488's "recovered from pending" wake never fires. And the transaction waiting on it registered it without stamping it (a later write's hold over a flight an earlier step observed), so a stamp-keyed wake named the wrong transaction.Fix (three corrections,
scheduler.ts+core.ts)reporterBlocksSource's deps scan is bounded at_depsTail— this pass's reads, not the committed frame's kept tail. A trimmed list ends there anyway; a pass that read nothing has a null tail; a pending reporter's source is in the prefix (linked before the throw), so the_pendingSourcesand error arms are unaffected.recompute's tail retires a reporter when its pass dropped a dep (deps past the tail, or a pass that read nothing) — not only when it recovered from pending.wakeParked), not the reporter's stamp. One idle pass per parked transaction; done ones return at re-entry.Effects only (reporters register from render-effect notification, INV-3). Same-flush pin flipped from
it.fails.What the fix uncovered
The posture matrix had been leaking parked transactions — dead reporters never re-judged, this exact bug — which kept
transitions.size > 0and silenced every__TEST__quiescence invariant for the rest of each run. With the leak gone, quiescence checks run again and surface a pre-existing INV-4: a projection leaf'slatest()shadow reads stale on the synchronous flush right after the store's root is disposed mid-refetch (spec O5). Reproduces standalone onnext; externally the leaf and itslatest()agree, so the stale pair is an internal companion owner (the projection's firewall node is the candidate); the window closes a microtask later. Under__TEST__the runtime's own scheduled flush throws in that window and leaves the scheduler mid-flush.it.failsintests/inv4-projection-dispose-shadow.test.ts(its own file: a live action in a sibling test masks the check).INVARIANT_VIOLATIONs per cell (newinvariantcolumn) instead of dying; the two cells that trigger INV-4 (gatedAway× the projection states) are excluded until it's fixed. Every other cell's served value is unchanged vs the pre-fix report.Gate
signals 2,734 + 2 expected fails (INV-4; S1/O4) / solid 620 / web 825. Floor 24,578 flat (0 B minified); two CSR brotli caps +30 / +40 B. CodSpeed pending.