perf(gateway): cache startup metadata probes

This commit is contained in:
Peter Steinberger
2026-05-03 16:06:05 +01:00
parent f8141da4a6
commit 9e56cfcc35
11 changed files with 138 additions and 13 deletions

View File

@@ -457,7 +457,7 @@ function resolveCanonicalSessionKeyFromSessionId(params: {
agentId: params.agentId,
},
);
const store = params.api.runtime.agent.session.loadSessionStore(storePath);
const store = params.api.runtime.agent.session.loadSessionStore(storePath, { clone: false });
let bestMatch:
| {
sessionKey: string;
@@ -546,7 +546,7 @@ function resolveRecallRunChannelContext(params: {
agentId: params.agentId,
},
);
const store = params.api.runtime.agent.session.loadSessionStore(storePath);
const store = params.api.runtime.agent.session.loadSessionStore(storePath, { clone: false });
const sessionEntry = resolveSessionStoreEntry({
store,
sessionKey: resolvedSessionKey,
@@ -1404,7 +1404,7 @@ async function persistPluginStatusLines(params: {
agentId ? { agentId } : undefined,
);
if (!params.statusLine && !debugLine) {
const store = params.api.runtime.agent.session.loadSessionStore(storePath);
const store = params.api.runtime.agent.session.loadSessionStore(storePath, { clone: false });
const existingEntry = resolveSessionStoreEntry({ store, sessionKey }).existing;
const hasActiveMemoryEntry = Array.isArray(existingEntry?.pluginDebugEntries)
? existingEntry.pluginDebugEntries.some((entry) => entry?.pluginId === "active-memory")

View File

@@ -1,4 +1,4 @@
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { AuthProfileStore } from "./auth-profiles/types.js";
import {
resetCliAuthEpochTestDeps,
@@ -358,4 +358,28 @@ describe("resolveCliAuthEpoch", () => {
expect(fourth).toBeDefined();
expect(fourth).not.toBe(third);
});
it("uses non-prompting Codex CLI credential reads for epoch fingerprints", async () => {
const readCodexCliCredentialsCached = vi.fn(() => ({
type: "oauth" as const,
provider: "openai-codex" as const,
access: "local-access",
refresh: "local-refresh",
expires: 1,
}));
setCliAuthEpochTestDeps({
readCodexCliCredentialsCached,
loadAuthProfileStoreForRuntime: () => ({
version: 1,
profiles: {},
}),
});
await resolveCliAuthEpoch({ provider: "codex-cli" });
expect(readCodexCliCredentialsCached).toHaveBeenCalledWith({
ttlMs: 5000,
allowKeychainPrompt: false,
});
});
});

View File

@@ -126,6 +126,7 @@ function getLocalCliCredentialFingerprint(provider: string): string | undefined
case "codex-cli": {
const credential = cliAuthEpochDeps.readCodexCliCredentialsCached({
ttlMs: 5000,
allowKeychainPrompt: false,
});
return credential ? hashCliAuthEpochPart(encodeCodexCredential(credential)) : undefined;
}

View File

@@ -8,7 +8,7 @@ const mocks = vi.hoisted(() => ({
resolveAuthProfileDisplayLabel: vi.fn(),
resolveUsableCustomProviderApiKey: vi.fn(() => null),
resolveEnvApiKey: vi.fn<() => { apiKey: string; source: string } | null>(() => null),
readCodexCliCredentialsCached: vi.fn<() => unknown>(() => null),
readCodexCliCredentialsCached: vi.fn<(options?: unknown) => unknown>(() => null),
}));
vi.mock("./auth-profiles.js", () => ({
@@ -144,6 +144,10 @@ describe("resolveModelAuthLabel", () => {
});
expect(label).toBe("oauth (codex-cli)");
expect(mocks.readCodexCliCredentialsCached).toHaveBeenCalledWith({
ttlMs: 5_000,
allowKeychainPrompt: false,
});
});
it("can skip external auth profile overlays for status labels", () => {

View File

@@ -74,7 +74,10 @@ export function resolveModelAuthLabel(params: {
return `api-key (${envKey.source})`;
}
if (providerKey === "codex" && readCodexCliCredentialsCached({ ttlMs: 5_000 })) {
if (
providerKey === "codex" &&
readCodexCliCredentialsCached({ ttlMs: 5_000, allowKeychainPrompt: false })
) {
return "oauth (codex-cli)";
}

View File

@@ -326,6 +326,10 @@ describe("resolveModelAuthMode", () => {
try {
expect(resolveModelAuthMode("codex", undefined, { version: 1, profiles: {} })).toBe("oauth");
expect(readCodexCliCredentialsCached).toHaveBeenCalledWith({
ttlMs: 5_000,
allowKeychainPrompt: false,
});
} finally {
readCodexCliCredentialsCached.mockRestore();
}

View File

@@ -800,7 +800,7 @@ export function resolveModelAuthMode(
if (
normalizeProviderId(resolved) === "codex" &&
cliCredentials.readCodexCliCredentialsCached({ ttlMs: 5_000 })
cliCredentials.readCodexCliCredentialsCached({ ttlMs: 5_000, allowKeychainPrompt: false })
) {
return "oauth";
}

View File

@@ -270,6 +270,34 @@ describe("shell env fallback", () => {
expect(exec).toHaveBeenCalledTimes(1);
});
it("caches login-shell env probe failures for repeated fallback reads", () => {
resetShellPathCacheForTests();
const env: NodeJS.ProcessEnv = {};
const logger = { warn: vi.fn() };
const exec = vi.fn(() => {
throw new Error("shell unavailable");
});
for (let i = 0; i < 2; i += 1) {
expect(
loadShellEnvFallback({
enabled: true,
env,
expectedKeys: ["OPENAI_API_KEY"],
exec: exec as unknown as Parameters<typeof loadShellEnvFallback>[0]["exec"],
logger,
}),
).toMatchObject({
ok: false,
applied: [],
error: "shell unavailable",
});
}
expect(exec).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledTimes(2);
});
it("tracks last applied keys across success, skip, and failure paths", () => {
const successEnv: NodeJS.ProcessEnv = {};
const successExec = vi.fn(() =>

View File

@@ -13,7 +13,10 @@ let lastAppliedKeys: string[] = [];
let cachedShellPath: string | null | undefined;
let cachedEtcShells: Set<string> | null | undefined;
let nextExecCacheId = 1;
const loginShellEnvProbeCache = new Map<string, Array<[string, string]>>();
const loginShellEnvProbeCache = new Map<
string,
{ ok: true; entries: Array<[string, string]> } | { ok: false; error: string }
>();
const execCacheIds = new WeakMap<object, number>();
function resolveShellExecEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
@@ -181,16 +184,18 @@ function probeLoginShellEnv(params: {
});
const cached = loginShellEnvProbeCache.get(cacheKey);
if (cached) {
return { ok: true, shellEnv: new Map(cached) };
return cached.ok ? { ok: true, shellEnv: new Map(cached.entries) } : cached;
}
try {
const stdout = execLoginShellEnvZero({ shell, env: execEnv, exec, timeoutMs });
const shellEnv = parseShellEnv(stdout);
loginShellEnvProbeCache.set(cacheKey, [...shellEnv.entries()]);
loginShellEnvProbeCache.set(cacheKey, { ok: true, entries: [...shellEnv.entries()] });
return { ok: true, shellEnv };
} catch (err) {
return { ok: false, error: formatErrorMessage(err) };
const result = { ok: false as const, error: formatErrorMessage(err) };
loginShellEnvProbeCache.set(cacheKey, result);
return result;
}
}

View File

@@ -19,6 +19,12 @@ type CandidateDir = {
const OPENCLAW_PACKAGE_ROOT = fileURLToPath(new URL("../..", import.meta.url));
const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
let manifestMetadataCache:
| {
key: string;
records: PluginManifestMetadataRecord[];
}
| undefined;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -106,6 +112,16 @@ function readManifestObject(pluginDir: string): Record<string, unknown> | undefi
return readJsonObject(path.join(pluginDir, PLUGIN_MANIFEST_FILENAME));
}
function manifestFileFingerprint(pluginDir: string): string {
const manifestPath = path.join(pluginDir, PLUGIN_MANIFEST_FILENAME);
try {
const stat = fs.statSync(manifestPath);
return `${manifestPath}:${stat.mtimeMs}:${stat.size}`;
} catch {
return `${manifestPath}:missing`;
}
}
function listPersistedIndexPluginDirs(env: NodeJS.ProcessEnv, startOrder: number): CandidateDir[] {
const index = readJsonObject(path.join(resolveStateDir(env), "plugins", "installs.json"));
if (!index || !Array.isArray(index.plugins)) {
@@ -167,9 +183,23 @@ export function listOpenClawPluginManifestMetadata(
...listChildPluginDirs(path.join(resolveStateDir(env), "extensions"), 4, order, "global"),
);
const uniqueCandidates = uniqueCandidateDirs(candidates);
const cacheKey = JSON.stringify(
uniqueCandidates.map((candidate) => [
candidate.pluginDir,
candidate.rank,
candidate.order,
candidate.origin ?? "",
manifestFileFingerprint(candidate.pluginDir),
]),
);
if (manifestMetadataCache?.key === cacheKey) {
return manifestMetadataCache.records.slice();
}
const byManifestId = new Map<string, CandidateDir>();
const records: PluginManifestMetadataRecord[] = [];
for (const candidate of uniqueCandidateDirs(candidates)) {
for (const candidate of uniqueCandidates) {
const manifest = readManifestObject(candidate.pluginDir);
if (!manifest) {
continue;
@@ -184,5 +214,6 @@ export function listOpenClawPluginManifestMetadata(
}
records.push({ pluginDir: candidate.pluginDir, manifest, origin: candidate.origin });
}
manifestMetadataCache = { key: cacheKey, records };
return records;
}

View File

@@ -1,7 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearCurrentPluginMetadataSnapshot,
resolvePluginMetadataControlPlaneFingerprint,
@@ -9,6 +9,7 @@ import {
} from "./current-plugin-metadata-snapshot.js";
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
import { listOpenClawPluginManifestMetadata } from "./manifest-metadata-scan.js";
import { normalizeProviderModelIdWithManifest } from "./manifest-model-id-normalization.js";
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
import { createEmptyPluginRegistry } from "./registry-empty.js";
@@ -243,4 +244,28 @@ describe("manifest model id normalization", () => {
process.env.OPENCLAW_STATE_DIR = stateDirB;
expect(normalizeDemoModel()).toBe("charlie/demo-model");
});
it("reuses manifest metadata while file fingerprints are unchanged", () => {
const stateDir = makeTempDir();
const pluginDir = path.join(stateDir, "extensions", "normalizer");
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
writeInstallIndex({ stateDir, pluginDir });
writeNormalizerManifest({ pluginDir, prefix: "alpha" });
process.env.OPENCLAW_STATE_DIR = stateDir;
process.env.OPENCLAW_HOME = undefined;
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = undefined;
const readFileSyncSpy = vi.spyOn(fs, "readFileSync");
expect(listOpenClawPluginManifestMetadata(process.env)).toHaveLength(1);
expect(listOpenClawPluginManifestMetadata(process.env)).toHaveLength(1);
const manifestReads = readFileSyncSpy.mock.calls.filter(
([filePath]) => String(filePath) === manifestPath,
);
expect(manifestReads).toHaveLength(1);
readFileSyncSpy.mockRestore();
});
});