diff --git a/src/provider/cursor-log-intercept.ts b/src/provider/cursor-log-intercept.ts index 80822dd..99d2779 100644 --- a/src/provider/cursor-log-intercept.ts +++ b/src/provider/cursor-log-intercept.ts @@ -37,6 +37,34 @@ function matchesKnownSdkWarning(line: string): boolean { return SDK_WARNING_PREFIXES.some((prefix) => line.startsWith(prefix)); } +/** + * Slow-cache diagnostic the SDK `console.warn`s when global context rebuild + * exceeds its threshold. Observed shape (colors stripped; the leading + * timestamp token varies in width, e.g. `13:04:05.123` vs `113:26:23.106`): + * + * 113:26:23.106 WARN computeGlobalCache: slow ctx-LocalRequestContextExecutor. rebuildGlobalCache/LocalRequestContextExecutor.computeGlobalCache meta=/totalMs: 1312, cloudRule: 0, codebaseRef: 0, subagents: 416, cursorRules: 1311, ruleCount: 701 + * + * The `/` after `meta=` is how the SDK prints its empty context object. + */ +const SLOW_CACHE_WARN_RE = + /^\d{2,3}:\d{2}:\d{2}\.\d{3}\s+WARN\s+(computeGlobalCache: slow .+?)\s+meta=\/?\s*(.+)$/; + +export interface ParsedSlowCacheWarn { + message: string; + meta: Record; +} + +/** Matches one line against the known slow-cache warn shape. */ +export function parseSlowCacheWarnLine( + line: string, +): ParsedSlowCacheWarn | undefined { + const match = SLOW_CACHE_WARN_RE.exec(stripAnsi(line)); + if (!match) return undefined; + const [, message, meta] = match; + if (!message) return undefined; + return { message: message.trim(), meta: parseCursorLogMeta(meta ?? "") }; +} + /** Parses the `meta={key: value, ...}` tail into a plain numeric object. */ export function parseCursorLogMeta(raw: string): Record { const out: Record = {}; @@ -109,6 +137,11 @@ export function installCursorLogInterceptor(): void { pluginLog("warn", line); return; } + const slowCache = parseSlowCacheWarnLine(line); + if (slowCache) { + pluginLog("warn", slowCache.message, slowCache.meta); + return; + } } warnPassthrough(...(args as Parameters)); }; diff --git a/src/sidecar/agent-host.mjs b/src/sidecar/agent-host.mjs index aca5f2b..5abd2d6 100644 --- a/src/sidecar/agent-host.mjs +++ b/src/sidecar/agent-host.mjs @@ -64,6 +64,12 @@ const SDK_WARNING_PREFIXES = [ "shell-parser: tree-sitter natives are unavailable in this artifact", ]; +// Slow global-cache rebuild diagnostic (timestamp token varies in width, +// e.g. `13:04:05.123` vs `113:26:23.106`; `/` after `meta=` is the SDK +// printing its empty context object). +const SLOW_CACHE_WARN_RE = + /^\d{2,3}:\d{2}:\d{2}\.\d{3}\s+WARN\s+(computeGlobalCache: slow .+?)\s+meta=\/?\s*(.+)$/; + function parseLogMeta(raw) { const out = {}; for (const part of raw.split(",")) { @@ -101,6 +107,17 @@ console.warn = (...args) => { write({ ev: "log", level: "warn", message: line }); return; } + const slowCache = SLOW_CACHE_WARN_RE.exec(line); + if (slowCache) { + const [, message, meta] = slowCache; + write({ + ev: "log", + level: "warn", + message: message.trim(), + meta: parseLogMeta(meta ?? ""), + }); + return; + } } originalConsoleWarn(...args); }; diff --git a/test/cursor-log-intercept.test.ts b/test/cursor-log-intercept.test.ts index 3703536..2b43c96 100644 --- a/test/cursor-log-intercept.test.ts +++ b/test/cursor-log-intercept.test.ts @@ -118,6 +118,44 @@ describe("installCursorLogInterceptor", () => { passthrough.mockRestore(); }); + it("routes the computeGlobalCache slow warn through pluginLog with parsed meta", () => { + const log = vi.fn().mockResolvedValue(undefined); + setLogBridge({ client: { app: { log } } } as never); + + const passthrough = vi.spyOn(console, "warn").mockImplementation(() => {}); + installCursorLogInterceptor(); + + console.warn( + "113:26:23.106 WARN computeGlobalCache: slow ctx-LocalRequestContextExecutor. rebuildGlobalCache/LocalRequestContextExecutor.computeGlobalCache meta=/totalMs: 1312, cloudRule: 0, codebaseRef: 0, subagents: 416, cursorRules: 1311, ruleCount: 701", + ); + console.warn("unrelated warning"); + + expect(log).toHaveBeenCalledTimes(1); + expect(log).toHaveBeenCalledWith({ + body: { + service: "opencode-cursor", + level: "warn", + message: + "computeGlobalCache: slow ctx-LocalRequestContextExecutor. rebuildGlobalCache/LocalRequestContextExecutor.computeGlobalCache", + extra: { + totalMs: 1312, + cloudRule: 0, + codebaseRef: 0, + subagents: 416, + cursorRules: 1311, + ruleCount: 701, + }, + }, + }); + + resetCursorLogInterceptor(); + // The spy is the pre-interceptor console.warn; passthrough calls must + // reach it, but the recognized line must not. + expect(passthrough).toHaveBeenCalledTimes(1); + expect(passthrough).toHaveBeenCalledWith("unrelated warning"); + passthrough.mockRestore(); + }); + it("is idempotent across repeated installs", () => { installCursorLogInterceptor(); const first = console.log; diff --git a/test/fixtures/fake-cursor-sdk.mjs b/test/fixtures/fake-cursor-sdk.mjs index 25eae27..1c85bc3 100644 --- a/test/fixtures/fake-cursor-sdk.mjs +++ b/test/fixtures/fake-cursor-sdk.mjs @@ -18,6 +18,11 @@ * `options.emitShellParserWarn` -> Agent.create/resume writes the shell-parser * "tree-sitter natives unavailable" diagnostic to console.warn, as the real * @cursor/sdk does on first shell parse, plus one unrelated console.warn. + * + * `options.emitSlowCacheWarn` -> Agent.create/resume writes the + * computeGlobalCache "slow" diagnostic to console.warn, as the real + * @cursor/sdk does on slow global-cache rebuilds, plus one unrelated + * console.warn. */ function makeAgent(agentId, options) { @@ -36,6 +41,12 @@ function makeAgent(agentId, options) { ); console.warn("some unrelated cursor sdk warning"); } + if (options?.emitSlowCacheWarn) { + console.warn( + "113:26:23.106 WARN computeGlobalCache: slow ctx-LocalRequestContextExecutor. rebuildGlobalCache/LocalRequestContextExecutor.computeGlobalCache meta=/totalMs: 1312, cloudRule: 0, codebaseRef: 0, subagents: 416, cursorRules: 1311, ruleCount: 701", + ); + console.warn("some unrelated slow-cache warning"); + } return { agentId, model: options?.model, diff --git a/test/sidecar.test.ts b/test/sidecar.test.ts index 8115f28..6551f57 100644 --- a/test/sidecar.test.ts +++ b/test/sidecar.test.ts @@ -160,7 +160,11 @@ describe("SidecarClient", () => { const client = makeClient((level, message, meta) => { logs.push({ level, message, meta }); }); - await client.createAgent({ ...CREATE_OPTIONS, emitShellParserWarn: true }); + await client.createAgent({ + ...CREATE_OPTIONS, + emitShellParserWarn: true, + emitSlowCacheWarn: true, + }); expect(logs).toEqual([ { @@ -168,6 +172,19 @@ describe("SidecarClient", () => { message: "shell-parser: tree-sitter natives are unavailable in this artifact; shell command analysis degrades to parsingFailed", }, + { + level: "warn", + message: + "computeGlobalCache: slow ctx-LocalRequestContextExecutor. rebuildGlobalCache/LocalRequestContextExecutor.computeGlobalCache", + meta: { + totalMs: 1312, + cloudRule: 0, + codebaseRef: 0, + subagents: 416, + cursorRules: 1311, + ruleCount: 701, + }, + }, ]); });