perf(gateway): trim session list hot path

This commit is contained in:
Peter Steinberger
2026-05-02 17:26:50 +01:00
parent da4ac53333
commit 3aaf30ffa6
6 changed files with 255 additions and 19 deletions

View File

@@ -7,7 +7,10 @@ import { normalizeAgentId } from "../../routing/session-key.js";
import type { OpenClawConfig } from "../types.openclaw.js";
import { resolveStorePath } from "./paths.js";
import { loadSessionStore } from "./store-load.js";
import { resolveAllAgentSessionStoreTargetsSync } from "./targets.js";
import {
resolveAgentSessionStoreTargetsSync,
resolveAllAgentSessionStoreTargetsSync,
} from "./targets.js";
import type { SessionEntry } from "./types.js";
function isStorePathTemplate(store?: string): boolean {
@@ -25,20 +28,31 @@ function mergeSessionEntryIntoCombined(params: {
const existing = combined[canonicalKey];
if (existing && (existing.updatedAt ?? 0) > (entry.updatedAt ?? 0)) {
const spawnedBy = canonicalizeSpawnedByForAgent(
cfg,
agentId,
existing.spawnedBy ?? entry.spawnedBy,
);
combined[canonicalKey] = {
...entry,
...existing,
spawnedBy: canonicalizeSpawnedByForAgent(cfg, agentId, existing.spawnedBy ?? entry.spawnedBy),
spawnedBy,
};
return;
}
const spawnedBy = canonicalizeSpawnedByForAgent(
cfg,
agentId,
entry.spawnedBy ?? existing?.spawnedBy,
);
if (!existing && entry.spawnedBy === spawnedBy) {
combined[canonicalKey] = entry;
} else {
combined[canonicalKey] = {
...existing,
...entry,
spawnedBy: canonicalizeSpawnedByForAgent(
cfg,
agentId,
entry.spawnedBy ?? existing?.spawnedBy,
),
spawnedBy,
};
}
}
@@ -77,9 +91,9 @@ export function loadCombinedSessionStoreForGateway(
typeof opts.agentId === "string" && opts.agentId.trim()
? normalizeAgentId(opts.agentId)
: undefined;
const targets = resolveAllAgentSessionStoreTargetsSync(cfg).filter(
(target) => !requestedAgentId || normalizeAgentId(target.agentId) === requestedAgentId,
);
const targets = requestedAgentId
? resolveAgentSessionStoreTargetsSync(cfg, requestedAgentId)
: resolveAllAgentSessionStoreTargetsSync(cfg);
const combined: Record<string, SessionEntry> = {};
for (const target of targets) {
const agentId = target.agentId;

View File

@@ -6,6 +6,7 @@ import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config.js";
import { resolveStorePath } from "./paths.js";
import {
resolveAgentSessionStoreTargetsSync,
resolveAllAgentSessionStoreTargets,
resolveAllAgentSessionStoreTargetsSync,
resolveSessionStoreTargets,
@@ -138,6 +139,38 @@ describe("resolveSessionStoreTargets", () => {
});
});
describe("resolveAgentSessionStoreTargetsSync", () => {
it("resolves one requested agent store from the direct path", async () => {
await withTempHome(async (home) => {
const customRoot = path.join(home, "custom-state");
const storePaths = await createAgentSessionStores(customRoot, ["main", "codex"]);
const cfg = createCustomRootCfg(customRoot, "main");
expect(resolveAgentSessionStoreTargetsSync(cfg, "codex", { env: process.env })).toEqual([
{
agentId: "codex",
storePath: storePaths.codex,
},
]);
});
});
it("finds discovered directories whose names normalize to the requested agent", async () => {
await withTempHome(async (home) => {
const customRoot = path.join(home, "custom-state");
const storePaths = await createAgentSessionStores(customRoot, ["main", "Retired Agent"]);
const cfg = createCustomRootCfg(customRoot, "main");
expect(
resolveAgentSessionStoreTargetsSync(cfg, "retired-agent", { env: process.env }),
).toContainEqual({
agentId: "retired-agent",
storePath: storePaths["Retired Agent"],
});
});
});
});
describe("resolveAllAgentSessionStoreTargets", () => {
it("includes discovered on-disk agent stores alongside configured targets", async () => {
await withTempHome(async (home) => {

View File

@@ -209,6 +209,91 @@ export function resolveAllAgentSessionStoreTargetsSync(
return dedupeTargetsByStorePath([...validatedConfiguredTargets, ...discoveredTargets]);
}
export function resolveAgentSessionStoreTargetsSync(
cfg: OpenClawConfig,
agentId: string,
params: { env?: NodeJS.ProcessEnv } = {},
): SessionStoreTarget[] {
const env = params.env ?? process.env;
const requested = normalizeAgentId(agentId);
const storePaths = new Set<string>([
resolveStorePath(cfg.session?.store, { agentId: requested, env }),
resolveStorePath(undefined, { agentId: requested, env }),
]);
const targets: SessionStoreTarget[] = [];
const realAgentsRoots = new Map<string, string | undefined>();
const getRealAgentsRoot = (agentsRoot: string): string | undefined => {
if (realAgentsRoots.has(agentsRoot)) {
return realAgentsRoots.get(agentsRoot);
}
try {
const realAgentsRoot = fsSync.realpathSync.native(agentsRoot);
realAgentsRoots.set(agentsRoot, realAgentsRoot);
return realAgentsRoot;
} catch (err) {
if (shouldSkipDiscoveryError(err)) {
realAgentsRoots.set(agentsRoot, undefined);
return undefined;
}
throw err;
}
};
for (const storePath of storePaths) {
const agentsRoot = resolveAgentsDirFromSessionStorePath(storePath);
if (!agentsRoot) {
targets.push({ agentId: requested, storePath });
continue;
}
const realAgentsRoot = getRealAgentsRoot(agentsRoot);
if (!realAgentsRoot) {
continue;
}
const validatedStorePath = resolveValidatedDiscoveredStorePathSync({
sessionsDir: path.dirname(storePath),
agentsRoot,
realAgentsRoot,
});
if (validatedStorePath) {
targets.push({ agentId: requested, storePath: validatedStorePath });
}
}
const { agentsRoots } = resolveSessionStoreDiscoveryState(cfg, env);
for (const agentsDir of agentsRoots) {
try {
const realAgentsRoot = getRealAgentsRoot(agentsDir);
if (!realAgentsRoot) {
continue;
}
for (const sessionsDir of resolveAgentSessionDirsFromAgentsDirSync(agentsDir)) {
const target = toDiscoveredSessionStoreTarget(
sessionsDir,
path.join(sessionsDir, "sessions.json"),
);
if (!target || normalizeAgentId(target.agentId) !== requested) {
continue;
}
const validatedStorePath = resolveValidatedDiscoveredStorePathSync({
sessionsDir,
agentsRoot: agentsDir,
realAgentsRoot,
});
if (validatedStorePath) {
targets.push({ ...target, storePath: validatedStorePath });
}
}
} catch (err) {
if (shouldSkipDiscoveryError(err)) {
continue;
}
throw err;
}
}
return dedupeTargetsByStorePath(targets);
}
export async function resolveAllAgentSessionStoreTargets(
cfg: OpenClawConfig,
params: { env?: NodeJS.ProcessEnv } = {},

View File

@@ -7,7 +7,7 @@ import {
resetSubagentRegistryForTests,
} from "../agents/subagent-registry.js";
import type { OpenClawConfig } from "../config/config.js";
import type { SessionEntry } from "../config/sessions.js";
import { loadSessionStore, type SessionEntry } from "../config/sessions.js";
import { registerAgentRunContext, resetAgentRunContextForTest } from "../infra/agent-events.js";
import { withStateDirEnv } from "../test-helpers/state-dir-env.js";
import { withEnv } from "../test-utils/env.js";
@@ -1208,4 +1208,42 @@ describe("loadCombinedSessionStoreForGateway includes disk-only agents (#32804)"
readSpy.mockRestore();
});
});
test("keeps canonical single-target entries by reference", async () => {
await withStateDirEnv("openclaw-acp-canonical-", async ({ stateDir }) => {
const customRoot = path.join(stateDir, "custom-state");
const codexDir = path.join(customRoot, "agents", "codex", "sessions");
fs.mkdirSync(codexDir, { recursive: true });
const codexStorePath = path.join(codexDir, "sessions.json");
fs.writeFileSync(
codexStorePath,
JSON.stringify({
"agent:codex:acp-task": {
sessionId: "s-codex",
updatedAt: 200,
spawnedBy: "agent:codex:main",
},
}),
"utf8",
);
const cfg = {
session: {
mainKey: "main",
store: path.join(customRoot, "agents", "{agentId}", "sessions", "sessions.json"),
},
agents: {
list: [{ id: "codex", default: true }],
},
} as OpenClawConfig;
const cachedStore = loadSessionStore(fs.realpathSync.native(codexStorePath), {
clone: false,
});
const { store } = loadCombinedSessionStoreForGateway(cfg, { agentId: "codex" });
expect(store["agent:codex:acp-task"]).toBe(cachedStore["agent:codex:acp-task"]);
});
});
});

View File

@@ -1217,6 +1217,30 @@ describe("listSessionsFromStore selected model display", () => {
}
});
test("uses bounded top-N selection for small limited lists", () => {
const now = Date.now();
const store: Record<string, SessionEntry> = {
"agent:main:old": { sessionId: "old", updatedAt: now - 10_000 } as SessionEntry,
"agent:main:newest": { sessionId: "newest", updatedAt: now } as SessionEntry,
"agent:main:middle-a": { sessionId: "middle-a", updatedAt: now - 5_000 } as SessionEntry,
"agent:main:middle-b": { sessionId: "middle-b", updatedAt: now - 5_000 } as SessionEntry,
"agent:main:newer": { sessionId: "newer", updatedAt: now - 1_000 } as SessionEntry,
};
const result = listSessionsFromStore({
cfg: createModelDefaultsConfig({ primary: "openai/gpt-5.4" }),
storePath: "/tmp/sessions.json",
store,
opts: { limit: 4 },
});
expect(result.sessions.map((session) => session.key)).toEqual([
"agent:main:newest",
"agent:main:newer",
"agent:main:middle-a",
"agent:main:middle-b",
]);
});
test("shows the selected override model even when a fallback runtime model exists", () => {
const cfg = createModelDefaultsConfig({
primary: "anthropic/claude-opus-4-6",

View File

@@ -1758,6 +1758,54 @@ export function loadGatewaySessionRow(
* avoiding excessive yielding overhead for small stores.
*/
const SESSIONS_LIST_YIELD_BATCH_SIZE = 10;
const SESSIONS_LIST_TOP_N_LIMIT = 200;
type SessionEntryPair = [string, SessionEntry];
function compareSessionEntryPairsByUpdatedAt(a: SessionEntryPair, b: SessionEntryPair): number {
return (b[1]?.updatedAt ?? 0) - (a[1]?.updatedAt ?? 0);
}
function resolveSessionsListLimit(
opts: import("./protocol/index.js").SessionsListParams,
): number | undefined {
if (typeof opts.limit !== "number" || !Number.isFinite(opts.limit)) {
return undefined;
}
return Math.max(1, Math.floor(opts.limit));
}
function selectNewestLimitedEntries(
entries: SessionEntryPair[],
limit: number,
): SessionEntryPair[] {
const selected: SessionEntryPair[] = [];
for (const entry of entries) {
const insertAt = selected.findIndex(
(candidate) => compareSessionEntryPairsByUpdatedAt(entry, candidate) < 0,
);
if (insertAt >= 0) {
selected.splice(insertAt, 0, entry);
if (selected.length > limit) {
selected.pop();
}
} else if (selected.length < limit) {
selected.push(entry);
}
}
return selected;
}
function sortAndLimitSessionEntries(
entries: SessionEntryPair[],
limit: number | undefined,
): SessionEntryPair[] {
if (limit !== undefined && limit <= SESSIONS_LIST_TOP_N_LIMIT) {
return selectNewestLimitedEntries(entries, limit);
}
const sorted = entries.toSorted(compareSessionEntryPairsByUpdatedAt);
return limit === undefined ? sorted : sorted.slice(0, limit);
}
export function filterAndSortSessionEntries(params: {
store: Record<string, SessionEntry>;
@@ -1829,8 +1877,7 @@ export function filterAndSortSessionEntries(params: {
return true;
}
return entry?.label === label;
})
.toSorted((a, b) => (b[1]?.updatedAt ?? 0) - (a[1]?.updatedAt ?? 0));
});
if (search) {
entries = entries.filter(([key, entry]) => {
@@ -1852,12 +1899,7 @@ export function filterAndSortSessionEntries(params: {
entries = entries.filter(([, entry]) => (entry?.updatedAt ?? 0) >= cutoff);
}
if (typeof opts.limit === "number" && Number.isFinite(opts.limit)) {
const limit = Math.max(1, Math.floor(opts.limit));
entries = entries.slice(0, limit);
}
return entries;
return sortAndLimitSessionEntries(entries, resolveSessionsListLimit(opts));
}
export function listSessionsFromStore(params: {