Skip to content

Commit 18bd7b6

Browse files
committed
fix(gateway): cache session list thinking enrichment
1 parent 36f8a86 commit 18bd7b6

3 files changed

Lines changed: 121 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
4545
- Release validation: install the cross-OS TypeScript harness through Windows-safe Node/npm shims so native Windows package checks reach the OpenClaw smoke suites instead of exiting before artifact capture. Thanks @vincentkoc.
4646
- Release validation: let Windows packaged-upgrade checks continue after the shipped 2026.5.2 updater hits its native-module swap cleanup fallback, verifying the fallback-installed candidate through package metadata and downstream smoke instead of crashing on the immediate update-status probe. Thanks @vincentkoc.
4747
- Doctor/plugins: skip channel-derived official plugin installs when another configured plugin is the effective owner for the same channel, so `doctor --repair` does not reinstall `feishu` while `openclaw-lark` handles `channels.feishu`. Fixes #76623. Thanks @fuyizheng3120.
48+
- Gateway/sessions: memoize repeated thinking-option enrichment and skip unused cost fallback checks while listing sessions, reducing per-row work on large multi-agent stores. Fixes #76931.
4849
- Agents/tools: use config-only runtime snapshots for plugin tool registration and live runtime config getters, avoiding expensive full secrets snapshot clones on the core-plugin-tools prep path. Fixes #76295.
4950
- Agents/tools: honor the effective tool denylist before constructing optional PDF/media tool factories, so `tools.deny: ["pdf"]` skips PDF setup before later policy filtering. Fixes #76997.
5051
- Agents/bootstrap: keep pending `BOOTSTRAP.md` and bootstrap truncation notices in system-prompt Project Context instead of copying setup text or raw warning diagnostics into WebChat user/runtime context. Fixes #76946.

src/gateway/session-utils.test.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from "node:fs";
22
import os from "node:os";
33
import path from "node:path";
4-
import { afterEach, describe, expect, test } from "vitest";
4+
import { afterEach, describe, expect, test, vi } from "vitest";
55
import { resetConfigRuntimeState, setRuntimeConfigSnapshot } from "../config/config.js";
66
import type { OpenClawConfig } from "../config/config.js";
77
import type { SessionEntry } from "../config/sessions.js";
@@ -201,6 +201,93 @@ describe("gateway session utils", () => {
201201
expect(row.thinkingDefault).toBe("medium");
202202
});
203203

