mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
perf(gateway): cache startup metadata probes
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user