diff --git a/apps/server/src/provider/Layers/CursorAdapter.test.ts b/apps/server/src/provider/Layers/CursorAdapter.test.ts index e9fcca364081..b58ebfa7a972 100644 --- a/apps/server/src/provider/Layers/CursorAdapter.test.ts +++ b/apps/server/src/provider/Layers/CursorAdapter.test.ts @@ -168,6 +168,48 @@ const cursorAdapterTestLayer = it.layer( ); cursorAdapterTestLayer("CursorAdapterLive", (it) => { + it.effect("enables ACP protocol logging from Cursor settings", () => + Effect.gen(function* () { + const settings = yield* ServerSettingsService; + const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper()); + yield* settings.updateSettings({ + providers: { + cursor: { binaryPath: wrapperPath, verboseProtocolLogging: true }, + }, + }); + const protocolLogged = yield* Deferred.make(); + const resolveSettings = yield* makeResolveCursorSettings; + const adapter = yield* makeCursorAdapter(decodeCursorSettings({}), { + resolveSettings, + nativeEventLogger: { + filePath: "memory://cursor-native-events", + write: (record: unknown) => + typeof record === "object" && + record !== null && + "event" in record && + typeof record.event === "object" && + record.event !== null && + "kind" in record.event && + record.event.kind === "protocol" + ? Deferred.succeed(protocolLogged, undefined).pipe(Effect.asVoid) + : Effect.void, + close: () => Effect.void, + }, + }); + const threadId = ThreadId.make("cursor-verbose-protocol-log"); + + yield* adapter.startSession({ + threadId, + provider: ProviderDriverKind.make("cursor"), + cwd: process.cwd(), + runtimeMode: "full-access", + modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, + }); + yield* Deferred.await(protocolLogged); + yield* adapter.stopSession(threadId); + }), + ); + it.effect("starts a session and maps mock ACP prompt flow to runtime events", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; diff --git a/apps/server/src/provider/Layers/CursorAdapter.ts b/apps/server/src/provider/Layers/CursorAdapter.ts index 4b4f2c84c17c..b9191d4e9ffb 100644 --- a/apps/server/src/provider/Layers/CursorAdapter.ts +++ b/apps/server/src/provider/Layers/CursorAdapter.ts @@ -514,12 +514,6 @@ export function makeCursorAdapter( let ctx!: CursorSessionContext; const resumeSessionId = parseCursorResume(input.resumeCursor)?.sessionId; - const acpNativeLoggers = makeAcpNativeLoggers({ - nativeEventLogger, - provider: PROVIDER, - threadId: input.threadId, - }); - // Resolve the CursorSettings used to spawn the ACP child. Production // leaves `options.resolveSettings` undefined so we use the value // captured at adapter construction — per-instance isolation is @@ -531,6 +525,12 @@ export function makeCursorAdapter( const effectiveCursorSettings = options?.resolveSettings ? yield* options.resolveSettings : cursorSettings; + const acpNativeLoggers = makeAcpNativeLoggers({ + nativeEventLogger, + provider: PROVIDER, + threadId: input.threadId, + verboseProtocolLogging: effectiveCursorSettings.verboseProtocolLogging, + }); const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); const mcpServers = [ diff --git a/apps/server/src/provider/Layers/CursorProvider.test.ts b/apps/server/src/provider/Layers/CursorProvider.test.ts index 4c6eb1af58d4..40fbf106d6ce 100644 --- a/apps/server/src/provider/Layers/CursorProvider.test.ts +++ b/apps/server/src/provider/Layers/CursorProvider.test.ts @@ -298,6 +298,7 @@ const baseCursorSettings: CursorSettings = { binaryPath: "cursor-agent", apiEndpoint: "", customModels: [], + verboseProtocolLogging: false, }; const cursorAcpDiscoveryFailedMessage = [ "Cursor ACP model discovery failed.", @@ -435,6 +436,7 @@ describe("checkCursorProviderStatus", () => { binaryPath: missingCursorBinaryPath, apiEndpoint: "", customModels: [], + verboseProtocolLogging: false, }), ); @@ -456,6 +458,7 @@ describe("checkCursorProviderStatus", () => { binaryPath: wrapperPath, apiEndpoint: "", customModels: [], + verboseProtocolLogging: false, }, { ...process.env, @@ -484,6 +487,7 @@ describe("discoverCursorModelsViaAcp", () => { binaryPath: wrapperPath, apiEndpoint: "", customModels: [], + verboseProtocolLogging: false, }).pipe(Effect.scoped), ); @@ -506,6 +510,7 @@ describe("discoverCursorModelsViaAcp", () => { binaryPath: wrapperPath, apiEndpoint: "", customModels: [], + verboseProtocolLogging: false, }), ); diff --git a/apps/server/src/provider/Layers/EventNdjsonLogger.test.ts b/apps/server/src/provider/Layers/EventNdjsonLogger.test.ts index dcea46a50811..8e33476c798b 100644 --- a/apps/server/src/provider/Layers/EventNdjsonLogger.test.ts +++ b/apps/server/src/provider/Layers/EventNdjsonLogger.test.ts @@ -318,7 +318,7 @@ describe("EventNdjsonLogger", () => { }), ); - it.effect("drops transient canonical events before serialization", () => + it.effect("drops transient provider events before serialization", () => Effect.gen(function* () { const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-provider-log-")); const basePath = NodePath.join(tempDir, "events.log"); @@ -334,6 +334,46 @@ describe("EventNdjsonLogger", () => { yield* canonical.write(circularDelta, threadId); yield* canonical.write({ type: "item.completed", id: "final" }, threadId); yield* native.write({ type: "content.delta", id: "native-delta" }, threadId); + yield* native.write( + { method: "item/agentMessage/delta", payload: circularDelta }, + threadId, + ); + yield* native.write( + { method: "thread/realtime/outputAudio/delta", payload: circularDelta }, + threadId, + ); + yield* native.write( + { method: "thread/realtime/transcript/delta", payload: circularDelta }, + threadId, + ); + yield* native.write( + { + event: { + method: "claude/stream_event/content_block_delta/text_delta", + payload: circularDelta, + }, + }, + threadId, + ); + yield* native.write( + { + event: { + method: "session/update", + payload: { update: { sessionUpdate: "agent_message_chunk" } }, + }, + }, + threadId, + ); + yield* native.write( + { + event: { + type: "message.part.updated", + payload: { properties: { part: { type: "text" } } }, + }, + }, + threadId, + ); + yield* native.write({ type: "turn.completed", id: "native-final" }, threadId); yield* store.close(); const lines = NodeFS.readFileSync(ownedLogPath(basePath, "thread-filtered"), "utf8") @@ -345,7 +385,7 @@ describe("EventNdjsonLogger", () => { lines.map(({ stream, payload }) => ({ stream, payload })), [ { stream: "CANON", payload: '{"type":"item.completed","id":"final"}' }, - { stream: "NTIVE", payload: '{"type":"content.delta","id":"native-delta"}' }, + { stream: "NTIVE", payload: '{"type":"turn.completed","id":"native-final"}' }, ], ); } finally { diff --git a/apps/server/src/provider/Layers/EventNdjsonLogger.ts b/apps/server/src/provider/Layers/EventNdjsonLogger.ts index 6e1d81470064..64d2cf3a4ed2 100644 --- a/apps/server/src/provider/Layers/EventNdjsonLogger.ts +++ b/apps/server/src/provider/Layers/EventNdjsonLogger.ts @@ -46,6 +46,17 @@ const transientCanonicalEventTypes = new Set([ "tool.progress", "turn.proposed.delta", ]); +const transientNativeMethods = new Set([ + "item/agentMessage/delta", + "item/commandExecution/outputDelta", + "item/fileChange/outputDelta", + "item/plan/delta", + "item/reasoning/summaryTextDelta", + "item/reasoning/textDelta", + "thread/realtime/outputAudio/delta", + "thread/realtime/transcript/delta", +]); +const transientAcpUpdates = new Set(["agent_message_chunk", "agent_thought_chunk"]); export type EventNdjsonStream = "native" | "canonical" | "orchestration"; @@ -128,7 +139,7 @@ export interface PendingRecord { } interface StoreState { - readonly pending: ReadonlyArray; + readonly pending: Array; readonly pendingBytes: number; readonly sinks: ReadonlyMap; readonly flushScheduled: boolean; @@ -180,12 +191,50 @@ function providerLogPath(directory: string, prefix: string, threadSegment: strin } function shouldPersist(stream: EventNdjsonStream, event: unknown): boolean { - if (stream !== "canonical" || typeof event !== "object" || event === null) { + if (stream === "orchestration" || typeof event !== "object" || event === null) { return true; } try { const type = Reflect.get(event, "type"); - return typeof type !== "string" || !transientCanonicalEventTypes.has(type); + if (typeof type === "string" && transientCanonicalEventTypes.has(type)) { + return false; + } + if (stream !== "native") return true; + + const nested = Reflect.get(event, "event"); + const nativeEvent = typeof nested === "object" && nested !== null ? nested : event; + const method = Reflect.get(nativeEvent, "method"); + if ( + typeof method === "string" && + (transientNativeMethods.has(method) || + method.startsWith("claude/stream_event/content_block_delta/")) + ) { + return false; + } + + const nativeType = Reflect.get(nativeEvent, "type"); + if (nativeType === "message.part.delta") return false; + + const payload = Reflect.get(nativeEvent, "payload"); + if (typeof payload !== "object" || payload === null) return true; + + if (method === "session/update") { + const update = Reflect.get(payload, "update"); + if (typeof update !== "object" || update === null) return true; + const updateType = Reflect.get(update, "sessionUpdate"); + return typeof updateType !== "string" || !transientAcpUpdates.has(updateType); + } + + if (nativeType === "message.part.updated") { + const properties = Reflect.get(payload, "properties"); + if (typeof properties !== "object" || properties === null) return true; + const part = Reflect.get(properties, "part"); + if (typeof part !== "object" || part === null) return true; + const partType = Reflect.get(part, "type"); + return partType !== "text" && partType !== "reasoning"; + } + + return true; } catch { return true; } @@ -598,10 +647,8 @@ export const makeEventNdjsonLogStore = Effect.fnUntraced(function* ( results.push(result); current = drained; } - const pending = [ - ...current.pending, - { stream, threadSegment: resolveThreadSegment(threadId), line, bytes }, - ]; + const pending = current.pending; + pending.push({ stream, threadSegment: resolveThreadSegment(threadId), line, bytes }); const pendingBytes = current.pendingBytes + bytes; const flushNow = resolved.batchWindowMs === 0 || diff --git a/apps/server/src/provider/Layers/GrokAdapter.test.ts b/apps/server/src/provider/Layers/GrokAdapter.test.ts index cf15e000f5cb..5f4b34fab406 100644 --- a/apps/server/src/provider/Layers/GrokAdapter.test.ts +++ b/apps/server/src/provider/Layers/GrokAdapter.test.ts @@ -1269,6 +1269,42 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => { }), ); + it.effect("enables ACP protocol logging from Grok settings", () => + Effect.gen(function* () { + const wrapperPath = yield* Effect.promise(() => makeMockGrokWrapper()); + const protocolLogged = yield* Deferred.make(); + const adapter = yield* makeGrokAdapter( + decodeGrokSettings({ binaryPath: wrapperPath, verboseProtocolLogging: true }), + { + nativeEventLogger: { + filePath: "memory://grok-native-events", + write: (record: unknown) => + typeof record === "object" && + record !== null && + "event" in record && + typeof record.event === "object" && + record.event !== null && + "kind" in record.event && + record.event.kind === "protocol" + ? Deferred.succeed(protocolLogged, undefined).pipe(Effect.asVoid) + : Effect.void, + close: () => Effect.void, + }, + }, + ).pipe(Effect.orDie); + const threadId = ThreadId.make("grok-verbose-protocol-log"); + + yield* adapter.startSession({ + threadId, + provider: ProviderDriverKind.make("grok"), + cwd: process.cwd(), + runtimeMode: "full-access", + }); + yield* Deferred.await(protocolLogged); + yield* adapter.stopSession(threadId); + }), + ); + // Production calls startSession from a request fiber that finishes as soon as // the session exists. `Effect.forkChild` made the notification consumer a // child of that fiber, and Effect interrupts a fiber's children when it diff --git a/apps/server/src/provider/Layers/GrokAdapter.ts b/apps/server/src/provider/Layers/GrokAdapter.ts index 5b945c402179..1bef100b299a 100644 --- a/apps/server/src/provider/Layers/GrokAdapter.ts +++ b/apps/server/src/provider/Layers/GrokAdapter.ts @@ -690,6 +690,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte nativeEventLogger, provider: PROVIDER, threadId: input.threadId, + verboseProtocolLogging: grokSettings.verboseProtocolLogging, }); const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts index a853103439be..b8bae30ae44d 100644 --- a/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts +++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts @@ -116,6 +116,7 @@ const makeGrokConfig = (overrides: Partial): GrokSettings => ({ enabled: false, binaryPath: "grok", customModels: [], + verboseProtocolLogging: false, ...overrides, }); diff --git a/apps/server/src/provider/acp/AcpNativeLogging.test.ts b/apps/server/src/provider/acp/AcpNativeLogging.test.ts index 7c949e040599..84926fbe1d61 100644 --- a/apps/server/src/provider/acp/AcpNativeLogging.test.ts +++ b/apps/server/src/provider/acp/AcpNativeLogging.test.ts @@ -28,6 +28,7 @@ nodeServicesIt("ACP native logging", (it) => { nativeEventLogger, provider: ProviderDriverKind.make("cursor"), threadId: ThreadId.make("thread-1"), + verboseProtocolLogging: true, }); const secret = "secret-token-value"; const requestLogger = logger.requestLogger; @@ -67,6 +68,174 @@ nodeServicesIt("ACP native logging", (it) => { }), ); + it.effect("keeps request diagnostics without enabling full protocol logging", () => + Effect.gen(function* () { + const records: Array = []; + const makeLogger = yield* makeAcpNativeLoggerFactory(); + const logger = makeLogger({ + nativeEventLogger: { + filePath: "/tmp/provider-native.ndjson", + write: (event) => Effect.sync(() => void records.push(event)), + close: () => Effect.void, + }, + provider: ProviderDriverKind.make("grok"), + threadId: ThreadId.make("thread-1"), + }); + + assert.isUndefined(logger.protocolLogging); + const requestLogger = logger.requestLogger; + assert.exists(requestLogger); + if (!requestLogger) return; + yield* requestLogger({ + method: "session/prompt", + payload: {}, + status: "started", + }); + assert.lengthOf(records, 1); + }), + ); + + it.effect("drops transient ACP chunks before formatting verbose protocol logs", () => + Effect.gen(function* () { + const records: Array = []; + const makeLogger = yield* makeAcpNativeLoggerFactory(); + const logger = makeLogger({ + nativeEventLogger: { + filePath: "/tmp/provider-native.ndjson", + write: (event) => Effect.sync(() => void records.push(event)), + close: () => Effect.void, + }, + provider: ProviderDriverKind.make("cursor"), + threadId: ThreadId.make("thread-1"), + verboseProtocolLogging: true, + }); + const protocolLogger = logger.protocolLogging?.logger; + assert.exists(protocolLogger); + if (!protocolLogger) return; + + for (const updateType of ["agent_message_chunk", "agent_thought_chunk"] as const) { + yield* protocolLogger({ + direction: "incoming", + stage: "raw", + payload: `${encodeUnknownJson({ + method: "session/update", + params: { update: { sessionUpdate: updateType } }, + })}\n`, + }); + yield* protocolLogger({ + direction: "incoming", + stage: "decoded", + payload: [ + { + _tag: "Request", + tag: "session/update", + payload: { update: { sessionUpdate: updateType } }, + }, + ], + }); + } + + assert.lengthOf(records, 0); + + yield* protocolLogger({ + direction: "incoming", + stage: "decoded", + payload: [ + { + _tag: "Request", + tag: "session/update", + payload: { update: { sessionUpdate: "tool_call" } }, + }, + ], + }); + assert.lengthOf(records, 1); + }), + ); + + it.effect("keeps mixed and incomplete raw diagnostics", () => + Effect.gen(function* () { + const records: Array = []; + const makeLogger = yield* makeAcpNativeLoggerFactory(); + const logger = makeLogger({ + nativeEventLogger: { + filePath: "/tmp/provider-native.ndjson", + write: (event) => Effect.sync(() => void records.push(event)), + close: () => Effect.void, + }, + provider: ProviderDriverKind.make("cursor"), + threadId: ThreadId.make("thread-1"), + verboseProtocolLogging: true, + }); + const protocolLogger = logger.protocolLogging?.logger; + assert.exists(protocolLogger); + if (!protocolLogger) return; + + const transient = encodeUnknownJson({ + method: "session/update", + params: { update: { sessionUpdate: "agent_message_chunk" } }, + }); + const lifecycle = encodeUnknownJson({ method: "session/new", params: {} }); + + yield* protocolLogger({ + direction: "incoming", + stage: "raw", + payload: `${transient}\n${lifecycle}\n`, + }); + yield* protocolLogger({ + direction: "incoming", + stage: "raw", + payload: transient, + }); + yield* protocolLogger({ + direction: "incoming", + stage: "raw", + payload: `${transient}\n{malformed}\n`, + }); + + assert.lengthOf(records, 3); + }), + ); + + it.effect("filters transient entries from mixed decoded batches", () => + Effect.gen(function* () { + const records: Array = []; + const makeLogger = yield* makeAcpNativeLoggerFactory(); + const logger = makeLogger({ + nativeEventLogger: { + filePath: "/tmp/provider-native.ndjson", + write: (event) => Effect.sync(() => void records.push(event)), + close: () => Effect.void, + }, + provider: ProviderDriverKind.make("grok"), + threadId: ThreadId.make("thread-1"), + verboseProtocolLogging: true, + }); + const protocolLogger = logger.protocolLogging?.logger; + assert.exists(protocolLogger); + if (!protocolLogger) return; + + yield* protocolLogger({ + direction: "incoming", + stage: "decoded", + payload: [ + { + _tag: "Request", + tag: "session/update", + payload: { update: { sessionUpdate: "agent_thought_chunk" } }, + }, + { + _tag: "Request", + tag: "session/new", + payload: {}, + }, + ], + }); + + assert.lengthOf(records, 1); + assert.include(encodeUnknownJson(records), '"itemCount":1'); + }), + ); + it.effect("logs a structural tag when the native writer defects", () => { const messages: Array = []; const logCapture = Logger.make(({ message }) => { diff --git a/apps/server/src/provider/acp/AcpNativeLogging.ts b/apps/server/src/provider/acp/AcpNativeLogging.ts index 06bff3aa6113..6d1bf6209d5d 100644 --- a/apps/server/src/provider/acp/AcpNativeLogging.ts +++ b/apps/server/src/provider/acp/AcpNativeLogging.ts @@ -9,6 +9,8 @@ import type * as EffectAcpProtocol from "effect-acp/protocol"; import type { EventNdjsonLogger } from "../Layers/EventNdjsonLogger.ts"; import type * as AcpSessionRuntime from "./AcpSessionRuntime.ts"; +const transientProtocolUpdates = new Set(["agent_message_chunk", "agent_thought_chunk"]); + function structuralMethod(value: string): string { return value.length <= 128 && /^[A-Za-z][A-Za-z0-9._:/-]*$/.test(value) ? value : "unknown"; } @@ -64,12 +66,61 @@ function formatProtocolLogPayload(event: EffectAcpProtocol.AcpProtocolLogEvent) }; } +function isTransientProtocolMessage(message: unknown): boolean { + if (typeof message !== "object" || message === null) return false; + const method = Reflect.get(message, "tag") ?? Reflect.get(message, "method"); + if (method !== "session/update") return false; + + const payload = Reflect.get(message, "payload") ?? Reflect.get(message, "params"); + if (typeof payload !== "object" || payload === null) return false; + const update = Reflect.get(payload, "update"); + if (typeof update !== "object" || update === null) return false; + const updateType = Reflect.get(update, "sessionUpdate"); + return typeof updateType === "string" && transientProtocolUpdates.has(updateType); +} + +function rawChunkContainsOnlyTransientMessages(payload: string): boolean { + const lines = payload.split("\n"); + const remainder = lines.pop() ?? ""; + if (remainder.trim().length > 0) return false; + + const messages: Array = []; + for (const line of lines) { + if (line.trim().length === 0) continue; + try { + messages.push(JSON.parse(line)); + } catch { + return false; + } + } + return messages.length > 0 && messages.every(isTransientProtocolMessage); +} + +function filterTransientProtocolLog( + event: EffectAcpProtocol.AcpProtocolLogEvent, +): EffectAcpProtocol.AcpProtocolLogEvent | undefined { + if (event.direction !== "incoming") return event; + + if (event.stage === "raw" && typeof event.payload === "string") { + return rawChunkContainsOnlyTransientMessages(event.payload) ? undefined : event; + } + + if (event.stage !== "decoded") return event; + if (!Array.isArray(event.payload)) { + return isTransientProtocolMessage(event.payload) ? undefined : event; + } + + const payload = event.payload.filter((message) => !isTransientProtocolMessage(message)); + return payload.length === 0 ? undefined : { ...event, payload }; +} + export const makeAcpNativeLoggerFactory = Effect.fn("makeAcpNativeLoggerFactory")(function* () { const crypto = yield* Crypto.Crypto; return (input: { readonly nativeEventLogger: EventNdjsonLogger | undefined; readonly provider: ProviderDriverKind; readonly threadId: ThreadId; + readonly verboseProtocolLogging?: boolean; }): Pick => { const writeNativeAcpLog = (logInput: { readonly kind: "request" | "protocol"; @@ -111,16 +162,20 @@ export const makeAcpNativeLoggerFactory = Effect.fn("makeAcpNativeLoggerFactory" kind: "request", payload: formatRequestLogPayload(event), }), - ...(input.nativeEventLogger + ...(input.nativeEventLogger && input.verboseProtocolLogging ? { protocolLogging: { logIncoming: true, logOutgoing: true, - logger: (event: EffectAcpProtocol.AcpProtocolLogEvent) => - writeNativeAcpLog({ - kind: "protocol", - payload: formatProtocolLogPayload(event), - }), + logger: (event: EffectAcpProtocol.AcpProtocolLogEvent) => { + const filtered = filterTransientProtocolLog(event); + return filtered + ? writeNativeAcpLog({ + kind: "protocol", + payload: formatProtocolLogPayload(filtered), + }) + : Effect.void; + }, } satisfies NonNullable, } : {}), diff --git a/docs/internals/overview.md b/docs/internals/overview.md index a7f01a7fca40..0cbdd3ce9b9c 100644 --- a/docs/internals/overview.md +++ b/docs/internals/overview.md @@ -115,6 +115,13 @@ Runtime receipts are a test-only mechanism. `RuntimeReceiptBusLive` in [`RuntimeReceiptBus.ts`][receipts] publishes nothing; only the test layer is PubSub-backed. Do not build production behavior on receipts. +Provider event logs in [`EventNdjsonLogger.ts`][event-log] drop canonical deltas and native +streaming chunks (Codex item/realtime methods, Claude content-block deltas, ACP +`agent_message_chunk`/`agent_thought_chunk`, OpenCode text/reasoning part updates) before +serialization. ACP request diagnostics stay on by default; full protocol logging is opt-in through +the Cursor and Grok provider settings and filters the same transient session updates. Async drain/close behavior for this file is owned +separately by the desktop log-drain work and is not changed here. + ## Provider drivers Six drivers ship built in, registered in [`builtInDrivers.ts`][drivers] as `BUILT_IN_DRIVERS`: @@ -158,6 +165,7 @@ already dispatch. [projector]: ../../apps/server/src/orchestration/projector.ts [worker]: ../../packages/shared/src/DrainableWorker.ts [ingest]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +[event-log]: ../../apps/server/src/provider/Layers/EventNdjsonLogger.ts [cmd]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts [checkpoint]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts [receipts]: ../../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 489bde1ac78a..841addfae78b 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -412,10 +412,24 @@ describe("provider enabled defaults", () => { expect(decoded.providers.claudeAgent.enabled).toBe(true); expect(decoded.providers.cursor.enabled).toBe(false); expect(decoded.providers.grok.enabled).toBe(false); + expect(decoded.providers.cursor.verboseProtocolLogging).toBe(false); + expect(decoded.providers.grok.verboseProtocolLogging).toBe(false); expect(decoded.providers.opencode.enabled).toBe(false); expect(decoded.providers.opencodeGo.enabled).toBe(true); }); + it("decodes ACP protocol logging opt-ins for Cursor and Grok", () => { + const decoded = decodeServerSettings({ + providers: { + cursor: { verboseProtocolLogging: true }, + grok: { verboseProtocolLogging: true }, + }, + }); + + expect(decoded.providers.cursor.verboseProtocolLogging).toBe(true); + expect(decoded.providers.grok.verboseProtocolLogging).toBe(true); + }); + it("derives per-driver defaults from the settings schemas", () => { expect(defaultEnabledForDriver(ProviderDriverKind.make("codex"))).toBe(true); expect(defaultEnabledForDriver(ProviderDriverKind.make("cursor"))).toBe(false); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index d534835e7162..b1d8ebf6289f 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -511,9 +511,17 @@ export const CursorSettings = makeProviderSettingsSchema( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), + verboseProtocolLogging: Schema.Boolean.pipe( + Schema.withDecodingDefault(Effect.succeed(false)), + Schema.annotateKey({ + title: "Verbose protocol logging", + description: "Record full ACP protocol diagnostics in provider event logs.", + providerSettingsForm: { control: "switch" }, + }), + ), }, { - order: ["binaryPath", "apiEndpoint"], + order: ["binaryPath", "apiEndpoint", "verboseProtocolLogging"], }, ); export type CursorSettings = typeof CursorSettings.Type; @@ -537,9 +545,17 @@ export const GrokSettings = makeProviderSettingsSchema( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), + verboseProtocolLogging: Schema.Boolean.pipe( + Schema.withDecodingDefault(Effect.succeed(false)), + Schema.annotateKey({ + title: "Verbose protocol logging", + description: "Record full ACP protocol diagnostics in provider event logs.", + providerSettingsForm: { control: "switch" }, + }), + ), }, { - order: ["binaryPath"], + order: ["binaryPath", "verboseProtocolLogging"], }, ); export type GrokSettings = typeof GrokSettings.Type; @@ -980,12 +996,14 @@ const CursorSettingsPatch = Schema.Struct({ binaryPath: Schema.optionalKey(TrimmedString), apiEndpoint: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(Schema.String)), + verboseProtocolLogging: Schema.optionalKey(Schema.Boolean), }); const GrokSettingsPatch = Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean), binaryPath: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(Schema.String)), + verboseProtocolLogging: Schema.optionalKey(Schema.Boolean), }); const KimiSettingsPatch = Schema.Struct({