From fff193402e8e78bb02c937332a316ed8a7e6876e Mon Sep 17 00:00:00 2001 From: JC Date: Sun, 14 Jun 2026 04:50:30 -0700 Subject: [PATCH] fix(gateway): build row metadata for single session lists Refs #92057. Build the request-scoped row metadata context for every non-empty sessions.list result, including limit=1, so single-row lists use the shared subagent metadata read index instead of direct per-row registry snapshot lookups. This keeps the existing single-row store child-session candidate optimization intact while removing the single-row metadata-cache gap. Verification: - node scripts/run-vitest.mjs src/gateway/session-utils.single-row-cache.test.ts --maxWorkers=1 - .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main - Crabbox run_f89b56ffea83 / cbx_f1b1f5013225: OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed - GitHub PR checks clean on 1ba6619f2ee6491c40b49dd425597bb1b50638ca --- .../session-utils.single-row-cache.test.ts | 62 ++++++++++++++++++- src/gateway/session-utils.ts | 4 +- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/gateway/session-utils.single-row-cache.test.ts b/src/gateway/session-utils.single-row-cache.test.ts index 461c9157effa..76d4edb2319b 100644 --- a/src/gateway/session-utils.single-row-cache.test.ts +++ b/src/gateway/session-utils.single-row-cache.test.ts @@ -72,7 +72,11 @@ const subagentRegistryReadMock = vi.hoisted(() => { vi.mock("../agents/subagent-registry-read.js", () => subagentRegistryReadMock); -import { loadGatewaySessionRow } from "./session-utils.js"; +import { + listSessionsFromStore, + listSessionsFromStoreAsync, + loadGatewaySessionRow, +} from "./session-utils.js"; const MAIN_AGENT_ID = "main"; const TEST_MODEL = "openai/gpt-5.4"; @@ -238,6 +242,62 @@ describe("single gateway session row child-session cache", () => { ); }); + test("builds shared subagent metadata context for single-row session lists", async () => { + await withSingleRowCacheStore( + "openclaw-single-row-list-context-", + "/tmp/openclaw-single-row-list-context", + async ({ now, storePath }) => { + const store: Record = { + "agent:main:discord:channel:parent": parentSession("parent", now), + }; + const cfg: OpenClawConfig = { + agents: { + list: [ + { + id: MAIN_AGENT_ID, + default: true, + workspace: "/tmp/openclaw-single-row-list-context", + }, + ], + defaults: { model: { primary: TEST_MODEL } }, + }, + } as OpenClawConfig; + + const syncListed = listSessionsFromStore({ + cfg, + storePath, + store, + opts: { agentId: MAIN_AGENT_ID, limit: 1 }, + }); + + expect(syncListed.sessions).toHaveLength(1); + expect(subagentRegistryReadMock.buildSubagentRunReadIndex).toHaveBeenCalledTimes( + 1, + ); + expect( + subagentRegistryReadMock.getSessionDisplaySubagentRunByChildSessionKey, + ).not.toHaveBeenCalled(); + + vi.clearAllMocks(); + + const asyncListed = await listSessionsFromStoreAsync({ + cfg, + storePath, + store, + opts: { agentId: MAIN_AGENT_ID, limit: 1 }, + }); + + expect(asyncListed.sessions).toHaveLength(1); + expect(subagentRegistryReadMock.buildSubagentRunReadIndex).toHaveBeenCalledTimes( + 1, + ); + expect( + subagentRegistryReadMock.getSessionDisplaySubagentRunByChildSessionKey, + ).not.toHaveBeenCalled(); + }, + ); + }); + test("rebuilds store child candidates after same-object session store writes", async () => { await withSingleRowCacheStore( "openclaw-single-row-cache-write-version-", diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 827ee64f7f98..7aa8e54d8281 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -2747,7 +2747,7 @@ export function listSessionsFromStore(params: { : undefined; const sharedRowContext = fullRowContext ?? - (entries.length > 1 ? buildSessionListRowMetadataContext({ now }) : undefined); + (entries.length > 0 ? buildSessionListRowMetadataContext({ now }) : undefined); const sessions = entries.map(([key, entry], index) => { const includeTranscriptFields = index < sessionListTranscriptFieldRows; @@ -2837,7 +2837,7 @@ export async function listSessionsFromStoreAsync(params: { : undefined; const sharedRowContext = fullRowContext ?? - (entries.length > 1 ? buildSessionListRowMetadataContext({ now }) : undefined); + (entries.length > 0 ? buildSessionListRowMetadataContext({ now }) : undefined); const sessions: GatewaySessionRow[] = []; for (let i = 0; i < entries.length; i++) {