perf(gateway): avoid extra session-list store work

This commit is contained in:
Peter Steinberger
2026-05-02 17:01:37 +01:00
parent ea098bcafa
commit 43de6ae725
6 changed files with 99 additions and 4 deletions

View File

@@ -108,6 +108,30 @@ describe("Session Store Cache", () => {
expect(loaded2["session:1"].skillsSnapshot?.skills?.[0]?.name).toBe("alpha");
});
it("honors explicit clone:false on cache hits", async () => {
const testStore = createSingleSessionStore(
createSessionEntry({
origin: { provider: "openai" },
}),
);
await saveSessionStore(storePath, testStore);
const parseSpy = vi.spyOn(JSON, "parse");
const loaded1 = loadSessionStore(storePath, { clone: false });
expect(parseSpy).not.toHaveBeenCalled();
loaded1["session:1"].origin = { provider: "mutated" };
const loaded2 = loadSessionStore(storePath, { clone: false });
expect(loaded2).toBe(loaded1);
expect(loaded2["session:1"].origin?.provider).toBe("mutated");
expect(parseSpy).not.toHaveBeenCalled();
parseSpy.mockRestore();
});
it("does not cache pre-migration or pre-normalization disk JSON", () => {
fs.writeFileSync(
storePath,

View File

@@ -43,7 +43,10 @@ function mergeSessionEntryIntoCombined(params: {
}
}
export function loadCombinedSessionStoreForGateway(cfg: OpenClawConfig): {
export function loadCombinedSessionStoreForGateway(
cfg: OpenClawConfig,
opts: { agentId?: string } = {},
): {
storePath: string;
store: Record<string, SessionEntry>;
} {
@@ -70,7 +73,13 @@ export function loadCombinedSessionStoreForGateway(cfg: OpenClawConfig): {
return { storePath, store: combined };
}
const targets = resolveAllAgentSessionStoreTargetsSync(cfg);
const requestedAgentId =
typeof opts.agentId === "string" && opts.agentId.trim()
? normalizeAgentId(opts.agentId)
: undefined;
const targets = resolveAllAgentSessionStoreTargetsSync(cfg).filter(
(target) => !requestedAgentId || normalizeAgentId(target.agentId) === requestedAgentId,
);
const combined: Record<string, SessionEntry> = {};
for (const target of targets) {
const agentId = target.agentId;
@@ -93,6 +102,10 @@ export function loadCombinedSessionStoreForGateway(cfg: OpenClawConfig): {
}
const storePath =
typeof storeConfig === "string" && storeConfig.trim() ? storeConfig.trim() : "(multiple)";
targets.length === 1
? targets[0].storePath
: typeof storeConfig === "string" && storeConfig.trim()
? storeConfig.trim()
: "(multiple)";
return { storePath, store: combined };
}

View File

@@ -63,6 +63,7 @@ export function readSessionStoreCache(params: {
storePath: string;
mtimeMs?: number;
sizeBytes?: number;
clone?: boolean;
}): Record<string, SessionEntry> | null {
const cached = SESSION_STORE_CACHE.get(params.storePath);
if (!cached) {
@@ -72,6 +73,9 @@ export function readSessionStoreCache(params: {
invalidateSessionStoreCache(params.storePath);
return null;
}
if (params.clone === false) {
return cached.store;
}
return cloneSessionStoreRecord(cached.store, cached.serialized);
}

View File

@@ -107,6 +107,7 @@ export function loadSessionStore(
storePath,
mtimeMs: currentFileStat?.mtimeMs,
sizeBytes: currentFileStat?.sizeBytes,
clone: opts.clone,
});
if (cached) {
return cached;

View File

@@ -668,7 +668,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
const p = params;
const cfg = context.getRuntimeConfig();
const { storePath, store } = loadCombinedSessionStoreForGateway(cfg);
const { storePath, store } = loadCombinedSessionStoreForGateway(cfg, { agentId: p.agentId });
const modelCatalog = await loadOptionalSessionsListModelCatalog(context);
const result = await listSessionsFromStoreAsync({
cfg,

View File

@@ -1155,4 +1155,57 @@ describe("loadCombinedSessionStoreForGateway includes disk-only agents (#32804)"
expect(store["agent:codex:acp-task"]).toBeDefined();
});
});
test("agent-scoped loads read only matching agent stores", async () => {
await withStateDirEnv("openclaw-acp-scoped-", async ({ stateDir }) => {
const customRoot = path.join(stateDir, "custom-state");
const agentsDir = path.join(customRoot, "agents");
const mainDir = path.join(agentsDir, "main", "sessions");
const codexDir = path.join(agentsDir, "codex", "sessions");
fs.mkdirSync(mainDir, { recursive: true });
fs.mkdirSync(codexDir, { recursive: true });
const mainStorePath = path.join(mainDir, "sessions.json");
const codexStorePath = path.join(codexDir, "sessions.json");
fs.writeFileSync(
mainStorePath,
JSON.stringify({
"agent:main:main": { sessionId: "s-main", updatedAt: 100 },
}),
"utf8",
);
fs.writeFileSync(
codexStorePath,
JSON.stringify({
"agent:codex:acp-task": { sessionId: "s-codex", updatedAt: 200 },
}),
"utf8",
);
const cfg = {
session: {
mainKey: "main",
store: path.join(customRoot, "agents", "{agentId}", "sessions", "sessions.json"),
},
agents: {
list: [{ id: "main", default: true }],
},
} as OpenClawConfig;
const readSpy = vi.spyOn(fs, "readFileSync");
const { store, storePath } = loadCombinedSessionStoreForGateway(cfg, { agentId: "codex" });
expect(storePath).toBe(fs.realpathSync.native(codexStorePath));
expect(store["agent:codex:acp-task"]).toBeDefined();
expect(store["agent:main:main"]).toBeUndefined();
const readPaths = readSpy.mock.calls
.map((call) => call[0])
.filter((arg): arg is string => typeof arg === "string");
expect(readPaths).toContain(fs.realpathSync.native(codexStorePath));
expect(readPaths).not.toContain(fs.realpathSync.native(mainStorePath));
readSpy.mockRestore();
});
});
});