perf(catalog): cadence-proof provider caches and parallelize the Claude scan (#115259)

This commit is contained in:
Peter Steinberger
2026-07-28 11:37:37 -04:00
committed by GitHub
parent d23ce0530b
commit 9ae79eed2d
14 changed files with 1277 additions and 369 deletions
+27 -14
View File
@@ -171,7 +171,9 @@ function isOpenCodeSessionCatalogEnabled(pluginConfig: unknown): boolean {
);
}
function createOpenCodeSessionNodeHostCommands(): OpenClawPluginNodeHostCommand[] {
function createOpenCodeSessionNodeHostCommands(
api: OpenClawPluginApi,
): OpenClawPluginNodeHostCommand[] {
const available = ({ config, env }: { config: unknown; env: NodeJS.ProcessEnv }) =>
fullConfigCatalogEnabled(config) && executableOnPath("opencode", env);
return [
@@ -181,7 +183,11 @@ function createOpenCodeSessionNodeHostCommands(): OpenClawPluginNodeHostCommand[
dangerous: false,
isAvailable: available,
handle: async (paramsJSON) =>
JSON.stringify(await listLocalOpenCodeSessionPage(parseNodeParams(paramsJSON))),
JSON.stringify(
await listLocalOpenCodeSessionPage(parseNodeParams(paramsJSON), {
configIdentity: currentOpenCodeCatalogConfig(api),
}),
),
},
{
command: OPENCODE_SESSION_READ_COMMAND,
@@ -334,8 +340,9 @@ async function listOpenCodeHosts(
query: Parameters<SessionCatalogProvider["list"]>[0],
): Promise<SessionCatalogHost[]> {
const runtime = api.runtime;
const config = currentOpenCodeCatalogConfig(api);
const canContinue = resolveAcpSessionAvailability({
config: currentOpenCodeCatalogConfig(api),
config,
backendId: ACPX_BACKEND_ID,
agentId: OPENCODE_ACP_AGENT_ID,
}).available;
@@ -355,11 +362,14 @@ async function listOpenCodeHosts(
label: "Local OpenCode",
kind: "gateway",
connected: true,
...(await listLocalOpenCodeSessionPage({
limit: query.limitPerHost,
...(query.search ? { searchTerm: query.search } : {}),
cursor: query.cursors?.[LOCAL_HOST_ID],
}).then((page) => setCatalogCapabilities(page, { canContinue, canOpenTerminal: true }))),
...(await listLocalOpenCodeSessionPage(
{
limit: query.limitPerHost,
...(query.search ? { searchTerm: query.search } : {}),
cursor: query.cursors?.[LOCAL_HOST_ID],
},
{ configIdentity: config },
).then((page) => setCatalogCapabilities(page, { canContinue, canOpenTerminal: true }))),
});
} catch {
hosts.push({
@@ -484,15 +494,18 @@ async function continueOpenCodeSession(
sourceKey,
findExisting: () => listAdoptedOpenCodeSessions(api).get(sourceKey),
create: async () => {
const page = await listLocalOpenCodeSessionPage({
searchTerm: threadId,
limit: MAX_PAGE_LIMIT,
}).catch(() => undefined);
const config = currentOpenCodeCatalogConfig(api);
const page = await listLocalOpenCodeSessionPage(
{
searchTerm: threadId,
limit: MAX_PAGE_LIMIT,
},
{ configIdentity: config, forceRefresh: true },
).catch(() => undefined);
const record = page?.sessions.find((session) => session.threadId === threadId);
if (!record) {
throw new OpenCodeCatalogParamsError("OpenCode session is unavailable");
}
const config = currentOpenCodeCatalogConfig(api);
const currentAvailability = resolveAcpSessionAvailability({
config,
backendId: ACPX_BACKEND_ID,
@@ -564,7 +577,7 @@ export function registerOpenCodeSessionCatalog(api: OpenClawPluginApi): void {
unwrapNodePayload,
}),
});
for (const command of createOpenCodeSessionNodeHostCommands()) {
for (const command of createOpenCodeSessionNodeHostCommands(api)) {
api.registerNodeHostCommand(command);
}
for (const policy of createOpenCodeSessionNodeInvokePolicies()) {
@@ -424,6 +424,37 @@ describe("OpenCode session catalog", () => {
},
);
it.runIf(process.platform !== "win32")(
"memoizes the CLI database query across cadence and invalidates by config identity",
async () => {
await installFakeOpenCode();
let now = 1_000;
const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => now);
const configIdentity = {};
try {
await listLocalOpenCodeSessionPage({ limit: 20 }, { configIdentity });
await listLocalOpenCodeSessionPage({ limit: 20 }, { configIdentity });
expect(childProcessMocks.spawn).toHaveBeenCalledOnce();
now += 31_999;
await listLocalOpenCodeSessionPage({ limit: 20 }, { configIdentity });
expect(childProcessMocks.spawn).toHaveBeenCalledOnce();
await listLocalOpenCodeSessionPage({ limit: 20 }, { configIdentity, forceRefresh: true });
expect(childProcessMocks.spawn).toHaveBeenCalledTimes(2);
await listLocalOpenCodeSessionPage({ limit: 20 }, { configIdentity: {} });
expect(childProcessMocks.spawn).toHaveBeenCalledTimes(3);
now += 32_001;
await listLocalOpenCodeSessionPage({ limit: 20 }, { configIdentity });
expect(childProcessMocks.spawn).toHaveBeenCalledTimes(4);
} finally {
nowSpy.mockRestore();
}
},
);
it.runIf(process.platform !== "win32")(
"hides and rejects Continue when ACP cannot resume OpenCode",
async () => {
+78 -2
View File
@@ -22,6 +22,8 @@ const MAX_CLI_OUTPUT_BYTES = 32 * 1024 * 1024;
const MAX_TRANSCRIPT_ITEM_BYTES = 512 * 1024;
const MAX_TRANSCRIPT_PAGE_BYTES = 20 * 1024 * 1024;
const CLI_TIMEOUT_MS = 30_000;
const OPENCODE_QUERY_CACHE_TTL_MS = 32_000;
const OPENCODE_QUERY_CACHE_MAX_ENTRIES = 32;
const SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
const SAFE_ENV_KEYS = [
"APPDATA",
@@ -46,6 +48,34 @@ const SAFE_ENV_KEYS = [
"XDG_STATE_HOME",
] as const;
type OpenCodeQueryCacheEntry = {
expiresAt: number;
result: Promise<unknown>;
resolved?: true;
};
type OpenCodeQueryCacheOptions = {
configIdentity?: object;
forceRefresh?: boolean;
};
const openCodeConfigIdentities = new WeakMap<object, number>();
// Query results are valid for one immutable OpenClaw config identity, CLI environment, and SQL text.
// Config/env changes or 32s expiry invalidate them; failures are removed so recovery retries at once.
// The bounded map prevents pagination variants from growing while avoiding a subprocess every poll.
const openCodeQueryCache = new Map<string, OpenCodeQueryCacheEntry>();
let nextOpenCodeConfigIdentity = 1;
function openCodeQueryCacheKey(query: string, configIdentity: object): string {
let identity = openCodeConfigIdentities.get(configIdentity);
if (identity === undefined) {
identity = nextOpenCodeConfigIdentity++;
openCodeConfigIdentities.set(configIdentity, identity);
}
const environment = SAFE_ENV_KEYS.map((key) => `${key}=${process.env[key] ?? ""}`).join("\0");
return `${String(identity)}\0${environment}\0${query}`;
}
export type OpenCodeSessionPage = {
sessions: SessionCatalogSession[];
nextCursor?: string;
@@ -307,6 +337,49 @@ export async function queryOpenCodeDatabase(query: string): Promise<unknown> {
return output.trim() ? (JSON.parse(output) as unknown) : [];
}
async function queryCachedOpenCodeSessions(
query: string,
options: OpenCodeQueryCacheOptions,
): Promise<unknown> {
const key = openCodeQueryCacheKey(query, options.configIdentity ?? process.env);
const cached = openCodeQueryCache.get(key);
if (options.forceRefresh !== true && cached && cached.expiresAt > Date.now()) {
openCodeQueryCache.delete(key);
openCodeQueryCache.set(key, cached);
return await cached.result;
}
if (cached) {
openCodeQueryCache.delete(key);
}
const result = queryOpenCodeDatabase(query);
const entry: OpenCodeQueryCacheEntry = {
expiresAt: Date.now() + OPENCODE_QUERY_CACHE_TTL_MS,
result,
};
openCodeQueryCache.set(key, entry);
while (openCodeQueryCache.size > OPENCODE_QUERY_CACHE_MAX_ENTRIES) {
const oldest = openCodeQueryCache.keys().next();
if (oldest.done) {
break;
}
openCodeQueryCache.delete(oldest.value);
}
try {
const value = await result;
entry.resolved = true;
return value;
} catch (error) {
if (openCodeQueryCache.get(key) === entry) {
if (cached?.resolved) {
openCodeQueryCache.set(key, cached);
} else {
openCodeQueryCache.delete(key);
}
}
throw error;
}
}
export async function exportOpenCodeSession(threadId: string): Promise<unknown> {
const output = await runOpenCode(["--pure", "export", threadId]);
return JSON.parse(output) as unknown;
@@ -341,7 +414,10 @@ function parseOpenCodeSession(value: unknown): SessionCatalogSession | undefined
};
}
export async function listLocalOpenCodeSessionPage(value?: unknown): Promise<OpenCodeSessionPage> {
export async function listLocalOpenCodeSessionPage(
value?: unknown,
options: OpenCodeQueryCacheOptions = {},
): Promise<OpenCodeSessionPage> {
const params = parseListParams(value);
const offset = decodeCursor(params.cursor);
const requestedCount = params.searchTerm
@@ -353,7 +429,7 @@ export async function listLocalOpenCodeSessionPage(value?: unknown): Promise<Ope
"WHERE parent_id IS NULL AND time_archived IS NULL",
`ORDER BY time_updated DESC, id DESC LIMIT ${String(requestedCount)}`,
].join(" ");
const parsed = await queryOpenCodeDatabase(query);
const parsed = await queryCachedOpenCodeSessions(query, options);
if (!Array.isArray(parsed) || parsed.length > MAX_CLI_LIST_SESSIONS) {
throw new Error("OpenCode returned an invalid session list");
}