Skip to content

[2.0 / next] probing leaks into a memo's pass pulled inside isPending — a nested latest() joins the pass to the hold #4429

Closed

Description

@ryansolid

Summary

isPending(fn) sets the module flag probing for the duration of fn. When a read inside fn pulls a dirty memo, verdictValue closes the window for the memo's pass (setVerdict(null) — A31: "the pulled memo's pass is its own, not the probe's") but leaves probing set. If that pass calls latest() itself, latest re-opens verdictValue with probing still true, and the pulled memo's read is treated as the outer probe's:

  • a held node born into the future (CONFIG_HELD, uninitialized, staged) read through latest() takes the probing && owned arm and joinPasses the pulled memo to the hold (REACTIVE_JOINED) — the memo's pass joins a world its own read (latest, a verdict read) does not derive from;
  • probeFound is set from inside the pulled pass (heldNotFinal, flying, a live affects() mark), so the outer isPending answers for reads the pulled memo made, not for the memo it read.

The same memo pulled by a plain read (or already clean) does neither. The pass's world depends on who woke it.

Reproduction

async-gen seed 7 case #1691 (packages/web/test/async-gen):

const sc = fc.sample(scenarioArb(), { seed: 7, numRuns: 1692 })[1691];
await runScenario(sc);
[s0=0 m0=memo(s0) m1=memo(latest(m0)+m0 !%4=1) m2=memo(s0 !%4=2)]
<Show when=latest(m2) fb>
  <span split[isPending(m0),isPending(s0)]>m0()</span>
  <span split[latest(s0)]>isPending(m1)</span>
</Show>
:: t A(s0=1) s0=2 s0=3 f s0=0 t s0=1 s0=3 m t m
scenario JSON
{"signals":[0],"memos":[{"async":false,"deps":[{"src":{"k":"s","i":0},"how":"plain"}],"poison":null},{"async":false,"deps":[{"src":{"k":"m","i":0},"how":"latest"},{"src":{"k":"m","i":0},"how":"plain"}],"poison":1},{"async":false,"deps":[{"src":{"k":"s","i":0},"how":"plain"}],"poison":2}],"tree":{"t":"show","when":{"how":"latest","src":{"k":"m","i":2}},"child":{"t":"group","kids":[{"t":"leaf","text":[{"how":"plain","src":{"k":"m","i":0}}],"attrs":[{"how":"pending","src":{"k":"m","i":0}},{"how":"pending","src":{"k":"s","i":0}}],"grouped":false},{"t":"leaf","text":[{"how":"pending","src":{"k":"m","i":1}}],"attrs":[{"how":"latest","src":{"k":"s","i":0}}],"grouped":false}]},"fallback":true},"steps":[{"t":"task"},{"t":"action","s":0,"v":1},{"t":"write","s":0,"v":2},{"t":"write","s":0,"v":3},{"t":"flush"},{"t":"write","s":0,"v":0},{"t":"task"},{"t":"write","s":0,"v":1},{"t":"write","s":0,"v":3},{"t":"micro"},{"t":"task"},{"t":"micro"}]}

isPending(m1) in a render effect pulls m1; m1's pass calls latest(m0) with m0 held and uninitialized in the flush's own transaction (born into it). Stack: joinPass < verdictValue (the uninitialized-held arm, probing) < read < latest < m1's fn < recompute < pullComputed < verdictValue < read < isPending.

Found by the S1 reader-seat differential (SEAT_SHADOW_REPORT): class Vl4 P/P — world after the read (old 2, new 0; Vl posture; s h u · vseat ahead oldVlane flushTx flushIsElTx), 1 instance at 2k cases × seed 7; none in the signals, solid or web suites. The slot agrees (the staging, P); the pass's world does not. No end-screen difference in this case.

Why it is a bug

  • A31: a memo's pass pulled inside a window is its own; the window answers for what the pass produced. verdictValue applies this to the dispatch (setVerdict(null)) and not to probing/probeFound, so a nested latest() inside the pulled pass re-enters the window with the outer probe's state.
  • F-vworld (2026-10-29): a verdict read leaves marks and does not raise the pass's world; latest() of a born-into-the-future node is a verdict read whichever pass makes it.

Intended fix

The reader-seat rebuild S1 (#4395 → S1 PR) decides the pass's world from its seat and the read's posture (row Vl4), not from a flag the puller left set. Until the swap lands, the shadow carries this as a known class (shadowKnown, this issue). (probeFound's leak is the same bug's other half; the differential compares worlds and slots, not probe answers — the swap should save/clear probing and probeFound around the pulled pass as it does verdict.)

Activity

  1. changed the title [-]signals: `probing` leaks into a memo's pass pulled inside `isPending` — a nested `latest()` joins the pass to the hold[/-] [+][2.0 / next] `probing` leaks into a memo's pass pulled inside `isPending` — a nested `latest()` joins the pass to the hold[/+] on Oct 29, 2026
  2. ryansolid commented on Oct 30, 2026

    @ryansolid
    MemberAuthor

    Fixed on next by #4465 (ab3d2ad); it ships in the next RC.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions