mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
perf(catalog): cadence-proof provider caches and parallelize the Claude scan (#115259)
This commit is contained in:
committed by
GitHub
parent
d23ce0530b
commit
9ae79eed2d
@@ -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 () => {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user