diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 8a06dddeeec10..d0b8600768ca6 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -44,6 +44,7 @@ import { ProviderTurnId, RunAttemptId, RunId, + ScheduledTaskId, ThreadId, } from "@t3tools/contracts"; import { assert, describe, it } from "@effect/vitest"; @@ -2429,7 +2430,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { const makeWakeHarnessWithOptions = (options?: { readonly close?: (sdkMessages: Queue.Queue) => Effect.Effect; - readonly interrupt?: Effect.Effect; + readonly interrupt?: Effect.Effect; readonly stopTask?: ( taskId: string, ) => Effect.Effect; @@ -2476,6 +2477,10 @@ describe("ClaudeAdapterV2 background wake turns", () => { yield* Queue.unbounded< Extract >(); + const runtimeRequestReceipts = + yield* Queue.unbounded< + Extract + >(); const systemNoticeReceipts = yield* Queue.unbounded< Extract @@ -2581,6 +2586,9 @@ describe("ClaudeAdapterV2 background wake turns", () => { Stream.runForEach((event) => Effect.gen(function* () { events.push(event); + if (event.type === "runtime_request.updated") { + yield* Queue.offer(runtimeRequestReceipts, event); + } if (event.type === "subagent.updated") { yield* Queue.offer(subagentReceipts, event); } @@ -2626,6 +2634,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { terminalReceipts, subagentReceipts, systemNoticeReceipts, + runtimeRequestReceipts, getOpenedOptions: () => openedOptions, terminalEvents, hasPendingBackgroundWork, @@ -3210,6 +3219,270 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect.each([ + { sender: "agent", createdBy: "agent", scheduled: false }, + { sender: "scheduled task", createdBy: "user", scheduled: true }, + ] as const)( + "delivers the $sender steer without interrupting the open turn", + ({ createdBy, scheduled }) => + Effect.scoped( + Effect.gen(function* () { + const harness = yield* makeWakeHarnessWithOptions({ + interrupt: Effect.die("Automatic steering must not interrupt foreground work"), + }); + const idAllocator = yield* IdAllocator.IdAllocatorV2; + const attemptId = RunAttemptId.make("attempt-automatic-steer"); + const input = makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId, + text: "Scan the repository.", + attachments: [], + }); + yield* harness.runtime.startTurn(input); + const providerTurnId = idAllocator.derive.providerTurn({ + driver: ClaudeAdapterV2.CLAUDE_PROVIDER, + nativeTurnId: `turn:${attemptId}`, + }); + yield* harness.runtime.steerTurn({ + threadId: harness.threadId, + runId: input.runId, + providerThread: harness.providerThread, + providerTurnId, + message: { + createdBy, + creationSource: "server", + messageId: MessageId.make("message-automatic-steer"), + text: "Include the latest task result.", + attachments: [], + ...(scheduled ? { scheduledTaskId: ScheduledTaskId.make("scheduled-scan") } : {}), + }, + }); + assert.lengthOf(harness.offeredMessages, 2); + assert.equal(harness.offeredMessages[1]?.priority, "now"); + yield* Queue.offer(harness.sdkMessages, wakeResult); + const terminal = yield* Queue.take(harness.terminalReceipts); + assert.equal(terminal.providerTurnId, providerTurnId); + assert.equal(terminal.status, "completed"); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + + it.effect( + "offers after interrupt failure but rejects a steer whose turn ends during interrupt", + () => + Effect.scoped( + Effect.gen(function* () { + let interrupts = 0; + const interruptStarted = yield* Deferred.make(); + const releaseInterrupt = yield* Deferred.make(); + const harness = yield* makeWakeHarnessWithOptions({ + interrupt: Effect.suspend(() => { + interrupts++; + return interrupts === 1 + ? Effect.fail( + new ClaudeAdapterV2.ClaudeAgentSdkQueryRunnerError({ + method: "interrupt", + cause: "forced interrupt failure", + }), + ) + : Deferred.succeed(interruptStarted, undefined).pipe( + Effect.andThen(Deferred.await(releaseInterrupt)), + ); + }), + }); + const idAllocator = yield* IdAllocator.IdAllocatorV2; + const attemptId = RunAttemptId.make("attempt-steer-interrupt-failed"); + const input = makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId, + text: "Scan the repository.", + attachments: [], + }); + yield* harness.runtime.startTurn(input); + const providerTurnId = idAllocator.derive.providerTurn({ + driver: ClaudeAdapterV2.CLAUDE_PROVIDER, + nativeTurnId: `turn:${attemptId}`, + }); + const steer = harness.runtime.steerTurn({ + threadId: harness.threadId, + runId: input.runId, + providerThread: harness.providerThread, + providerTurnId, + message: { + createdBy: "user", + creationSource: "web", + messageId: MessageId.make("message-steer-interrupt-failed"), + text: "Stop waiting and summarize what you have.", + attachments: [], + }, + }); + yield* steer; + assert.equal(interrupts, 1); + assert.lengthOf(harness.offeredMessages, 2); + assert.equal(harness.offeredMessages[1]?.priority, "now"); + const steering = yield* Effect.forkScoped(steer); + yield* Deferred.await(interruptStarted); + assert.lengthOf(harness.offeredMessages, 2); + yield* harness.offerAndWait(wakeResult); + const terminal = yield* Queue.take(harness.terminalReceipts); + assert.equal(terminal.providerTurnId, providerTurnId); + assert.equal(terminal.status, "completed"); + yield* Deferred.succeed(releaseInterrupt, undefined); + const steered = yield* Fiber.await(steering); + assert.isTrue(Exit.isFailure(steered)); + assert.lengthOf(harness.offeredMessages, 2); + assert.equal(interrupts, 2); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + + it.effect( + "steers past a pending approval with a background subagent, then Stop closes its query", + () => + Effect.scoped( + Effect.gen(function* () { + const permissionAbort = new AbortController(); + const interruptStarted = yield* Deferred.make(); + const releaseInterrupt = yield* Deferred.make(); + let closes = 0; + const harness = yield* makeWakeHarnessWithOptions({ + interrupt: Effect.sync(() => permissionAbort.abort()).pipe( + Effect.andThen(Deferred.succeed(interruptStarted, undefined)), + Effect.andThen(Deferred.await(releaseInterrupt)), + ), + close: (messages) => + Effect.sync(() => { + closes++; + }).pipe(Effect.andThen(Queue.shutdown(messages))), + }); + const attemptId = RunAttemptId.make("attempt-steer-approval"); + const input = makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId, + text: "Audit while the subagent runs.", + attachments: [], + runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "approval-required", + interactionMode: "default", + cwd: "/workspace", + }), + }); + yield* harness.runtime.startTurn(input); + const idAllocator = yield* IdAllocator.IdAllocatorV2; + const providerTurnId = idAllocator.derive.providerTurn({ + driver: ClaudeAdapterV2.CLAUDE_PROVIDER, + nativeTurnId: `turn:${attemptId}`, + }); + yield* harness.offerAndWait( + makeSubagentTaskStartedFrame({ + taskId: "audit", + toolUseId: "audit-launch", + uuid: "audit-started", + }), + ); + const started = yield* Queue.take(harness.subagentReceipts); + assert.equal(started.subagent.status, "running"); + const permission = yield* Effect.promise(() => + harness.getOpenedOptions()!.canUseTool!( + "Bash", + { command: "git status" }, + { + signal: permissionAbort.signal, + toolUseID: "pending-bash", + requestId: "pending-approval", + }, + ), + ).pipe(Effect.forkScoped); + const request = yield* Queue.take(harness.runtimeRequestReceipts); + assert.equal(request.runtimeRequest.status, "pending"); + const steering = yield* harness.runtime + .steerTurn({ + threadId: harness.threadId, + runId: input.runId, + providerThread: harness.providerThread, + providerTurnId, + message: { + createdBy: "user", + creationSource: "web", + messageId: MessageId.make("steer-pending-approval"), + text: "Skip that command and summarize.", + attachments: [], + }, + }) + .pipe(Effect.forkScoped); + yield* Deferred.await(interruptStarted); + assert.lengthOf(harness.offeredMessages, 1, "the interrupt must finish before the offer"); + assert.equal((yield* Fiber.join(permission))?.behavior, "deny"); + yield* harness.offerAndWait( + makeResultFrame({ + uuid: "approval-aborted", + result: "", + terminalReason: "aborted_tools", + }), + ); + assert.lengthOf( + harness.terminalEvents(), + 0, + "the interrupted result must leave the turn open", + ); + yield* Deferred.succeed(releaseInterrupt, undefined); + yield* Fiber.join(steering); + assert.equal(harness.offeredMessages[1]?.priority, "now"); + assert.equal(closes, 0); + assert.isTrue(harness.getOpenedOptions()!.perTaskStopAffordance); + yield* harness.offerAndWait( + makeSubagentAssistantFrames({ + parentToolUseId: "audit-launch", + uuid: "audit-after-steer", + text: "Still auditing.", + })[0]!, + ); + yield* Queue.offer( + harness.sdkMessages, + makeResultFrame({ + uuid: "steer-completed", + result: "Skipped the command.", + }), + ); + const terminal = yield* Queue.take(harness.terminalReceipts); + assert.equal(terminal.providerTurnId, providerTurnId); + assert.equal(terminal.status, "completed"); + const routing = subagentRouting(harness.events, []); + assert.deepEqual(routing.assistantTexts(started.subagent.childThreadId ?? undefined), [ + "Still auditing.", + ]); + assert.lengthOf(harness.terminalEvents(), 1); + assert.equal(harness.processQueues.length, 1); + + assert.isTrue(yield* harness.hasPendingBackgroundWork); + yield* harness.runtime.interruptTurn({ + providerThread: harness.providerThread, + providerTurnId, + requestRuntimeRestart: true, + }); + assert.equal(closes, 1); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + it.effect("announces usage-limit pauses once per window and again on a new turn", () => Effect.gen(function* () { const harness = yield* makeWakeHarness; @@ -15175,8 +15448,15 @@ describe("ClaudeAdapterV2 background wake turns", () => { const NESTED_TOOL_USE_ID = "toolu-steer-nested"; const FINISHED_TOOL_USE_ID = "toolu-steer-finished"; const calls: Array = []; + const offersAtInterrupt: number[] = []; + let backgroundAnswered = false; let beforeAnswer: Effect.Effect = Effect.void; const harness = yield* makeWakeHarnessWithOptions({ + interrupt: Effect.sync(() => { + assert.deepEqual(calls, [FOREGROUND_TOOL_USE_ID]); + if (backgrounding === "succeeds") assert.isTrue(backgroundAnswered); + offersAtInterrupt.push(harness.offeredMessages.length); + }), backgroundTasks: (toolUseId) => Effect.sync(() => calls.push(toolUseId)).pipe( Effect.andThen( @@ -15196,6 +15476,11 @@ describe("ClaudeAdapterV2 background wake turns", () => { ? Effect.sleep("6 seconds").pipe(Effect.as(true)) : Effect.succeed(true), ), + Effect.tap(() => + Effect.sync(() => { + backgroundAnswered = true; + }), + ), ), }); const idAllocator = yield* IdAllocator.IdAllocatorV2; @@ -15360,10 +15645,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { assert.deepEqual(calls, [FOREGROUND_TOOL_USE_ID]); if (backgrounding === "answers after the turn ended") { assert.isTrue(Exit.isFailure(steered)); + assert.deepEqual(offersAtInterrupt, []); assert.equal(harness.offeredMessages.length, offered); return; } assert.isTrue(Exit.isSuccess(steered)); + assert.deepEqual(offersAtInterrupt, [offered]); assert.equal(harness.offeredMessages.length, offered + 1); assert.equal(harness.offeredMessages.at(-1)?.priority, "now"); if (backgrounding === "never answers") { diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts index ec3ec5b052461..bd552a3184a63 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts @@ -1727,9 +1727,16 @@ async function recordClaudeActiveSteeringQuery(input: { promptQueue.offer(message); offeredPrompts.add(index); }; - const offerSteeringPrompts = () => { + // Like ClaudeAdapterV2.steerTurn, a user's steer interrupts the turn before it is offered. + const offerSteeringPrompts = async () => { for (let index = 1; index < input.prompts.length; index += 1) { if (!offeredPrompts.has(index)) { + input.entries.push({ + type: "expect_outbound", + label: `query.interrupt:${index}`, + frame: { type: "query.interrupt" }, + }); + await queryRuntime.interrupt(); offerPrompt(index, "now"); } } @@ -1796,7 +1803,7 @@ async function recordClaudeActiveSteeringQuery(input: { const iterator = queryRuntime[Symbol.asyncIterator](); try { offerPrompt(0); - offerSteeringPrompts(); + await offerSteeringPrompts(); const completed = await recordMessagesUntilTurnResults({ iterator, entries: input.entries, diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index be276af5f0edf..f4cca07afc215 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1191,6 +1191,9 @@ export function makeClaudeQueryOptions(input: { ...(input.settings?.binaryPath ? { pathToClaudeCodeExecutable: input.settings.binaryPath } : {}), + // Keep background subagents alive across interrupts; T3 exposes stop_task + // for individual subagents and closes the query on Stop. + perTaskStopAffordance: true, ...(input.environment === undefined ? {} : { env: input.environment }), ...(input.mcpServers === undefined ? {} : { mcpServers: input.mcpServers }), systemPrompt: { @@ -9066,6 +9069,31 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( next.add(turnInput.providerTurnId); return next; }); + // A now-priority offer alone waits for running tools. Interrupt first + // so user steers take effect promptly; automatic deliveries can wait. + if ( + turnInput.message.createdBy === "user" && + turnInput.message.scheduledTaskId === undefined + ) { + yield* existing.query.interrupt.pipe( + Effect.catch((cause) => + Effect.logWarning("orchestration-v2.claude-steer-interrupt-failed", { + providerTurnId: turnInput.providerTurnId, + cause, + }), + ), + ); + } + // Completion or Stop can release ownership while interrupt is pending. + if ( + (yield* Ref.get(activeTurn))?.providerTurnId !== turnInput.providerTurnId || + (yield* Ref.get(queryContext)) !== existing + ) { + return yield* new ProviderAdapter.ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Claude provider turn ${turnInput.providerTurnId} ended before the steer was sent.`, + }); + } yield* existing.query.offer(userMessage); }, (effect, turnInput) => diff --git a/apps/server/src/orchestration-v2/ClaudeAutomaticDelivery.integration.test.ts b/apps/server/src/orchestration-v2/ClaudeAutomaticDelivery.integration.test.ts index 9bec96d31daf1..52940c0a46b3d 100644 --- a/apps/server/src/orchestration-v2/ClaudeAutomaticDelivery.integration.test.ts +++ b/apps/server/src/orchestration-v2/ClaudeAutomaticDelivery.integration.test.ts @@ -100,7 +100,9 @@ const toolResult = (id: string, cancelled: boolean) => }, ], }, - ...(cancelled ? { tool_result_meta: [{ id, non_execution_kind: "cancelled" }] } : {}), + ...(cancelled && id !== "build" + ? { tool_result_meta: [{ id, non_execution_kind: "cancelled" }] } + : {}), }); const result = (uuid: string, aborted: boolean) => @@ -139,6 +141,9 @@ it.effect.each(deliveries)( const offers: SDKUserMessage[] = []; const nativeQueue: SDKUserMessage[] = []; const batchAbort = new AbortController(); + const commandAbort = new AbortController(); + let closes = 0; + const offersAtInterrupt: number[] = []; const adapter = yield* ClaudeAdapterV2.makeClaudeAdapterV2({ instanceId: modelSelection.instanceId, settings, @@ -155,6 +160,12 @@ it.effect.each(deliveries)( messages: Stream.fromQueue(sdkMessages), offer: (message) => Effect.sync(() => { + if (message.priority === "now") { + assert.isTrue( + commandAbort.signal.aborted, + "interrupt the foreground command before offering the steer", + ); + } offers.push(message); nativeQueue.push(message); // Claude Code 2.1.289's queue watcher aborts when a now-priority @@ -168,8 +179,16 @@ it.effect.each(deliveries)( setPermissionMode: () => Effect.void, backgroundTasks: () => Effect.succeed(false), stopTask: () => Effect.void, - interrupt: Effect.die("automatic delivery must never interrupt"), - close: Effect.void, + interrupt: + delivery === "user steering" || delivery === "promoted queued message" + ? Effect.sync(() => { + offersAtInterrupt.push(offers.length); + commandAbort.abort(); + }) + : Effect.die("automatic delivery must never interrupt"), + close: Effect.sync(() => { + closes++; + }), }), forkSession: () => Effect.die("unused"), subagentLaunchToolUseId: () => Effect.succeed(null), @@ -394,14 +413,17 @@ it.effect.each(deliveries)( const explicitSteer = delivery === "user steering" || delivery === "promoted queued message"; assert.equal(batchAbort.signal.aborted, explicitSteer); + assert.equal(commandAbort.signal.aborted, explicitSteer); + assert.equal(closes, 0); assert.equal(offers.length, explicitSteer ? 2 : 1); + assert.deepEqual(offersAtInterrupt, explicitSteer ? [1] : []); const finished = yield* watch( (event) => event.type === "run.updated" && event.payload.id === parent.id && event.payload.status === "waiting", ); - yield* Queue.offer(sdkMessages, toolResult("build", false)); + yield* Queue.offer(sdkMessages, toolResult("build", commandAbort.signal.aborted)); for (const id of statusIds) yield* Queue.offer(sdkMessages, toolResult(id, batchAbort.signal.aborted)); if (explicitSteer) yield* Queue.offer(sdkMessages, result("aborted", true)); @@ -432,6 +454,17 @@ it.effect.each(deliveries)( if (delivered !== null) yield* Fiber.join(delivered); } const after = yield* orchestrator.getThreadProjection(threadId); + assert.equal(closes, 0, "the steer must complete without closing the query"); + if (explicitSteer) { + assert.lengthOf( + after.runs.filter((run) => run.status !== "cancelled"), + 1, + ); + assert.lengthOf(after.providerTurns, 1); + assert.equal(after.providerTurns[0]?.status, "completed"); + } + const command = after.turnItems.find((item) => item.nativeItemRef?.nativeId === "build"); + assert.equal(command?.status, explicitSteer ? "failed" : "completed"); const reads = after.turnItems.filter((item) => statusIds.includes(item.nativeItemRef?.nativeId ?? ""), ); diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/claude_transcript.ndjson index a1cacf97d8002..499eca217d7d2 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/claude_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/claude_transcript.ndjson @@ -1,13 +1,21 @@ -{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"message_steering","metadata":{"prompts":["Respond with exactly: steering fixture initial response","Actually, respond with exactly: steering fixture observed"],"model":"claude-sonnet-4-6","nativeSessionId":"05f72f7d-81d5-4db9-a3cf-234191cf79f9","queryMode":"active_steering","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} -{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"05f72f7d-81d5-4db9-a3cf-234191cf79f9","settings":{"showThinkingSummaries":true}}}} +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"message_steering","metadata":{"prompts":["Respond with exactly: steering fixture initial response","Actually, respond with exactly: steering fixture observed"],"model":"claude-sonnet-4-6","nativeSessionId":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","queryMode":"active_steering","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","settings":{"showThinkingSummaries":true},"allowDangerouslySkipPermissions":true,"sessionId":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea"}}} {"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: steering fixture initial response"},"parent_tool_use_id":null}}} +{"type":"expect_outbound","label":"query.interrupt:1","frame":{"type":"query.interrupt"}} {"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Actually, respond with exactly: steering fixture observed"},"parent_tool_use_id":null,"priority":"now"}}} -{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"2961831d-241e-47c4-8099-b8779c6b3878","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"d45438eb-d485-490f-9668-a02c5721a6b3","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}} -{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"2961831d-241e-47c4-8099-b8779c6b3878","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"40ef0c52-1168-4049-99af-4c3beb507994","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}} -{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-message_steering","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"71579637-9bc5-4182-b104-4ebb8f2bda0d","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}} -{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"error_during_execution","duration_ms":747,"duration_api_ms":0,"is_error":true,"num_turns":1,"stop_reason":null,"session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"terminal_reason":"aborted_streaming","fast_mode_state":"off","uuid":"781fa8c7-98dd-4679-bb99-82d4c77c9b0c","errors":["[ede_diagnostic] result_type=user last_content_type=n/a stop_reason=null","Error: Request was aborted.\n at vn8 (/tmp/claude-replay-message_steering/node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.111+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/cli.js:5550:1432)\n at next (native:1:11)\n at a85 (/tmp/claude-replay-message_steering/node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.111+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/cli.js:8321:5086)\n at processTicksAndRejections (native:7:39)"]}} -{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-message_steering","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"60494b15-57a4-417d-a0d0-0ce42590c36e","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}} -{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01Cjp5MnmN6F7wwrzNss9zVW","type":"message","role":"assistant","content":[{"type":"text","text":"steering fixture observed"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2347,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2347},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9","uuid":"2faa9bee-1c72-4b8f-ab04-9796a7ecf6a2"}} -{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"b0bfc7e0-1698-4480-821d-16262ceb4448","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}} -{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":3836,"duration_api_ms":1808,"num_turns":1,"result":"steering fixture observed","stop_reason":"end_turn","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9","total_cost_usd":0.01173675,"usage":{"input_tokens":3,"cache_creation_input_tokens":2347,"cache_read_input_tokens":9455,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2347,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":9455,"cache_creation_input_tokens":2347,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2347},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":9455,"cacheCreationInputTokens":2347,"webSearchRequests":0,"costUSD":0.01173675,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"7ac0393a-c862-449c-95f7-bbf391264476"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.289","cwd":"/tmp/claude-replay-message_steering","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"4a998955-9210-4073-98ae-c8b023ac1dd2","session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"status","status":"requesting","session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","uuid":"6cd1afa2-f7f5-4bc3-bd6a-641629d5d47b"}} +{"type":"emit_inbound","label":"result","frame":{"duration_api_ms":0,"stop_reason":null,"session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","total_cost_usd":0,"usage":{"output_tokens_details":{"thinking_tokens":0},"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard","fallback_credit":null},"modelUsage":{},"permission_denials":[],"terminal_reason":"aborted_streaming","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subagent_stats":{"spawned":0,"requested":{"background":0,"foreground":0,"unset":0},"started_in_background":0,"max_depth":0,"spawned_by_subagents":0,"completed":0,"failed":0,"killed":{"parent":0,"user":0,"system":0},"refused":{"depth_limit":0,"concurrency_limit":0,"budget":0},"by_type":{}},"is_error":true,"num_turns":1,"subtype":"error_during_execution","errors":["[ede_diagnostic] result_type=user last_content_type=n/a stop_reason=null"],"type":"result","duration_ms":38,"uuid":"b7d2147d-b68d-4dd0-af3b-0a7d4bb6886b","queued_turn_count":0,"result_index":0}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.289","cwd":"/tmp/claude-replay-message_steering","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"c132f543-cb7e-4b08-b51b-a65711733396","session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"status","status":"requesting","session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","uuid":"c370b16e-fa48-44fd-9e02-ba1bd1218546"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"33b5e760-986a-4114-9da7-c58b307e0d5c","session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_start","message":{"model":"claude-sonnet-4-6","id":"msg_011Cfhhu3DL1j9kN4t1K48eU","type":"message","role":"assistant","content":[],"container":null,"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":6269,"cache_read_input_tokens":13494,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":6269},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"input_transformations":[],"diagnostics":null,"context_management":null}},"session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","parent_tool_use_id":null,"uuid":"81cad35b-4fb7-4d66-b326-1399bef42d36","ttft_ms":1679,"thinking_display":"summarized"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}},"session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","parent_tool_use_id":null,"uuid":"c3a59149-5659-4679-98a5-29822e8b6e1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"steering"}},"session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","parent_tool_use_id":null,"uuid":"da145a47-bd21-4308-b670-9baf79734d94"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" fixture observed"}},"session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","parent_tool_use_id":null,"uuid":"0db9e148-24fb-4500-a76f-ad0d7ab666bc"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_011Cfhhu3DL1j9kN4t1K48eU","type":"message","role":"assistant","content":[{"type":"text","text":"steering fixture observed"}],"container":null,"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":6269,"cache_read_input_tokens":13494,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":6269},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"input_transformations":[],"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","uuid":"9807df62-d3dc-4f1b-b309-50928a7005ca","timestamp":"2026-10-04T18:55:31.240Z","request_id":"req_011Cfhhu2xhzdhfAcDbhJ8yP"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_stop","index":0},"session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","parent_tool_use_id":null,"uuid":"704fbe60-a9bd-4791-b2be-766d71812dc3"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"container":null},"usage":{"input_tokens":3,"cache_creation_input_tokens":6269,"cache_read_input_tokens":13494,"output_tokens":6,"output_tokens_details":{"thinking_tokens":0},"iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":13494,"cache_creation_input_tokens":6269,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":6269},"type":"message"}]},"context_management":{"applied_edits":[]}},"session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","parent_tool_use_id":null,"uuid":"440a8b26-2f0a-4687-9b13-e069936e4ffc"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_stop"},"session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","parent_tool_use_id":null,"uuid":"02386a50-c2f1-44e9-9c42-3a9800227e96"}} +{"type":"emit_inbound","label":"result","frame":{"duration_api_ms":2925,"stop_reason":"end_turn","session_id":"780a3b17-ae3f-4e71-9594-8ae6f4fa7cea","total_cost_usd":0.0427262,"usage":{"input_tokens":3,"cache_creation_input_tokens":6269,"cache_read_input_tokens":13494,"output_tokens":6,"output_tokens_details":{"thinking_tokens":0},"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":6269,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":13494,"cache_creation_input_tokens":6269,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":6269},"type":"message"}],"speed":"standard","fallback_credit":null},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":885,"outputTokens":16,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.000965,"contextWindow":200000,"maxOutputTokens":32000,"thinkingTokens":0,"canonicalModel":"claude-haiku-4-5","provider":"firstParty","costBasis":"list"},"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":13494,"cacheCreationInputTokens":6269,"webSearchRequests":0,"costUSD":0.0417612,"contextWindow":200000,"maxOutputTokens":32000,"thinkingTokens":0,"canonicalModel":"claude-sonnet-4-6","provider":"firstParty","costBasis":"list"}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subagent_stats":{"spawned":0,"requested":{"background":0,"foreground":0,"unset":0},"started_in_background":0,"max_depth":0,"spawned_by_subagents":0,"completed":0,"failed":0,"killed":{"parent":0,"user":0,"system":0},"refused":{"depth_limit":0,"concurrency_limit":0,"budget":0},"by_type":{}},"is_error":false,"num_turns":1,"subtype":"success","api_error_status":null,"result":"steering fixture observed","ttft_ms":2047,"type":"result","duration_ms":2079,"uuid":"cbece92a-7f08-40a7-b26f-e9f1fc7437ff","ttft_stream_ms":1703,"time_to_request_ms":24,"first_content_frame_ms":1704,"queued_turn_count":0,"result_index":1}} {"type":"runtime_exit","status":"success"} diff --git a/docs/user/providers-claude.md b/docs/user/providers-claude.md index 514673beb268f..5c898544d845d 100644 --- a/docs/user/providers-claude.md +++ b/docs/user/providers-claude.md @@ -57,6 +57,13 @@ history for that message. The cache lasts about an hour on a Claude subscription and 5 minutes with an API key, Bedrock, or Vertex. See [commands and skills](./composer.md#commands-and-skills) for using composer commands. +## Steer a running turn + +A [steer](./composer.md#send-while-the-agent-is-working) interrupts Claude's running +command so Claude can read your message promptly. Foreground subagents move to the +background first so they can continue, and background commands and subagents keep +running. Queue the message instead if the current command should finish first. + ## Usage limits If your Claude subscription runs out of usage mid-turn, the thread shows which