204+
test("session list memoizes repeated thinking enrichment per provider model", async () => {
205+
const resolveThinkingProfile = vi.fn(() => ({
206+
levels: [{ id: "off" as const }, { id: "medium" as const }],
207+
defaultLevel: "medium" as const,
208+
}));
209+
const registry = createEmptyPluginRegistry();
210+
registry.providers.push({
211+
pluginId: "test",
212+
source: "test",
213+
provider: {
214+
id: "openai-codex",
215+
label: "OpenAI Codex",
216+
auth: [],
217+
resolveThinkingProfile,
218+
},
219+
});
220+
setActivePluginRegistry(registry);
221+
222+
const cfg = createModelDefaultsConfig({ primary: "openai-codex/gpt-5.5" });
223+
const store = Object.fromEntries(
224+
Array.from({ length: 5 }, (_value, index) => [
225+
`session-${index}`,
226+
{
227+
sessionId: `session-${index}`,
228+
modelProvider: "openai-codex",
229+
model: "gpt-5.5",
230+
updatedAt: Date.now() - index,
231+
} satisfies SessionEntry,
232+
]),
233+
);
234+
235+
const result = await listSessionsFromStoreAsync({
236+
cfg,
237+
storePath: "",
238+
store,
239+
opts: {},
240+
});
241+
242+
expect(result.sessions).toHaveLength(5);
243+
expect(resolveThinkingProfile).toHaveBeenCalledTimes(3);
244+
});
245+
246+
test("session list thinking cache preserves case-distinct model catalog entries", async () => {
247+
const cfg = createModelDefaultsConfig({ primary: "custom/CaseModel" });
248+
const modelCatalog = [
249+
{
250+
provider: "custom",
251+
id: "CaseModel",
252+
name: "CaseModel",
253+
reasoning: true,
254+
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
255+
},
256+
{
257+
provider: "custom",
258+
id: "casemodel",
259+
name: "casemodel",
260+
reasoning: true,
261+
compat: { supportedReasoningEfforts: ["low", "medium", "high"] },
262+
},
263+
];
264+
const result = await listSessionsFromStoreAsync({
265+
cfg,
266+
storePath: "",
267+
modelCatalog,
268+
store: {
269+
upper: {
270+
sessionId: "upper",
271+
modelProvider: "custom",
272+
model: "CaseModel",
273+
updatedAt: 2,
274+
} satisfies SessionEntry,
275+
lower: {
276+
sessionId: "lower",
277+
modelProvider: "custom",
278+
model: "casemodel",
279+
updatedAt: 1,
280+
} satisfies SessionEntry,
281+
},
282+
opts: {},
283+
});
284+
285+
const upper = result.sessions.find((session) => session.key === "upper");
286+
const lower = result.sessions.find((session) => session.key === "lower");
287+
expect(upper?.thinkingLevels?.map((level) => level.id)).toContain("xhigh");
288+
expect(lower?.thinkingLevels?.map((level) => level.id)).not.toContain("xhigh");
289+
});
290+
204291
test("session defaults and rows expose xhigh from configured catalog compat", () => {
205292
const cfg = createModelDefaultsConfig({ primary: "gmn/gpt-5.4" });
206293
const catalog = [

src/gateway/session-utils.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ function shouldKeepStoreOnlyChildLink(entry: SessionEntry, now: number): boolean
372372
type SessionListRowContext = {
373373
subagentRuns: ReturnType<typeof buildSubagentRunReadIndex>;
374374
storeChildSessionsByKey: Map<string, string[]>;
375+
thinkingLevelsByModelRef: Map<string, ReturnType<typeof listThinkingLevelOptions>>;
375376
};
376377

377378
function resolveRuntimeChildSessionKeys(
@@ -488,9 +489,33 @@ function buildSessionListRowContext(params: {
488489
return {
489490
subagentRuns,
490491
storeChildSessionsByKey: buildStoreChildSessionIndex(params.store, params.now, subagentRuns),
492+
thinkingLevelsByModelRef: new Map(),
491493
};
492494
}
493495

496+
function createSessionRowModelCacheKey(provider: string | undefined, model: string | undefined) {
497+
return `${normalizeLowercaseStringOrEmpty(provider)}\0${normalizeOptionalString(model) ?? ""}`;
498+
}
499+
500+
function resolveSessionRowThinkingLevels(params: {
501+
provider: string;
502+
model: string;
503+
modelCatalog?: ModelCatalogEntry[];
504+
rowContext?: SessionListRowContext;
505+
}): ReturnType<typeof listThinkingLevelOptions> {
506+
if (!params.rowContext) {
507+
return listThinkingLevelOptions(params.provider, params.model, params.modelCatalog);
508+
}
509+
const key = createSessionRowModelCacheKey(params.provider, params.model);
510+
const cached = params.rowContext.thinkingLevelsByModelRef.get(key);
511+
if (cached) {
512+
return cached;
513+
}
514+
const levels = listThinkingLevelOptions(params.provider, params.model, params.modelCatalog);
515+
params.rowContext.thinkingLevelsByModelRef.set(key, levels);
516+
return levels;
517+
}
518+
494519
function mergeChildSessionKeys(
495520
runtimeChildSessions: string[] | undefined,
496521
storeChildSessions: string[] | undefined,
@@ -1530,6 +1555,7 @@ export function buildGatewaySessionRow(params: {
15301555
resolvePositiveNumber(resolveFreshSessionTotalTokens(entry)) === undefined;
15311556
const needsTranscriptContextTokens = resolvePositiveNumber(entry?.contextTokens) === undefined;
15321557
const needsTranscriptEstimatedCostUsd =
1558+
!skipTranscriptUsage &&
15331559
resolveEstimatedSessionCostUsd({
15341560
cfg,
15351561
provider: resolvedModel.provider,
@@ -1635,11 +1661,12 @@ export function buildGatewaySessionRow(params: {
16351661

16361662
const thinkingProvider = rowModelProvider ?? DEFAULT_PROVIDER;
16371663
const thinkingModel = rowModel ?? DEFAULT_MODEL;
1638-
const thinkingLevels = listThinkingLevelOptions(
1639-
thinkingProvider,
1640-
thinkingModel,
1641-
params.modelCatalog,
1642-
);
1664+
const thinkingLevels = resolveSessionRowThinkingLevels({
1665+
provider: thinkingProvider,
1666+
model: thinkingModel,
1667+
modelCatalog: params.modelCatalog,
1668+
rowContext,
1669+
});
16431670
const pluginExtensions =
16441671
!lightweight && entry ? projectPluginSessionExtensionsSync({ sessionKey: key, entry }) : [];
16451672

0 commit comments

Comments
 (0)