mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:01:01 +00:00
fix(gateway): cache session list thinking enrichment
This commit is contained in:
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { resetConfigRuntimeState, setRuntimeConfigSnapshot } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
@@ -201,6 +201,93 @@ describe("gateway session utils", () => {
|
||||
expect(row.thinkingDefault).toBe("medium");
|
||||
});
|
||||
|
||||
test("session list memoizes repeated thinking enrichment per provider model", async () => {
|
||||
const resolveThinkingProfile = vi.fn(() => ({
|
||||
levels: [{ id: "off" as const }, { id: "medium" as const }],
|
||||
defaultLevel: "medium" as const,
|
||||
}));
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.providers.push({
|
||||
pluginId: "test",
|
||||
source: "test",
|
||||
provider: {
|
||||
id: "openai-codex",
|
||||
label: "OpenAI Codex",
|
||||
auth: [],
|
||||
resolveThinkingProfile,
|
||||
},
|
||||
});
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
const cfg = createModelDefaultsConfig({ primary: "openai-codex/gpt-5.5" });
|
||||
const store = Object.fromEntries(
|
||||
Array.from({ length: 5 }, (_value, index) => [
|
||||
`session-${index}`,
|
||||
{
|
||||
sessionId: `session-${index}`,
|
||||
modelProvider: "openai-codex",
|
||||
model: "gpt-5.5",
|
||||
updatedAt: Date.now() - index,
|
||||
} satisfies SessionEntry,
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await listSessionsFromStoreAsync({
|
||||
cfg,
|
||||
storePath: "",
|
||||
store,
|
||||
opts: {},
|
||||
});
|
||||
|
||||
expect(result.sessions).toHaveLength(5);
|
||||
expect(resolveThinkingProfile).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("session list thinking cache preserves case-distinct model catalog entries", async () => {
|
||||
const cfg = createModelDefaultsConfig({ primary: "custom/CaseModel" });
|
||||
const modelCatalog = [
|
||||
{
|
||||
provider: "custom",
|
||||
id: "CaseModel",
|
||||
name: "CaseModel",
|
||||
reasoning: true,
|
||||
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
|
||||
},
|
||||
{
|
||||
provider: "custom",
|
||||
id: "casemodel",
|
||||
name: "casemodel",
|
||||
reasoning: true,
|
||||
compat: { supportedReasoningEfforts: ["low", "medium", "high"] },
|
||||
},
|
||||
];
|
||||
const result = await listSessionsFromStoreAsync({
|
||||
cfg,
|
||||
storePath: "",
|
||||
modelCatalog,
|
||||
store: {
|
||||
upper: {
|
||||
sessionId: "upper",
|
||||
modelProvider: "custom",
|
||||
model: "CaseModel",
|
||||
updatedAt: 2,
|
||||
} satisfies SessionEntry,
|
||||
lower: {
|
||||
sessionId: "lower",
|
||||
modelProvider: "custom",
|
||||
model: "casemodel",
|
||||
updatedAt: 1,
|
||||
} satisfies SessionEntry,
|
||||
},
|
||||
opts: {},
|
||||
});
|
||||
|
||||
const upper = result.sessions.find((session) => session.key === "upper");
|
||||
const lower = result.sessions.find((session) => session.key === "lower");
|
||||
expect(upper?.thinkingLevels?.map((level) => level.id)).toContain("xhigh");
|
||||
expect(lower?.thinkingLevels?.map((level) => level.id)).not.toContain("xhigh");
|
||||
});
|
||||
|
||||
test("session defaults and rows expose xhigh from configured catalog compat", () => {
|
||||
const cfg = createModelDefaultsConfig({ primary: "gmn/gpt-5.4" });
|
||||
const catalog = [
|
||||
|
||||
@@ -372,6 +372,7 @@ function shouldKeepStoreOnlyChildLink(entry: SessionEntry, now: number): boolean
|
||||
type SessionListRowContext = {
|
||||
subagentRuns: ReturnType<typeof buildSubagentRunReadIndex>;
|
||||
storeChildSessionsByKey: Map<string, string[]>;
|
||||
thinkingLevelsByModelRef: Map<string, ReturnType<typeof listThinkingLevelOptions>>;
|
||||
};
|
||||
|
||||
function resolveRuntimeChildSessionKeys(
|
||||
@@ -488,9 +489,33 @@ function buildSessionListRowContext(params: {
|
||||
return {
|
||||
subagentRuns,
|
||||
storeChildSessionsByKey: buildStoreChildSessionIndex(params.store, params.now, subagentRuns),
|
||||
thinkingLevelsByModelRef: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
function createSessionRowModelCacheKey(provider: string | undefined, model: string | undefined) {
|
||||
return `${normalizeLowercaseStringOrEmpty(provider)}\0${normalizeOptionalString(model) ?? ""}`;
|
||||
}
|
||||
|
||||
function resolveSessionRowThinkingLevels(params: {
|
||||
provider: string;
|
||||
model: string;
|
||||
modelCatalog?: ModelCatalogEntry[];
|
||||
rowContext?: SessionListRowContext;
|
||||
}): ReturnType<typeof listThinkingLevelOptions> {
|
||||
if (!params.rowContext) {
|
||||
return listThinkingLevelOptions(params.provider, params.model, params.modelCatalog);
|
||||
}
|
||||
const key = createSessionRowModelCacheKey(params.provider, params.model);
|
||||
const cached = params.rowContext.thinkingLevelsByModelRef.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const levels = listThinkingLevelOptions(params.provider, params.model, params.modelCatalog);
|
||||
params.rowContext.thinkingLevelsByModelRef.set(key, levels);
|
||||
return levels;
|
||||
}
|
||||
|
||||
function mergeChildSessionKeys(
|
||||
runtimeChildSessions: string[] | undefined,
|
||||
storeChildSessions: string[] | undefined,
|
||||
@@ -1530,6 +1555,7 @@ export function buildGatewaySessionRow(params: {
|
||||
resolvePositiveNumber(resolveFreshSessionTotalTokens(entry)) === undefined;
|
||||
const needsTranscriptContextTokens = resolvePositiveNumber(entry?.contextTokens) === undefined;
|
||||
const needsTranscriptEstimatedCostUsd =
|
||||
!skipTranscriptUsage &&
|
||||
resolveEstimatedSessionCostUsd({
|
||||
cfg,
|
||||
provider: resolvedModel.provider,
|
||||
@@ -1635,11 +1661,12 @@ export function buildGatewaySessionRow(params: {
|
||||
|
||||
const thinkingProvider = rowModelProvider ?? DEFAULT_PROVIDER;
|
||||
const thinkingModel = rowModel ?? DEFAULT_MODEL;
|
||||
const thinkingLevels = listThinkingLevelOptions(
|
||||
thinkingProvider,
|
||||
thinkingModel,
|
||||
params.modelCatalog,
|
||||
);
|
||||
const thinkingLevels = resolveSessionRowThinkingLevels({
|
||||
provider: thinkingProvider,
|
||||
model: thinkingModel,
|
||||
modelCatalog: params.modelCatalog,
|
||||
rowContext,
|
||||
});
|
||||
const pluginExtensions =
|
||||
!lightweight && entry ? projectPluginSessionExtensionsSync({ sessionKey: key, entry }) : [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user