diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 2c0d13aa0b32..17bf4ac5b2a3 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -3686,6 +3686,78 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect("shows a hook-dropped Claude prompt once and skips info-level lines", () => + Effect.gen(function* () { + const harness = yield* makeWakeHarness; + const now = yield* DateTime.now; + yield* harness.runtime.startTurn( + makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now, + attemptId: RunAttemptId.make("claude-hook-dropped-prompt-attempt"), + text: "Deploy to production", + attachments: [], + }), + ); + const notice = "Prompt dropped by a hook: Deploys are frozen until Monday."; + const uuid = "00000000-0000-4000-8000-000000000305"; + yield* Queue.offer( + harness.sdkMessages, + claudeSdkFrame({ + type: "system", + subtype: "informational", + content: "Started in the background: agent-1", + level: "info", + session_id: WAKE_NATIVE_SESSION, + uuid: "00000000-0000-4000-8000-000000000307", + }), + ); + yield* Queue.offer( + harness.sdkMessages, + claudeSdkFrame({ + type: "system", + subtype: "informational", + content: notice, + level: "warning", + prevent_continuation: true, + session_id: WAKE_NATIVE_SESSION, + uuid, + }), + ); + yield* Queue.offer( + harness.sdkMessages, + makeResultFrame({ + uuid: "00000000-0000-4000-8000-000000000306", + result: notice, + numTurns: 0, + terminalReason: null, + }), + ); + yield* Queue.take(harness.terminalReceipts); + const notices = harness.events.flatMap((event) => + event.type === "turn_item.updated" && event.turnItem.type === "system_notice" + ? [event.turnItem] + : [], + ); + assert.lengthOf(notices, 1); + assert.equal(notices[0]?.message, notice); + assert.equal(notices[0]?.status, "completed"); + assert.equal(notices[0]?.nativeItemRef?.nativeId, uuid); + assert.equal(harness.terminalEvents()[0]?.status, "completed"); + assert.isFalse( + harness.events.some( + (event) => event.type === "message.updated" && event.message.role === "assistant", + ), + ); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll(NodeServices.layer, IdAllocator.layer, McpProviderSessions.layer), + ), + ), + ); + it.effect( "runs native compaction and keeps its context watermark separate from billed usage", () => diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 0a289a522663..c856ee8835c6 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -2971,6 +2971,8 @@ interface ActiveClaudeTurnContext { fallbackText: string; fallbackNativeItemId: string; emittedNativeItemIds: Set; + // The CLI repeats a notice that ends a prompt as the turn's result text. + noticeTexts: Set; }; readonly reasoning: { messageId: string | null; @@ -6770,9 +6772,14 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( return; } - if (message.type === "system" && message.subtype === "model_refusal_fallback") { + if ( + message.type === "system" && + (message.subtype === "model_refusal_fallback" || + (message.subtype === "informational" && message.level !== "info")) + ) { const now = yield* DateTime.now; const nativeItemId = message.uuid; + context.assistant.noticeTexts.add(message.content); yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, @@ -7451,7 +7458,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( context.assistant.emittedNativeItemIds.size === 0 && context.assistant.fallbackText.length === 0 && resultText !== null && - resultText.text.length > 0 + resultText.text.length > 0 && + !context.assistant.noticeTexts.has(resultText.text) ) { context.assistant.fallbackText = resultText.text; context.assistant.fallbackNativeItemId = resultText.nativeItemId; @@ -8309,6 +8317,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( fallbackText: "", fallbackNativeItemId: `assistant:${turnInput.runId}`, emittedNativeItemIds: new Set(), + noticeTexts: new Set(), }, reasoning: { messageId: null,