perf(ui): stop long-lived request and session caches from growing forever (#127818)

* perf(ui): bound long-lived request and session state

* fix(gateway-client): prevent request id collisions

* fix(ui): preserve ordering across agent caches

* fix(ui): preserve session order after pruning

* fix(ui): retain cached session order during loading

* fix(ui): evict removed agent session caches

* test(ui): cover canonical session cache pruning
This commit is contained in:
Vyctor H. Brzezowski
2026-08-22 22:15:46 -03:00
committed by GitHub
parent 6ed8954c8a
commit d75164c46d
10 changed files with 356 additions and 79 deletions
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { GatewayPendingRequests } from "./pending-request.js";
describe("GatewayPendingRequests", () => {
it("does not retain settled IDs for the socket generation", async () => {
let requestId = 0;
const requests = new GatewayPendingRequests({
createRequestId: () => `request-${requestId++}`,
nowMs: () => 0,
});
const sender = {
send: () => {
throw new Error("synthetic send failure");
},
};
for (let index = 0; index < 100; index += 1) {
await requests.request(sender, "bounded", {}, { timeoutMs: null }).catch(() => undefined);
}
const retained = (requests as unknown as { retiredIds?: ReadonlySet<string> }).retiredIds;
expect(retained?.size ?? 0).toBe(0);
});
});
+6 -17
View File
@@ -46,8 +46,7 @@ type GatewayPendingRequestsOptions = {
/** Owns request deadlines, correlation, settlement, and generation-scoped IDs. */
export class GatewayPendingRequests {
private readonly pending = new Map<string, GatewayPendingRequest>();
private readonly retiredIds = new Set<string>();
private collisionSuffix = 0;
private requestSequence = 0;
constructor(private readonly opts: GatewayPendingRequestsOptions) {}
@@ -101,7 +100,6 @@ export class GatewayPendingRequests {
return false;
}
this.pending.delete(id);
this.retiredIds.add(id);
cleanup();
this.finishTiming(id, pending, false, errorCode);
return true;
@@ -186,23 +184,14 @@ export class GatewayPendingRequests {
pending.reject(error);
}
this.pending.clear();
// IDs are tombstoned only for one socket generation. Retired socket frames
// are fenced by GatewayProtocolClient before a replacement generation runs.
this.retiredIds.clear();
this.collisionSuffix = 0;
// Request sequences belong to one socket generation. Retired socket frames
// are fenced by GatewayProtocolClient before the sequence restarts.
this.requestSequence = 0;
}
private allocateRequestId(): string {
const id = this.opts.createRequestId();
if (!this.pending.has(id) && !this.retiredIds.has(id)) {
return id;
}
let uniqueId: string;
do {
this.collisionSuffix += 1;
uniqueId = `${id}:${this.collisionSuffix}`;
} while (this.pending.has(uniqueId) || this.retiredIds.has(uniqueId));
return uniqueId;
this.requestSequence += 1;
return `${this.requestSequence}:${this.opts.createRequestId()}`;
}
private finishTiming(
@@ -210,20 +210,45 @@ describe("GatewayProtocolClient requests", () => {
await expect(aborted).rejects.toThrow("gateway request aborted for aborted");
const replacement = client.request("replacement", {}, { timeoutMs: null });
expect(latestFrame(connection)).toMatchObject({ id: "same-id:1", method: "replacement" });
respond(connection, "same-id", { stale: true });
expect(latestFrame(connection)).toMatchObject({ id: "2:same-id", method: "replacement" });
respond(connection, "1:same-id", { stale: true });
expect(client.hasPendingRequests).toBe(true);
respond(connection, "same-id:1", { current: true });
respond(connection, "2:same-id", { current: true });
await expect(replacement).resolves.toEqual({ current: true });
await expect(client.request("send.failure", {}, { timeoutMs: null })).rejects.toThrow(
"synthetic send failure",
);
expect(latestFrame(connection)).toMatchObject({ id: "same-id:2", method: "send.failure" });
expect(latestFrame(connection)).toMatchObject({ id: "3:same-id", method: "send.failure" });
expect(client.hasPendingRequests).toBe(false);
client.stop();
});
it("keeps concurrent requests distinct when generated IDs contain sequence suffixes", async () => {
const generatedIds = ["same-id:1", "same-id"];
const { client, connections } = createRequestHarness({
createRequestId: () => generatedIds.shift() ?? "same-id",
});
const connection = connections[0];
if (!connection) {
throw new Error("expected request connection");
}
const first = client.request("first", {}, { timeoutMs: null });
const second = client.request("second", {}, { timeoutMs: null });
const [firstFrame, secondFrame] = connection.frames;
if (!firstFrame || !secondFrame) {
throw new Error("expected concurrent request frames");
}
expect(firstFrame.id).not.toBe(secondFrame.id);
respond(connection, firstFrame.id, { request: "first" });
respond(connection, secondFrame.id, { request: "second" });
await expect(first).resolves.toEqual({ request: "first" });
await expect(second).resolves.toEqual({ request: "second" });
client.stop();
});
it("ignores late accepted and final replies after a timeout collision", async () => {
vi.useFakeTimers();
const onAccepted = vi.fn();
@@ -242,14 +267,14 @@ describe("GatewayProtocolClient requests", () => {
{},
{ timeoutMs: null, expectFinal: true, onAccepted },
);
expect(latestFrame(connection)).toMatchObject({ id: "same-id:1", method: "agent" });
respond(connection, "same-id", { status: "accepted", runId: "old" });
respond(connection, "same-id", { status: "ok", runId: "old" });
expect(latestFrame(connection)).toMatchObject({ id: "2:same-id", method: "agent" });
respond(connection, "1:same-id", { status: "accepted", runId: "old" });
respond(connection, "1:same-id", { status: "ok", runId: "old" });
expect(onAccepted).not.toHaveBeenCalled();
expect(client.hasPendingRequests).toBe(true);
respond(connection, "same-id:1", { status: "accepted", runId: "new" });
respond(connection, "same-id:1", { status: "ok", runId: "new" });
respond(connection, "2:same-id", { status: "accepted", runId: "new" });
respond(connection, "2:same-id", { status: "ok", runId: "new" });
await expect(replacement).resolves.toEqual({ status: "ok", runId: "new" });
expect(onAccepted).toHaveBeenCalledExactlyOnceWith({ status: "accepted", runId: "new" });
client.stop();
@@ -463,7 +488,7 @@ describe("GatewayProtocolClient requests", () => {
client.stop();
});
it("clears generation tombstones when the socket flushes", async () => {
it("restarts the request sequence when the socket flushes", async () => {
vi.useFakeTimers();
const { client, connections } = createRequestHarness({ createRequestId: () => "same-id" });
const firstConnection = connections[0];
@@ -482,8 +507,8 @@ describe("GatewayProtocolClient requests", () => {
throw new Error("expected replacement request connection");
}
const replacement = client.request("second", {}, { timeoutMs: null });
expect(latestFrame(secondConnection)).toMatchObject({ id: "same-id", method: "second" });
respond(secondConnection, "same-id", { ok: true });
expect(latestFrame(secondConnection)).toMatchObject({ id: "1:same-id", method: "second" });
respond(secondConnection, "1:same-id", { ok: true });
await expect(replacement).resolves.toEqual({ ok: true });
client.stop();
});
+3 -3
View File
@@ -205,7 +205,7 @@ type ConnectFrame = {
};
};
const REQUEST_FRAME_ID = "00000000-0000-4000-8000-000000000000";
const REQUEST_FRAME_ID = "2:00000000-0000-4000-8000-000000000000";
function requestFrameBytes(method: string, params?: unknown): number {
const frame =
@@ -1679,7 +1679,7 @@ describe("GatewayBrowserClient", () => {
const { connectFrame } = await startConnect(client);
expect(connectFrame.id).toBe("req-insecure");
expect(connectFrame.id).toBe("1:req-insecure");
expect(connectFrame.method).toBe("connect");
expect(connectFrame.params?.auth).toEqual({
token: "shared-auth-token",
@@ -1699,7 +1699,7 @@ describe("GatewayBrowserClient", () => {
const { connectFrame } = await startConnect(client);
expect(connectFrame.id).toBe("req-insecure");
expect(connectFrame.id).toBe("1:req-insecure");
expect(connectFrame.method).toBe("connect");
expect(connectFrame.params?.auth).toEqual({
token: undefined,
+18 -10
View File
@@ -296,7 +296,24 @@ export class GatewayBrowserClient {
constructor(private opts: GatewayBrowserClientOptions) {
this.client = new GatewayProtocolClient<ConnectPlan>({
createSocket: (handlers) => createBrowserGatewaySocket(this.opts.url, handlers),
createSocket: (handlers) => {
this.maxPayloadBytes = undefined;
const socket = createBrowserGatewaySocket(this.opts.url, handlers);
return {
...socket,
send: (data) => {
if (
this.maxPayloadBytes !== undefined &&
new TextEncoder().encode(data).byteLength > this.maxPayloadBytes
) {
throw new Error(
"Request exceeds the Gateway payload limit. Shorten the message or remove one or more attachments and retry.",
);
}
socket.send(data);
},
};
},
createRequestId: generateUUID,
createRequestError: (error) =>
new GatewayRequestError({
@@ -654,15 +671,6 @@ export class GatewayBrowserClient {
params?: unknown,
options?: GatewayProtocolRequestOptions,
): Promise<T> {
// The UUID request envelope adds 75 bytes with params, 61 when params is omitted.
const requestBytes =
new TextEncoder().encode(JSON.stringify([method, params])).byteLength +
(params === undefined ? 61 : 75);
if (this.maxPayloadBytes !== undefined && requestBytes > this.maxPayloadBytes) {
throw new Error(
"Request exceeds the Gateway payload limit. Shorten the message or remove one or more attachments and retry.",
);
}
return await this.client.request<T>(method, params, options);
}
@@ -0,0 +1,99 @@
// @vitest-environment node
import { describe, expect, it } from "vitest";
import type { SessionsListResult } from "../api/types.ts";
import { compareSidebarSessionRowsByMode } from "./app-sidebar-session-navigation-logic.ts";
import { publishSidebarSessionList } from "./session-data-controller-events.ts";
describe("publishSidebarSessionList", () => {
const createOwner = () => ({
context: undefined,
sessionCreatedOrder: new Map<string, number>(),
sessionResultsByAgent: {} as Record<string, SessionsListResult>,
sessionsResult: null as SessionsListResult | null,
sessionsAgentId: null as string | null,
sessionsLoading: false,
sessionMutationError: null,
expandedAgentId: () => "main",
requestSessionDataUpdate: () => undefined,
});
const publish = (owner: ReturnType<typeof createOwner>, agentId: string | null, keys: string[]) =>
publishSidebarSessionList(owner, {
result: {
sessions: keys.map((key, index) => ({ key, kind: "direct", updatedAt: index })),
count: keys.length,
} as SessionsListResult,
agentId,
loading: false,
error: null,
});
it("keeps observed creation order only for rows in the current accumulated result", () => {
const owner = createOwner();
publish(owner, "main", ["first", "second"]);
publish(owner, "main", ["second", "third"]);
expect([...owner.sessionCreatedOrder.keys()]).toEqual(["second", "third"]);
});
it("keeps observed order after pruning and adding a session", () => {
const owner = createOwner();
publish(owner, "main", ["removed", "z-retained"]);
publish(owner, "main", ["z-retained"]);
publish(owner, "main", ["z-retained", "a-added"]);
const ordered = owner.sessionResultsByAgent.main?.sessions.toSorted((a, b) =>
compareSidebarSessionRowsByMode({
a,
b,
sortMode: "created",
owners: undefined,
createdOrder: owner.sessionCreatedOrder,
}),
);
expect(ordered?.map((row) => row.key)).toEqual(["z-retained", "a-added"]);
});
it("keeps creation order for every retained agent result", () => {
const owner = createOwner();
publish(owner, "alpha", ["alpha-first", "alpha-second"]);
publish(owner, "beta", ["beta-first"]);
publish(owner, "alpha", ["alpha-first", "alpha-second"]);
expect([...owner.sessionCreatedOrder.keys()]).toEqual([
"alpha-first",
"alpha-second",
"beta-first",
]);
});
it("keeps cached agent order while an uncached agent has no result", () => {
const owner = createOwner();
publish(owner, "alpha", ["alpha-first", "alpha-second"]);
publishSidebarSessionList(owner, {
result: null,
agentId: "beta",
loading: true,
error: null,
});
expect([...owner.sessionCreatedOrder.keys()]).toEqual(["alpha-first", "alpha-second"]);
});
it("preserves promoted order for an unscoped canonical result", () => {
const owner = createOwner();
owner.sessionCreatedOrder.set("first", 1);
owner.sessionCreatedOrder.set("second", 0);
publish(owner, null, ["first", "second"]);
expect([...owner.sessionCreatedOrder]).toEqual([
["first", 1],
["second", 0],
]);
});
});
@@ -1,6 +1,7 @@
import type { RouteId } from "../app-route-paths.ts";
import type { ApplicationContext } from "../app/context.ts";
import { readPresenceEntries, type PresencePayload } from "../app/user-profile.ts";
import type { AgentCapability } from "../lib/agents/index.ts";
import type { SessionCapability, SessionListSnapshot } from "../lib/sessions/index.ts";
import { normalizeAgentId } from "../lib/sessions/session-key.ts";
import {
@@ -20,6 +21,63 @@ type SidebarSessionListOwner = {
requestSessionDataUpdate(): void;
};
function pruneSidebarSessionOrder(
owner: SidebarSessionListOwner,
retainedResults: readonly NonNullable<SessionListSnapshot["result"]>[],
): void {
const visibleKeys = new Set(
retainedResults.flatMap((result) => result.sessions.map((row) => row.key).filter(Boolean)),
);
for (const key of owner.sessionCreatedOrder.keys()) {
if (!visibleKeys.has(key)) {
owner.sessionCreatedOrder.delete(key);
}
}
}
function pruneSidebarAgentSessionCaches(
owner: SidebarSessionListOwner,
agentIds: readonly string[],
): void {
const retainedAgentIds = new Set(agentIds.map(normalizeAgentId));
for (const agentId of Object.keys(owner.sessionResultsByAgent)) {
if (!retainedAgentIds.has(agentId)) {
delete owner.sessionResultsByAgent[agentId];
}
}
if (owner.sessionsAgentId && !retainedAgentIds.has(normalizeAgentId(owner.sessionsAgentId))) {
owner.sessionsResult = null;
owner.sessionsAgentId = null;
}
const retainedResults = Object.values(owner.sessionResultsByAgent);
if (owner.sessionsResult) {
retainedResults.push(owner.sessionsResult);
}
pruneSidebarSessionOrder(owner, retainedResults);
}
export function subscribeSidebarAgentSessionCaches(
agents: AgentCapability,
owner: SidebarSessionListOwner,
notify: () => void,
): () => void {
const synchronize = () => {
const roster = agents.state.agentsList;
// A null roster is transient during reconnect; only a concrete list can evict agent caches.
if (roster) {
pruneSidebarAgentSessionCaches(
owner,
roster.agents.map((agent) => agent.id),
);
}
};
synchronize();
return agents.subscribe(() => {
synchronize();
notify();
});
}
function filteredSidebarSessionQuery(agentId: string, archivedFilter: SidebarSessionStatusFilter) {
return {
agentId,
@@ -36,14 +94,23 @@ export function publishSidebarSessionList(
): void {
owner.sessionsResult = snapshot.result;
owner.sessionsAgentId = snapshot.agentId;
for (const row of snapshot.result?.sessions ?? []) {
if (row.key && !owner.sessionCreatedOrder.has(row.key)) {
owner.sessionCreatedOrder.set(row.key, owner.sessionCreatedOrder.size);
}
}
const sessions = snapshot.result?.sessions ?? [];
if (snapshot.result && snapshot.agentId) {
owner.sessionResultsByAgent[normalizeAgentId(snapshot.agentId)] = snapshot.result;
}
const retainedResults = snapshot.result
? [snapshot.result, ...Object.values(owner.sessionResultsByAgent)]
: Object.values(owner.sessionResultsByAgent);
pruneSidebarSessionOrder(owner, retainedResults);
let nextCreatedOrder = 0;
for (const order of owner.sessionCreatedOrder.values()) {
nextCreatedOrder = Math.max(nextCreatedOrder, order + 1);
}
for (const row of sessions) {
if (row.key && !owner.sessionCreatedOrder.has(row.key)) {
owner.sessionCreatedOrder.set(row.key, nextCreatedOrder++);
}
}
}
export function subscribeFilteredSidebarSessions(
@@ -25,6 +25,13 @@ function createFilteredSessionController(statusFilter: "archived" | "all", rowCo
kind: "direct" as const,
updatedAt: index + 1,
}));
const resultForKeys = (keys: string[]) => ({
ts: 1,
path: "",
count: keys.length,
defaults: { modelProvider: null, model: null, contextTokens: null },
sessions: keys.map((key) => ({ key, kind: "direct" as const })),
});
const list = vi.fn(async (options?: Parameters<SessionCapability["list"]>[0]) => {
const offset = options?.offset ?? 0;
const limit = options?.limit ?? 60;
@@ -86,19 +93,24 @@ function createFilteredSessionController(statusFilter: "archived" | "all", rowCo
const sessions = createSessionCapability(gateway);
let selectedAgentId = "main";
let selectedStatusFilter = statusFilter;
const agentsState = {
connected: true,
client,
agentsList: {
defaultId: "main",
agents: [{ id: "main" }, { id: "research" }],
} as ApplicationContext["agents"]["state"]["agentsList"],
};
const agentListeners = new Set<(state: ApplicationContext["agents"]["state"]) => void>();
const context = {
gateway,
sessions,
agents: {
state: {
connected: true,
client,
agentsList: {
defaultId: "main",
agents: [{ id: "main" }, { id: "research" }],
},
state: agentsState,
subscribe(listener: (state: ApplicationContext["agents"]["state"]) => void) {
agentListeners.add(listener);
return () => agentListeners.delete(listener);
},
subscribe: () => () => undefined,
},
agentSelection: {
get state() {
@@ -127,6 +139,7 @@ function createFilteredSessionController(statusFilter: "archived" | "all", rowCo
return {
controller,
list,
resultForKeys,
selectAgent: (agentId: string) => {
selectedAgentId = agentId;
controller.synchronizeSessionScope();
@@ -150,10 +163,63 @@ function createFilteredSessionController(statusFilter: "archived" | "all", rowCo
listener(event);
}
},
publishAgentRoster: (agentIds: string[] | null) => {
agentsState.agentsList = agentIds
? {
defaultId: "main",
mainKey: "main",
scope: "global",
agents: agentIds.map((id) => ({ id })),
}
: null;
for (const listener of agentListeners) {
listener(agentsState as ApplicationContext["agents"]["state"]);
}
},
};
}
describe("filtered sidebar session event refresh", () => {
it("evicts cached sessions when an agent leaves the authoritative roster", () => {
const { controller, publishAgentRoster, resultForKeys } =
createFilteredSessionController("all");
controller.hostConnected();
controller.sessionResultsByAgent = {
main: resultForKeys(["agent:main:kept"]),
research: resultForKeys(["agent:research:removed"]),
};
controller.sessionsResult = controller.sessionResultsByAgent.research ?? null;
controller.sessionsAgentId = "research";
controller.sessionCreatedOrder = new Map([
["agent:main:kept", 0],
["agent:research:removed", 1],
]);
publishAgentRoster(null);
expect(Object.keys(controller.sessionResultsByAgent)).toEqual(["main", "research"]);
publishAgentRoster(["main"]);
expect(Object.keys(controller.sessionResultsByAgent)).toEqual(["main"]);
expect(controller.sessionsResult).toBeNull();
expect(controller.sessionsAgentId).toBeNull();
expect([...controller.sessionCreatedOrder.keys()]).toEqual(["agent:main:kept"]);
controller.hostDisconnected();
});
it("retains the current canonical result outside the per-agent cache", () => {
const { controller, publishAgentRoster, resultForKeys } =
createFilteredSessionController("all");
controller.hostConnected();
controller.sessionsResult = resultForKeys(["agent:main:current"]);
controller.sessionsAgentId = "main";
controller.sessionCreatedOrder = new Map([["agent:main:current", 0]]);
publishAgentRoster(["main"]);
expect([...controller.sessionCreatedOrder.keys()]).toEqual(["agent:main:current"]);
controller.hostDisconnected();
});
it.each(["archived", "all"] as const)(
"refreshes the %s list once for duplicate remote session events",
async (statusFilter) => {
+12 -13
View File
@@ -48,6 +48,7 @@ import {
import {
publishSidebarSessionList,
refreshSidebarSessionList,
subscribeSidebarAgentSessionCaches,
subscribeFilteredSidebarSessions,
subscribeSessionDataGatewayEvents,
} from "./session-data-controller-events.ts";
@@ -141,7 +142,7 @@ export class SessionDataController implements ReactiveController, SessionCatalog
)
.watch(
() => this.context?.agents,
(agents, notify) => agents.subscribe(notify),
(agents, notify) => subscribeSidebarAgentSessionCaches(agents, this, notify),
)
.watch(
() => this.context?.agentSelection,
@@ -201,10 +202,7 @@ export class SessionDataController implements ReactiveController, SessionCatalog
this.gatewayConnected = false;
this.retireSessionCatalogData();
this.scroll.dispose();
if (this.activeSessionLineageRetryTimer) {
globalThis.clearTimeout(this.activeSessionLineageRetryTimer);
this.activeSessionLineageRetryTimer = null;
}
this.clearActiveSessionLineageRetry();
this.subscriptions.hostDisconnected();
}
@@ -365,6 +363,13 @@ export class SessionDataController implements ReactiveController, SessionCatalog
return this.scroll.state;
}
private clearActiveSessionLineageRetry(): void {
if (this.activeSessionLineageRetryTimer) {
globalThis.clearTimeout(this.activeSessionLineageRetryTimer);
this.activeSessionLineageRetryTimer = null;
}
}
updateSessionsScrollState(element: HTMLElement): void {
this.scroll.update(element);
}
@@ -387,10 +392,7 @@ export class SessionDataController implements ReactiveController, SessionCatalog
}
this.activeSessionLineageLoaded = false;
this.activeSessionLineageRequest = null;
if (this.activeSessionLineageRetryTimer) {
globalThis.clearTimeout(this.activeSessionLineageRetryTimer);
this.activeSessionLineageRetryTimer = null;
}
this.clearActiveSessionLineageRetry();
}
private readonly updateSessions = (sessions: SessionCapability) => {
@@ -624,10 +626,7 @@ export class SessionDataController implements ReactiveController, SessionCatalog
this.activeSessionLineageRequest = null;
this.activeSessionLineageRoot = null;
this.activeSessionLineageSelectedRow = null;
if (this.activeSessionLineageRetryTimer) {
globalThis.clearTimeout(this.activeSessionLineageRetryTimer);
this.activeSessionLineageRetryTimer = null;
}
this.clearActiveSessionLineageRetry();
this.notify();
}
const { gateway, sessions } = this.context ?? {};
@@ -383,31 +383,31 @@ describe("session connection hydration", () => {
try {
connect();
expect(sent).toEqual([{ id: "request", method: "sessions.subscribe" }]);
expect(sent).toEqual([{ id: "1:request", method: "sessions.subscribe" }]);
await vi.advanceTimersByTimeAsync(DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS);
expect(sent).toContainEqual({ id: "request:1", method: "sessions.list" });
respond("request:1", initialResult);
expect(sent).toContainEqual({ id: "2:request", method: "sessions.list" });
respond("2:request", initialResult);
await vi.advanceTimersByTimeAsync(0);
expect(sessions.state.result).toEqual(initialResult);
await vi.advanceTimersByTimeAsync(500);
expect(sent).toContainEqual({ id: "request:2", method: "sessions.subscribe" });
expect(sent).toContainEqual({ id: "3:request", method: "sessions.subscribe" });
respond("request", { status: "accepted" });
respond("request", { subscribed: true });
respond("1:request", { status: "accepted" });
respond("1:request", { subscribed: true });
await vi.advanceTimersByTimeAsync(0);
expect(sessions.state.error).not.toBeNull();
expect(sent.filter(({ method }) => method === "sessions.list")).toHaveLength(1);
respond("request:2", { subscribed: true });
respond("3:request", { subscribed: true });
await vi.advanceTimersByTimeAsync(0);
expect(sent).toContainEqual({ id: "request:3", method: "sessions.list" });
respond("request:3", recoveredResult);
expect(sent).toContainEqual({ id: "4:request", method: "sessions.list" });
respond("4:request", recoveredResult);
await vi.advanceTimersByTimeAsync(0);
respond("request", { status: "accepted" });
respond("request", { subscribed: true });
respond("1:request", { status: "accepted" });
respond("1:request", { subscribed: true });
await vi.advanceTimersByTimeAsync(0);
expect(sessions.state.error).toBeNull();