diff --git a/.changeset/same-tick-first-answer-reask.md b/.changeset/same-tick-first-answer-reask.md
new file mode 100644
index 0000000000..5bc1ffb546
--- /dev/null
+++ b/.changeset/same-tick-first-answer-reask.md
@@ -0,0 +1,5 @@
+---
+"@solidjs/signals": patch
+---
+
+An async memo's first answer that resolves in the same tick as a plain write re-asking it is answered, then re-asked (D-22, F-9): `isPending` of it is `true` while the new question flies, and an identity memo or derived store over it reads the answer through `latest` like the memo itself.
diff --git a/packages/signals/docs/RULES-DERIVED.md b/packages/signals/docs/RULES-DERIVED.md
index 1c3bbe4137..810de0280b 100644
--- a/packages/signals/docs/RULES-DERIVED.md
+++ b/packages/signals/docs/RULES-DERIVED.md
@@ -180,7 +180,7 @@ An armed boundary catches a lane the same way it catches a transition (D-12, D-1
**From:** F-6, F-10, F-9. **Scenarios:** S-25, S-45.
-`latest(x)` answers the held write (the newest value a flush has processed, held or not — for a memo nobody has read, the value the flushes it missed would have given it), the optimistic value where one displays, the arrived truth under a superseded override, and the committed value of a node whose own flight is up (below the async nothing newer exists). A sync derivation of a held source is visible through it (the held recompute is a write path like any other). It throws `NotReadyError` only when no value has been produced — before a first landing, including an async memo with no resolution — in every scope. It returns `T`, never `undefined`. A lane held on async downstream of the read does not paint, and does not hide a value `latest` can already return. A memo behind a fallback, or in a branch not yet attached, has still produced its value, so `latest` returns it — a memo born held included: its plain read throws, and `latest` returns its pending value (F-9). Resolved async never pairs `[false, undefined]`. **A first landing a hold carries is a produced value (clarified 2026-10-28):** an async memo's first answer that lands in the same tick as an action writing its input is held with that action (D-4), and `latest` of it returns that answer while the plain read still throws (F-9); a `Loading` whose only read is `latest` of it shows the answer now (S-40). The frame is whole: the screen's committed input beside the answer to it. **A memo nobody has read has still produced what the flushes it missed would have given it (ruled 2026-10-30, #4474):** `latest` of a lazy memo whose sources have committed values is that value, computed on the committed world (laziness is never observable — #3078, #4366, #4474). An identity memo over an async memo, read for the first time in the flush that re-asks the async memo, shows the async memo's landed answer and is pending on the re-ask, the same screen as with a prior reader of it; a plain read of it in that mount is not display-ahead (D-27): its reader is born held (D-2) and shows the re-ask's answer. **Inside a flush, `latest()` of a staging not yet held is the frame's read like any other (clarified 2026-10-28):** it serves the staging, opens no hold, and leaves a plain read beside it in the same pass reading the staging too (F-10: one frame for every channel); if the flush parks, the pass re-derives once as a verdict reader (D-2, A10). A `latest()` read that manufactured a hold for the frame so its pass could run once made the plain reads after it see the screen in a flush that then committed (#4365).
+`latest(x)` answers the held write (the newest value a flush has processed, held or not — for a memo nobody has read, the value the flushes it missed would have given it), the optimistic value where one displays, the arrived truth under a superseded override, and the committed value of a node whose own flight is up (below the async nothing newer exists). A sync derivation of a held source is visible through it (the held recompute is a write path like any other). It throws `NotReadyError` only when no value has been produced — before a first landing, including an async memo with no resolution — in every scope. It returns `T`, never `undefined`. A lane held on async downstream of the read does not paint, and does not hide a value `latest` can already return. A memo behind a fallback, or in a branch not yet attached, has still produced its value, so `latest` returns it — a memo born held included: its plain read throws, and `latest` returns its pending value (F-9). Resolved async never pairs `[false, undefined]`. **A first landing a hold carries is a produced value (clarified 2026-10-28, amended 2026-10-30):** an async memo's first answer that lands in the same tick as a write to its input that a hold carries — an action's, or a plain write re-asking it — is held with it (D-4), and `latest` of it returns that answer while the plain read still throws (F-9); a `Loading` whose only read is `latest` of it shows the answer now (S-40). The frame is whole: the screen's committed input beside the answer to it. **A memo nobody has read has still produced what the flushes it missed would have given it (ruled 2026-10-30, #4474):** `latest` of a lazy memo whose sources have committed values is that value, computed on the committed world (laziness is never observable — #3078, #4366, #4474). An identity memo over an async memo, read for the first time in the flush that re-asks the async memo, shows the async memo's landed answer and is pending on the re-ask, the same screen as with a prior reader of it; a plain read of it in that mount is not display-ahead (D-27): its reader is born held (D-2) and shows the re-ask's answer. **Inside a flush, `latest()` of a staging not yet held is the frame's read like any other (clarified 2026-10-28):** it serves the staging, opens no hold, and leaves a plain read beside it in the same pass reading the staging too (F-10: one frame for every channel); if the flush parks, the pass re-derives once as a verdict reader (D-2, A10). A `latest()` read that manufactured a hold for the frame so its pass could run once made the plain reads after it see the screen in a flush that then committed (#4365).
**Collapses:** [A28](./SPEC-ASYNC-SEMANTICS.md#a28-a-write-becomes-visible-at-flush--to-every-channel) (`latest` reads the flushed staged world), [A11](./SPEC-ASYNC-SEMANTICS.md#a11-sync-derivations-of-held-sources-are-visible-through-latestispending), [A7](./SPEC-ASYNC-SEMANTICS.md#a7-resolved-async-never-reads-false-undefined), [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) (`latest` returns the arrived value). **Status:** standing. **Deviation today:** a lazy memo released unread at its source's landing (#2934) and woken in a flush that re-asks the source has no value; latest of it returns undefined, a plain read of it is born held. Ruled 2026-10-30: it has the value the missed flush would have given it (laziness is never observable). Fix belongs at the wake, both read orders (#4474). Pinned `it.fails` at the ruled screen in `tests/latest-unread-identity-memo-4474.test.ts`; a memo in a root is never released and already reads as ruled.
@@ -188,7 +188,7 @@ An armed boundary catches a lane the same way it catches a transition (D-12, D-1
**From:** F-9, F-1. **Scenarios:** S-26, S-27.
-`isPending(x)` is `true` while any of three causes holds: (i) a write held by a live hold; (ii) the node's own async in flight for a new question; (iii) a fresh value landed but held uncommitted by a hold it is entangled with — and `false` the moment none does. Exception 1: a node with no committed value — a first load, or a memo born held over a held value (D-2) — is loading, not pending. It has no verdict: inside an owner `isPending` of it does what its read does — a first load's `NotReadyError` goes to a boundary or holds the change (F-2); a reader of a memo born held is held with it, or caught by a boundary that has not shown content — and the reader runs at the commit, where the answer is final; unowned it is `false` (D-24). A node with a committed value is judged wherever its readers are: on screen, behind a fallback, or in a branch not yet attached. A verdict on content that may have no committed value is read inside the boundary that owns it — `
{m()}
{isPending(m) ? "updating" : ""}` — where its reader is caught with the content; read outside, it holds the change that mounted it. Either way no answer is shown before the commit: `` is never seen enabled over an unjudged value (`packages/web/test/ispending-guard-born-held.spec.tsx`). No verdict is the honest answer: `true` would say "updating" for something never shown, and `false` would read as settled to a guard like `if (!isPending(x)) save(x())`. Exception 2: a re-ask of the same question is quiet; an `affects()` mark is a declared new question and pends exactly the marked data and what derives from it, released when the declaring hold settles (or at flush end, ambient). `[isPending(x), x()]` read in one scope never pairs `true` with the fresh value: a verdict reader sees the screen — in either read order — unless it is a node a hold's fetch is waiting on (D-25). `isPending(() => latest(x))` follows `x`'s own non-quiet async only — on a signal or sync memo the latest form is never pending — so `[isPending(() => latest(x)), latest(x)]` never pairs `true` with the fresh value either. A spinner driven by `isPending` fires while the owner's async is still in flight (it does not wait for the hold).
+`isPending(x)` is `true` while any of three causes holds: (i) a write held by a live hold; (ii) the node's own async in flight for a new question; (iii) a fresh value landed but held uncommitted by a hold it is entangled with — and `false` the moment none does. Exception 1: a node with no committed value — a first load, or a memo born held over a held value (D-2) — is loading, not pending. A node answered and then re-asked in the same flush — an async first answer that lands in the tick a write re-asks it — is not a first load (amended 2026-10-30): its answer is a produced value, so `latest` of it shows the answer and `isPending` of it is `true`, the re-ask being a new question; so is a derivation of it with no value of its own, at any depth (F-9, D-22). It has no verdict: inside an owner `isPending` of it does what its read does — a first load's `NotReadyError` goes to a boundary or holds the change (F-2); a reader of a memo born held is held with it, or caught by a boundary that has not shown content — and the reader runs at the commit, where the answer is final; unowned it is `false` (D-24). A node with a committed value is judged wherever its readers are: on screen, behind a fallback, or in a branch not yet attached. A verdict on content that may have no committed value is read inside the boundary that owns it — `
{m()}
{isPending(m) ? "updating" : ""}` — where its reader is caught with the content; read outside, it holds the change that mounted it. Either way no answer is shown before the commit: `` is never seen enabled over an unjudged value (`packages/web/test/ispending-guard-born-held.spec.tsx`). No verdict is the honest answer: `true` would say "updating" for something never shown, and `false` would read as settled to a guard like `if (!isPending(x)) save(x())`. Exception 2: a re-ask of the same question is quiet; an `affects()` mark is a declared new question and pends exactly the marked data and what derives from it, released when the declaring hold settles (or at flush end, ambient). `[isPending(x), x()]` read in one scope never pairs `true` with the fresh value: a verdict reader sees the screen — in either read order — unless it is a node a hold's fetch is waiting on (D-25). `isPending(() => latest(x))` follows `x`'s own non-quiet async only — on a signal or sync memo the latest form is never pending — so `[isPending(() => latest(x)), latest(x)]` never pairs `true` with the fresh value either. A spinner driven by `isPending` fires while the owner's async is still in flight (it does not wait for the hold).
**Collapses:** [A19](./SPEC-ASYNC-SEMANTICS.md#a19-ispendingx--the-observable-value-is-not-final-three-causes), [A24](./SPEC-ASYNC-SEMANTICS.md#a24-question-scoped-pending-pending-iff-a-value-change-is-in-flight-or-an-affects-mark-is-live), [A10](./SPEC-ASYNC-SEMANTICS.md#a10-ispendingx-x-is-atomic-within-one-scope), [A8](./SPEC-ASYNC-SEMANTICS.md#a8-ispending--latestx-follows-xs-own-async-only--verdicts-are-per-channel), [A14](./SPEC-ASYNC-SEMANTICS.md#a14-companion-nodes-get-child-lanes-that-do-not-merge-with-the-owner), [A20](./SPEC-ASYNC-SEMANTICS.md#a20-superseded-optimistic-writes-announce-a-store-wide-pending)/[A21](./SPEC-ASYNC-SEMANTICS.md#a21-superseded-the-store-wide-mask) (superseded — the mask is gone), recovery 6.3. **Status:** standing. A probe alone holds nothing; a plain read beside it holds (`optimistic-settle-verdicts.test.ts` #3409, `question-scoped-pending.test.ts` 3.4; #3977). `isPending` of a memo born held, read inside and outside the boundary: `tests/fresh-boundary-born-held-reader.test.ts` (#3977; the engine joins the reader to the hold as the read does, which is the throw's observable effect). **Known deviation (#4469, 2026-10-30):** a verdict reader is re-run at every landing of a hold it observed, even when the landing changes nothing it read; the gate that would skip it needs a deps walk. Measured 2026-10-30: two cheaper gates broke F-9/D-23 (verdict-memo-flight-no-reask) and #4189 (errored-store-probe-settled-3962) pins and failed the frozen floors by +31/+36 B. A plain reader of the same recovered hold is not in this: it shows the committed value for the whole hold and runs once, at the commit, since a recovery to a value equal to the last good one is still a change of answer (D-41, D-9). Pinned in `tests/hold-recovers-landing-reruns-4469.test.ts` (the verdict rows `it.fails`). The same seam reaches a verdict reader of a signal whose writes net to its committed value in the tick an action opens a hold (#4485): row V2 (`stagedScreen`) runs before any netted check, so the reader is listed as the hold's stale reader and re-runs at the landing, and `isPending(s)` reads `true` for the hold although the writes made no proposal (D-8). That is a bug, not this deviation; pinned `it.fails` in `tests/netted-verdict-reader-4485.test.ts`.
diff --git a/packages/signals/src/core/async.ts b/packages/signals/src/core/async.ts
index 371f741b83..3eefc8c82d 100644
--- a/packages/signals/src/core/async.ts
+++ b/packages/signals/src/core/async.ts
@@ -584,7 +584,13 @@ export function handleAsync(
// If the node was dirtied by a newer write (optimistic override or regular),
// skip this stale async result — the upcoming flush will recompute the node
// with the new value, creating a fresh Promise that supersedes this one.
- if (el._flags & REACTIVE_DIRTY) return;
+ // Not a first answer (D-22, F-9): it resolved before the write — only its
+ // reaction ran after — so it is a produced value, and the flush that
+ // re-asks lands it first (the node is answered, then re-asked). Only
+ // `latest`/`isPending` tell such a node from one with no value, so the
+ // landing is the verdict engine's (`holdWorkHook`).
+ if (el._flags & REACTIVE_DIRTY && !(holdWorkHook && el._statusFlags & STATUS_UNINITIALIZED))
+ return;
const wasUninitialized = el._statusFlags & STATUS_UNINITIALIZED;
landStatus(el);
// Attribution hook: lets the engine snapshot state before the landing
diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts
index 5d2418358a..bd34b91e96 100644
--- a/packages/signals/src/core/core.ts
+++ b/packages/signals/src/core/core.ts
@@ -1966,6 +1966,46 @@ export function devRead(el: Signal | Computed): void {
);
}
+/**
+ * D-22 (amended 2026-10-30), F-9: `el`'s first value was staged this flush
+ * and `el` (`owner`) went pending on a re-ask in it — an async first answer
+ * landing in the tick a write re-asks it: answered, then re-asked. A memo
+ * `c` with no value — waiting on `el` (on the source a read of `el` parks
+ * on: `el` itself, or the async it derives from — at any depth), or not
+ * waiting at all (its wait released by the landing, or a first pass) —
+ * derives from that value, as a flush of the answer alone would have let it
+ * (whether the landing ran before the re-ask reached `c` is the flush's
+ * order, not the reader's: D-22, at any depth), then runs
+ * again (the missed-wake latch) and waits on the re-ask holding it:
+ * `latest`/`isPending` of it read as of `el`. One pass: every read of it is
+ * served (the latch marks it); its wait on `el` is dropped, so the re-run
+ * throws and waits on `el` afresh. One served pass a flush (`_served`): a
+ * pass that throws anyway, on a read beside it, parks on `el` again, and its
+ * re-run is not served — served, it would re-run without end. Not a reader
+ * waiting on another source, a render effect (the frame: its plain read
+ * throws), lane work
+ * or a verdict lane's pass (D-37: the screen plus the lane's own guesses),
+ * or a held node (the staging is the hold's). A plain read of such a memo
+ * throws as one of a memo with no value; only `latest`/`isPending` tell them
+ * apart, so it is the verdict engine's (`read` calls it behind
+ * `observeFlightHook`, on the path where a pending read throws).
+ */
+function servesStaged(c: Computed, el: Signal | Computed, owner: Computed) {
+ return (
+ passLane === null &&
+ c._statusFlags & owner._statusFlags & STATUS_UNINITIALIZED &&
+ el._pendingValue !== NOT_PENDING &&
+ !(el._config & (CONFIG_HELD | CONFIG_STAGED_ERROR)) &&
+ !(c as any)._type &&
+ (c._flags & REACTIVE_MISSED_WAKE ||
+ (c._pendingValue === NOT_PENDING &&
+ c._x?._served !== clock &&
+ (!(c._statusFlags & STATUS_PENDING) ||
+ c._x?._pendingSources?.delete((owner._x!._error as NotReadyError).source)) &&
+ ((ext(c)._served = clock), (c._flags |= REACTIVE_MISSED_WAKE))))
+ );
+}
+
export function read(el: Signal | Computed): T {
// Shadow (S0): every read is run through `shadowRead`, which re-enters
// here once (the bypass) and compares the slot the table predicts with
@@ -2181,6 +2221,13 @@ export function read(el: Signal | Computed): T {
);
}
if (!(c._config & CONFIG_CHILDREN_FORBIDDEN) || owner._statusFlags & STATUS_UNINITIALIZED) {
+ // D-22 (amended 2026-10-30), F-9: an answered-then-reasked first
+ // answer (`servesStaged`), the verdict engine's — out of line, so
+ // `read` gains a call only where a pending read would throw.
+ if (observeFlightHook && servesStaged(c as Computed, el, owner)) {
+ if (__TEST__) shadowActual(SLOT_P);
+ return el._pendingValue as T;
+ }
// An untracked read of a pending node still re-runs its reader when
// the node settles — unless nobody is reading (`spectating`, #3528)
// or the reader is the node itself.
diff --git a/packages/signals/src/core/seat.ts b/packages/signals/src/core/seat.ts
index 69de9c7552..84eb70803b 100644
--- a/packages/signals/src/core/seat.ts
+++ b/packages/signals/src/core/seat.ts
@@ -59,6 +59,7 @@ import {
EFFECT_RENDER,
NOT_PENDING,
REACTIVE_JOINED,
+ REACTIVE_MISSED_WAKE,
REACTIVE_RECOMPUTING_DEPS,
REACTIVE_STAGED_READ,
STATUS_ERROR,
@@ -68,6 +69,7 @@ import {
import {
clock,
flushTransaction,
+ observeFlightHook,
globalQueue,
GlobalQueue,
latestOpened,
@@ -922,6 +924,28 @@ export function slot(
if (running) return carry(pred("V4", screen, MARK_ST | MARK_AH, flushTransaction));
return carry(pred("V4", screen));
}
+ // D-22 (amended 2026-10-30), F-9: a first answer staged this flush and
+ // re-asked in it — answered, then re-asked. A pass with no value of its
+ // own, waiting on it (on the source a read of it parks on — the node,
+ // or the async it derives from, at any depth) or not waiting at all (its
+ // wait released by the landing, or a first pass), derives from it, once
+ // a flush (`read`). Not lane work or a verdict lane's pass (D-37).
+ if (
+ observeFlightHook &&
+ cUninit &&
+ uninit &&
+ staged &&
+ !held &&
+ !stagedError &&
+ !isEffect &&
+ !inSeat &&
+ (c!._flags & REACTIVE_MISSED_WAKE ||
+ (c!._pendingValue === NOT_PENDING &&
+ c!._x?._served !== clock &&
+ (!(c!._statusFlags & STATUS_PENDING) ||
+ !!c!._x?._pendingSources?.has(((el as Computed)._x!._error as any).source))))
+ )
+ return carry(pred(rowBase + "D3'", SLOT_P));
return carry(pred(leaf ? "L5" : rowBase + "D3", SLOT_NR)); // F-2 D-28 / F-2 D-9
}
diff --git a/packages/signals/src/core/types.ts b/packages/signals/src/core/types.ts
index 4012b192f6..3c15060f04 100644
--- a/packages/signals/src/core/types.ts
+++ b/packages/signals/src/core/types.ts
@@ -86,6 +86,9 @@ export interface NodeExtension {
_error: unknown;
_blocked: boolean | undefined;
_pendingSources: Set> | undefined;
+ /** The clock of this node's pass served an answered-then-reasked first
+ * answer (D-22, F-9; `read`): one served pass a flush. */
+ _served?: number;
_unobserved: (() => void) | undefined;
_snapshotValue: any;
/** L2 — the previous pass's frame, parked for this node's commit: its child
diff --git a/packages/signals/src/core/verdict.ts b/packages/signals/src/core/verdict.ts
index 6138eba09c..5c3dcea075 100644
--- a/packages/signals/src/core/verdict.ts
+++ b/packages/signals/src/core/verdict.ts
@@ -240,7 +240,6 @@ function heldNotFinal(owner: Computed): boolean {
/** `isPending`'s answer for an unheld node whose flight is up (cause ii). */
function pendingVerdict(c: Computed | null, el: Computed): void {
- if (el._statusFlags & STATUS_UNINITIALIZED) return;
if (probing && !quietPending(el)) probeFound = true;
if (c && tracking) {
if (el._config & CONFIG_HELD) verdictRead(c, txOf(el));
@@ -540,7 +539,14 @@ function verdictValue(el: Signal | Computed, c: Computed | null):
return staged(el);
}
} else if (el._pendingValue !== NOT_PENDING) {
- if (owner._statusFlags & STATUS_PENDING && !uninitialized)
+ // A first value staged this flush and re-asked in it (an async first
+ // answer landing in the tick a write re-asks it) is answered: the
+ // re-ask is a new question (F-9, D-22). An error is no value (D-41).
+ // `latest` serves the staging below.
+ if (
+ owner._statusFlags & STATUS_PENDING &&
+ (!uninitialized || (probing && !(el._config & CONFIG_STAGED_ERROR)))
+ )
pendingVerdict(tracked ? c : null, owner);
else if (tracked && globalQueue._running)
provisionalVerdict(c!, !latestActive && !uninitialized);
diff --git a/packages/signals/tests/answered-then-reasked-released-reader.test.ts b/packages/signals/tests/answered-then-reasked-released-reader.test.ts
new file mode 100644
index 0000000000..a6494e9591
--- /dev/null
+++ b/packages/signals/tests/answered-then-reasked-released-reader.test.ts
@@ -0,0 +1,84 @@
+import { describe, expect, it } from "vitest";
+import {
+ createLoadingBoundary,
+ createMemo,
+ createRenderEffect,
+ createRoot,
+ createSignal,
+ flush,
+ latest
+} from "../src/index.js";
+
+// D-22 (amended 2026-10-30), F-9, async generator E2 (3289 #310, 91501 #1993,
+// #2609): an async memo answered, then re-asked in one flush. When the
+// memo's input is read through `latest()` (or through a sync memo), the
+// write's re-ask reaches the memo after its landing has run, and the landing
+// released the identity memo's wait: it runs with no value and waits on
+// nothing. It still derives from the answer — which of the two the flush ran
+// first is the flush's order, not the reader's — so `latest` of it shows
+// what `latest` of the memo shows.
+const task = () => new Promise(r => setTimeout(r, 0));
+
+async function run(shape: "latest" | "superseded" | "via-memo", view: "memo" | "identity") {
+ const asks: { v: number; resolve: (v: number) => void }[] = [];
+ const [s, setS] = createSignal(shape === "via-memo" ? 2 : 1);
+ const flight = (v: number) => new Promise(resolve => asks.push({ v, resolve }));
+ let m: () => number;
+ if (shape === "via-memo") {
+ const m0 = createMemo(() => latest(s) + 1);
+ m = createMemo(() => flight(m0() + 1));
+ } else m = createMemo(() => flight(latest(s) + 1));
+ const x = view === "memo" ? m : createMemo(() => m());
+ let leaf = "";
+ let output = "";
+ createRoot(() => {
+ const b = createLoadingBoundary(
+ () => {
+ createRenderEffect(
+ () => `l:${latest(x)}`,
+ v => {
+ leaf = v;
+ }
+ );
+ return "content";
+ },
+ () => "fallback"
+ );
+ createRenderEffect(b, v => {
+ output = v;
+ });
+ });
+ const settle = (pick: number) => {
+ const a = asks.splice(pick % asks.length, 1)[0];
+ a.resolve(a.v);
+ };
+ flush();
+ if (shape === "superseded") {
+ // A second question while the first is in flight; its answer lands in
+ // the tick a write asks a third.
+ setS(2);
+ await Promise.resolve();
+ settle(1);
+ setS(1);
+ } else {
+ await task();
+ flush();
+ settle(0);
+ setS(0);
+ }
+ await task();
+ flush();
+ return output === "content" ? leaf : output;
+}
+
+describe("an answered-then-reasked first answer reaches a reader its landing released", () => {
+ for (const [shape, shown] of [
+ ["latest", "l:2"],
+ ["superseded", "l:3"],
+ ["via-memo", "l:4"]
+ ] as const)
+ it(`${shape}: an identity memo shows what the memo shows`, async () => {
+ expect(await run(shape, "memo")).toBe(shown);
+ expect(await run(shape, "identity")).toBe(shown);
+ });
+});
diff --git a/packages/signals/tests/answered-then-reasked-same-tick.test.ts b/packages/signals/tests/answered-then-reasked-same-tick.test.ts
new file mode 100644
index 0000000000..cbff9c9452
--- /dev/null
+++ b/packages/signals/tests/answered-then-reasked-same-tick.test.ts
@@ -0,0 +1,140 @@
+import { describe, expect, it } from "vitest";
+import {
+ createLoadingBoundary,
+ createMemo,
+ createRenderEffect,
+ createRoot,
+ createSignal,
+ createStore,
+ flush,
+ isPending,
+ latest
+} from "../src/index.js";
+
+// D-22 (amended 2026-10-30), F-9: an async memo's first answer resolves, and
+// in the same synchronous turn a plain write re-asks it; the promise reaction
+// runs after the write, so the landing and the re-ask are processed in one
+// flush. The answer exists from when it resolved: the memo is answered, then
+// re-asked — `latest` shows the answer, the re-ask is a new question
+// (`isPending` true), and a derivation of the memo reads the same (async
+// generator E2/E3, `t S5 s0=2 t` and `t S6 s0=0 t`) — at every depth: an
+// identity memo of an identity memo reads as the memo does (D-22).
+const task = () => new Promise(r => setTimeout(r, 0));
+
+async function sameTick(
+ view: "memo" | "identity" | "identity2" | "store",
+ probe: "pending" | "latest"
+) {
+ const asks: { v: number; resolve: (v: number) => void }[] = [];
+ const [s, setS] = createSignal(1);
+ const m = createMemo(() => {
+ const v = 1 + s();
+ return new Promise(resolve => asks.push({ v, resolve }));
+ });
+ let x: () => number = m;
+ if (view === "identity") x = createMemo(() => m());
+ else if (view === "identity2") {
+ const i = createMemo(() => m());
+ x = createMemo(() => i());
+ } else if (view === "store") {
+ const [st] = createStore<{ v?: number }>(d => {
+ d.v = m();
+ }, {});
+ x = () => st.v!;
+ }
+ let leaf = "";
+ let output = "";
+ createRoot(() => {
+ const b = createLoadingBoundary(
+ () => {
+ createRenderEffect(
+ () => (probe === "pending" ? `p:${isPending(x)}` : `l:${latest(x)}`),
+ v => {
+ leaf = v;
+ }
+ );
+ return "content";
+ },
+ () => "fallback"
+ );
+ createRenderEffect(b, v => {
+ output = v;
+ });
+ });
+ const shown = () => (output === "content" ? leaf : output);
+ flush();
+ await task();
+ flush();
+ expect(shown()).toBe("fallback");
+ // `m:2` resolves; before its reaction runs, `s = 2` asks `m:3`.
+ const first = asks.shift()!;
+ first.resolve(first.v);
+ setS(2);
+ await task();
+ flush();
+ expect(asks.map(a => a.v)).toEqual([3]);
+ return shown();
+}
+
+describe("a first answer landing in the tick a plain write re-asks it", () => {
+ for (const view of ["memo", "identity", "identity2", "store"] as const) {
+ it(`${view}: isPending is true — the re-ask is a new question (F-9)`, async () => {
+ expect(await sameTick(view, "pending")).toBe("p:true");
+ });
+ it(`${view}: latest shows the answer (D-22)`, async () => {
+ expect(await sameTick(view, "latest")).toBe("l:2");
+ });
+ }
+});
+
+// The derivation's pass that reads the answer is served it once: a second read
+// in the same pass is served too, and its re-run waits on the re-ask — also
+// when the pass first stopped on another source still loading.
+it("a derivation reading the answer twice, or beside a source still loading, settles", async () => {
+ const asks: { v: number; resolve: (v: number) => void }[] = [];
+ const [s, setS] = createSignal(1);
+ const m = createMemo(() => {
+ const v = 1 + s();
+ return new Promise(resolve => asks.push({ v, resolve }));
+ });
+ let resolveOther!: (v: number) => void;
+ const other = createMemo(() => new Promise(r => (resolveOther = r)));
+ const twice = createMemo(() => m() + m());
+ const beside = createMemo(() => m() + other());
+ const seen: string[] = [];
+ const shown = (probe: () => string, name: string) => {
+ const b = createLoadingBoundary(
+ () => {
+ createRenderEffect(probe, v => void seen.push(`${name} ${v}`));
+ return "content";
+ },
+ () => "fallback"
+ );
+ createRenderEffect(b, () => {});
+ };
+ createRoot(() => {
+ shown(() => `${latest(twice)} ${isPending(twice)}`, "twice");
+ shown(() => `${latest(beside)} ${isPending(beside)}`, "beside");
+ });
+ flush();
+ await task();
+ flush();
+ const first = asks.shift()!;
+ first.resolve(first.v);
+ setS(2);
+ await task();
+ flush();
+ expect(asks.map(a => a.v)).toEqual([3]);
+ expect(seen).toEqual(["twice 4 true"]);
+ resolveOther(10);
+ await task();
+ flush();
+ // `beside` never derived a value: its pass over the answer stopped on
+ // `other`, and its re-run waits on the re-ask.
+ expect(seen).toEqual(["twice 4 true"]);
+ const next = asks.shift()!;
+ next.resolve(next.v);
+ await task();
+ flush();
+ expect(seen).toEqual(["twice 4 true", "twice 6 false", "beside 13 false"]);
+});
diff --git a/packages/web/test/async-gen/e2-released-reader.spec.ts b/packages/web/test/async-gen/e2-released-reader.spec.ts
new file mode 100644
index 0000000000..f7402f4fd0
--- /dev/null
+++ b/packages/web/test/async-gen/e2-released-reader.spec.ts
@@ -0,0 +1,28 @@
+/**
+ * @vitest-environment jsdom
+ */
+// async-gen E2/E3 (3289 #310, 91501 #1993, 91501 #2609; D-22 amended
+// 2026-10-30, F-9): an async memo answered, then re-asked in one flush, with
+// its input read through `latest()`. The landing ran before the re-ask
+// reached the memo, so it released the identity memo's (and the derived
+// store's) wait, and the served-only-while-waiting rule left them on
+// `[ROOT-L]` while the plain form showed the answer.
+import { expect, test } from "vitest";
+import fc from "fast-check";
+import { runScenario } from "./run.js";
+import { scenarioArb } from "./scenario.js";
+
+for (const [seed, i] of [
+ [3289, 310],
+ [91501, 1993],
+ [91501, 2609]
+] as [number, number][])
+ test(`${seed} #${i}: an identity memo and a derived store show what the memo shows`, async () => {
+ const sc = fc.sample(scenarioArb(), { seed, numRuns: i + 1 })[i];
+ const a = await runScenario(sc);
+ for (const variant of [{ identity: true }, { store: true }]) {
+ const b = await runScenario(sc, variant);
+ const at = (r: typeof a) => r.screens.filter((_, j) => a.settled[j] && b.settled[j]);
+ expect(at(b), JSON.stringify(variant)).toEqual(at(a));
+ }
+ });
diff --git a/packages/web/test/async-gen/reask-served-once-a-flush.spec.ts b/packages/web/test/async-gen/reask-served-once-a-flush.spec.ts
new file mode 100644
index 0000000000..a3a3826f32
--- /dev/null
+++ b/packages/web/test/async-gen/reask-served-once-a-flush.spec.ts
@@ -0,0 +1,56 @@
+/**
+ * @vitest-environment jsdom
+ */
+// async-gen 91501 #749, minimized (#4459 follow-up; D-22 amended 2026-10-30,
+// F-9): `m0` is answered, then re-asked in one flush, and a boundary's
+// children memo reads two `Show`s that both park on `m0`. Its first read of
+// the first `Show`'s output is served the answer; its read of the second
+// throws, so the pass parks on `m0` again with no value of its own. Served
+// on every re-run, the missed-wake latch re-ran it in the same flush without
+// end (a synchronous loop). One served pass a flush: the re-run throws and
+// waits on `m0`.
+import { expect, test } from "vitest";
+import { runScenario } from "./run.js";
+import type { Scenario } from "./scenario.js";
+
+// [s0=3 m0=async(s0 !%4=2) m1=async(m0 !%4=1)]
+//
+// :: s0=2 S1 t S0 s0=1
+const sc: Scenario = {
+ signals: [3],
+ memos: [
+ { async: true, deps: [{ src: { k: "s", i: 0 }, how: "plain" }], poison: 2 },
+ { async: true, deps: [{ src: { k: "m", i: 0 }, how: "plain" }], poison: 1 }
+ ],
+ tree: {
+ t: "group",
+ kids: [
+ {
+ t: "show",
+ when: { how: "plain", src: { k: "m", i: 1 } },
+ child: { t: "leaf", text: [], attrs: [], grouped: false },
+ fallback: true
+ },
+ {
+ t: "show",
+ when: { how: "plain", src: { k: "m", i: 0 } },
+ child: { t: "leaf", text: [], attrs: [], grouped: false },
+ fallback: true
+ }
+ ]
+ },
+ steps: [
+ { t: "write", s: 0, v: 2 },
+ { t: "settle", pick: 1 },
+ { t: "task" },
+ { t: "settle", pick: 0 },
+ { t: "write", s: 0, v: 1 }
+ ]
+};
+
+test("a reader served an answered-then-reasked first answer is served one pass a flush", async () => {
+ for (const variant of [{}, { identity: true }, { store: true }, { split: true }]) {
+ const r = await runScenario(sc, variant);
+ expect(r.screens, JSON.stringify(variant)).toEqual(Array(6).fill("[ROOT-L]"));
+ }
+});
diff --git a/packages/web/test/async-gen/same-tick-first-answer-reask.spec.ts b/packages/web/test/async-gen/same-tick-first-answer-reask.spec.ts
new file mode 100644
index 0000000000..1171852731
--- /dev/null
+++ b/packages/web/test/async-gen/same-tick-first-answer-reask.spec.ts
@@ -0,0 +1,40 @@
+/**
+ * @vitest-environment jsdom
+ */
+// async-gen rerun 2026-10-30, seed 3289 (E2, E3; D-22 amended 2026-10-30,
+// F-9): an async memo's first answer resolves, and in the same synchronous
+// turn a plain write re-asks it. The promise reaction runs after the write,
+// so the landing and the re-ask are processed in one flush. The answer
+// exists from when it resolved: `m0` is answered, then re-asked. `latest`
+// shows the answer, the re-ask is a new question (`isPending` true), and an
+// identity memo or a derived store over `m0` reads the same. The plain form
+// showed `pm0:false`; the identity and store forms stayed on `[ROOT-L]`.
+import { expect, test } from "vitest";
+import { runScenario } from "./run.js";
+import type { Scenario } from "./scenario.js";
+
+const scenario = (s0: number, how: "pending" | "latest", pick: number, write: number) =>
+ ({
+ signals: [s0],
+ memos: [{ async: true, deps: [{ src: { k: "s", i: 0 }, how: "plain" }], poison: null }],
+ tree: {
+ t: "leaf",
+ text: [],
+ attrs: [{ how, src: { k: "m", i: 0 } }],
+ grouped: how === "pending"
+ },
+ steps: [{ t: "task" }, { t: "settle", pick }, { t: "write", s: 0, v: write }, { t: "task" }]
+ }) as Scenario;
+
+for (const [name, sc, final] of [
+ // [s0=1 m0=async(s0)] :: t S5 s0=2 t
+ ["isPending", scenario(1, "pending", 5, 2), ""],
+ // [s0=2 m0=async(s0)] :: t S6 s0=0 t
+ ["latest", scenario(2, "latest", 6, 0), ""]
+] as const)
+ test(`${name}: a first answer landing in the tick a write re-asks it is answered`, async () => {
+ for (const variant of [{}, { identity: true }, { store: true }]) {
+ const r = await runScenario(sc, variant);
+ expect(r.screens.at(-1), JSON.stringify(variant)).toBe(final);
+ }
+ });