import * as DocumentText from "../../provider/DocumentText.ts"; import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts"; import { claudeTaskOutputRoot, tailClaudeTaskOutput } from "./ClaudeTaskOutputTail.ts"; import { dynamicToolTitle, formatReadToolLabel, formatSearchToolLabel, } from "@t3tools/shared/toolActivity"; import { field, text } from "../unknownField.ts"; import { mergeWorkflowAgentMetadata, type WorkflowAgentMetadata } from "../workflowAgentAnswers.ts"; import { initialWorkflowAgentTranscriptCursor, readWorkflowAgentTranscript, type WorkflowAgentTranscriptCursor, } from "../workflowAgentTranscript.ts"; import { CLAUDE_WORKFLOW_TASK_TYPE, hasClaudeWorkflowSnapshot, mergeClaudeWorkflowProgress, parseClaudeWorkflowRunHandles, } from "./claudeWorkflowProgress.ts"; import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview"; import { addClaudeLedgerUsage, claudeLedgerDelta, claudeModelLedger, claudeReportedContextWindows, normalizeClaudeTurnTokenUsage, type ClaudeLedgerUsage, type ClaudeModelLedger, } from "../../provider/ClaudeTurnTokenUsage.ts"; import { toMcpElicitationResponse } from "../../provider/CodexMcpElicitation.ts"; import { type CanUseTool, type ElicitationRequest, type ElicitationResult, forkSession as forkClaudeSession, type EffortLevel, type ForkSessionOptions, type ForkSessionResult, getSessionMessages, getSubagentMessages, type HookCallback, query, resolveSettings as resolveClaudeSettings, type Options as ClaudeQueryOptions, type PermissionMode, type PermissionResult, type PermissionUpdate, type Query as ClaudeQuery, type Settings as ClaudeSdkSettings, type SpawnOptions as ClaudeSpawnOptions, type SpawnedProcess as ClaudeSpawnedProcess, type SDKAssistantMessage, type SDKAPIRetryMessage, type SDKMessage, type SDKRateLimitInfo, type SDKResultMessage, type SDKUserMessage, } from "@anthropic-ai/claude-agent-sdk"; import type { AskUserQuestionInput, WebSearchOutput, } from "@anthropic-ai/claude-agent-sdk/sdk-tools"; // @effect-diagnostics-next-line nodeBuiltinImport:off -- the SDK takes a Node ChildProcess for the CLI. import * as NodeChildProcess from "node:child_process"; import * as NodeUtil from "node:util"; import { parseCliArgs } from "@t3tools/shared/cliArgs"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; import { AgentScope } from "@t3tools/shared/AgentScope"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model"; import { CLAUDE_RESUME_COMPACTION_COMPACT_ANSWER, CLAUDE_RESUME_COMPACTION_NEVER_ANSWER, formatClaudeResumeCompactionQuestion, } from "@t3tools/shared/claudeCompaction"; import { type ChatAttachment, PROVIDER_SEND_TURN_MAX_FILE_BYTES, ClaudeSettings, TrimmedNonEmptyString, defaultInstanceIdForDriver, isCompactionCommandMessage, type ModelSelection, type OrchestrationV2ConversationMessage, type OrchestrationV2ExecutionNode, type OrchestrationV2ProviderCapabilities, type OrchestrationV2ProviderFailure, OrchestrationV2ProviderGoal, type OrchestrationV2PlanArtifact, type OrchestrationV2PlanStep, type OrchestrationV2PendingBackgroundTask, type OrchestrationV2ProviderRetry, type OrchestrationV2ProviderSession, type OrchestrationV2ProviderThread, type OrchestrationV2ProviderTurn, type OrchestrationV2RuntimeRequest, type OrchestrationV2UserInputQuestion, type OrchestrationV2Subagent, type OrchestrationV2AppThread, type OrchestrationV2SubagentWorkflow, type OrchestrationV2WorkflowRunHandles, type OrchestrationV2TurnItem, type OrchestrationV2WebSearchResult, type ProviderApprovalDecision, type ProviderApprovalFullAccessOverride, type ProviderApprovalOption, ProviderDriverKind, type ProviderInstanceId, type ProviderRequestKind, type ProviderUserInputAnswers, type ProviderThreadId, type RunId, type ThreadId, resolveIdleCompaction, } from "@t3tools/contracts"; import { FILE_HEADERS_ONLY, formatPatch, structuredPatch } from "diff"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as SecureRandom from "effect/crypto/SecureRandom"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Equal from "effect/Equal"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { resolveClaudeSdkExecutablePath } from "../../provider/Drivers/ClaudeExecutable.ts"; import { planClaudeSkillDispatch, type ClaudeSkillTarget, } from "../../provider/Drivers/ClaudeSkillDispatch.ts"; import { claudeLaunchArgDirectories, discoverClaudeSkills, } from "../../provider/Drivers/ClaudeSkills.ts"; import { compileClaudeModelSelection, type CompiledClaudeModelSelection, } from "../../claudeModelOptions.ts"; import * as ServerConfig from "../../config.ts"; import * as PortScanner from "../../preview/PortScanner.ts"; import * as ServerSettings from "../../serverSettings.ts"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import { spawnAndCollect } from "@t3tools/provider-core/server/snapshotProbe"; import { signalProcessGroup } from "@t3tools/provider-core/server/processGroup"; import { claudeSignedOutMessage, makeClaudeEnvironment, mergeClaudeProcessEnvironment, resolveClaudeHomePath, } from "../../provider/Drivers/ClaudeHome.ts"; import { BUNDLED_CLAUDE_MODEL_CATALOG, resolveClaudeCatalogContextWindowTokens, type ClaudeModelCatalog, getClaudeCatalogModelCapabilities, resolveClaudeModelSlug, scopeClaudeModelCatalog, } from "../../provider/ClaudeModelCatalog.ts"; import { boundProviderEventForLogging, shouldPersistProviderEvent, } from "../../provider/EventNdjsonLogger.ts"; import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers"; import { claudeRateLimitEventToUpdate, type ClaudeScopedLimitNames, } from "../../provider/claudeUsageLimits.ts"; import type { ManagedServerProvider } from "@t3tools/provider-core/server/snapshot"; import { T3_CODE_DEVICE_TOOL_INSTRUCTIONS_WITH_TOOL_SEARCH, T3_CODE_ORCHESTRATION_INSTRUCTIONS, } from "@t3tools/provider-core/server/orchestrationInstructions"; import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions"; import { mcpToolPresentation, normalizeMcpText, } from "@t3tools/provider-core/server/mcpToolPresentation"; import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession"; import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import { makeProviderFailure, makeProviderRetryTurnItem, } from "@t3tools/provider-core/server/failure"; import { turnScopedSelectionTransition } from "@t3tools/provider-core/server/selectionTransition"; import { providerMessageTextWithAttachmentPaths } from "@t3tools/provider-core/server/attachmentPrompt"; import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; import { ProviderAdapterDriverCreateError, type ProviderAdapterDriverCreateInput, } from "@t3tools/provider-core/server/adapterDriver"; import { type BackgroundWorkReport, backgroundWorkNotification, } from "@t3tools/provider-core/server/notification"; import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests"; import { makeSubagentChildThread, makeSubagentConversationArtifacts, subagentThreadTitle, } from "@t3tools/provider-core/server/subagentProjection"; export const CLAUDE_PROVIDER = ProviderDriverKind.make("claudeAgent"); const CLAUDE_AGENT_SDK_QUERY_PROTOCOL = "claude-agent-sdk.query" as const; interface ClaudeCacheCreation { readonly ephemeral_1h_input_tokens: number; readonly ephemeral_5m_input_tokens: number; } function claudePromptCacheTtlMs( cacheCreation: ClaudeCacheCreation | null | undefined, ): number | undefined { if (!cacheCreation) return undefined; if (cacheCreation.ephemeral_1h_input_tokens > 0) return 60 * 60_000; if (cacheCreation.ephemeral_5m_input_tokens > 0) return 5 * 60_000; return undefined; } // Claude caps the window at the model's context, so a setting at or above it never applies. // Below that, Claude compacts 33,000 tokens short of the window: Claude Code 2.1.305's // getContextUsage reports a 150,000 window as a 117,000 threshold, for every catalog model. const CLAUDE_AUTO_COMPACT_RESERVE_TOKENS = 33_000; function claudeAutoCompactThreshold(autoCompactWindow: string, maxTokens: number | null) { const windowTokens = Number(autoCompactWindow); return windowTokens > 0 && (maxTokens === null || windowTokens < maxTokens) ? { autoCompactThreshold: windowTokens - CLAUDE_AUTO_COMPACT_RESERVE_TOKENS } : {}; } function claudeMessageInputTokens(usage: { readonly input_tokens: number; readonly cache_creation_input_tokens?: number | null; readonly cache_read_input_tokens?: number | null; }): number { return ( usage.input_tokens + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) ); } export function claudeProviderTurnTokenUsage( usage: { readonly input_tokens: number; readonly cache_creation_input_tokens?: number | null; readonly cache_read_input_tokens?: number | null; readonly cache_creation?: ClaudeCacheCreation | null; readonly output_tokens: number; }, maxTokens: number | null, updatedAt: string, autoCompactWindow = "", requestedAt?: DateTime.Utc, ) { const inputTokens = claudeMessageInputTokens(usage); const outputTokens = usage.output_tokens; const promptCacheTtlMs = claudePromptCacheTtlMs(usage.cache_creation); // The cache lifetime starts with the request, not when its response ends. const promptCacheExpiresAt = promptCacheTtlMs === undefined || requestedAt === undefined ? undefined : DateTime.formatIso(DateTime.add(requestedAt, { milliseconds: promptCacheTtlMs })); return { usedTokens: inputTokens + outputTokens, maxTokens, inputTokens, cachedInputTokens: usage.cache_read_input_tokens ?? 0, outputTokens, reasoningOutputTokens: 0, ...(promptCacheTtlMs === undefined ? {} : { promptCacheTtlMs }), ...claudeAutoCompactThreshold(autoCompactWindow, maxTokens), ...(promptCacheExpiresAt === undefined ? {} : { promptCacheExpiresAt }), updatedAt, }; } type ClaudeMessageUsage = Parameters[0]; // message_delta usage is per-message cumulative; null or absent fields retain message_start values and zeros replace them. function mergeClaudeStreamUsage( current: ClaudeMessageUsage, delta: { readonly input_tokens?: number | null; readonly cache_creation_input_tokens?: number | null; readonly cache_read_input_tokens?: number | null; readonly output_tokens: number; }, ): ClaudeMessageUsage { return { ...current, input_tokens: delta.input_tokens ?? current.input_tokens, cache_creation_input_tokens: delta.cache_creation_input_tokens ?? current.cache_creation_input_tokens ?? null, cache_read_input_tokens: delta.cache_read_input_tokens ?? current.cache_read_input_tokens ?? null, output_tokens: delta.output_tokens, }; } export const CLAUDE_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(CLAUDE_PROVIDER); const CLAUDE_SCRIPT_EXTENSIONS = [".js", ".mjs", ".tsx", ".ts", ".jsx"]; // The SDK has no control-request deadline; the CLI waits up to 30 s per MCP server. const CLAUDE_MCP_REGISTRATION_TIMEOUT = "90 seconds"; // Claude can consume a subagent result before the launching turn ends, leaving // its notification with no native wake turn to answer the continuation. const CLAUDE_WAKE_TURN_START_DEADLINE = "2 minutes"; // Claude Code flushes transcript writes every 100 ms, so a workflow member's // reply can land after the progress frame that settles it. Its transcript // stays the source of its answer this long after it settles. const WORKFLOW_MEMBER_TRANSCRIPT_FLUSH_MS = 500; // A steer waits at most this long, from its request, for its foreground // subagents to move to the background before it is sent. const BACKGROUND_MOVE_TIMEOUT_MS = 5_000; // How long a Stop waits for the CLI to exit before the user is offered Force stop. const CLAUDE_STOP_CONFIRM_TIMEOUT_MS = 10_000; // Matches the stderr tail the SDK keeps for a CLI it spawns itself. const CLAUDE_STDERR_TAIL_LENGTH = 2_048; // The process group is killed on exit, so its stderr closes promptly. const CLAUDE_STDERR_DRAIN_TIMEOUT = Duration.seconds(1); // Claude Code's refusal of bypassPermissions as root, which it explains only on stderr. const CLAUDE_ROOT_REFUSAL = "--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons"; const CLAUDE_GROUP_WATCH_SCRIPT = [ "{ trap '' TERM; read -r _ <&3; kill -TERM 0; sleep 5; kill -KILL 0; } /dev/null 2>&1 &", 'exec "$@" 3<&-', ].join("\n"); /** * Kill the CLI's group on exit to stop MCP grandchildren; `onExit` stops its agent scope for * setsid daemons. The CLI keeps working after its pipes close, so a watcher stops the group on * EOF from a pipe only this server holds, including when the server crashes. */ export function spawnClaudeProcessGroup( options: ClaudeSpawnOptions, onExit?: () => void, onStderr: (text: string) => void = () => {}, onStderrClose: () => void = () => {}, ): ClaudeSpawnedProcess { const child = NodeChildProcess.spawn( "/bin/sh", ["-c", CLAUDE_GROUP_WATCH_SCRIPT, "t3-claude", options.command, ...options.args], { cwd: options.cwd, env: options.env, signal: options.signal, stdio: ["pipe", "pipe", "pipe", "pipe"], detached: true, }, ); // Always drained, so a chatty CLI never blocks on a full stderr pipe. child.stderr?.setEncoding("utf8").on("data", onStderr).once("close", onStderrClose); child.once("exit", () => { try { signalProcessGroup(child.pid ?? 0, "SIGKILL"); } catch {} onExit?.(); }); // Node's typings make stdin and stdout nullable once stdio has a fourth entry; both are pipes. return child as ClaudeSpawnedProcess; } export const ClaudeProviderCapabilitiesV2 = { sessions: { supportsMultipleProviderThreadsPerSession: false, supportsModelSwitchInSession: true, supportsProviderSwitchingViaHandoff: true, supportsRuntimeModeSwitchInSession: false, pendingRequestsSurviveRestart: false, }, threads: { canCreateEmptyThread: true, canReadThreadSnapshot: false, canRollbackThread: true, canForkThread: true, canForkFromTurn: true, canForkFromSubagentThread: false, exposesNativeThreadId: true, }, turns: { exposesNativeTurnId: false, emitsTurnStarted: true, emitsTurnCompleted: true, supportsInterrupt: true, supportsActiveSteering: true, activeSteeringInterruptsTools: true, supportsSteeringByInterruptRestart: false, supportsQueuedMessages: true, terminalStatusQuality: "strong", }, streaming: { streamsAssistantText: true, streamsReasoning: true, streamsToolOutput: false, streamsPlanText: false, emitsMessageCompleted: true, }, tools: { exposesToolItemIds: true, emitsToolStarted: true, emitsToolCompleted: true, emitsToolOutput: true, supportsMcpTools: true, supportsDynamicToolCallbacks: true, }, approvals: { supportsCommandApproval: true, supportsFileReadApproval: true, supportsFileChangeApproval: true, supportsApplyPatchApproval: false, approvalsHaveNativeRequestIds: true, approvalCallbacksAreLiveOnly: true, approvalsCanOriginateFromSubagents: false, }, planning: { emitsPlanUpdated: true, emitsTodoList: true, emitsProposedPlan: true, supportsStructuredQuestions: true, planDeltasHaveItemIds: false, }, subagents: { supportsSubagents: true, exposesSubagentThreadIds: false, emitsSubagentLifecycle: true, canWaitForSubagents: false, canCloseSubagents: false, canForkSubagentThread: false, }, context: { acceptsSystemContext: true, acceptsDeveloperContext: true, acceptsSyntheticUserContext: true, canGenerateSummaries: true, canConsumeHandoffSummaries: true, supportsDeltaHandoff: true, supportsFullThreadHandoff: true, maxRecommendedHandoffChars: null, }, checkpointing: { appCanCheckpointFilesystem: true, supportsNestedCheckpointScopes: true, providerCanRollbackConversation: true, providerRollbackReturnsSnapshot: true, providerCanReadConversationSnapshot: false, }, identity: { nativeThreadIds: "strong", nativeTurnIds: "weak", nativeItemIds: "strong", nativeRequestIds: "strong", }, runtimePolicy: { enforcement: "native", }, } satisfies OrchestrationV2ProviderCapabilities; const CLAUDE_CODE_PRESET_TOOLS = { type: "preset", preset: "claude_code", } satisfies NonNullable; export type ClaudeAgentSdkQueryToolList = ReadonlyArray; export interface ClaudeAgentSdkQueryPresetTools { readonly type: "preset"; readonly preset: "claude_code"; } export type ClaudeAgentSdkQueryTools = ClaudeAgentSdkQueryToolList | ClaudeAgentSdkQueryPresetTools; export const CLAUDE_READ_ONLY_ALLOWED_TOOLS = ["Read", "Glob", "Grep"] as const; function claudeAgentSdkQueryToolsForSdk( tools: ClaudeAgentSdkQueryTools, ): NonNullable { if (isClaudeAgentSdkQueryToolList(tools)) { return [...tools]; } return { type: tools.type, preset: tools.preset }; } function isClaudeAgentSdkQueryToolList( tools: ClaudeAgentSdkQueryTools, ): tools is ClaudeAgentSdkQueryToolList { return Array.isArray(tools); } type ClaudeAgentSdkThreadIdentity = | { readonly sessionId: string; readonly resume?: never; } | { readonly sessionId?: never; readonly resume: string; }; export type ClaudeAgentSdkQueryOptions = Omit< ClaudeQueryOptions, "maxTurns" | "model" | "permissionMode" | "resume" | "sessionId" | "tools" > & { readonly model: string; readonly tools: NonNullable; readonly permissionMode: NonNullable; } & ClaudeAgentSdkThreadIdentity; export interface ClaudeAgentSdkQueryOpenInput { readonly options: ClaudeAgentSdkQueryOptions; readonly threadId: ThreadId; readonly providerSessionId: OrchestrationV2ProviderSession["id"]; } export interface ClaudeAgentSdkQuerySession { readonly messages: Stream.Stream; readonly offer: ( message: SDKUserMessage, onQueued?: () => void, ) => Effect.Effect; readonly setModel: (model: string) => Effect.Effect; readonly applyFlagSettings: ( settings: ClaudeFlagSettings, ) => Effect.Effect; readonly setPermissionMode: ( mode: PermissionMode, ) => Effect.Effect; readonly stopTask: (taskId: string) => Effect.Effect; readonly backgroundTasks: ( toolUseId: string, ) => Effect.Effect; readonly interrupt: Effect.Effect; readonly close: Effect.Effect; } type ClaudeQueryStreamExit = Exit.Exit; export class ClaudeAgentSdkQueryRunnerError extends Schema.TaggedError()( "ClaudeAgentSdkQueryRunnerError", { method: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return "Claude Agent SDK query failed."; } } export class ClaudeCodeRootRefusalError extends Schema.TaggedError()( "ClaudeCodeRootRefusalError", { cause: Schema.Defect(), }, ) { override get message(): string { return `Claude Code exited before it started: ${CLAUDE_ROOT_REFUSAL}`; } } class ClaudeMcpRegistrationTimeoutError extends Schema.TaggedError()( "ClaudeMcpRegistrationTimeoutError", {}, ) { override get message(): string { return "Claude timed out while registering its MCP tools. Retry the turn; if it keeps failing, check Claude and the server logs."; } } export class ClaudeBackgroundWorkBlocksQueryReplacementError extends Schema.TaggedError()( "ClaudeBackgroundWorkBlocksQueryReplacementError", {}, ) { override get message(): string { return "Claude is still running background agents or commands, and this model or setting change would end them. Wait for them to finish, or press Stop, then send the message again."; } } export class ClaudeAppendSystemPromptFileError extends Schema.TaggedError()( "ClaudeAppendSystemPromptFileError", { path: Schema.String, cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return `Claude could not read the --append-system-prompt-file launch argument at ${this.path}. Fix the path in the Claude launch arguments, then send the message again.`; } } export class ClaudeAppendSystemPromptMissingValueError extends Schema.TaggedError()( "ClaudeAppendSystemPromptMissingValueError", { flag: Schema.Literals(["--append-system-prompt", "--append-system-prompt-file"]) }, ) { override get message(): string { return `The ${this.flag} launch argument has no ${this.flag === "--append-system-prompt" ? "text" : "file path"}. Add it after the flag in the Claude launch arguments, then send the message again.`; } } export class ClaudeSettingsLaunchArgError extends Schema.TaggedError()( "ClaudeSettingsLaunchArgError", { path: Schema.optional(Schema.String), cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return `Claude could not read the --settings launch argument${this.path === undefined ? "" : ` at ${this.path}`} as a JSON object. Fix it in the Claude launch arguments, then send the message again.`; } } export interface ClaudeAgentSdkQueryRunnerShape { readonly allocateSessionId: Effect.Effect; readonly open: ( input: ClaudeAgentSdkQueryOpenInput, ) => Effect.Effect; readonly forkSession: ( input: ClaudeAgentSdkSessionForkInput, ) => Effect.Effect< ForkSessionResult & { readonly headMessageId?: string }, ClaudeAgentSdkQueryRunnerError >; readonly subagentLaunchToolUseId: ( input: ClaudeAgentSdkSubagentLookupInput, ) => Effect.Effect; readonly assertComplete: Effect.Effect; } export class ClaudeAgentSdkQueryRunner extends Context.Service< ClaudeAgentSdkQueryRunner, ClaudeAgentSdkQueryRunnerShape >()("t3/orchestration-v2/Adapters/ClaudeAdapterV2/ClaudeAgentSdkQueryRunner") {} export interface ClaudeAgentSdkSessionForkInput { readonly sessionId: string; readonly options: ForkSessionOptions; readonly environment: NodeJS.ProcessEnv; readonly threadId: ThreadId; readonly providerSessionId: OrchestrationV2ProviderSession["id"]; } export interface ClaudeAgentSdkSubagentLookupInput { readonly sessionId: string; readonly agentId: string; readonly dir: string | null; readonly threadId: ThreadId; readonly providerSessionId: OrchestrationV2ProviderSession["id"]; } const isClaudeAgentSdkQueryRunnerError = Schema.is(ClaudeAgentSdkQueryRunnerError); const isClaudeMcpRegistrationTimeoutError = Schema.is(ClaudeMcpRegistrationTimeoutError); const isProviderAdapterRuntimeRequestResponseError = Schema.is( ProviderAdapter.ProviderAdapterRuntimeRequestResponseError, ); const isProviderAdapterRollbackThreadError = Schema.is( ProviderAdapter.ProviderAdapterRollbackThreadError, ); function queryRunnerError(cause: unknown, method: string): ClaudeAgentSdkQueryRunnerError { return isClaudeAgentSdkQueryRunnerError(cause) ? cause : new ClaudeAgentSdkQueryRunnerError({ cause, method }); } function closeClaudeQuery(queryRuntime: ClaudeQuery) { return Effect.try({ try: () => queryRuntime.close(), catch: (cause) => queryRunnerError(cause, "close"), }); } // Iterate Query itself: the raw generator's return() queues behind a pending CLI read and deadlocks interruption. export function claudeQueryMessages(queryRuntime: ClaudeQuery): AsyncIterable { return { [Symbol.asyncIterator]: () => queryRuntime }; } export interface ClaudeAgentSdkLoggedQueryOptions { readonly model: ClaudeAgentSdkQueryOptions["model"]; readonly tools: ClaudeAgentSdkQueryOptions["tools"]; readonly permissionMode: ClaudeAgentSdkQueryOptions["permissionMode"]; readonly sessionId?: string; readonly resume?: string; readonly resumeSessionAt?: ClaudeAgentSdkQueryOptions["resumeSessionAt"]; readonly cwd?: ClaudeAgentSdkQueryOptions["cwd"]; readonly allowedTools?: ClaudeAgentSdkQueryOptions["allowedTools"]; readonly disallowedTools?: ClaudeAgentSdkQueryOptions["disallowedTools"]; readonly settingKeys?: ReadonlyArray; readonly effort?: ClaudeAgentSdkQueryOptions["effort"]; readonly includePartialMessages?: true; readonly pathToClaudeCodeExecutable?: ClaudeAgentSdkQueryOptions["pathToClaudeCodeExecutable"]; readonly hasExtraArgs?: true; readonly allowDangerouslySkipPermissions?: true; readonly hasCanUseTool?: true; readonly hasEnvironment?: true; readonly hasMcpServers?: true; } export type ClaudeAgentSdkProtocolLogEvent = | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "query.open"; readonly options: ClaudeAgentSdkLoggedQueryOptions; }; } | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "prompt.offer"; readonly message: SDKUserMessage; }; } | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "query.set_model"; readonly model: string; }; } | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "query.apply_flag_settings"; readonly settings: ClaudeFlagSettings; }; } | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "query.set_permission_mode"; readonly mode: PermissionMode; }; } | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "query.stop_task"; readonly taskId: string; }; } | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "query.background_tasks"; readonly toolUseId: string; }; } | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "query.interrupt"; }; } | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "query.close"; }; } | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "session.fork"; readonly sessionId: string; readonly options: ForkSessionOptions; }; } | { readonly direction: "incoming"; readonly stage: "decoded"; readonly payload: { readonly type: "session.forked"; readonly sessionId: string; }; } | { readonly direction: "outgoing"; readonly stage: "decoded"; readonly payload: { readonly type: "subagent.lookup"; readonly sessionId: string; readonly agentId: string; }; } | { readonly direction: "incoming"; readonly stage: "decoded"; readonly payload: { readonly type: "subagent.found"; readonly toolUseId: string | null; }; } | { readonly direction: "incoming"; readonly stage: "decoded"; readonly payload: SDKMessage; }; export type ClaudeAgentSdkProtocolLogger = ( event: ClaudeAgentSdkProtocolLogEvent, ) => Effect.Effect; export function loggedClaudeQueryOptions( options: ClaudeAgentSdkQueryOptions, ): ClaudeAgentSdkLoggedQueryOptions { return { model: options.model, tools: options.tools, permissionMode: options.permissionMode, ...(options.sessionId === undefined ? {} : { sessionId: options.sessionId }), ...(options.resume === undefined ? {} : { resume: options.resume }), ...(options.resumeSessionAt === undefined ? {} : { resumeSessionAt: options.resumeSessionAt }), ...(options.cwd === undefined ? {} : { cwd: options.cwd }), ...(options.allowedTools === undefined ? {} : { allowedTools: options.allowedTools }), ...(options.disallowedTools === undefined ? {} : { disallowedTools: options.disallowedTools }), // Settings can carry the user's --settings values, such as an env block with credentials. ...(typeof options.settings === "object" && options.settings !== null ? { settingKeys: Object.keys(options.settings) } : {}), ...(options.effort === undefined ? {} : { effort: options.effort }), ...(options.includePartialMessages === true ? { includePartialMessages: true } : {}), ...(options.pathToClaudeCodeExecutable === undefined ? {} : { pathToClaudeCodeExecutable: options.pathToClaudeCodeExecutable }), ...(options.extraArgs === undefined ? {} : { hasExtraArgs: true }), ...(options.allowDangerouslySkipPermissions === true ? { allowDangerouslySkipPermissions: true } : {}), ...(options.canUseTool === undefined ? {} : { hasCanUseTool: true }), ...(options.env === undefined ? {} : { hasEnvironment: true }), ...(options.mcpServers === undefined ? {} : { hasMcpServers: true }), }; } function makeClaudeAgentSdkProtocolLogger(input: { readonly nativeEventLogger: ProviderEventLoggers.EventNdjsonLogger | undefined; readonly threadId: ThreadId; readonly providerSessionId: OrchestrationV2ProviderSession["id"]; }): ClaudeAgentSdkProtocolLogger | undefined { const { nativeEventLogger } = input; if (nativeEventLogger === undefined) { return undefined; } return (event) => { if (!shouldPersistProviderEvent("native", event)) return Effect.void; return nativeEventLogger .write( { provider: CLAUDE_PROVIDER, protocol: CLAUDE_AGENT_SDK_QUERY_PROTOCOL, kind: "protocol", providerSessionId: input.providerSessionId, event: boundProviderEventForLogging(event), }, input.threadId, ) .pipe(Effect.ignore); }; } const encodeClaudeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); const decodeClaudeHistoryFork = Schema.decodeUnknownEffect( Schema.fromJsonString( Schema.Struct({ sessionId: Schema.String, headMessageId: Schema.optionalKey(Schema.String) }), ), ); const claudeConfigDirOf = ( path: Path.Path, environment: NodeJS.ProcessEnv, homeDirectory: string, ) => path.resolve(environment.CLAUDE_CONFIG_DIR ?? path.join(homeDirectory, ".claude")); // The SDK reads CLAUDE_CONFIG_DIR from process.env. Isolate custom homes in a // worker so concurrent providers keep the server's environment. const runClaudeWorker = Effect.fn("runClaudeWorker")(function* (input: { readonly method: "forkSession"; readonly sessionId: string; readonly options?: ForkSessionOptions; readonly environment: NodeJS.ProcessEnv; }) { const path = yield* Path.Path; const fromSource = import.meta.url.endsWith(".ts"); // A single executable hosts the worker itself; it has no sibling script. const workerArguments = (yield* HostProcess.IsExecutable) ? ["__claude-history"] : [ ...(fromSource ? ["--experimental-strip-types"] : []), yield* path.fromFileUrl( new URL( fromSource ? "../../claude-history-worker.ts" : "./claude-history-worker.mjs", import.meta.url, ), ), ]; const executablePath = yield* HostProcess.ExecutablePath; const result = yield* spawnAndCollect( executablePath, ChildProcess.make( executablePath, [...workerArguments, input.method, input.sessionId, encodeClaudeJson(input.options ?? {})], { env: { ...input.environment, ELECTRON_RUN_AS_NODE: "1", }, }, ), ).pipe(Effect.timeout("30 seconds")); if (result.code !== 0) { return yield* new ClaudeAgentSdkQueryRunnerError({ method: input.method, cause: result.stderr.trim() || `Claude history worker exited with code ${result.code}.`, }); } return result.stdout; }); const forkClaudeSessionInWorker = Effect.fn("forkClaudeSessionInWorker")(function* ( input: ClaudeAgentSdkSessionForkInput, ) { return yield* runClaudeWorker({ ...input, method: "forkSession" }).pipe( Effect.flatMap(decodeClaudeHistoryFork), ); }); export const layerQueryRunner: Layer.Layer< ClaudeAgentSdkQueryRunner, never, | ChildProcessSpawner.ChildProcessSpawner | SecureRandom.SecureRandom | Path.Path | PortScanner.PortDiscovery | ProviderEventLoggers.ProviderEventLoggers > = Layer.effect( ClaudeAgentSdkQueryRunner, Effect.gen(function* () { const random = yield* SecureRandom.SecureRandom; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const path = yield* Path.Path; const { native: nativeEventLogger } = yield* ProviderEventLoggers.ProviderEventLoggers; const agentScope = yield* AgentScope; const platform = yield* HostProcess.Platform; const runFork = Effect.runForkWith(yield* Effect.context()); const hostEnvironment = yield* HostProcess.Environment; const portDiscovery = yield* PortScanner.PortDiscovery; return ClaudeAgentSdkQueryRunner.of({ allocateSessionId: random.uuidV4(), open: Effect.fn("ClaudeAgentSdkQueryRunner.open")( function* (input: ClaudeAgentSdkQueryOpenInput) { const protocolLogger = makeClaudeAgentSdkProtocolLogger({ nativeEventLogger, threadId: input.threadId, providerSessionId: input.providerSessionId, }); const logProtocolEvent = (event: ClaudeAgentSdkProtocolLogEvent) => protocolLogger === undefined ? Effect.void : protocolLogger(event); // A failed open shuts down its queue before retrying, so the old CLI cannot take the prompt. const promptQueue = yield* Queue.unbounded(); const prompt = Stream.fromQueue(promptQueue).pipe( Stream.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Stream.empty : Stream.failCause(cause), ), Stream.toAsyncIterable, ); // Send MCP credentials over stdin to keep them out of SDK argv and inherited command environments. const { mcpServers, ...sdkOptions } = input.options; // Jobs the agent starts inherit the thread, so port discovery can attribute their servers. const queryOptions = { ...sdkOptions, env: { ...(sdkOptions.env ?? hostEnvironment), [PortScanner.AGENT_THREAD_ENV]: input.threadId, }, }; // The SDK can prepend a scope wrapper to native binaries; script entries run through node. const binaryPath = queryOptions.pathToClaudeCodeExecutable; const launch = binaryPath === undefined || CLAUDE_SCRIPT_EXTENSIONS.some((ext) => binaryPath.endsWith(ext)) ? undefined : yield* agentScope.wrap({ command: binaryPath, args: [], name: "claude", threadId: input.threadId, env: queryOptions.env, }); const options = launch === undefined || launch.args.length === 0 ? queryOptions : { ...queryOptions, pathToClaudeCodeExecutable: launch.command, executableArgs: [...launch.args, ...(queryOptions.executableArgs ?? [])], }; const unit = launch?.unit; const stopScope = unit === undefined ? undefined : () => void runFork(agentScope.stop(unit)); // Only a CLI that fails before its first message explains itself on // stderr. The known root refusal becomes a curated failure; any other // stderr can hold private text, so it stays out of turn failures. let stderrTail = ""; let stderrClosed: Promise = Promise.resolve(); let answered = false; const appendStderr = (text: string) => { if (answered) return; stderrTail = (stderrTail + text).slice(-CLAUDE_STDERR_TAIL_LENGTH); }; const spawnProcess = options.spawnClaudeCodeProcess ?? ((spawnOptions: ClaudeSpawnOptions) => { if (platform !== "win32") { // Node can report "exit" before it has read the stderr the CLI wrote. let stderrDone = () => {}; stderrClosed = new Promise((resolve) => (stderrDone = resolve)); return spawnClaudeProcessGroup(spawnOptions, stopScope, appendStderr, stderrDone); } const child = NodeChildProcess.spawn(spawnOptions.command, spawnOptions.args, { cwd: spawnOptions.cwd, env: spawnOptions.env, signal: spawnOptions.signal, windowsHide: true, stdio: ["pipe", "pipe", "pipe"], }); child.stderr .setEncoding("utf8") .on("data", (chunk: string) => options.stderr?.(chunk)); return child; }); let spawnedProcess: ClaudeSpawnedProcess | undefined; let processExited = false; let processExit: Promise | undefined; const spawnClaudeCodeProcess = (spawnOptions: ClaudeSpawnOptions) => { const child = spawnProcess(spawnOptions); spawnedProcess = child; // A spawn that fails emits only "error". const releaseAgent = portDiscovery.trackAgent(); child.once("exit", releaseAgent); child.on("error", releaseAgent); processExited = child.exitCode !== null || child.signalCode != null; processExit = new Promise((resolve) => { if (processExited) resolve(); else child.once("exit", () => { processExited = true; resolve(); }); }); return child; }; const queryRuntime = yield* Effect.uninterruptibleMask((restore) => Effect.try({ try: () => query({ prompt, options: { ...options, spawnClaudeCodeProcess }, }), catch: (cause) => queryRunnerError(cause, "query"), }).pipe( Effect.tap((queryRuntime) => restore( Effect.gen(function* () { if (mcpServers !== undefined) { // Errors and mcp_status can echo headers; log only server names. const failed = yield* Effect.tryPromise(() => queryRuntime.setMcpServers(mcpServers), ).pipe( Effect.map((result) => Object.keys(result.errors)), Effect.orElseSucceed(() => Object.keys(mcpServers)), Effect.timeoutOrElse({ duration: CLAUDE_MCP_REGISTRATION_TIMEOUT, orElse: () => Effect.logWarning("orchestration-v2.claude-mcp-registration-timeout", { threadId: input.threadId, providerSessionId: input.providerSessionId, }).pipe( Effect.andThen( Effect.fail( queryRunnerError( new ClaudeMcpRegistrationTimeoutError(), "setMcpServers", ), ), ), ), }), ); if (failed.length > 0) { yield* Effect.logWarning( "orchestration-v2.claude-mcp-servers-unavailable", { threadId: input.threadId, servers: failed, }, ); } } yield* logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "query.open", options: loggedClaudeQueryOptions(input.options), }, }); }), ).pipe( Effect.onError(() => Queue.shutdown(promptQueue).pipe( // SDK disposal can return before process exit; await the owned child's exit before retrying. Effect.andThen( Effect.tryPromise({ try: async () => { if (spawnedProcess !== undefined && !processExited) { spawnedProcess.kill("SIGKILL"); } await processExit; }, catch: (cause) => queryRunnerError(cause, "close"), }).pipe(Effect.timeout("10 seconds")), ), Effect.ensuring( Effect.tryPromise({ try: () => queryRuntime[Symbol.asyncDispose](), catch: (cause) => queryRunnerError(cause, "close"), }).pipe(Effect.orDie), ), Effect.orDie, ), ), ), ), ), ); return { messages: Stream.fromAsyncIterable( claudeQueryMessages(queryRuntime), (cause) => cause, ).pipe( Stream.catch((cause) => Stream.fromEffect( Effect.gen(function* () { if (!answered) { yield* Effect.promise(() => stderrClosed).pipe( Effect.timeoutOption(CLAUDE_STDERR_DRAIN_TIMEOUT), ); } const refusedRoot = !answered && stderrTail.includes(CLAUDE_ROOT_REFUSAL); return yield* queryRunnerError( refusedRoot ? new ClaudeCodeRootRefusalError({ cause }) : cause, "fromAsyncIterable", ); }), ), ), Stream.tap(() => Effect.sync(() => { answered = true; }), ), Stream.tap((message) => logProtocolEvent({ direction: "incoming", stage: "decoded", payload: message, }), ), ), offer: (message, onQueued) => Effect.uninterruptibleMask((restore) => restore(Queue.offer(promptQueue, message)).pipe( Effect.tap(() => Effect.sync(() => onQueued?.())), ), ).pipe( Effect.asVoid, Effect.tap(() => logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "prompt.offer", message, }, }), ), ), setModel: (model) => Effect.tryPromise({ try: () => queryRuntime.setModel(model), catch: (cause) => queryRunnerError(cause, "setModel"), }).pipe( Effect.tap(() => logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "query.set_model", model, }, }), ), ), applyFlagSettings: (settings) => Effect.tryPromise({ try: () => queryRuntime.applyFlagSettings(settings), catch: (cause) => queryRunnerError(cause, "applyFlagSettings"), }).pipe( Effect.tap(() => logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "query.apply_flag_settings", settings, }, }), ), ), setPermissionMode: (mode) => Effect.tryPromise({ try: () => queryRuntime.setPermissionMode(mode), catch: (cause) => queryRunnerError(cause, "setPermissionMode"), }).pipe( Effect.tap(() => logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "query.set_permission_mode", mode, }, }), ), ), stopTask: (taskId) => Effect.tryPromise({ try: () => queryRuntime.stopTask(taskId), catch: (cause) => queryRunnerError(cause, "stopTask"), }).pipe( Effect.tap(() => logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "query.stop_task", taskId }, }), ), ), backgroundTasks: (toolUseId) => Effect.tryPromise({ try: () => queryRuntime.backgroundTasks(toolUseId), catch: (cause) => queryRunnerError(cause, "backgroundTasks"), }).pipe( Effect.tap(() => logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "query.background_tasks", toolUseId }, }), ), ), interrupt: Effect.tryPromise({ try: () => queryRuntime.interrupt(), catch: (cause) => queryRunnerError(cause, "interrupt"), }).pipe( Effect.tap(() => logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "query.interrupt", }, }), ), ), close: Queue.shutdown(promptQueue).pipe( Effect.andThen(closeClaudeQuery(queryRuntime)), Effect.tap(() => logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "query.close", }, }), ), ), } satisfies ClaudeAgentSdkQuerySession; }, // Fresh IDs may already have a metadata-only transcript; only resumed opens can reuse their ID. (effect, input) => effect.pipe( Effect.retry({ times: 1, while: (error) => input.options.resume !== undefined && isClaudeMcpRegistrationTimeoutError(error.cause), }), ), ), forkSession: Effect.fn("ClaudeAgentSdkQueryRunner.forkSession")(function* ( input: ClaudeAgentSdkSessionForkInput, ) { const protocolLogger = makeClaudeAgentSdkProtocolLogger({ nativeEventLogger, threadId: input.threadId, providerSessionId: input.providerSessionId, }); const logProtocolEvent = (event: ClaudeAgentSdkProtocolLogEvent) => protocolLogger === undefined ? Effect.void : protocolLogger(event); yield* logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "session.fork", sessionId: input.sessionId, options: input.options, }, }); const hostEnvironment = yield* HostProcess.Environment; const homeDirectory = yield* HostProcess.HomeDirectory; const usesHostConfig = claudeConfigDirOf(path, input.environment, homeDirectory) === claudeConfigDirOf(path, hostEnvironment, homeDirectory); const fork: Effect.Effect< ForkSessionResult & { readonly headMessageId?: string }, unknown > = usesHostConfig ? Effect.tryPromise(() => forkClaudeSession(input.sessionId, input.options)) : forkClaudeSessionInWorker(input).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(Path.Path, path), ); const result = yield* fork.pipe( Effect.mapError((cause) => queryRunnerError(cause, "forkSession")), ); yield* logProtocolEvent({ direction: "incoming", stage: "decoded", payload: { type: "session.forked", sessionId: result.sessionId, }, }); // The SDK remaps copied UUIDs. Capture the inherited head before the // fork's first run appends messages. const headMessageId = usesHostConfig ? yield* Effect.tryPromise({ try: () => getSessionMessages( result.sessionId, input.options.dir === undefined ? {} : { dir: input.options.dir }, ), catch: (cause) => queryRunnerError(cause, "getSessionMessages"), }).pipe( Effect.map((messages) => messages.at(-1)?.uuid), Effect.orElseSucceed(() => undefined), ) : result.headMessageId; return headMessageId === undefined ? result : { ...result, headMessageId }; }), subagentLaunchToolUseId: Effect.fn("ClaudeAgentSdkQueryRunner.subagentLaunchToolUseId")( function* (input: ClaudeAgentSdkSubagentLookupInput) { const protocolLogger = makeClaudeAgentSdkProtocolLogger({ nativeEventLogger, threadId: input.threadId, providerSessionId: input.providerSessionId, }); const logProtocolEvent = (event: ClaudeAgentSdkProtocolLogEvent) => protocolLogger === undefined ? Effect.void : protocolLogger(event); yield* logProtocolEvent({ direction: "outgoing", stage: "decoded", payload: { type: "subagent.lookup", sessionId: input.sessionId, agentId: input.agentId, }, }); const messages = yield* Effect.tryPromise({ try: () => getSubagentMessages(input.sessionId, input.agentId, { ...(input.dir === null ? {} : { dir: input.dir }), limit: 1, }), catch: (cause) => queryRunnerError(cause, "getSubagentMessages"), }); const toolUseId = messages[0]?.parent_tool_use_id ?? null; yield* logProtocolEvent({ direction: "incoming", stage: "decoded", payload: { type: "subagent.found", toolUseId }, }); return toolUseId; }, ), assertComplete: Effect.void, }); }), ); type ClaudeFlagSettings = Parameters[0]; /** Thinking requires a new process. Clearing effort also requires one to reload user settings instead of model defaults. */ function canApplyClaudeSelectionLive( live: CompiledClaudeModelSelection, next: CompiledClaudeModelSelection, ): boolean { return ( live.settings.alwaysThinkingEnabled === next.settings.alwaysThinkingEnabled && (next.effort !== undefined || live.effort === undefined) ); } function claudeLiveSelectionFlagSettings( selection: CompiledClaudeModelSelection, ): ClaudeFlagSettings { return { ...(selection.effort === undefined ? {} : { effortLevel: selection.effort as EffortLevel }), fastMode: selection.settings.fastMode ?? null, ultracode: selection.settings.ultracode ?? null, }; } // A delegated child is its own SDK session, so Claude Code gives it the main-conversation cache // TTL. When the user sets Claude's subagentPromptCacheTtl, the child follows it like a native // subagent would. An explicit CLAUDE_CODE_PROMPT_CACHE_TTL on the instance still wins. export function claudeQueryEnvironment( environment: NodeJS.ProcessEnv, appThread: Pick, subagentPromptCacheTtl: "5m" | "1h" | undefined, ): NodeJS.ProcessEnv { if ( appThread.lineage.relationshipToParent !== "subagent" || subagentPromptCacheTtl === undefined || environment.CLAUDE_CODE_PROMPT_CACHE_TTL !== undefined ) { return environment; } return { ...environment, CLAUDE_CODE_PROMPT_CACHE_TTL: subagentPromptCacheTtl }; } // Claude Code's auto tool search sends every MCP schema in full while they fit under a share of the // context window, which T3's tools do on 1M models. auto:0 already means on and auto:100 off. export function claudeToolSearchOverride(value: string | undefined): "true" | undefined { if (value === undefined) return undefined; if (value === "auto") return "true"; if (!value.startsWith("auto:")) return undefined; const percent = parseClaudeInteger(value.slice("auto:".length)); // Claude Code treats an unparsable auto:N as auto, and clamps N to 0..100. if (Number.isNaN(percent)) return "true"; const clamped = Math.min(100, Math.max(0, percent)); return clamped > 0 && clamped < 100 ? "true" : undefined; } // Claude Code 2.1.310's integer parsing for auto:N. Scientific notation counts only when it is an // integer, grouped digits (1_000, 1,000, or 1 000) are joined, and anything else uses parseInt. const CLAUDE_SCIENTIFIC_INTEGER = /^[+-]?(\d+(\.\d*)?|\.\d+)[eE][+-]?\d+$/; const CLAUDE_GROUPED_INTEGER = /^[+-]?\d{1,3}([_,\u00A0\u202F ])\d{3}(?:\1\d{3})*$/; function parseClaudeInteger(text: string): number { const value = text.trim(); if (value.length <= 32 && CLAUDE_SCIENTIFIC_INTEGER.test(value)) { const number = Number(value); return Number.isInteger(number) ? number : Number.NaN; } if (value.length <= 32 && CLAUDE_GROUPED_INTEGER.test(value)) { return Number.parseInt(value.replace(/[_,\u00A0\u202F ]/g, ""), 10); } return Number.parseInt(value, 10); } // Claude Code defers MCP tools behind ToolSearch, so the agent only finds // t3_background_wait when the system prompt says when to reach for it. const CLAUDE_BACKGROUND_WAIT_INSTRUCTIONS = ` ### Background work you wait on To wait for a background command or Monitor you started (tests, a build), call \`t3_background_wait\` with its task ID before you end your turn. Do not mark work you leave running, or PRs. T3 MCP tools are deferred: load one with ToolSearch before you call it. `; // Native subagents start from their task, not this session's system prompt. const subagentDeviceInstructions: HookCallback = () => Promise.resolve({ hookSpecificOutput: { hookEventName: "SubagentStart", additionalContext: T3_CODE_DEVICE_TOOL_INSTRUCTIONS_WITH_TOOL_SEARCH, }, }); export function makeClaudeQueryOptions(input: { readonly modelSelection: ModelSelection; readonly nativeThreadId: string; readonly title?: string; readonly resume: boolean; readonly resumeSessionAt?: string; readonly cwd: string | null; readonly attachmentsDir?: string; readonly settings?: ClaudeSettings; readonly sdkSettings?: string | ClaudeSdkSettings; readonly environment?: NodeJS.ProcessEnv; readonly mcpServers?: ClaudeQueryOptions["mcpServers"]; /** The thread's T3 MCP session holds the `device` capability. */ readonly deviceToolsAvailable?: boolean; readonly tools?: ClaudeAgentSdkQueryTools; readonly allowedTools?: ReadonlyArray; readonly disallowedTools?: ReadonlyArray; readonly permissionMode?: PermissionMode; readonly canUseTool?: CanUseTool; readonly onUserDialog?: ClaudeQueryOptions["onUserDialog"]; readonly onElicitation?: ClaudeQueryOptions["onElicitation"]; readonly supportedDialogKinds?: ClaudeQueryOptions["supportedDialogKinds"]; readonly allowDangerouslySkipPermissions?: boolean; readonly appendSystemPromptFileText?: string; // Flag settings, because a user's settings.json env beats the process env in Claude Code. readonly toolSearchOverride?: "true"; }): ClaudeAgentSdkQueryOptions { const compiledSelection = compileClaudeModelSelection(input.modelSelection); const { // claudeLaunchArgDirectories reads every --add-dir into additionalDirectories. "add-dir": _launchArgAddDir, "permission-mode": launchArgPermissionMode, "dangerously-skip-permissions": launchArgSkipPermissions, "append-system-prompt": launchArgAppendSystemPrompt, "append-system-prompt-file": _launchArgAppendSystemPromptFile, // The SDK replaces this flag with options.settings, so it arrives read as sdkSettings. settings: _launchArgSettings, ...extraArgs } = input.settings === undefined ? {} : parseCliArgs(input.settings.launchArgs).flags; // Claude Code overwrites its CLI append text with the SDK's initialize // appendSystemPrompt, so launch-argument text has to travel in that value. // The CLI puts file text before inline text and trims one trailing newline. const launchAppendSystemPrompt = [ input.appendSystemPromptFileText?.replace(/\r?\n$/, ""), launchArgAppendSystemPrompt, ].filter((text): text is string => typeof text === "string" && text.length > 0); const requestThinkingSummaries = compiledSelection.settings.alwaysThinkingEnabled !== false && extraArgs["thinking-display"] !== "omitted"; if (requestThinkingSummaries && extraArgs["thinking-display"] === undefined) { extraArgs["thinking-display"] = "summarized"; } const threadIdentity: ClaudeAgentSdkThreadIdentity = input.resume ? { resume: input.nativeThreadId } : { sessionId: input.nativeThreadId }; const selectedTools = input.tools ?? CLAUDE_CODE_PRESET_TOOLS; const selectionSettings = Object.keys(compiledSelection.settings).length === 0 ? undefined : (compiledSelection.settings as ClaudeSdkSettings); const querySettings = selectionSettings === undefined ? input.sdkSettings : typeof input.sdkSettings === "object" && input.sdkSettings !== null ? ({ ...input.sdkSettings, ...selectionSettings } as ClaudeSdkSettings) : selectionSettings; const compactQuerySettings = input.settings?.autoCompactWindow === undefined || input.settings.autoCompactWindow.length === 0 ? querySettings : ({ ...(typeof querySettings === "object" && querySettings !== null ? querySettings : {}), autoCompactWindow: Number(input.settings.autoCompactWindow), } as ClaudeSdkSettings); // A settings file path cannot take extra keys, so it goes through unchanged. const effectiveQuerySettings = input.toolSearchOverride === undefined || typeof compactQuerySettings === "string" ? compactQuerySettings : ({ ...compactQuerySettings, env: { ...compactQuerySettings?.env, ENABLE_TOOL_SEARCH: input.toolSearchOverride }, } as ClaudeSdkSettings); const deviceInstructions = input.mcpServers !== undefined && input.deviceToolsAvailable === true; const options: ClaudeAgentSdkQueryOptions = { model: compiledSelection.apiModelId, // T3 names threads; a supplied title skips Claude's redundant naming request. ...(input.title === undefined ? {} : { title: input.title }), tools: claudeAgentSdkQueryToolsForSdk(selectedTools), permissionMode: (launchArgPermissionMode as PermissionMode | null | undefined) ?? (launchArgSkipPermissions === null || launchArgSkipPermissions === "true" ? "bypassPermissions" : (input.permissionMode ?? "default")), includePartialMessages: true, ...(compiledSelection.effort === undefined ? {} : { effort: compiledSelection.effort as NonNullable, }), ...threadIdentity, ...(input.resumeSessionAt === undefined ? {} : { resumeSessionAt: input.resumeSessionAt }), ...(input.allowedTools === undefined ? {} : { allowedTools: [...input.allowedTools] }), ...(input.disallowedTools === undefined ? {} : { disallowedTools: [...input.disallowedTools] }), ...(input.canUseTool === undefined ? {} : { canUseTool: input.canUseTool }), ...(input.allowDangerouslySkipPermissions === true ? { allowDangerouslySkipPermissions: true } : {}), ...(requestThinkingSummaries ? { thinking: { type: "adaptive" as const, display: "summarized" as const }, settings: typeof effectiveQuerySettings === "string" ? effectiveQuerySettings : { ...effectiveQuerySettings, showThinkingSummaries: true, }, } : effectiveQuerySettings === undefined ? {} : { settings: effectiveQuerySettings }), ...(input.onUserDialog === undefined ? {} : { onUserDialog: input.onUserDialog }), ...(input.onElicitation === undefined ? {} : { onElicitation: input.onElicitation }), ...(input.supportedDialogKinds === undefined ? {} : { supportedDialogKinds: input.supportedDialogKinds }), ...(input.settings?.binaryPath ? { pathToClaudeCodeExecutable: input.settings.binaryPath } : {}), ...(input.environment === undefined ? {} : { env: input.environment }), ...(input.mcpServers === undefined ? {} : { mcpServers: input.mcpServers }), systemPrompt: { type: "preset" as const, preset: "claude_code" as const, append: [ buildRuntimeInstructions({ harness: "Claude Code" }) + (input.mcpServers === undefined ? "" : T3_CODE_ORCHESTRATION_INSTRUCTIONS + CLAUDE_BACKGROUND_WAIT_INSTRUCTIONS + (deviceInstructions ? `\n${T3_CODE_DEVICE_TOOL_INSTRUCTIONS_WITH_TOOL_SEARCH}\n` : "")), ...launchAppendSystemPrompt, ].join("\n\n"), }, ...(deviceInstructions ? { hooks: { SubagentStart: [{ hooks: [subagentDeviceInstructions] }] } } : {}), ...(Object.keys(extraArgs).length === 0 ? {} : { extraArgs }), }; const additionalDirectories = [ ...(input.cwd === null ? [] : [input.cwd]), ...(input.attachmentsDir === undefined ? [] : [input.attachmentsDir]), ...claudeLaunchArgDirectories(input.settings?.launchArgs), ]; const withDirectories = additionalDirectories.length === 0 ? options : { ...options, additionalDirectories }; return input.cwd === null ? withDirectories : { ...withDirectories, cwd: input.cwd }; } /** * Checks the append launch arguments the way the Claude CLI would and reads * the file relative to the process cwd. makeClaudeQueryOptions folds these * flags out of extraArgs, so a value the CLI would have rejected has to be * rejected here or it silently disappears. */ const readClaudeAppendSystemPromptFile = Effect.fnUntraced(function* (input: { readonly settings: ClaudeSettings | undefined; readonly cwd: string | null; readonly fileSystem: FileSystem.FileSystem; readonly path: Path.Path; }) { if (input.settings === undefined) return undefined; const flags = parseCliArgs(input.settings.launchArgs).flags; // parseCliArgs gives null for a flag with no following value. An explicit // `--append-system-prompt ""` is a value the CLI accepts, so it stays. if (flags["append-system-prompt"] === null) { return yield* new ClaudeAppendSystemPromptMissingValueError({ flag: "--append-system-prompt", }); } const file = flags["append-system-prompt-file"]; if (file === undefined) return undefined; // `--append-system-prompt-file=` is "" and names no file either. if (file === null || file === "") { return yield* new ClaudeAppendSystemPromptMissingValueError({ flag: "--append-system-prompt-file", }); } const filePath = input.path.resolve(input.cwd ?? ".", file); return yield* input.fileSystem .readFileString(filePath) .pipe( Effect.mapError((cause) => new ClaudeAppendSystemPromptFileError({ path: filePath, cause })), ); }); const decodeClaudeLaunchArgSettings = Schema.decodeUnknownEffect( fromLenientJson(Schema.Record(Schema.String, Schema.Unknown)), ); /** * Reads the --settings launch argument: a value starting with a brace is inline * JSON, anything else is a file path relative to the cwd. makeClaudeQueryOptions * merges the result beneath T3's own settings. */ const readClaudeLaunchArgSettings = Effect.fn("ClaudeAdapterV2.readLaunchArgSettings")( function* (input: { readonly settings: ClaudeSettings | undefined; readonly cwd: string | null; readonly fileSystem: FileSystem.FileSystem; readonly path: Path.Path; }) { if (input.settings === undefined) return undefined; const value = parseCliArgs(input.settings.launchArgs).flags.settings; if (value === undefined) return undefined; const trimmed = value?.trim() ?? ""; if (trimmed.length === 0) return yield* new ClaudeSettingsLaunchArgError({}); // The CLI also wants a closing brace, but then malformed JSON, secrets included, would // become the file path in the error message. if (trimmed.startsWith("{")) { return (yield* decodeClaudeLaunchArgSettings(trimmed).pipe( Effect.mapError((cause) => new ClaudeSettingsLaunchArgError({ cause })), )) as ClaudeSdkSettings; } const filePath = input.path.resolve(input.cwd ?? ".", trimmed); return (yield* input.fileSystem.readFileString(filePath).pipe( Effect.flatMap(decodeClaudeLaunchArgSettings), Effect.mapError((cause) => new ClaudeSettingsLaunchArgError({ path: filePath, cause })), )) as ClaudeSdkSettings; }, ); export const CLAUDE_T3_MCP_TOOL_WILDCARD = "mcp__t3-code__*"; // Keep in sync with OrchestratorToolkit Tool.Readonly annotations; ClaudeAdapterV2.test.ts checks this. export const CLAUDE_READ_ONLY_T3_MCP_ALLOWED_TOOLS: ReadonlyArray = [ "mcp__t3-code__orchestrator_capabilities", "mcp__t3-code__list_scheduled_tasks", "mcp__t3-code__t3_thread_list", "mcp__t3-code__t3_thread_wait", "mcp__t3-code__t3_worktree_status", "mcp__t3-code__t3_worktree_list", "mcp__t3-code__t3_project_list", "mcp__t3-code__t3_project_read", "mcp__t3-code__t3_thread_search", "mcp__t3-code__t3_preview_list", "mcp__t3-code__t3_environment_read", "mcp__t3-code__t3_environment_links", "mcp__t3-code__html_preview", "mcp__t3-code__html_render", ]; // T3 waits can block for an hour; exceed Claude's default 60 s timeout. export const CLAUDE_T3_MCP_TOOL_TIMEOUT_MS = 65 * 60 * 1_000; // allowedTools pre-approves calls; tools controls availability. Pre-approve T3 tools for headless modes, // limited to annotated read-only tools in read-only sandboxes. export function claudeMcpQueryOverrides(input: { readonly mcpSession: McpProviderSession.McpProviderSessionConfig | undefined; readonly readOnlySandbox: boolean; readonly allowedTools?: ReadonlyArray; }): { readonly allowedTools?: ReadonlyArray; readonly mcpServers?: ClaudeQueryOptions["mcpServers"]; } { const session = input.mcpSession; if (session === undefined) { return input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }; } const mcpAllowedTools = input.readOnlySandbox ? CLAUDE_READ_ONLY_T3_MCP_ALLOWED_TOOLS : [CLAUDE_T3_MCP_TOOL_WILDCARD]; return { allowedTools: Array.from(new Set([...(input.allowedTools ?? []), ...mcpAllowedTools])), mcpServers: { "t3-code": { type: "http", url: session.endpoint, headers: { Authorization: session.authorizationHeader, }, timeout: CLAUDE_T3_MCP_TOOL_TIMEOUT_MS, }, }, }; } function providerSession(input: { readonly providerSessionId: OrchestrationV2ProviderSession["id"]; readonly providerInstanceId: ProviderInstanceId; readonly cwd: string | null; readonly model: string; readonly now: DateTime.Utc; }): OrchestrationV2ProviderSession { return { id: input.providerSessionId, driver: CLAUDE_PROVIDER, providerInstanceId: input.providerInstanceId, status: "ready", cwd: input.cwd ?? process.cwd(), model: input.model, capabilities: ClaudeProviderCapabilitiesV2, createdAt: input.now, updatedAt: input.now, lastError: null, }; } function textFromClaudeContent(content: SDKAssistantMessage["message"]["content"]): string { return content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join(""); } // Claude 5.1+ emits at most one pre-tool text block as thinking; treat only a second consecutive thinking block as text. function claudeWritesUpdatesAsThinking(model: string): boolean { const match = /claude-[a-z]+-(\d+)(?:-(\d{1,2}))?(?!\d)/u.exec(model); if (match === null) return false; const major = Number(match[1]); return major > 5 || (major === 5 && Number(match[2] ?? 0) >= 1); } // In SDK mode, /goal appears only in transcripts: command output names it and Stop hooks report unmet checks. const CLAUDE_GOAL_SET_PREFIX = "Goal set: "; const CLAUDE_GOAL_ACTIVE = /^Goal active: ([\s\S]+?) \((?:not yet evaluated|(\d+) turns?)\)/u; function nextClaudeGoal( current: OrchestrationV2ProviderGoal | null, message: SDKMessage, ): OrchestrationV2ProviderGoal | null | undefined { if (message.type === "assistant") { if (message.parent_tool_use_id !== null || message.message.model !== "") { return undefined; } const text = textFromClaudeContent(message.message.content).trim(); if (text.startsWith(CLAUDE_GOAL_SET_PREFIX)) { const objective = text.slice(CLAUDE_GOAL_SET_PREFIX.length).trim(); return objective.length === 0 ? undefined : { objective, status: "active", checks: 0 }; } if (text.startsWith("Goal cleared: ") || text.startsWith("No goal set")) return null; const active = CLAUDE_GOAL_ACTIVE.exec(text); if (active?.[1] === undefined) return undefined; return { ...(current?.objective === active[1] ? current : {}), objective: active[1], status: "active", checks: Number(active[2] ?? 0), }; } if ( message.type !== "user" || message.parent_tool_use_id !== null || message.isSynthetic !== true || current === null ) { return undefined; } const prefix = `Stop hook feedback:\n[${current.objective}]: `; const content = message.message.content; const text = typeof content === "string" ? content : content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join(""); if (!text.startsWith(prefix)) return undefined; return { ...current, status: "active", checks: (current.checks ?? 0) + 1, lastCheck: text.slice(prefix.length).trim(), }; } /** The CLI confirms a goal change as a synthetic reply; the goal row shows that change instead. */ function isClaudeGoalAcknowledgement(message: SDKMessage): boolean { if ( message.type !== "assistant" || message.parent_tool_use_id !== null || message.message.model !== "" ) { return false; } const text = textFromClaudeContent(message.message.content).trim(); return text.startsWith(CLAUDE_GOAL_SET_PREFIX) || text.startsWith("Goal cleared: "); } const providerGoalsEqual = Schema.toEquivalence(Schema.NullOr(OrchestrationV2ProviderGoal)); function normalizeClaudeResultText(text: string): string { return text.replace(/\s+/g, " ").trim(); } function assistantTextFromSdkMessage( message: SDKMessage, ): { readonly nativeItemId: string; readonly text: string } | null { if (message.type !== "assistant") { return null; } return { nativeItemId: message.uuid, text: textFromClaudeContent(message.message.content), }; } function resultTextFromSdkMessage( message: SDKMessage, ): { readonly nativeItemId: string; readonly text: string } | null { if (message.type !== "result" || message.subtype !== "success") { return null; } return { nativeItemId: message.uuid, text: message.result, }; } function makeProviderThread(input: { readonly idAllocator: IdAllocator.IdAllocatorV2["Service"]; readonly providerInstanceId: ProviderInstanceId; readonly appThreadId: OrchestrationV2ProviderThread["appThreadId"]; readonly ownerNodeId?: OrchestrationV2ProviderThread["ownerNodeId"]; readonly providerSessionId: OrchestrationV2ProviderThread["providerSessionId"]; readonly nativeThreadId: string; readonly forkedFrom?: NonNullable; readonly nativeMetadata?: NonNullable; readonly now: DateTime.Utc; }): OrchestrationV2ProviderThread { return { id: input.idAllocator.derive.providerThread({ driver: CLAUDE_PROVIDER, nativeThreadId: input.nativeThreadId, }), driver: CLAUDE_PROVIDER, providerInstanceId: input.providerInstanceId, providerSessionId: input.providerSessionId, appThreadId: input.appThreadId, ownerNodeId: input.ownerNodeId ?? null, nativeThreadRef: { driver: CLAUDE_PROVIDER, nativeId: input.nativeThreadId, strength: "strong", }, nativeConversationHeadRef: null, status: "idle", firstRunOrdinal: null, lastRunOrdinal: null, handoffIds: [], forkedFrom: input.forkedFrom ?? null, ...(input.nativeMetadata === undefined ? {} : { nativeMetadata: input.nativeMetadata }), createdAt: input.now, updatedAt: input.now, }; } const decodeClaudeTranscriptLine = Schema.decodeUnknownOption( Schema.fromJsonString(Schema.Struct({ type: Schema.String })), ); const isClaudeConversationLine = (line: string): boolean => Option.match(decodeClaudeTranscriptLine(line), { onNone: () => false, onSome: (entry) => entry.type === "user" || entry.type === "assistant", }); const getNativeThreadId = Effect.fnUntraced(function* ( providerThread: OrchestrationV2ProviderThread, ) { const nativeThreadId = providerThread.nativeThreadRef?.nativeId; if (nativeThreadId === undefined || nativeThreadId === null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Provider thread ${providerThread.id} is missing a native Claude session id.`, }); } return nativeThreadId; }); const isSyntheticClaudeTurnId = (nativeTurnId: string): boolean => nativeTurnId.startsWith("turn:"); const claudeTurnCursor = (turn: OrchestrationV2ProviderTurn): string | null => { const ref = turn.nativeTurnRef; return ref !== null && ref.driver === CLAUDE_PROVIDER && ref.nativeId !== null && !isSyntheticClaudeTurnId(ref.nativeId) ? ref.nativeId : null; }; const isTerminalProviderTurn = (turn: OrchestrationV2ProviderTurn): boolean => turn.status === "completed" || turn.status === "interrupted" || turn.status === "failed" || turn.status === "cancelled"; const getNativeConversationHeadId = Effect.fnUntraced(function* ( providerThread: OrchestrationV2ProviderThread, ) { const nativeHeadRef = providerThread.nativeConversationHeadRef; if (nativeHeadRef === null) { return undefined; } if (nativeHeadRef.driver !== CLAUDE_PROVIDER) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Provider thread ${providerThread.id} has a non-Claude native conversation head reference.`, }); } if (nativeHeadRef.nativeId === null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Provider thread ${providerThread.id} has a Claude native conversation head reference without a native id.`, }); } return nativeHeadRef.nativeId; }); const resolveClaudeForkUpToMessageId = Effect.fn("ClaudeAdapterV2.resolveForkUpToMessageId")( function* (input: ProviderAdapter.ProviderAdapterV2ForkThreadInput) { if (input.providerTurnId === undefined || input.sourceProviderTurns === undefined) { return undefined; } const sourceTurns = input.sourceProviderTurns .filter((turn) => turn.providerThreadId === input.sourceProviderThread.id) .toSorted((left, right) => left.ordinal - right.ordinal); const boundaryIndex = sourceTurns.findIndex((turn) => turn.id === input.providerTurnId); if (boundaryIndex < 0) { return yield* new ProviderAdapter.ProviderAdapterForkThreadError({ driver: CLAUDE_PROVIDER, providerThreadId: input.sourceProviderThread.id, cause: `Cannot fork Claude thread from provider turn ${input.providerTurnId}: source turn was not found in provider thread ${input.sourceProviderThread.id}.`, }); } const boundaryNativeId = sourceTurns[boundaryIndex]?.nativeTurnRef?.nativeId; if ( boundaryNativeId !== undefined && boundaryNativeId !== null && !isSyntheticClaudeTurnId(boundaryNativeId) ) { return boundaryNativeId; } const terminalTurnsAfterBoundary = sourceTurns .slice(boundaryIndex + 1) .filter(isTerminalProviderTurn); if (terminalTurnsAfterBoundary.length === 0) { return undefined; } return yield* new ProviderAdapter.ProviderAdapterForkThreadError({ driver: CLAUDE_PROVIDER, providerThreadId: input.sourceProviderThread.id, cause: `Cannot fork Claude thread from prior provider turn ${input.providerTurnId}: no SDK assistant message cursor was recorded for that turn.`, }); }, ); const resolveClaudeRollbackResumeSessionAt = Effect.fn( "ClaudeAdapterV2.resolveRollbackResumeSessionAt", )(function* (input: ProviderAdapter.ProviderAdapterV2RollbackThreadInput) { switch (input.target.type) { case "thread_start": return input.providerThread.nativeMetadata?.forkHeadMessageId ?? null; case "provider_turn": { const target = input.target; if (target.providerTurn.providerThreadId !== input.providerThread.id) { return yield* new ProviderAdapter.ProviderAdapterRollbackThreadError({ driver: CLAUDE_PROVIDER, providerThreadId: input.providerThread.id, cause: `Cannot roll back Claude thread ${input.providerThread.id} to provider turn ${target.providerTurn.id}: target turn belongs to provider thread ${target.providerTurn.providerThreadId}.`, }); } const cursor = claudeTurnCursor(target.providerTurn); if (cursor !== null) { return cursor; } const providerTurnsAfterTarget = input.providerThreadTurns.filter( (turn) => turn.ordinal > target.providerTurn.ordinal && isTerminalProviderTurn(turn), ); if (providerTurnsAfterTarget.length === 0) { return null; } // Only compaction can be omitted without dropping a retained user prompt. if (target.message !== undefined && isCompactionCommandMessage(target.message)) { const earlierTurn = input.providerThreadTurns .filter((turn) => turn.ordinal < target.providerTurn.ordinal) .toSorted((left, right) => left.ordinal - right.ordinal) .at(-1); const earlierCursor = earlierTurn === undefined ? null : claudeTurnCursor(earlierTurn); if (earlierCursor != null) { return earlierCursor; } } return yield* new ProviderAdapter.ProviderAdapterRollbackThreadError({ driver: CLAUDE_PROVIDER, providerThreadId: input.providerThread.id, cause: `Cannot roll back Claude thread ${input.providerThread.id} to provider turn ${target.providerTurn.id}: no SDK assistant message cursor was recorded for that turn.`, }); } } }); type ClaudeUserContent = SDKUserMessage["message"]["content"]; type ClaudeUserContentBlock = Exclude[number]; const SUPPORTED_CLAUDE_IMAGE_MIME_TYPES = [ "image/gif", "image/jpeg", "image/png", "image/webp", ] as const; type SupportedClaudeImageMimeType = (typeof SUPPORTED_CLAUDE_IMAGE_MIME_TYPES)[number]; const supportedClaudeImageMimeTypes = new Set(SUPPORTED_CLAUDE_IMAGE_MIME_TYPES); function isSupportedClaudeImageMimeType( mimeType: string, ): mimeType is SupportedClaudeImageMimeType { return supportedClaudeImageMimeTypes.has(mimeType); } export function makeClaudeUserMessage(input: { readonly text: string; readonly priority?: SDKUserMessage["priority"]; readonly skills?: ReadonlyMap; readonly uuid?: SDKUserMessage["uuid"]; }): SDKUserMessage { // Claude expands a skill only when `/name` starts the last text block. // Keep the whole request before that block; see ClaudeSkillDispatch. const dispatch = input.skills === undefined ? undefined : planClaudeSkillDispatch(input.text, input.skills); return { type: "user", message: { role: "user", content: dispatch ? [ ...(dispatch.leadingText === undefined ? [] : [{ type: "text" as const, text: dispatch.leadingText }]), { type: "text" as const, text: dispatch.commandText }, ] : input.text, }, parent_tool_use_id: null, ...(input.priority === undefined ? {} : { priority: input.priority }), ...(input.uuid === undefined ? {} : { uuid: input.uuid }), }; } // Never decode streams just to enforce a provider limit. Only accept an explicit, // uncompressed page tree whose /Count agrees with the visible page objects. const estimateClaudePdfPages = (bytes: Buffer): number | null => { if (bytes.length > 20 * 1024 * 1024 || bytes.subarray(0, 5).toString() !== "%PDF-") return null; if (!bytes.subarray(-1024).includes("%%EOF")) return null; let pages = 0; let count = 0; let catalog = false; let pageTree = false; let previous = ""; const delimiter = (byte: number) => byte <= 32 || "()<>[]{}/%".includes(String.fromCharCode(byte)); for (let i = 0; i < bytes.length;) { const byte = bytes[i]!; if (byte === 37) { while (i < bytes.length && bytes[i] !== 10 && bytes[i] !== 13) i++; continue; } if (byte <= 32) { i++; continue; } if (previous === "/Type" && byte !== 47) return null; if (previous === "/Kids") { if (byte !== 91) return null; previous = ""; } if (byte === 40) { let depth = 1; for (i++; i < bytes.length && depth > 0; i++) { if (bytes[i] === 92) i++; else if (bytes[i] === 40) depth++; else if (bytes[i] === 41) depth--; } if (depth !== 0) return null; previous = ""; continue; } if (byte === 60 && bytes[i + 1] !== 60) { i = bytes.indexOf(62, i + 1); if (i < 0) return null; i++; previous = ""; continue; } if (byte === 60 && bytes[i + 1] === 60) { i += 2; previous = ""; continue; } if (delimiter(byte) && byte !== 47) { i++; continue; } const start = i++; while (i < bytes.length && !delimiter(bytes[i]!)) i++; if (i - start > 64) return null; const token = bytes.toString("latin1", start, i); if (token.includes("#") || ["/ObjStm", "/XRef", "/Encrypt", "/Prev"].includes(token)) return null; if (token === "stream") { i = bytes.indexOf("endstream", i); if (i < 0) return null; i += 9; previous = ""; continue; } if (previous === "/Type") { if (token === "/Page") pages++; if (token === "/Catalog") catalog = true; if (token === "/Pages") pageTree = true; } if (previous === "/Count") { if (!/^[1-9]\d{0,8}$/.test(token)) return null; let next = i; while (next < bytes.length) { if (bytes[next]! <= 32) next++; else if (bytes[next] === 37) { while (next < bytes.length && bytes[next] !== 10 && bytes[next] !== 13) next++; } else break; } // A second scalar is an indirect reference, not a direct page count. if (next < bytes.length && !delimiter(bytes[next]!)) return null; count = Math.max(count, Number(token)); } previous = token; } return catalog && pageTree && pages > 0 && count === pages ? pages : null; }; const CLAUDE_ATTACHMENT_MAX_BYTES = 28 * 1024 * 1024; const CLAUDE_ATTACHMENT_MAX_PAGES = 100; const makeClaudeAttachmentBudget = (used?: { pagesUsed: number; bytesUsed: number }) => ({ committed: { pages: used?.pagesUsed ?? 0, bytes: used?.bytesUsed ?? 0 }, pending: { pages: 0, bytes: 0 }, compacting: null as { pages: number; bytes: number } | null, get remainingPages() { return CLAUDE_ATTACHMENT_MAX_PAGES - this.committed.pages - this.pending.pages; }, get remainingBytes() { return CLAUDE_ATTACHMENT_MAX_BYTES - this.committed.bytes - this.pending.bytes; }, }); const reserveClaudePdfBudget = ( budget: ReturnType, persist: Effect.Effect, ) => Effect.acquireRelease( Effect.sync(() => ({ budget, pages: 0, bytes: 0, delivered: false, commit() { if (this.delivered) return; this.delivered = true; budget.pending.pages -= this.pages; budget.pending.bytes -= this.bytes; budget.committed.pages += this.pages; budget.committed.bytes += this.bytes; }, })), (reservation) => Effect.sync(() => { if (reservation.delivered) return; budget.pending.pages -= reservation.pages; budget.pending.bytes -= reservation.bytes; }).pipe(Effect.andThen(persist)), ); const makeClaudeUserMessageWithAttachments = Effect.fnUntraced(function* (input: { readonly text: string; readonly attachments: ReadonlyArray; readonly priority?: SDKUserMessage["priority"]; readonly uuid?: SDKUserMessage["uuid"]; readonly attachmentsDir: string; readonly fileSystem: FileSystem.FileSystem; readonly pdfReservation: Effect.Success>; readonly skills?: ReadonlyMap; }) { if (input.attachments.length === 0) { const message = makeClaudeUserMessage({ text: input.text, ...(input.skills === undefined ? {} : { skills: input.skills }), ...(input.priority === undefined ? {} : { priority: input.priority }), ...(input.uuid === undefined ? {} : { uuid: input.uuid }), }); const bytes = Buffer.byteLength(encodeClaudeJson(message.message.content)); input.pdfReservation.budget.pending.bytes += bytes; input.pdfReservation.bytes += bytes; return message; } const textWithAttachmentPaths = yield* providerMessageTextWithAttachmentPaths({ text: input.text, attachments: input.attachments, resolveExtractedTextPath: (path) => DocumentText.resolveExtractedTextPath(path).pipe( Effect.provideService(FileSystem.FileSystem, input.fileSystem), ), resolveAttachmentPath: (attachment) => resolveAttachmentPath({ attachmentsDir: input.attachmentsDir, attachment }), }); const dispatch = input.skills === undefined ? undefined : planClaudeSkillDispatch(textWithAttachmentPaths, input.skills); const content: Array = []; // Base64 adds a third to the payload. Leave room below Anthropic's 32 MB // request limit for text, instructions, and SDK framing. The SDK does not // expose document/context-window capabilities, so use the 100-page limit // that also applies to models with less than a 1M-token context window. if (dispatch?.leadingText !== undefined) { content.push({ type: "text", text: dispatch.leadingText }); } // Reserve all text and actual image bytes before considering any document, // including images that follow it in attachment order. const images = new Map(); const textBytes = Buffer.byteLength( encodeClaudeJson( dispatch ? [ ...(dispatch.leadingText === undefined ? [] : [{ type: "text", text: dispatch.leadingText }]), { type: "text", text: dispatch.commandText }, ] : [{ type: "text", text: textWithAttachmentPaths }], ), ); input.pdfReservation.budget.pending.bytes += textBytes; input.pdfReservation.bytes += textBytes; for (const [index, attachment] of input.attachments.entries()) { if (attachment.type !== "image") continue; if (!isSupportedClaudeImageMimeType(attachment.mimeType)) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Unsupported Claude image attachment type '${attachment.mimeType}'`, }); } const attachmentPath = resolveAttachmentPath({ attachmentsDir: input.attachmentsDir, attachment, }); if (attachmentPath === null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Invalid attachment id '${attachment.id}'`, }); } const bytes = yield* input.fileSystem.readFile(attachmentPath).pipe( Effect.mapError( (cause) => new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Failed to read attachment '${attachment.id}'`, reason: "attachment_unreadable", payload: cause, }), ), ); const retainedBytes = 4 * Math.ceil(bytes.length / 3) + 128; if (retainedBytes > input.pdfReservation.budget.remainingBytes) continue; input.pdfReservation.budget.pending.bytes += retainedBytes; input.pdfReservation.bytes += retainedBytes; const block: ClaudeUserContentBlock = { type: "image", source: { type: "base64", media_type: attachment.mimeType, data: Buffer.from(bytes).toString("base64"), }, }; images.set(index, block); } for (const [index, attachment] of input.attachments.entries()) { if (attachment.type === "file") { if ( input.pdfReservation.budget.remainingPages <= 0 || attachment.mimeType.toLowerCase().split(";", 1)[0]?.trim() !== "application/pdf" || ("source" in attachment && attachment.source?._tag === "pasted-text") ) continue; const attachmentPath = resolveAttachmentPath({ attachmentsDir: input.attachmentsDir, attachment, }); if (attachmentPath === null) continue; const remainingBytes = Math.min( PROVIDER_SEND_TURN_MAX_FILE_BYTES, 20 * 1024 * 1024, Math.floor(((input.pdfReservation.budget.remainingBytes - 1024) * 3) / 4), ); const info = yield* input.fileSystem .stat(attachmentPath) .pipe(Effect.orElseSucceed(() => null)); if (info?.type !== "File" || Number(info.size) > remainingBytes || remainingBytes <= 0) continue; const bytes = yield* input.fileSystem .stream(attachmentPath, { bytesToRead: remainingBytes + 1 }) .pipe( Stream.runCollect, Effect.map((chunks) => Buffer.concat(chunks)), Effect.orElseSucceed(() => null), ); if (bytes === null || bytes.length > remainingBytes) continue; const pageCount = estimateClaudePdfPages(bytes); if (pageCount === null) { yield* Effect.logDebug("Claude PDF page count is unknown; keeping attachment path-only."); continue; } const retainedBytes = 4 * Math.ceil(bytes.length / 3) + 128; if ( pageCount <= 0 || pageCount > input.pdfReservation.budget.remainingPages || retainedBytes > input.pdfReservation.budget.remainingBytes ) continue; input.pdfReservation.budget.pending.pages += pageCount; input.pdfReservation.pages += pageCount; const block: ClaudeUserContentBlock = { type: "document", source: { type: "base64", media_type: "application/pdf", data: bytes.toString("base64") }, }; input.pdfReservation.budget.pending.bytes += retainedBytes; input.pdfReservation.bytes += retainedBytes; content.push(block); continue; } const image = images.get(index); if (image !== undefined) content.push(image); } // Images must precede the command block for skill expansion. if (dispatch) { content.push({ type: "text", text: dispatch.commandText }); } else if (textWithAttachmentPaths.length > 0) { content.push({ type: "text", text: textWithAttachmentPaths }); } return { type: "user", message: { role: "user", content, }, parent_tool_use_id: null, ...(input.priority === undefined ? {} : { priority: input.priority }), ...(input.uuid === undefined ? {} : { uuid: input.uuid }), } satisfies SDKUserMessage; }); export function isClaudePromptUuid(value: string): value is NonNullable { return /^[^-]+-[^-]+-[^-]+-[^-]+-[^-]+$/.test(value); } type ClaudeAssistantContentBlock = SDKAssistantMessage["message"]["content"][number]; type ClaudeToolUseContentBlock = Extract< ClaudeAssistantContentBlock, { readonly id: string; readonly name: string; readonly input: unknown; } >; type ClaudeAssistantToolResultContentBlock = Extract< ClaudeAssistantContentBlock, { readonly tool_use_id: string; } >; type ClaudeUserToolResultContentBlock = Extract< ClaudeUserContentBlock, { readonly tool_use_id: string; } >; type ClaudeToolResultContentBlock = | ClaudeAssistantToolResultContentBlock | ClaudeUserToolResultContentBlock; type ClaudeTypedToolResultContentBlock = Exclude< ClaudeToolResultContentBlock, { readonly type: "mcp_tool_result" | "tool_result" } >; type ClaudeTypedToolResultContent = ClaudeTypedToolResultContentBlock["content"]; type ClaudeToolResultOutput = | Extract["content"] | Extract["content"] | ClaudeTypedToolResultContent; function assertNever(value: never): never { throw new Error(`Unhandled Claude SDK variant: ${jsonStringifyForTool(value)}`); } const ClaudeRuntimeSandboxPolicyKind = Schema.Struct({ type: Schema.Literals(["dangerFullAccess", "externalSandbox", "readOnly", "workspaceWrite"]), }); type ClaudeRuntimeSandboxPolicy = typeof ClaudeRuntimeSandboxPolicyKind.Type; type ClaudeRuntimeSandboxPolicyKindName = ClaudeRuntimeSandboxPolicy["type"]; const isClaudeRuntimeSandboxPolicyKind = Schema.is(ClaudeRuntimeSandboxPolicyKind); const ClaudeRuntimeReadOnlyFullAccessSandboxPolicy = Schema.Struct({ type: Schema.Literal("readOnly"), access: Schema.Struct({ type: Schema.Literal("fullAccess"), }), }); const isClaudeRuntimeReadOnlyFullAccessSandboxPolicy = Schema.is( ClaudeRuntimeReadOnlyFullAccessSandboxPolicy, ); function sandboxPolicyKindForClaudeRuntimePolicy( runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy, ): ClaudeRuntimeSandboxPolicyKindName | undefined { return runtimePolicy.sandboxPolicy !== undefined && isClaudeRuntimeSandboxPolicyKind(runtimePolicy.sandboxPolicy) ? runtimePolicy.sandboxPolicy.type : undefined; } function readOnlyPolicyAllowsGlobalReads( runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy, ): boolean { return ( runtimePolicy.sandboxPolicy !== undefined && isClaudeRuntimeReadOnlyFullAccessSandboxPolicy(runtimePolicy.sandboxPolicy) ); } function permissionModeForClaudeRuntimePolicy( runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy, ): PermissionMode { if (runtimePolicy.interactionMode === "plan") { return "plan"; } if (runtimePolicy.approvalPolicy === "never") { switch (sandboxPolicyKindForClaudeRuntimePolicy(runtimePolicy)) { case "readOnly": return "dontAsk"; case "dangerFullAccess": case "externalSandbox": return "bypassPermissions"; case "workspaceWrite": case undefined: return runtimePolicy.runtimeMode === "approval-required" ? "dontAsk" : runtimePolicy.runtimeMode === "auto-accept-edits" ? "acceptEdits" : "bypassPermissions"; } } if (runtimePolicy.approvalPolicy !== undefined && runtimePolicy.approvalPolicy !== "never") { return "default"; } switch (sandboxPolicyKindForClaudeRuntimePolicy(runtimePolicy)) { case "readOnly": return "dontAsk"; case "dangerFullAccess": return runtimePolicy.runtimeMode === "approval-required" ? "default" : "bypassPermissions"; case "externalSandbox": case "workspaceWrite": case undefined: break; } switch (runtimePolicy.runtimeMode) { case "approval-required": return "default"; case "auto-accept-edits": return "acceptEdits"; case "auto": return "auto"; case "full-access": return "bypassPermissions"; } } export interface ClaudeRuntimeQueryPolicy { readonly permissionMode: PermissionMode; readonly tools?: ClaudeAgentSdkQueryTools; readonly allowedTools?: ReadonlyArray; readonly allowDangerouslySkipPermissions?: true; readonly installPermissionCallback: boolean; } interface ClaudeBypassAvailability { readonly bypassDisabled: boolean; readonly autoDisabled: boolean; } const CLAUDE_BYPASS_FULLY_AVAILABLE: ClaudeBypassAvailability = { bypassDisabled: false, autoDisabled: false, }; export function claudeRuntimeQueryPolicyForRuntimePolicy( runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy, availability: ClaudeBypassAvailability = CLAUDE_BYPASS_FULLY_AVAILABLE, ): ClaudeRuntimeQueryPolicy { const requestedMode = permissionModeForClaudeRuntimePolicy(runtimePolicy); const downgraded = requestedMode === "bypassPermissions" && availability.bypassDisabled; const permissionMode = downgraded ? availability.autoDisabled ? "default" : "auto" : requestedMode; const readOnlyTools = sandboxPolicyKindForClaudeRuntimePolicy(runtimePolicy) === "readOnly" ? CLAUDE_READ_ONLY_ALLOWED_TOOLS : undefined; const allowedTools = readOnlyTools !== undefined && readOnlyPolicyAllowsGlobalReads(runtimePolicy) ? readOnlyTools : undefined; // acceptEdits approves edits before the callback; the callback must ask for everything else. const installPermissionCallback = downgraded || (runtimePolicy.approvalPolicy === undefined ? runtimePolicy.runtimeMode === "approval-required" || runtimePolicy.runtimeMode === "auto-accept-edits" : runtimePolicy.approvalPolicy !== "never"); if (permissionMode === "plan") { return { permissionMode, ...(readOnlyTools === undefined ? {} : { tools: readOnlyTools }), ...(allowedTools === undefined ? {} : { allowedTools }), // Claude routes Plan-mode writes to the callback even in Auto or Full access. installPermissionCallback: true, }; } return { permissionMode, ...(readOnlyTools === undefined ? {} : { tools: readOnlyTools }), ...(allowedTools === undefined ? {} : { allowedTools }), ...(permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}), installPermissionCallback, }; } const ClaudeBypassSettings = Schema.StructWithRest( Schema.Struct({ disableAutoMode: Schema.optionalKey(Schema.Literals(["disable", false])), permissions: Schema.optionalKey( Schema.StructWithRest( Schema.Struct({ disableBypassPermissionsMode: Schema.optionalKey(Schema.Literals(["disable", false])), disableAutoMode: Schema.optionalKey(Schema.Literals(["disable", false])), }), [Schema.Record(Schema.String, Schema.Unknown)], ), ), }), [Schema.Record(Schema.String, Schema.Unknown)], ); const decodeClaudeBypassSettings = Schema.decodeUnknownEffect( fromLenientJson(ClaudeBypassSettings), ); const decodeClaudeLaunchArgBypassSettings = Schema.decodeUnknownEffect(ClaudeBypassSettings); function claudeSdkBypassSettings(settings: typeof ClaudeBypassSettings.Type) { const { disableAutoMode, permissions, ...rest } = settings; const { disableBypassPermissionsMode, disableAutoMode: nestedAutoMode, ...permissionRest } = permissions ?? {}; return { ...rest, ...(disableAutoMode === "disable" ? { disableAutoMode } : {}), ...(permissions ? { permissions: { ...permissionRest, ...(disableBypassPermissionsMode === "disable" ? { disableBypassPermissionsMode } : {}), ...(nestedAutoMode === "disable" ? { disableAutoMode: nestedAutoMode } : {}), }, } : {}), }; } // --settings travels to the SDK as options.settings, whose type only allows "disable". // Shapes the schema rejects keep their current pass-through; resolveBypassAvailability // already treats them as a bypass failure. const decodeClaudeLaunchArgBypassSettingsOption = Schema.decodeUnknownOption(ClaudeBypassSettings); function claudeSdkLaunchArgSettings(settings: ClaudeSdkSettings): ClaudeSdkSettings { const decoded = decodeClaudeLaunchArgBypassSettingsOption(settings); return Option.isNone(decoded) ? settings : (claudeSdkBypassSettings(decoded.value) as ClaudeSdkSettings); } const ClaudePromptCacheTtl = Schema.Literals(["5m", "1h"]); const decodeClaudePromptCacheTtl = Schema.decodeUnknownOption(ClaudePromptCacheTtl); const decodeClaudeSubagentCacheSettings = Schema.decodeUnknownEffect( fromLenientJson( Schema.StructWithRest( Schema.Struct({ subagentPromptCacheTtl: Schema.optionalKey(ClaudePromptCacheTtl) }), [Schema.Record(Schema.String, Schema.Unknown)], ), ), ); const decodeClaudeEnvSettings = Schema.decodeUnknownEffect( fromLenientJson( Schema.StructWithRest( Schema.Struct({ env: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)) }), [Schema.Record(Schema.String, Schema.Unknown)], ), ), ); function claudeBypassAvailability( settings: typeof ClaudeBypassSettings.Type, ): ClaudeBypassAvailability { return { bypassDisabled: settings.permissions?.disableBypassPermissionsMode === "disable", autoDisabled: settings.disableAutoMode === "disable" || settings.permissions?.disableAutoMode === "disable", }; } function requiresClaudeApproval(context: ActiveClaudeTurnContext): boolean { // CLI-forwarded requests need a person's decision even under bypass (ask rules, // safety checks, or settings fallback). Do not wait for queued init/status updates. const policy = context.queryPolicy; return policy.installPermissionCallback || policy.permissionMode === "bypassPermissions"; } function claudeRuntimeQueryPolicyKey(policy: ClaudeRuntimeQueryPolicy): string { return JSON.stringify({ permissionMode: policy.permissionMode, tools: policy.tools, allowedTools: policy.allowedTools, allowDangerouslySkipPermissions: policy.allowDangerouslySkipPermissions, installPermissionCallback: policy.installPermissionCallback, }); } // Live-query reuse must include MCP pre-approvals, which can differ for the same policy key. export function claudeEffectiveQueryPolicyKey( queryPolicy: ClaudeRuntimeQueryPolicy, mcpOverrides: { readonly allowedTools?: ReadonlyArray; readonly mcpServers?: ClaudeQueryOptions["mcpServers"]; }, ): string { return JSON.stringify({ runtimePolicy: claudeRuntimeQueryPolicyKey({ ...queryPolicy, ...(mcpOverrides.allowedTools === undefined ? {} : { allowedTools: mcpOverrides.allowedTools }), }), mcpServers: mcpOverrides.mcpServers, }); } type ClaudeToolItemType = Extract< OrchestrationV2TurnItem["type"], "command_execution" | "file_change" | "dynamic_tool" | "web_search" >; interface ClaudeToolClassification { readonly normalizedName: string; readonly itemType: ClaudeToolItemType; readonly requestKind: ProviderRequestKind; } function normalizedClaudeToolName(toolName: string): string { return toolName.toLowerCase().replaceAll(/[\s_-]/g, ""); } const CLAUDE_KNOWN_TOOL_CLASSIFICATIONS: Record< string, { readonly itemType: ClaudeToolItemType; readonly requestKind: ProviderRequestKind; } > = { agent: { itemType: "dynamic_tool", requestKind: "command" }, bash: { itemType: "command_execution", requestKind: "command" }, edit: { itemType: "file_change", requestKind: "file-change" }, glob: { itemType: "dynamic_tool", requestKind: "file-read" }, grep: { itemType: "dynamic_tool", requestKind: "file-read" }, ls: { itemType: "dynamic_tool", requestKind: "file-read" }, monitor: { itemType: "dynamic_tool", requestKind: "command" }, multiedit: { itemType: "file_change", requestKind: "file-change" }, notebookedit: { itemType: "file_change", requestKind: "file-change" }, read: { itemType: "dynamic_tool", requestKind: "file-read" }, sendmessage: { itemType: "dynamic_tool", requestKind: "command" }, task: { itemType: "dynamic_tool", requestKind: "command" }, taskstop: { itemType: "dynamic_tool", requestKind: "command" }, todowrite: { itemType: "dynamic_tool", requestKind: "command" }, toolsearch: { itemType: "dynamic_tool", requestKind: "command" }, webfetch: { itemType: "web_search", requestKind: "command" }, websearch: { itemType: "web_search", requestKind: "command" }, write: { itemType: "file_change", requestKind: "file-change" }, }; function classifyClaudeNativeTool(toolName: string): ClaudeToolClassification { const normalizedName = normalizedClaudeToolName(toolName); const known = CLAUDE_KNOWN_TOOL_CLASSIFICATIONS[normalizedName]; return known === undefined ? { normalizedName, itemType: "dynamic_tool", requestKind: "command", } : { normalizedName, ...known, }; } function providerRequestKindFromClaudeTool(toolName: string): ProviderRequestKind { return classifyClaudeNativeTool(toolName).requestKind; } function isClaudeWebSearchOutput(output: unknown): output is WebSearchOutput { return ( typeof output === "object" && output !== null && typeof Reflect.get(output, "query") === "string" && Array.isArray(Reflect.get(output, "results")) && typeof Reflect.get(output, "durationSeconds") === "number" ); } const ClaudeNativeToolInputRecord = Schema.Record(Schema.String, Schema.Unknown); type ClaudeNativeToolInputRecord = typeof ClaudeNativeToolInputRecord.Type; const isClaudeNativeToolInputRecord = Schema.is(ClaudeNativeToolInputRecord); type ClaudeNativeToolInput = | { readonly type: "record"; readonly value: ClaudeNativeToolInputRecord; } | { readonly type: "non_record"; readonly value: unknown; }; const EMPTY_CLAUDE_NATIVE_TOOL_INPUT = { type: "record", value: {}, } satisfies ClaudeNativeToolInput; function claudeNativeToolInputFromUnknown(input: unknown): ClaudeNativeToolInput { return isClaudeNativeToolInputRecord(input) ? { type: "record", value: input } : { type: "non_record", value: input }; } function claudeNativeToolInputFromRecord(input: Record): ClaudeNativeToolInput { return { type: "record", value: input }; } function claudeNativeToolInputValue(input: ClaudeNativeToolInput): unknown { return input.value; } function inputRecordValue(input: ClaudeNativeToolInput, key: string): unknown { return input.type === "record" ? input.value[key] : undefined; } function firstStringInputField( input: ClaudeNativeToolInput, keys: ReadonlyArray, ): string | undefined { for (const key of keys) { const value = inputRecordValue(input, key); if (typeof value === "string" && value.trim().length > 0) { return value.trim(); } } return undefined; } function jsonStringifyForTool(value: unknown): string { if (typeof value === "string") { return value; } return JSON.stringify(value) ?? String(value); } function commandInputFromClaudeTool(toolName: string, input: ClaudeNativeToolInput): string { return ( firstStringInputField(input, ["command", "cmd", "script"]) ?? `${toolName}: ${jsonStringifyForTool(claudeNativeToolInputValue(input))}` ); } // background_tasks_changed can include subagents already counted by their lifecycle. const CLAUDE_OPAQUE_BACKGROUND_TASK_KINDS: ReadonlyMap< string, Exclude > = new Map([["local_bash", "command"]]); // Claude Code 2.1.292 defers its goal Stop hook for these subagent types. // local_bash tasks defer it through the opaque-task roster. const CLAUDE_GOAL_DEFERRING_TASK_TYPES: ReadonlySet = new Set([ "local_agent", "remote_agent", "in_process_teammate", "local_workflow", ]); function isClaudeOpaqueBackgroundTaskType(taskType: string | null | undefined): boolean { return typeof taskType === "string" && CLAUDE_OPAQUE_BACKGROUND_TASK_KINDS.has(taskType); } function claudePendingBackgroundTask(input: { readonly taskId: string; readonly taskType: string | null; // Only the launching Monitor tool distinguishes a local_bash Monitor from background Bash. readonly startedByMonitor: boolean; readonly description: string | undefined; // Undeclared SDK field: the subagent whose own background work this is. readonly parentTaskId: unknown; }): OrchestrationV2PendingBackgroundTask { return { taskId: input.taskId, kind: input.startedByMonitor ? "monitor" : ((input.taskType === null ? undefined : CLAUDE_OPAQUE_BACKGROUND_TASK_KINDS.get(input.taskType)) ?? "background_task"), ...(input.description !== undefined && input.description.trim().length > 0 ? { description: input.description } : {}), ...(typeof input.parentTaskId === "string" && input.parentTaskId.trim().length > 0 ? { parentTaskId: input.parentTaskId } : {}), }; } function claudeTaskTypeFromSdkMessage(message: SDKMessage): string | null { if (typeof message !== "object" || message === null) { return null; } const taskType = Reflect.get(message, "task_type"); return typeof taskType === "string" ? taskType : null; } function isClaudeNonSubagentTask(message: SDKMessage): boolean { return isClaudeOpaqueBackgroundTaskType(claudeTaskTypeFromSdkMessage(message)); } function isClaudeBackgroundTasksChangedMessage(message: SDKMessage): boolean { return message.type === "system" && (message.subtype as string) === "background_tasks_changed"; } function isClaudeTurnStartMessage(message: SDKMessage): boolean { return message.type === "system" && message.subtype === "init"; } function isClaudeRootTurnFrame(message: SDKMessage): boolean { return ( isClaudeTurnStartMessage(message) || ((message.type === "user" || message.type === "assistant" || message.type === "stream_event") && message.parent_tool_use_id == null) ); } function claudePendingBackgroundTasksFromRoster( roster: ReadonlyMap, awaitedTaskIds: ReadonlySet, ): ReadonlyArray { return Array.from(roster.values(), (task) => awaitedTaskIds.has(task.taskId) ? { ...task, awaited: true as const } : task, ); } function parseClaudeBackgroundTaskEntry( entry: unknown, monitorTasks: ReadonlyMap, ): OrchestrationV2PendingBackgroundTask | null { if (entry === null || typeof entry !== "object") { return null; } const taskId = Reflect.get(entry, "task_id"); if (typeof taskId !== "string" || taskId.length === 0) { return null; } const rawTaskType = Reflect.get(entry, "task_type"); const taskType = typeof rawTaskType === "string" ? rawTaskType : null; if (!isClaudeOpaqueBackgroundTaskType(taskType)) { return null; } const description = Reflect.get(entry, "description"); return claudePendingBackgroundTask({ taskId, taskType, startedByMonitor: monitorTasks.has(taskId), description: typeof description === "string" ? description : undefined, parentTaskId: Reflect.get(entry, "parent_task_id"), }); } function fileNameFromClaudeTool(toolName: string, input: ClaudeNativeToolInput): string { return ( firstStringInputField(input, ["file_path", "notebook_path", "path", "filename", "fileName"]) ?? `${toolName} result` ); } type ClaudeNativeToolOutput = | { readonly type: "none"; } | { readonly type: "content_block"; readonly value: ClaudeToolResultOutput; } | { readonly type: "structured_tool_use_result"; readonly value: unknown; readonly fallbackValue?: ClaudeToolResultOutput; }; const NO_CLAUDE_NATIVE_TOOL_OUTPUT = { type: "none" } satisfies ClaudeNativeToolOutput; function claudeNativeToolOutputFromToolResult( toolResult: ClaudeToolResultContentBlock, ): ClaudeNativeToolOutput { const value = outputFromClaudeToolResult(toolResult); return value === undefined ? NO_CLAUDE_NATIVE_TOOL_OUTPUT : { type: "content_block", value }; } function claudeNativeToolOutputFromStructuredResult(input: { readonly structuredOutput: unknown; readonly fallbackValue?: ClaudeToolResultOutput; }): ClaudeNativeToolOutput { return { type: "structured_tool_use_result", value: input.structuredOutput, ...(input.fallbackValue === undefined ? {} : { fallbackValue: input.fallbackValue }), }; } function claudeNativeToolOutputValue(output: ClaudeNativeToolOutput): unknown | undefined { switch (output.type) { case "none": return undefined; case "content_block": case "structured_tool_use_result": return output.value; default: return assertNever(output); } } function claudeNativeToolOutputText(output: ClaudeNativeToolOutput): string { const value = claudeNativeToolOutputValue(output); return typeof value === "string" ? value : value === undefined ? "" : jsonStringifyForTool(value); } const decodeClaudeFileEditResult = Schema.decodeUnknownOption( Schema.Struct({ type: Schema.optional(Schema.String), filePath: Schema.String, content: Schema.optional(Schema.String), structuredPatch: Schema.mutable( Schema.Array( Schema.Struct({ oldStart: Schema.Number, oldLines: Schema.Number, newStart: Schema.Number, newLines: Schema.Number, lines: Schema.mutable(Schema.Array(Schema.String)), }), ), ), }), ); function claudeFileChangeText(output: ClaudeNativeToolOutput): string { const result = output.type === "structured_tool_use_result" ? Option.getOrUndefined(decodeClaudeFileEditResult(output.value)) : undefined; if (result === undefined) return claudeNativeToolOutputText(output); const patch = result.structuredPatch.length === 0 && result.type === "create" && result.content !== undefined ? structuredPatch("/dev/null", result.filePath, "", result.content) : { oldFileName: result.filePath, newFileName: result.filePath, oldHeader: undefined, newHeader: undefined, hunks: result.structuredPatch, }; return patch.hunks.length === 0 ? "" : formatPatch(patch, FILE_HEADERS_ONLY); } function claudeCommandOutputText(output: ClaudeNativeToolOutput): string { const value = claudeNativeToolOutputValue(output); if (typeof value === "object" && value !== null) { const stdout = Reflect.get(value, "stdout"); const stderr = Reflect.get(value, "stderr"); if (typeof stdout === "string" || typeof stderr === "string") { const out = typeof stdout === "string" && stdout.trim().length > 0 ? stdout : ""; const err = typeof stderr === "string" && stderr.trim().length > 0 ? stderr : ""; // Claude trims each stream's trailing newline, so stderr may need one to start its line. const text = out.length > 0 && err.length > 0 && !out.endsWith("\n") ? `${out}\n${err}` : out + err; if (text.length > 0) return text; return output.type === "structured_tool_use_result" && output.fallbackValue !== undefined ? claudeSubagentResultText({ type: "content_block", value: output.fallbackValue }) : ""; } } return claudeNativeToolOutputText(output); } function claudeSubagentResultText(output: ClaudeNativeToolOutput): string { const value = claudeNativeToolOutputValue(output); const content = Array.isArray(value) ? value : typeof value === "object" && value !== null && "content" in value ? value.content : undefined; if (Array.isArray(content)) { const text = content .flatMap((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string" ? [part.text] : [], ) .join("\n"); if (text.length > 0) { return text; } } return claudeNativeToolOutputText(output); } function isClaudeSubagentAsyncLaunchAck(output: ClaudeNativeToolOutput): boolean { const value = claudeNativeToolOutputValue(output); if (typeof value === "object" && value !== null) { if ("isAsync" in value && value.isAsync === true) { return true; } if ("status" in value && value.status === "async_launched") { return true; } } return claudeSubagentResultText(output).startsWith("Async agent launched successfully."); } const WEB_FETCH_SNIPPET_MAX_CHARS = 8_000; function webSearchPatternsFromClaudeTool(input: { readonly toolInput: ClaudeNativeToolInput; readonly output: ClaudeNativeToolOutput; }): ReadonlyArray { const output = claudeNativeToolOutputValue(input.output); const pattern = firstStringInputField(input.toolInput, ["query", "url", "pattern"]) ?? (isClaudeWebSearchOutput(output) ? output.query : undefined); return pattern === undefined || pattern.trim().length === 0 ? [] : [pattern]; } function webSearchResultsFromClaudeOutput( output: ClaudeNativeToolOutput, ): ReadonlyArray { const value = claudeNativeToolOutputValue(output); if (!isClaudeWebSearchOutput(value)) { return []; } return value.results.flatMap((result) => { if (typeof result === "string") { return []; } return result.content.map((content) => ({ title: content.title, url: content.url, })); }); } function summarizeClaudeToolRequest(toolName: string, input: ClaudeNativeToolInput): string { const command = firstStringInputField(input, ["command", "cmd", "script"]); if (command !== undefined) { return `${toolName}: ${command.slice(0, 400)}`; } const path = firstStringInputField(input, ["file_path", "path", "filename", "fileName"]); if (path !== undefined) { return `${toolName}: ${path.slice(0, 400)}`; } const serialized = jsonStringifyForTool(claudeNativeToolInputValue(input)); return serialized.length <= 400 ? `${toolName}: ${serialized}` : `${toolName}: ${serialized.slice(0, 397)}...`; } function outputFromClaudeToolResult( toolResult: ClaudeToolResultContentBlock, ): ClaudeToolResultOutput | undefined { switch (toolResult.type) { case "tool_result": return toolResult.content; case "mcp_tool_result": return toolResult.content; case "bash_code_execution_tool_result": case "code_execution_tool_result": case "advisor_tool_result": case "text_editor_code_execution_tool_result": case "tool_search_tool_result": case "web_fetch_tool_result": case "web_search_tool_result": return toolResult.content; default: return assertNever(toolResult); } } function isClaudeTypedToolResultErrorContent(content: ClaudeTypedToolResultContent): boolean { if (Array.isArray(content)) { return false; } switch (content.type) { case "advisor_tool_result_error": case "bash_code_execution_tool_result_error": case "code_execution_tool_result_error": case "text_editor_code_execution_tool_result_error": case "tool_search_tool_result_error": case "web_fetch_tool_result_error": case "web_search_tool_result_error": return true; default: return false; } } function isClaudeToolResultError(toolResult: ClaudeToolResultContentBlock): boolean { switch (toolResult.type) { case "tool_result": return toolResult.is_error === true; case "mcp_tool_result": return toolResult.is_error; case "bash_code_execution_tool_result": case "code_execution_tool_result": case "advisor_tool_result": case "text_editor_code_execution_tool_result": case "tool_search_tool_result": case "web_fetch_tool_result": case "web_search_tool_result": return isClaudeTypedToolResultErrorContent(toolResult.content); default: return assertNever(toolResult); } } function toolNameFromClaudeToolResult(toolResult: ClaudeToolResultContentBlock): string { switch (toolResult.type) { case "bash_code_execution_tool_result": return "bash_code_execution"; case "code_execution_tool_result": return "code_execution"; case "advisor_tool_result": return "advisor"; case "mcp_tool_result": return "mcp_tool"; case "text_editor_code_execution_tool_result": return "text_editor_code_execution"; case "tool_result": return "tool"; case "tool_search_tool_result": return "tool_search"; case "web_fetch_tool_result": return "web_fetch"; case "web_search_tool_result": return "web_search"; default: return assertNever(toolResult); } } function isClaudeAssistantToolResultContentBlock( part: ClaudeAssistantContentBlock, ): part is ClaudeAssistantToolResultContentBlock { return "tool_use_id" in part && typeof part.tool_use_id === "string"; } function isClaudeUserToolResultContentBlock( part: ClaudeUserContentBlock, ): part is ClaudeUserToolResultContentBlock { return "tool_use_id" in part && typeof part.tool_use_id === "string"; } function isClaudeToolUseContentBlock( part: ClaudeAssistantContentBlock, ): part is ClaudeToolUseContentBlock { return ( "id" in part && typeof part.id === "string" && "name" in part && typeof part.name === "string" && "input" in part ); } function claudeToolUseBlocksFromAssistantMessage( message: SDKMessage, ): ReadonlyArray { if (message.type !== "assistant") { return []; } return message.message.content.filter(isClaudeToolUseContentBlock); } type ClaudeToolPresentation = ReturnType; function claudeToolPresentationsFromAssistantMessage( message: SDKMessage, ): ReadonlyMap { const presentations = new Map(); const meta = message.type === "assistant" ? Reflect.get(message, "tool_use_meta") : undefined; if (!Array.isArray(meta)) return presentations; const toolNames = new Map( claudeToolUseBlocksFromAssistantMessage(message).map((tool) => [ tool.id, tool.name.replace(/^mcp__claude_ai_/u, "mcp__"), ]), ); for (const entry of meta) { if (typeof entry !== "object" || entry === null) continue; const id = normalizeMcpText(Reflect.get(entry, "id"), 512); if (id === undefined) continue; presentations.set( id, mcpToolPresentation({ toolName: toolNames.get(id), title: Reflect.get(entry, "display_name"), serverDisplayName: Reflect.get(entry, "server_display_name"), iconUrl: Reflect.get(entry, "icon_url"), }), ); } return presentations; } function claudeToolResultBlocksFromAssistantMessage( message: SDKMessage, ): ReadonlyArray { if (message.type !== "assistant") { return []; } return message.message.content.filter(isClaudeAssistantToolResultContentBlock); } function claudeToolResultBlocksFromUserMessage( message: SDKMessage, ): ReadonlyArray { if (message.type !== "user" || typeof message.message.content === "string") { return []; } return message.message.content.filter(isClaudeUserToolResultContentBlock); } function claudeToolResultEntriesFromMessage(message: SDKMessage): ReadonlyArray<{ readonly toolResult: ClaudeToolResultContentBlock; readonly output: ClaudeNativeToolOutput; }> { const assistantResults = claudeToolResultBlocksFromAssistantMessage(message).map( (toolResult) => ({ toolResult, output: claudeNativeToolOutputFromToolResult(toolResult) }), ); const userResults = claudeToolResultBlocksFromUserMessage(message); const structuredOutput = message.type === "user" && userResults.length === 1 ? message.tool_use_result : undefined; return [ ...assistantResults, ...userResults.map((toolResult) => ({ toolResult, output: structuredOutput === undefined ? claudeNativeToolOutputFromToolResult(toolResult) : claudeNativeToolOutputFromStructuredResult({ structuredOutput, fallbackValue: outputFromClaudeToolResult(toolResult), }), })), ]; } const ClaudeToolResultMetadata = Schema.Struct({ tool_result_meta: Schema.Array( Schema.Struct({ id: Schema.String, non_execution_kind: Schema.optional(Schema.NullOr(Schema.String)), }), ), }); const isClaudeToolResultMetadata = Schema.is(ClaudeToolResultMetadata); function claudeToolNonExecutionKind(message: SDKMessage, toolUseId: string) { return isClaudeToolResultMetadata(message) ? (message.tool_result_meta.find((meta) => meta.id === toolUseId)?.non_execution_kind ?? undefined) : undefined; } function parentToolUseIdFromSdkMessage(message: SDKMessage): string | null { return message.type === "assistant" || message.type === "user" ? message.parent_tool_use_id : null; } const CLAUDE_NO_STANDING_GRANT_OPTIONS = [ { decision: "decline", label: "Decline" }, { decision: "accept", label: "Approve" }, ] satisfies ReadonlyArray; export function claudeApprovalPresentation(input: { readonly permissionMode: PermissionMode; readonly prompt: string; readonly callbackOptions: Pick< Parameters[2], "decisionReason" | "matchedAskRule" | "suppressAlwaysAllowRule" >; }): { readonly reason?: string; readonly fullAccessOverride?: ProviderApprovalFullAccessOverride; readonly options?: ReadonlyArray; } { // The reason can carry terminal escapes from the CLI's own prompt renderer. const reason = NodeUtil.stripVTControlCharacters( input.callbackOptions.decisionReason ?? "", ).trim(); // Bypass auto-allows everything it can, so any ask that still reaches the // callback is one bypass refused. The SDK drops the CLI's decision_reason_type, // leaving the matched ask rule as the only way to tell the two causes apart. const fullAccessOverride: ProviderApprovalFullAccessOverride | undefined = input.permissionMode !== "bypassPermissions" ? undefined : input.callbackOptions.matchedAskRule === undefined ? "provider_check" : "ask_rule"; return { ...(reason === "" || reason === input.prompt ? {} : { reason }), ...(fullAccessOverride === undefined ? {} : { fullAccessOverride }), ...(input.callbackOptions.suppressAlwaysAllowRule === true ? { options: CLAUDE_NO_STANDING_GRANT_OPTIONS } : {}), }; } export function permissionResultFromDecision(input: { readonly toolName: string; readonly decision: ProviderApprovalDecision; readonly toolInput: Record; readonly toolUseID: string; readonly suggestions?: Parameters[2]["suggestions"]; readonly feedback?: string; /** Under an administrator's ceiling, T3 Code's mode is the only one; Claude cannot switch it. */ readonly managedCeiling?: boolean; }): PermissionResult { if (input.decision === "accept" || input.decision === "acceptForSession") { return { behavior: "allow", updatedInput: input.toolInput, toolUseID: input.toolUseID, decisionClassification: input.decision === "acceptForSession" ? "user_permanent" : "user_temporary", ...(input.decision === "acceptForSession" ? { updatedPermissions: toSessionPermissionUpdates( input.toolName, input.managedCeiling === true ? input.suggestions?.filter((suggestion) => suggestion.type !== "setMode") : input.suggestions, ), } : {}), }; } return { behavior: "deny", message: input.feedback?.trim() || (input.decision === "cancel" ? "User cancelled tool execution." : "User declined tool execution."), toolUseID: input.toolUseID, decisionClassification: "user_reject", ...(input.decision === "cancel" ? { interrupt: true } : {}), }; } function toSessionPermissionUpdates( toolName: string, suggestions: ReadonlyArray | undefined, ): Array { const updates = (suggestions ?? []).map((suggestion): PermissionUpdate => ({ ...suggestion, destination: "session", })); if (updates.length > 0) { return updates; } return [ { type: "addRules", rules: [{ toolName }], behavior: "allow", destination: "session", }, ]; } export const awaitClaudeApprovalDecision = Effect.fn("awaitClaudeApprovalDecision")(function* ( decision: Deferred.Deferred, signal: AbortSignal, ) { const cancellation = Effect.callback((resume) => { const abort = () => resume(Effect.succeed({ decision: "cancel" })); signal.addEventListener("abort", abort, { once: true }); if (signal.aborted) { abort(); } return Effect.sync(() => signal.removeEventListener("abort", abort)); }); return yield* Effect.raceFirst(Deferred.await(decision), cancellation); }); // Web and mobile offer a session grant when options are omitted, and this // path only ever answers once. const CLAUDE_MCP_ELICITATION_OPTIONS: ReadonlyArray = [ { decision: "cancel", label: "Cancel" }, { decision: "decline", label: "Decline" }, { decision: "accept", label: "Approve" }, ]; type ClaudeMcpElicitationContent = NonNullable; const isClaudeMcpElicitationValue = ( value: unknown, ): value is ClaudeMcpElicitationContent[string] => typeof value === "string" || typeof value === "number" || typeof value === "boolean" || (Array.isArray(value) && value.every((item) => typeof item === "string")); const withoutSchemaDefaults = (schema: Record): Record => { const properties = schema.properties; if (typeof properties !== "object" || properties === null || Array.isArray(properties)) { return schema; } return { ...schema, properties: Object.fromEntries( Object.entries(properties).map(([key, field]) => [ key, typeof field === "object" && field !== null ? Object.fromEntries(Object.entries(field).filter(([name]) => name !== "default")) : field, ]), ), }; }; // The card collects consent only; schema defaults must never grant unseen permissions. export function claudeMcpElicitationAcceptance( request: ElicitationRequest, ): ElicitationResult | undefined { if (request.mode === "url" || request.requestedSchema === undefined) return undefined; const response = toMcpElicitationResponse( { mode: "form", requestedSchema: withoutSchemaDefaults(request.requestedSchema) }, "accept", ); // The helper leaves content out when the schema is not a form it can read. const filled = response.content; if ( response.action !== "accept" || typeof filled !== "object" || filled === null || Array.isArray(filled) ) { return undefined; } const content: ClaudeMcpElicitationContent = {}; for (const [key, value] of Object.entries(filled)) { if (!isClaudeMcpElicitationValue(value)) return undefined; content[key] = value; } return { action: "accept", content }; } const awaitClaudeUserInputAnswers = Effect.fn("awaitClaudeUserInputAnswers")(function* ( answers: Deferred.Deferred, signal: AbortSignal, ) { const cancellation = Effect.callback((resume) => { const abort = () => resume(Effect.succeed({})); signal.addEventListener("abort", abort, { once: true }); if (signal.aborted) abort(); return Effect.sync(() => signal.removeEventListener("abort", abort)); }); return yield* Effect.raceFirst(Deferred.await(answers), cancellation); }); /** Exclude CLI-internal "[ede_diagnostic] ..." telemetry from error banners. */ function resultUserFacingError(result: SDKResultMessage): string | undefined { const errors = "errors" in result && Array.isArray(result.errors) ? result.errors : []; if (result.subtype === "success" && !result.is_error) { return undefined; } return errors.find( (error): error is string => typeof error === "string" && !error.startsWith("[ede_diagnostic]"), ); } const CONTEXT_WINDOW_FULL_ERROR = "Claude stopped: the prompt exceeds the model's context window. Run /compact or start a new thread."; function terminalResultError( reason: SDKResultMessage["terminal_reason"], failureHint?: string, ): string | undefined { switch (reason) { case "api_error": return failureHint ?? "Claude gave up after repeated API errors."; case "malformed_tool_use_exhausted": return "Claude gave up after repeated malformed tool calls."; case "budget_exhausted": return "Claude stopped: the turn's token budget was exhausted."; case "structured_output_retry_exhausted": return "Claude could not produce the requested structured output."; case "tool_deferred_unavailable": return "Claude could not resume a deferred tool call: the tool is no longer available."; case "turn_setup_failed": return "Claude could not start the turn."; case "blocking_limit": // blocking_limit also means context exhaustion; only rate-limit evidence sets the hint. return failureHint ?? CONTEXT_WINDOW_FULL_ERROR; case "rapid_refill_breaker": return "Claude stopped: the context refilled too quickly after compaction."; case "prompt_too_long": return CONTEXT_WINDOW_FULL_ERROR; case "image_error": return "Claude stopped: an image in the conversation could not be processed."; case "model_error": return "Claude stopped: the model returned an error."; default: return undefined; } } function isOverloadedResult(result: SDKResultMessage): boolean { return result.subtype === "success" && result.api_error_status === 529; } function terminalStatusFromResult( message: SDKResultMessage, failureHint?: string, ): Extract< OrchestrationV2ProviderTurn["status"], "completed" | "interrupted" | "failed" | "cancelled" > { // Explicit abort reasons override success envelopes with is_error=false. if ( message.terminal_reason === "aborted_tools" || message.terminal_reason === "aborted_streaming" ) { return "interrupted"; } if (message.subtype === "success") { // The SDK can report API failures as subtype "success" with is_error set. return isOverloadedResult(message) || message.api_error_status === 429 || terminalResultError(message.terminal_reason, failureHint) !== undefined || (message.is_error && failureHint !== undefined) ? "failed" : "completed"; } const errorText = message.errors.join("\n").toLowerCase(); if (errorText.includes("interrupt")) { return "interrupted"; } if (errorText.includes("cancel")) { return "cancelled"; } return "failed"; } function isClaudeActiveSteeringAbortResult(message: SDKResultMessage): boolean { return ( message.terminal_reason === "aborted_streaming" || message.terminal_reason === "aborted_tools" ); } function isClaudeProviderContinuationTurn( input: ProviderAdapter.ProviderAdapterV2TurnInput, ): boolean { return input.message.createdBy === "agent" && input.message.creationSource === "provider"; } function isClaudePromptEchoGatedFrame(message: SDKMessage): boolean { switch (message.type) { case "assistant": case "stream_event": case "user": return message.parent_tool_use_id === null; case "result": return true; default: return false; } } function claudeFrameToolUseIds(message: SDKMessage): ReadonlyArray { const ids: Array = []; const parent = Reflect.get(message, "parent_tool_use_id"); if (typeof parent === "string") { ids.push(parent); } if (message.type === "system") { const toolUseId = Reflect.get(message, "tool_use_id"); if (typeof toolUseId === "string") { ids.push(toolUseId); } } return ids; } function claudeEchoedPromptUuids(message: SDKMessage): ReadonlyArray { const uuids = Reflect.get(message, "user_message_uuids"); if (Array.isArray(uuids)) { return uuids.filter((uuid): uuid is string => typeof uuid === "string"); } const uuid = Reflect.get(message, "user_message_uuid"); return typeof uuid === "string" ? [uuid] : []; } function claudePromptLifecycle( message: SDKMessage, ): { readonly uuid: string; readonly state: string } | null { const type: unknown = Reflect.get(message, "type"); if (type !== "command_lifecycle") return null; const uuid: unknown = Reflect.get(message, "command_uuid"); const state: unknown = Reflect.get(message, "state"); return typeof uuid === "string" && typeof state === "string" ? { uuid, state } : null; } // Without an echo, non-human origin proves a foreign turn only on CLIs known to echo prompts. function isClaudeResultForOtherTurn(input: { readonly message: SDKResultMessage; readonly promptUuid: string; readonly promptEchoMode: ClaudeLiveQueryContext["promptEchoMode"]; }): boolean { const echoed = claudeEchoedPromptUuids(input.message); if (echoed.length > 0) return !echoed.includes(input.promptUuid); return ( input.promptEchoMode !== "unknown" && input.message.origin !== undefined && input.message.origin.kind !== "human" ); } function isClaudeTaskNotificationOriginResult(message: SDKMessage): message is SDKResultMessage & { readonly origin: Extract< NonNullable, { readonly kind: "task-notification" } >; } { return message.type === "result" && message.origin?.kind === "task-notification"; } // The CLI throttles unchanged rate_limit_event frames, but repeats this // undeclared payload on each blocked assistant response. const isClaudeBlockedAssistantRateLimit = Schema.is( Schema.Struct({ api_error_params: Schema.Struct({ rate_limit_info: Schema.Struct({ status: Schema.Literal("rejected"), rateLimitType: Schema.optionalKey(Schema.Unknown), resetsAt: Schema.optionalKey(Schema.Unknown), overageStatus: Schema.optionalKey(Schema.String), isUsingOverage: Schema.optionalKey(Schema.Boolean), overageInUse: Schema.optionalKey(Schema.Boolean), }), }), }), ); function claudeUsageLimitResetAt(resetsAt: unknown): string | null { const resetMs = typeof resetsAt === "number" ? resetsAt * 1000 : NaN; return Number.isFinite(resetMs) && resetMs > 0 && resetMs < 8.64e15 ? DateTime.formatIso(DateTime.makeUnsafe(resetMs)) : null; } function gatewayCooldownResetAt( cooldown: RegExpExecArray | null, completedAt: DateTime.Utc, ): string | undefined { if (cooldown === null) return undefined; const seconds = Number(cooldown[1] ?? 0) * 3_600 + Number(cooldown[2] ?? 0) * 60 + Number(cooldown[3] ?? 0); const resetMs = DateTime.toEpochMillis(completedAt) + seconds * 1_000; return seconds > 0 && resetMs < 8.64e15 ? DateTime.formatIso(DateTime.makeUnsafe(resetMs)) : undefined; } function providerFailureFromResult( message: SDKResultMessage, receivedAt: DateTime.Utc, failureHint?: string, usageLimited = false, ): OrchestrationV2ProviderFailure | null { const gatewayResetAt = failureHint === undefined && message.subtype === "success" && message.is_error && message.terminal_reason === "api_error" && message.api_error_status === 503 ? gatewayCooldownResetAt( /^API Error: 503 \[[^\]\r\n]+\] \[429\]: \{"type":"error","error":\{"type":"rate_limit_error",[^\r\n]*\} \(reset after (?:(\d+)h ?)?(?:(\d+)m ?)?(?:(\d+)s)?\)/.exec( message.result, ), receivedAt, ) : undefined; if (gatewayResetAt !== undefined) { return makeProviderFailure({ class: "usage_limit", message: "Claude API rate limit reached. Try again later.", code: "api_error_429", retryable: true, resetAt: gatewayResetAt, }); } const failureClass = (message.subtype === "success" && message.api_error_status === 429) || usageLimited ? "usage_limit" : "provider_error"; const listedError = resultUserFacingError(message); const apiErrorMessage = message.subtype === "success" && message.terminal_reason === "api_error" && message.result.trim().startsWith("API Error:") ? makeProviderFailure({ message: message.result }).message.slice(0, 500) : undefined; const structuredError = isOverloadedResult(message) ? "Claude API is overloaded (529). Try again shortly." : message.subtype === "success" && message.api_error_status === 429 ? "Claude API rate limit reached. Try again later." : terminalResultError(message.terminal_reason, failureHint ?? apiErrorMessage); if (message.subtype !== "success") { return makeProviderFailure({ message: listedError ?? structuredError ?? message.errors.join("\n"), code: message.subtype, class: failureClass, }); } if (!message.is_error && structuredError === undefined) { return null; } const apiErrorStatus = message.api_error_status ?? null; // API-key sessions get no rate_limit_info, so a gateway's 429 carries its reset only as text. const resetAt = apiErrorStatus === 429 && message.result.startsWith("API Error:") ? gatewayCooldownResetAt( /\(reset after (?:(\d+)h ?)?(?:(\d+)m ?)?(?:(\d+)s)?\)/.exec(message.result), receivedAt, ) : undefined; return makeProviderFailure({ message: listedError ?? structuredError ?? failureHint ?? message.result, code: apiErrorStatus === null ? (message.terminal_reason ?? "sdk_result_error") : `api_error_${apiErrorStatus}`, class: failureClass, retryable: apiErrorStatus === 429 || apiErrorStatus === 529 ? true : null, resetAt: resetAt ?? null, }); } function providerFailureFromApiRetry(message: SDKAPIRetryMessage): OrchestrationV2ProviderFailure { const errorName = message.error.replaceAll("_", " "); return makeProviderFailure({ message: `Claude API ${errorName}.`, code: message.error_status === null ? message.error : `api_error_${Math.trunc(message.error_status)}`, class: message.error_status === 429 ? "usage_limit" : message.error_status === null ? "transport_error" : "provider_error", retryable: true, }); } function buildAssistantArtifacts(input: { readonly idAllocator: IdAllocator.IdAllocatorV2["Service"]; readonly turnInput: ProviderAdapter.ProviderAdapterV2TurnInput; readonly providerTurnId: OrchestrationV2ProviderTurn["id"]; readonly nativeItemId: string; readonly text: string; readonly ordinal: number; readonly startedAt: DateTime.Utc; readonly completedAt: DateTime.Utc; }): { readonly node: OrchestrationV2ExecutionNode; readonly message: OrchestrationV2ConversationMessage; readonly turnItem: OrchestrationV2TurnItem; } { const nodeId = input.idAllocator.derive.nodeFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: input.nativeItemId, }); const messageId = input.idAllocator.derive.messageFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: input.nativeItemId, }); const turnItemId = input.idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: input.nativeItemId, }); const nativeItemRef = { driver: CLAUDE_PROVIDER, nativeId: input.nativeItemId, strength: "strong" as const, }; return { node: { id: nodeId, threadId: input.turnInput.threadId, runId: input.turnInput.runId, parentNodeId: input.turnInput.rootNodeId, rootNodeId: input.turnInput.rootNodeId, kind: "assistant_message", status: "completed", countsForRun: false, providerThreadId: input.turnInput.providerThread.id, providerTurnId: input.providerTurnId, nativeItemRef, runtimeRequestId: null, checkpointScopeId: null, startedAt: input.startedAt, completedAt: input.completedAt, }, message: { createdBy: "agent", creationSource: "provider", id: messageId, threadId: input.turnInput.threadId, runId: input.turnInput.runId, nodeId, role: "assistant", text: input.text, attachments: [], streaming: false, createdAt: input.completedAt, updatedAt: input.completedAt, }, turnItem: { id: turnItemId, threadId: input.turnInput.threadId, runId: input.turnInput.runId, nodeId, providerThreadId: input.turnInput.providerThread.id, providerTurnId: input.providerTurnId, nativeItemRef, parentItemId: null, ordinal: input.ordinal, status: "completed", title: null, startedAt: input.startedAt, completedAt: input.completedAt, updatedAt: input.completedAt, type: "assistant_message", messageId, text: input.text, streaming: false, }, }; } const CLAUDE_USAGE_LIMIT_WINDOWS = { five_hour: "5-hour", seven_day: "7-day", seven_day_opus: "7-day Opus", seven_day_sonnet: "7-day Sonnet", seven_day_overage_included: "7-day model", overage: "overage", } satisfies Record, string>; const CLAUDE_USAGE_LIMIT_MAX_WAIT_MS = 30 * 24 * 60 * 60 * 1000; function describeClaudeUsageLimit( info: SDKRateLimitInfo, nowMs: number, names: ClaudeScopedLimitNames, ): string { const label = info.rateLimitType === "seven_day_overage_included" && names.overageIncluded ? `7-day ${names.overageIncluded}` : info.rateLimitType ? CLAUDE_USAGE_LIMIT_WINDOWS[info.rateLimitType] : undefined; const resetsAtMs = info.resetsAt === undefined ? undefined : info.resetsAt * 1000; const waitMs = resetsAtMs === undefined || !Number.isFinite(nowMs) ? undefined : resetsAtMs - nowMs; const wait = waitMs !== undefined && waitMs > 0 && waitMs <= CLAUDE_USAGE_LIMIT_MAX_WAIT_MS ? formatClaudeUsageLimitWait(waitMs) : undefined; return `Claude usage limit reached. This turn is paused until the ${ label ? `${label} ` : "" }limit resets${wait ? ` in ${wait}` : ""}.`; } function formatClaudeUsageLimitWait(waitMs: number): string { const totalMinutes = Math.ceil(waitMs / 60_000); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; if (hours === 0) return `${totalMinutes}m`; return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`; } interface ActiveClaudeTurnContext { readonly queryPolicy: ClaudeRuntimeQueryPolicy; readonly input: ProviderAdapter.ProviderAdapterV2TurnInput; readonly nativeThreadId: string; readonly nativeTurnId: string; nativeMessageCursor: string | null; readonly providerTurnId: OrchestrationV2ProviderTurn["id"]; readonly providerTurnOrdinal: number; readonly startedAt: DateTime.Utc; readonly itemOrdinals: Map; readonly assistant: { 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; readonly streamBlocks: Map; readonly nextBlockIndex: Map; readonly snapshotBlockIndex: Map; readonly snapshots: Set; lastThinking: { readonly messageId: string; readonly itemId: string; readonly text: string; readonly followsThinking: boolean; } | null; readonly blocks: Map< string, { readonly startedAt: DateTime.Utc; readonly ordinal: number; text: string } >; }; // Snapshots repeat message_start input, sometimes as zeros, so only fill gaps in streamed usage. // Compaction clears usage without forgetting the root request. // `requestedAt` starts the request's prompt cache lifetime. streamUsage: { readonly messageId: string; requestedAt?: DateTime.Utc; usage: ClaudeMessageUsage | null; } | null; compactionTokens: number | undefined; readonly toolCalls: Map; // A successful status can precede the boundary that supplies its token counts. compaction: Extract | null; readonly ignoredTaskIds: Set; readonly announcedUsageLimits: Set; authenticationFailureMessage: string | undefined; readonly rejectedRateLimitTypes: Set; readonly rateLimitResetTimes: Map; latestAssistantRateLimit: { readonly type: string | null; readonly resetAt: string | null; } | null; // Set while this turn's latest compaction status reports a failure, with // Claude's error text when it sent one. compactionFailure: { readonly message: string | undefined } | null; // Steers end segments whose usage is excluded from the final result; bill them too, oldest first. readonly steeredSegmentUsage: Array; // Ledger growth of the steered segments; undefined once any segment's growth is unknown. steeredLedgerUsage: ClaudeLedgerUsage | undefined; readonly rootModels: Set; readonly subagentsByTaskId: Map; readonly subagentsByToolUseId: Map; readonly promptUuid: string | null; promptEcho: "pending" | "confirmed"; promptStarted: boolean; nativeWakeStarted: boolean; gatedFramesBeforeEcho: number; readonly heldRootFrames: Array; } interface ActiveClaudeProviderRetry { readonly retry: OrchestrationV2ProviderRetry; readonly failure: OrchestrationV2ProviderFailure; readonly startedAt: DateTime.Utc; readonly itemOrdinal: number; } interface ActiveClaudeSubagent { task: OrchestrationV2Subagent; readonly nativeTaskType: string | undefined; readonly confirmedOutcome: ProviderAdapter.ProviderAdapterV2SubagentOutcome | undefined; readonly rootNodeId: OrchestrationV2ExecutionNode["id"]; readonly childThreadId: ThreadId; childThread: OrchestrationV2AppThread | null; readonly childRootNodeId: OrchestrationV2ExecutionNode["id"]; readonly turnItemId: OrchestrationV2TurnItem["id"]; readonly turnItemOrdinal: number; readonly runToolUseId: string | null; readonly effort: string | undefined; nextChildItemOrdinal: number; resultItemOrdinal: number | null; lastAssistantText: string | null; lastAssistantMessageId: string | null; } interface ClaudeLiveQueryContext { readonly queryPolicy: ClaudeRuntimeQueryPolicy; readonly nativeThreadId: string; readonly query: ClaudeAgentSdkQuerySession; readonly queryPolicyKey: string; selection: CompiledClaudeModelSelection; readonly closed: Deferred.Deferred; lastTurn: ActiveClaudeTurnContext | null; // "early" echoes on the first frame; "acknowledged" confirms uuid acceptance, but resume turns can still precede the prompt. promptEchoMode: "unknown" | "acknowledged" | "early" | "result_only"; // The CLI may fall back from the requested mode on its first init frame. openedPermissionMode: PermissionMode; receivedInit: boolean; permissionMode: PermissionMode; stopping: boolean; readonly subagentsFromEarlierProcesses: ReadonlySet; // Subagents this process still ran when a replacement took over; only its exit can end them. handedOffSubagents: ReadonlySet; // The SDK's per-process model ledger at the last result; a new process starts at zero. modelLedger: ClaudeModelLedger | undefined; // Per process, not per session: a relative temp dir follows the cwd this CLI // was launched in, and a later turn's cwd can differ. readonly taskOutputRoot: string | undefined; } interface ActiveClaudeToolCall { readonly nativeItemId: string; readonly toolName: string; readonly classification: ClaudeToolClassification; readonly input: ClaudeNativeToolInput; readonly threadId: ThreadId; readonly runId: ProviderAdapter.ProviderAdapterV2TurnInput["runId"] | null; readonly rootNodeId: OrchestrationV2ExecutionNode["id"]; readonly parentNodeId: OrchestrationV2ExecutionNode["id"]; readonly ordinal: number; readonly startedAt: DateTime.Utc; readonly presentation?: ClaudeToolPresentation; } interface ClaudeSubagentProjectionTarget { readonly childThreadId: ThreadId; readonly childRootNodeId: OrchestrationV2ExecutionNode["id"]; readonly nextOrdinal: () => number; readonly toolCalls: Map; } /** Claude writes blocks separately with stop_reason null until the last. * Missing stop_reason keeps the turn open; pre-tool text is not the reply. */ function isClaudeTranscriptTurnEnd(entry: unknown): boolean { if (field(entry, "type") !== "assistant") return false; const stopReason = field(field(entry, "message"), "stop_reason"); return typeof stopReason === "string" && stopReason !== "tool_use"; } function isClaudeTranscriptConversationLine( entry: unknown, ): entry is SDKAssistantMessage | SDKUserMessage { const type = field(entry, "type"); const content = field(field(entry, "message"), "content"); return ( typeof field(entry, "uuid") === "string" && ((type === "assistant" && Array.isArray(content)) || (type === "user" && (Array.isArray(content) || typeof content === "string"))) ); } interface PendingClaudeSubagentLaunch { readonly model?: string; readonly effort?: string; readonly ownerToolUseId?: string; } function canonicalClaudeSubagentModel(catalog: ClaudeModelCatalog, model: string): string { return resolveClaudeModelSlug(catalog, model); } function claudeSubagentModelSelection( catalog: ClaudeModelCatalog, instanceId: ModelSelection["instanceId"], model: string, effort: string | undefined, ): ModelSelection { const descriptor = getClaudeCatalogModelCapabilities(catalog, model).optionDescriptors?.find( (candidate) => candidate.id === "effort", ); const offered = effort !== undefined && descriptor?.type === "select" && descriptor.options.some((option) => option.id === effort); return { instanceId, model, ...(offered ? { options: [{ id: "effort", value: effort }] } : {}) }; } const PENDING_CLAUDE_SUBAGENT_CAP = 64; const PENDING_CLAUDE_SUBAGENT_FRAME_CAP = 256; function rememberPendingClaudeSubagentLaunch( pending: Map, toolUseId: string, launch: PendingClaudeSubagentLaunch, ): void { pending.set(toolUseId, { ...pending.get(toolUseId), ...launch }); if (pending.size <= PENDING_CLAUDE_SUBAGENT_CAP) { return; } const oldest = pending.keys().next(); if (!oldest.done) { pending.delete(oldest.value); } } function rememberClaudeSubagentLaunch( catalog: ClaudeModelCatalog, context: ActiveClaudeTurnContext, pending: Map, toolUseId: string, input: ClaudeNativeToolInput, ownerToolUseId: string | null, ): void { if (context.subagentsByToolUseId.has(toolUseId)) return; const requested = firstStringInputField(input, ["model"]); const model = requested !== "inherit" ? requested && canonicalClaudeSubagentModel(catalog, requested) : ownerToolUseId === null ? context.input.modelSelection.model : undefined; const effort = firstStringInputField(input, ["effort"]); const launch: PendingClaudeSubagentLaunch = { ...(model === undefined || pending.get(toolUseId)?.model !== undefined ? {} : { model }), ...(effort === undefined ? {} : { effort }), ...(ownerToolUseId === null ? {} : { ownerToolUseId }), }; if ( launch.model !== undefined || launch.effort !== undefined || launch.ownerToolUseId !== undefined ) { rememberPendingClaudeSubagentLaunch(pending, toolUseId, launch); } } type ClaudeApprovalResponse = { readonly decision: ProviderApprovalDecision; readonly feedback?: string; }; type PendingClaudeRuntimeRequest = | { readonly type: "approval"; readonly requestKind: ProviderRequestKind; readonly decision: Deferred.Deferred; } | { readonly type: "user_input"; readonly answers: Deferred.Deferred; }; export function claudeUserInputQuestions( input: unknown, ): ReadonlyArray { const value = typeof input === "object" && input !== null && Reflect.get(input, "type") === "record" ? Reflect.get(input, "value") : input; const questions = typeof value === "object" && value !== null ? Reflect.get(value, "questions") : undefined; if (!Array.isArray(questions)) return []; return questions.flatMap((value: unknown, index: number) => { if (typeof value !== "object" || value === null) return []; const record = value as Record; const question = typeof record.question === "string" ? record.question.trim() : ""; if (question.length === 0) return []; const header = typeof record.header === "string" && record.header.trim().length > 0 ? record.header.trim() : `Question ${index + 1}`; const options = Array.isArray(record.options) ? record.options.flatMap((option) => { if (typeof option !== "object" || option === null) return []; const optionRecord = option as Record; const label = typeof optionRecord.label === "string" ? optionRecord.label.trim() : ""; if (label.length === 0) return []; const description = typeof optionRecord.description === "string" ? optionRecord.description.trim() : ""; return [ { label, description: description || label, ...(typeof optionRecord.preview === "string" ? { preview: optionRecord.preview } : {}), }, ]; }) : []; return [ { id: question, header, question, options, multiSelect: record.multiSelect === true, }, ]; }); } export function claudeSdkUserInputAnswers( answers: ProviderUserInputAnswers, ): NonNullable { return Object.fromEntries( Object.entries(answers).map(([question, answer]) => [ question, Array.isArray(answer) ? answer.filter((value): value is string => typeof value === "string").join(", ") : typeof answer === "string" ? answer : String(answer ?? ""), ]), ); } function claudeTodoSteps(input: unknown): ReadonlyArray { const value = typeof input === "object" && input !== null && Reflect.get(input, "type") === "record" ? Reflect.get(input, "value") : input; const todos = typeof value === "object" && value !== null ? Reflect.get(value, "todos") : undefined; if (!Array.isArray(todos)) return []; return todos.flatMap((todo, index) => { if (typeof todo !== "object" || todo === null) return []; const text = typeof Reflect.get(todo, "content") === "string" ? String(Reflect.get(todo, "content")).trim() : ""; if (text.length === 0) return []; const nativeStatus = Reflect.get(todo, "status"); return [ { id: `todo-${index}`, text, status: nativeStatus === "completed" ? "completed" : nativeStatus === "in_progress" ? "running" : "pending", }, ]; }); } type ClaudeTaskList = ReadonlyMap>; function claudeTaskField(value: unknown, key: string): unknown { return typeof value === "object" && value !== null ? Reflect.get(value, key) : undefined; } function claudeTaskString(value: unknown, key: string): string | undefined { const field = claudeTaskField(value, key); return typeof field === "string" && field.trim().length > 0 ? field.trim() : undefined; } // A Claude Code mod can hold a tool call until the user acts in its terminal // pane. T3 cannot provide the requested pane interaction. // Passive panes never request focus and keep working. function claudeFocusRequestedPanePlugin(message: SDKMessage): string | null { if (message.type !== "system" || claudeTaskField(message, "subtype") !== "ui_panes") return null; const paneId = claudeTaskString(message, "focus_requested_id"); if (paneId === undefined) return null; const panes = claudeTaskField(message, "panes"); const pane = Array.isArray(panes) ? panes.find((entry) => claudeTaskField(entry, "id") === paneId) : undefined; return claudeTaskString(pane, "plugin") ?? paneId; } function claudeTaskStatus(value: unknown): OrchestrationV2PlanStep["status"] | undefined { switch (value) { case "pending": return "pending"; case "in_progress": return "running"; case "completed": return "completed"; default: return undefined; } } function applyClaudeTaskTool( tasks: ClaudeTaskList, toolName: string, input: unknown, result: unknown, ): ClaudeTaskList | undefined { if (toolName === "TaskList") { const listed = claudeTaskField(result, "tasks"); if (!Array.isArray(listed)) return undefined; const next = new Map>(); for (const task of listed) { const id = claudeTaskString(task, "id"); const text = claudeTaskString(task, "subject"); const status = claudeTaskStatus(claudeTaskField(task, "status")); if (id !== undefined && text !== undefined && status !== undefined) { next.set(id, { text, status }); } } return next.size === 0 && tasks.size === 0 ? undefined : next; } if (toolName === "TaskCreate") { const created = claudeTaskField(result, "task"); const fromText = claudeCreatedTaskFromText(result); const id = claudeTaskString(created, "id") ?? fromText?.id; const text = claudeTaskString(created, "subject") ?? fromText?.subject ?? claudeTaskString(input, "subject"); if (id === undefined || text === undefined) return undefined; return new Map(tasks).set(id, { text, status: "pending" }); } if ( toolName !== "TaskUpdate" || claudeTaskField(result, "success") === false || (typeof result === "string" && !result.startsWith("Updated task #")) ) { return undefined; } const id = claudeTaskString(input, "taskId") ?? claudeTaskString(result, "taskId"); const task = id === undefined ? undefined : tasks.get(id); if (id === undefined || task === undefined) return undefined; const nativeStatus = claudeTaskField(input, "status"); if (nativeStatus === "deleted") { const next = new Map(tasks); next.delete(id); return next; } const text = claudeTaskString(input, "subject") ?? task.text; const status = claudeTaskStatus(nativeStatus) ?? task.status; if (text === task.text && status === task.status) return undefined; return new Map(tasks).set(id, { text, status }); } // Subagent results carry no structured `tool_use_result`, only the text // "Task #3 created successfully: ". function claudeCreatedTaskFromText( result: unknown, ): { readonly id: string; readonly subject: string } | undefined { const match = typeof result === "string" ? /^Task #(\S+) created successfully: (.+)$/s.exec(result) : null; return match === null ? undefined : { id: match[1]!, subject: match[2]!.trim() }; } const CLAUDE_TASK_TOOL_NAMES = new Set(["TaskCreate", "TaskUpdate", "TaskList"]); const decodeClaudeTaskFile = Schema.decodeUnknownOption( Schema.fromJsonString( Schema.Struct({ id: TrimmedNonEmptyString, subject: TrimmedNonEmptyString, status: Schema.Literals(["pending", "in_progress", "completed"]), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }), ), ); const readClaudeTaskList = Effect.fnUntraced( function* (input: { readonly environment: NodeJS.ProcessEnv; readonly cwd: string | null | undefined; readonly nativeThreadId: string; }) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const configDirOverride = input.environment.CLAUDE_CONFIG_DIR?.trim(); const configDir = configDirOverride ? path.resolve(input.cwd ?? ".", configDirOverride) : yield* resolveClaudeHomePath({ homePath: "" }); const listId = input.environment.CLAUDE_CODE_TASK_LIST_ID?.trim() || input.nativeThreadId; const directory = path.join(configDir, "tasks", listId.replace(/[^a-zA-Z0-9_-]/g, "-")); const tasks = new Map>(); const names = (yield* fileSystem.readDirectory(directory)) .filter((name) => /^\d+\.json$/.test(name)) .toSorted((left, right) => Number.parseInt(left, 10) - Number.parseInt(right, 10)); for (const name of names) { const contents = yield* fileSystem .readFileString(path.join(directory, name)) .pipe(Effect.orElseSucceed(() => "")); const task = Option.getOrUndefined(decodeClaudeTaskFile(contents)); if (task === undefined || task.metadata?._internal) continue; tasks.set(task.id, { text: task.subject, status: task.status === "in_progress" ? "running" : task.status, }); } return tasks; }, Effect.orElseSucceed((): ClaudeTaskList | undefined => undefined), ); function claudeTaskSteps(tasks: ClaudeTaskList): ReadonlyArray { return [...tasks].map(([id, task]) => ({ id: `task-${id}`, ...task })); } function claudeProposedPlan(input: unknown): string | null { const value = typeof input === "object" && input !== null && Reflect.get(input, "type") === "record" ? Reflect.get(input, "value") : input; const plan = typeof value === "object" && value !== null ? Reflect.get(value, "plan") : undefined; return typeof plan === "string" && plan.trim().length > 0 ? plan.trim() : null; } export interface ClaudeAdapterV2Options { readonly instanceId: ProviderInstanceId; readonly settings: ClaudeSettings; readonly environment: NodeJS.ProcessEnv; readonly attachmentsDir: string; readonly fileSystem: FileSystem.FileSystem; readonly path: Path.Path; readonly random: SecureRandom.SecureRandom["Service"]; readonly idAllocator: IdAllocator.IdAllocatorV2["Service"]; readonly queryRunner: ClaudeAgentSdkQueryRunnerShape; readonly scopedLimitNames?: Ref.Ref; readonly onUsageLimits?: ManagedServerProvider["applyUsageLimits"]; readonly continuationRequests?: { readonly offer: ( request: ProviderContinuationRequests.ProviderContinuationRequest, ) => Effect.Effect; }; } export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( adapterOptions: ClaudeAdapterV2Options, ) { const mcpSessions = yield* McpProviderSessions.McpProviderSessions; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const serverSettings = yield* Effect.serviceOption(ServerSettings.ServerSettingsService); const { attachmentsDir, fileSystem, path, random, idAllocator, queryRunner } = adapterOptions; const subagentModelCatalog = scopeClaudeModelCatalog( BUNDLED_CLAUDE_MODEL_CATALOG, adapterOptions.settings.customModels, ); // Results report the window Claude Code compacts against; outside the catalog it is the only source. const reportedContextWindows = new Map(); const claudeContextWindow = (modelSelection: ModelSelection): number | null => resolveClaudeCatalogContextWindowTokens(BUNDLED_CLAUDE_MODEL_CATALOG, modelSelection) ?? reportedContextWindows.get(compileClaudeModelSelection(modelSelection).apiModelId) ?? null; const continuationRequests = adapterOptions.continuationRequests ?? { offer: () => Effect.void, }; const homePath = yield* resolveClaudeHomePath( adapterOptions.settings, adapterOptions.environment, ).pipe(Effect.provideService(Path.Path, path)); const hostEnvironment = yield* HostProcess.Environment; const hostPlatform = yield* HostProcess.Platform; const customHome = adapterOptions.settings.homePath.trim().length > 0 || adapterOptions.environment.CLAUDE_CONFIG_DIR !== hostEnvironment.CLAUDE_CONFIG_DIR; const announcedBypassDowngrades = new Set(); const readBypassSettings = (filePath: string) => fileSystem.readFileString(filePath).pipe( Effect.flatMap(decodeClaudeBypassSettings), Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed({} as typeof ClaudeBypassSettings.Type), ), ); const resolveBypassAvailability = Effect.fn("ClaudeAdapterV2.resolveBypassAvailability")( function* (cwd: string | null) { // resolveSettings does not initialize remote-cache eligibility in the SDK host. // Keep the payload intact so the SDK also honors managedSourcesBehavior. const remote = yield* readBypassSettings(path.join(homePath, "remote-settings.json")); const home = customHome ? yield* readBypassSettings(path.join(homePath, "settings.json")) : {}; const resolved = yield* Effect.tryPromise(() => resolveClaudeSettings({ ...(cwd === null ? {} : { cwd }), settingSources: customHome ? ["project", "local"] : ["user", "project", "local"], serverManagedSettings: claudeSdkBypassSettings(remote), }), ); const launchArgSettings = yield* readClaudeLaunchArgSettings({ settings: adapterOptions.settings, cwd, fileSystem, path, }); const launch = yield* decodeClaudeLaunchArgBypassSettings(launchArgSettings ?? {}); const sources = [resolved.effective, home, remote, launch].map(claudeBypassAvailability); return { bypassDisabled: sources.some((source) => source.bypassDisabled), autoDisabled: sources.some((source) => source.autoDisabled), }; }, ); // Claude Code's own subagent TTL setting, in its precedence order. Unset or unreadable means // Anthropic's default applies. const resolveSubagentPromptCacheTtl = Effect.fn("ClaudeAdapterV2.resolveSubagentPromptCacheTtl")( function* (cwd: string | null) { const fromEnvironment = decodeClaudePromptCacheTtl( adapterOptions.environment.CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL, ); if (Option.isSome(fromEnvironment)) return fromEnvironment.value; const resolved = yield* Effect.tryPromise(() => resolveClaudeSettings({ ...(cwd === null ? {} : { cwd }), settingSources: customHome ? ["project", "local"] : ["user", "project", "local"], }), ).pipe(Effect.orElseSucceed(() => undefined)); const home = customHome ? yield* fileSystem.readFileString(path.join(homePath, "settings.json")).pipe( Effect.flatMap(decodeClaudeSubagentCacheSettings), Effect.orElseSucceed(() => ({ subagentPromptCacheTtl: undefined })), ) : undefined; return resolved?.effective.subagentPromptCacheTtl ?? home?.subagentPromptCacheTtl; }, ); const readToolSearchSetting = (filePath: string) => fileSystem.readFileString(filePath).pipe( Effect.flatMap(decodeClaudeEnvSettings), Effect.map((settings) => { const value = settings.env?.ENABLE_TOOL_SEARCH; return typeof value === "string" ? value : undefined; }), Effect.orElseSucceed(() => undefined), ); // A worktree's .git file points at
/.git/worktrees/, whose commondir leads back to //
/.git. A bare repository has no main checkout, so its worktree keeps its own file. const mainCheckoutRoot = (checkout: string) => fileSystem.readFileString(path.join(checkout, ".git")).pipe( Effect.flatMap((pointer) => { const gitDir = /^gitdir:\s*(.+)$/m.exec(pointer)?.[1]?.trim(); if (gitDir === undefined) return Effect.succeed(checkout); const worktreeGitDir = path.resolve(checkout, gitDir); return fileSystem.readFileString(path.join(worktreeGitDir, "commondir")).pipe( Effect.map((commonDir) => { const common = path.resolve(worktreeGitDir, commonDir.trim()); return path.basename(common) === ".git" ? path.dirname(common) : checkout; }), ); }), Effect.orElseSucceed(() => checkout), ); // Claude Code keeps .claude/settings.local.json at the repository root, or in cwd outside a // repository, on Windows, or when the root is home. Its owner check is skipped: missing it only // leaves tool search on, Claude Code's default. const localSettingsDirectory = Effect.fn("ClaudeAdapterV2.localSettingsDirectory")(function* ( cwd: string, ) { if ((yield* HostProcess.Platform) === "win32") return cwd; for (let directory = cwd; ; directory = path.dirname(directory)) { const isCheckout = yield* fileSystem .exists(path.join(directory, ".git")) .pipe(Effect.orElseSucceed(() => false)); if (isCheckout) { const root = yield* mainCheckoutRoot(directory); return root === (yield* HostProcess.HomeDirectory) ? cwd : root; } if (path.dirname(directory) === directory) return cwd; } }); // Settings env beats the process env, --settings over local over project over user, and a local // file an older Claude Code left in cwd applies under the root one. resolveSettings reads local // from cwd only. const resolveToolSearchOverride = Effect.fn("ClaudeAdapterV2.resolveToolSearchOverride")( function* (cwd: string | null, launchArgSettings: ClaudeSdkSettings | undefined) { const launchValue = launchArgSettings?.env?.ENABLE_TOOL_SEARCH; if (typeof launchValue === "string") return claudeToolSearchOverride(launchValue); const localDirectory = cwd === null ? null : yield* localSettingsDirectory(cwd); const settingsFiles = [ ...(cwd === null || localDirectory === null ? [] : [ path.join(localDirectory, ".claude", "settings.local.json"), ...(localDirectory === cwd ? [] : [path.join(cwd, ".claude", "settings.local.json")]), path.join(cwd, ".claude", "settings.json"), ]), path.join(homePath, "settings.json"), ]; for (const settingsFile of settingsFiles) { const value = yield* readToolSearchSetting(settingsFile); if (value !== undefined) return claudeToolSearchOverride(value); } return claudeToolSearchOverride(adapterOptions.environment.ENABLE_TOOL_SEARCH); }, ); const userInvocableSkills = (cwd: string | null) => discoverClaudeSkills( adapterOptions.settings, cwd ?? undefined, adapterOptions.environment, ).pipe( Effect.map( (skills) => new Map( skills .filter((skill) => skill.enabled && skill.userInvocable !== false) .map((skill) => [skill.name, skill]), ), ), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), ); return ProviderAdapter.ProviderAdapterV2.of({ instanceId: adapterOptions.instanceId, driver: CLAUDE_PROVIDER, getCapabilities: Effect.fn("ClaudeAdapterV2.getCapabilities")(function () { return Effect.succeed(ClaudeProviderCapabilitiesV2); }), planSelectionTransition: Effect.fn("ClaudeAdapterV2.planSelectionTransition")(function () { return Effect.succeed(turnScopedSelectionTransition()); }), openSession: Effect.fn("ClaudeAdapterV2.openSession")( function* (input: ProviderAdapter.ProviderAdapterV2OpenSessionInput) { const sessionScope = yield* Effect.scope; const now = yield* DateTime.now; const session = providerSession({ providerSessionId: input.providerSessionId, providerInstanceId: adapterOptions.instanceId, cwd: input.runtimePolicy.cwd, model: input.modelSelection.model, now, }); const events = yield* Queue.unbounded(); const activeTurn = yield* Ref.make(null); const taskOutputTails = new Map< string, { readonly fiber: Fiber.Fiber; readonly stop: Deferred.Deferred } >(); // Waits for the tail's last read so its output lands before the call settles. const stopTaskOutputTail = (toolUseId: string) => Effect.suspend(() => { const tail = taskOutputTails.get(toolUseId); if (tail === undefined) return Effect.void; taskOutputTails.delete(toolUseId); return Deferred.succeed(tail.stop, undefined).pipe( Effect.andThen(Fiber.await(tail.fiber)), Effect.asVoid, ); }); const sdkMessageLock = yield* Semaphore.make(1); // Members can finish after the launching turn; retain state to emit each transition once. const workflowMemberStates = yield* Ref.make( new Map< string, { readonly status: OrchestrationV2Subagent["status"]; readonly attempt: number; readonly transcriptReadAttempts: number; readonly finalTranscriptRead: boolean; readonly agentId: string | undefined; readonly transcriptCursor: WorkflowAgentTranscriptCursor; readonly transcriptMetadata: WorkflowAgentMetadata; readonly nextItemOrdinal: number; readonly toolCalls: Map; readonly textShown: boolean; readonly turnEnded: boolean; readonly resultShown: boolean; readonly result: string | undefined; readonly label: string; readonly prompt: string; readonly model: string; readonly createdAt: DateTime.Utc; readonly startedAt: DateTime.Utc | null; readonly completedAt: DateTime.Utc | null; } >(), ); const interruptedTurns = yield* Ref.make(new Set()); const steeredTurns = yield* Ref.make(new Set()); const queryContext = yield* Ref.make(null); // Wake buffers replay result frames, so ledger growth is measured once on arrival. const resultLedgerUsage = new WeakMap(); const resultsReportingNewContextWindow = new WeakSet(); // A worktree handoff changes the project settings even within one session. let bypassCache: { cwd: string | null; availability: ClaudeBypassAvailability } | null = null; const queryPolicyForTurn = Effect.fnUntraced(function* ( turnInput: ProviderAdapter.ProviderAdapterV2TurnInput, ) { if ( permissionModeForClaudeRuntimePolicy(turnInput.runtimePolicy) !== "bypassPermissions" ) { return claudeRuntimeQueryPolicyForRuntimePolicy(turnInput.runtimePolicy); } const cwd = turnInput.runtimePolicy.cwd; if (bypassCache === null || bypassCache.cwd !== cwd) { const resolved = yield* resolveBypassAvailability(cwd).pipe(Effect.option); if (Option.isNone(resolved)) { return claudeRuntimeQueryPolicyForRuntimePolicy(turnInput.runtimePolicy, { bypassDisabled: true, autoDisabled: false, }); } bypassCache = { cwd, availability: resolved.value }; } return claudeRuntimeQueryPolicyForRuntimePolicy( turnInput.runtimePolicy, bypassCache.availability, ); }); const openedNativeThreads = yield* Ref.make(new Set()); const pdfBudgets = new Map>(); const latestPlanByKind = yield* Ref.make(new Map()); // Claude 5 task tools edit one list per native session, across turns. const taskListsByNativeThread = yield* Ref.make( new Map(), ); const planIdsByNativeItem = yield* Ref.make( new Map(), ); const providerRetries = yield* Ref.make( new Map(), ); const pendingRuntimeRequests = yield* Ref.make( new Map(), ); // Background local_bash tasks can trigger wake turns on the query stream with no active provider turn. const lastTurnRouteByNativeThread = yield* Ref.make( new Map< string, { readonly threadId: ThreadId; readonly providerThreadId: ProviderThreadId } >(), ); const pendingBackgroundTasksByNativeThread = yield* Ref.make( new Map>(), ); // Survives roster REPLACE levels, which rebuild every entry without the agent's mark. const awaitedBackgroundTasksByNativeThread = yield* Ref.make( new Map>(), ); // Wake eligibility is separate from the Waiting roster. It survives empty // background_tasks_changed levels (SDK: empty level can precede task_notification) and // is consumed when the first idle notification is buffered/offered so duplicates cannot // re-buffer. const wakeEligibleBackgroundTasksByNativeThread = yield* Ref.make( new Map>(), ); const opaqueBackgroundTaskReplayTombstonesByNativeThread = yield* Ref.make( new Map>(), ); const lastProviderThreadByNativeThread = yield* Ref.make( new Map(), ); const goalsByNativeThread = new Map(); const goalCheckedTurns = new Set(); // Background subagents can finish after root settlement; retain their registry for wake evidence and hydration. const sessionSubagentsByTaskId = yield* Ref.make(new Map()); const sessionSubagentTaskIdsByToolUseId = yield* Ref.make(new Map()); // Claude may never answer a background move; later steers must wait on the same request. const backgroundMovesByToolUseId = new Map>(); const subagentToolCalls = new Map(); const pendingSubagentLaunchesByToolUseId = new Map(); const settledTurnByNativeThread = new Map(); const subagentStopOutcomes = new Map< string, Deferred.Deferred >(); // Buffered idle outcomes must not settle a resumed child again. const appliedSubagentOutcomes = new WeakSet(); // Subagent frames can precede the task_started that registers their owner. const pendingSubagentFramesByToolUseId = yield* Ref.make( new Map>(), ); // A gateway's relative reset counts from the result's arrival, not its replay. const bufferedResultArrivals = new WeakMap(); const wakeBuffers = yield* Ref.make( new Map< string, { readonly messages: ReadonlyArray; readonly detail: string | null } >(), ); // Wake results lack task ids and can follow queued prompts; retain reports for one user turn. // Keep report stamps atomic with turn changes. const wakeReportsByNativeThread = yield* Ref.make<{ readonly userTurns: ReadonlyMap; readonly reports: ReadonlyMap< string, ReadonlyMap >; }>({ userTurns: new Map(), reports: new Map() }); const backgroundedSubagentTaskIds = yield* Ref.make>(new Set()); // Nested subagent completions notify their owner and never wake the root. const nestedSubagentTaskIds = yield* Ref.make>(new Set()); const isNestedSubagentTask = (taskId: string) => Ref.get(nestedSubagentTaskIds).pipe(Effect.map((ids) => ids.has(taskId))); const recordWakeReport = ( nativeThreadId: string, taskId: string, report: BackgroundWorkReport, ) => Ref.update(wakeReportsByNativeThread, ({ userTurns, reports }) => ({ userTurns, reports: new Map(reports).set( nativeThreadId, new Map(reports.get(nativeThreadId)).set(taskId, { report, turn: userTurns.get(nativeThreadId) ?? 0, }), ), })); const startUserTurnForWakeReports = (nativeThreadId: string) => Ref.update(wakeReportsByNativeThread, ({ userTurns, reports }) => { const turn = (userTurns.get(nativeThreadId) ?? 0) + 1; const updatedTurns = new Map(userTurns).set(nativeThreadId, turn); const threadReports = reports.get(nativeThreadId); if (threadReports === undefined) return { userTurns: updatedTurns, reports }; const kept = new Map([...threadReports].filter(([, entry]) => entry.turn >= turn - 1)); const updatedReports = new Map(reports); if (kept.size === 0) updatedReports.delete(nativeThreadId); else updatedReports.set(nativeThreadId, kept); return { userTurns: updatedTurns, reports: updatedReports }; }); const takeWakeReports = (nativeThreadId: string) => Ref.modify(wakeReportsByNativeThread, (current) => { const taken = current.reports.get(nativeThreadId); if (taken === undefined) return [taken, current] as const; const reports = new Map(current.reports); reports.delete(nativeThreadId); return [taken, { userTurns: current.userTurns, reports }] as const; }); const clearWakeReports = (nativeThreadId: string) => takeWakeReports(nativeThreadId).pipe(Effect.asVoid); // Empty roster levels can precede notifications; retain tasks for wake attribution. const lastKnownOpaqueTasks = yield* Ref.make( new Map(), ); const claudeMonitors = yield* Ref.make<{ readonly calls: ReadonlySet; readonly tasks: ReadonlyMap< string, { readonly toolUseId: string; readonly nativeThreadId: string } >; }>({ calls: new Set(), tasks: new Map() }); const trackClaudeMonitorCalls = (message: SDKMessage) => { const started = claudeToolUseBlocksFromAssistantMessage(message).flatMap((toolUse) => toolUse.name === "Monitor" ? [toolUse.id] : [], ); const returned = [ ...claudeToolResultBlocksFromAssistantMessage(message), ...claudeToolResultBlocksFromUserMessage(message), ].map((toolResult) => toolResult.tool_use_id); if (started.length === 0 && returned.length === 0) return Effect.void; return Ref.update(claudeMonitors, (current) => { const linked = new Set([...current.tasks.values()].map((task) => task.toolUseId)); const opened = started.filter((id) => !linked.has(id) && !current.calls.has(id)); const closed = returned.filter((id) => current.calls.has(id)); if (opened.length === 0 && closed.length === 0) return current; const calls = new Set([...current.calls, ...opened]); for (const id of closed) calls.delete(id); return { ...current, calls }; }); }; const isClaudeMonitorTask = (input: { readonly nativeThreadId: string; readonly taskId: string; readonly toolUseId: string | undefined; }) => Ref.modify(claudeMonitors, (current) => { if (current.tasks.has(input.taskId)) return [true, current] as const; if (input.toolUseId === undefined || !current.calls.has(input.toolUseId)) { return [false, current] as const; } const calls = new Set(current.calls); calls.delete(input.toolUseId); const tasks = new Map(current.tasks).set(input.taskId, { toolUseId: input.toolUseId, nativeThreadId: input.nativeThreadId, }); return [true, { calls, tasks }] as const; }); const endClaudeMonitorTasks = ( ended: (taskId: string, task: { readonly nativeThreadId: string }) => boolean, ) => Ref.modify(claudeMonitors, (current) => { const endedIds = [...current.tasks].filter(([taskId, task]) => ended(taskId, task)); if (endedIds.length === 0) return [current.tasks, current] as const; const tasks = new Map(current.tasks); for (const [taskId] of endedIds) tasks.delete(taskId); return [tasks, { ...current, tasks }] as const; }); const claudeTaskOutcome = (status: "completed" | "failed" | "stopped") => status === "completed" ? "completed" : status === "stopped" ? "cancelled" : "failed"; // Reads the roster, so call it before the notification clears the task from it. const opaqueTaskWakeReport = Effect.fnUntraced(function* ( nativeThreadId: string, message: Extract, ): Effect.fn.Return { const task = rosterForNativeThread( yield* Ref.get(pendingBackgroundTasksByNativeThread), nativeThreadId, ).get(message.task_id) ?? (yield* Ref.get(lastKnownOpaqueTasks)).get(message.task_id); yield* Ref.update(lastKnownOpaqueTasks, (current) => { if (!current.has(message.task_id)) return current; const updated = new Map(current); updated.delete(message.task_id); return updated; }); const outcome = claudeTaskOutcome(message.status); const label = task?.description; switch (task?.kind) { case "subagent": return { kind: "subagent", label, outcome, childThreadId: task.childThreadId }; case "monitor": case "background_task": return { kind: task.kind, label, outcome }; case "command": case undefined: return { kind: "command", label, outcome }; } }); const requestedContinuations = yield* Ref.make(new Set()); const absorbedWakes = yield* Ref.make(new Set()); const heldProposedPlansByToolUseId = new Map(); const runtimeContext = yield* Effect.context(); const runPromise = Effect.runPromiseWith(runtimeContext); const emitProviderEvent = (event: ProviderAdapter.ProviderAdapterV2Event) => { if (event.type === "provider_thread.updated") { const budget = pdfBudgets.get(event.providerThread.nativeThreadRef?.nativeId ?? ""); if (budget !== undefined) { event = { ...event, providerThread: { ...event.providerThread, nativeMetadata: { ...event.providerThread.nativeMetadata, claudeAttachmentBudget: { bytesUsed: budget.committed.bytes, pagesUsed: budget.committed.pages, }, }, }, }; } } return Queue.offer(events, event).pipe(Effect.asVoid); }; const persistAttachmentBudget = Effect.fnUntraced(function* (nativeThreadId: string) { const providerThread = (yield* Ref.get(lastProviderThreadByNativeThread)).get( nativeThreadId, ); if (providerThread === undefined) return; const active = yield* Ref.get(activeTurn); yield* emitProviderEvent({ type: "provider_thread.updated", driver: CLAUDE_PROVIDER, providerThread: { ...providerThread, status: active?.nativeThreadId === nativeThreadId ? "active" : providerThread.status, updatedAt: yield* DateTime.now, }, }); }); // Usage without input is a placeholder, not a context measurement. const emitContextUsage = Effect.fnUntraced(function* ( context: ActiveClaudeTurnContext, usage: ClaudeMessageUsage | number, requestStarted = false, ) { const now = yield* DateTime.now; // Only message_start marks the request start; an assistant snapshot // arrives after generation, so it leaves the cache expiry unknown. if (requestStarted && context.streamUsage !== null) context.streamUsage.requestedAt = now; const maxTokens = claudeContextWindow(context.input.modelSelection); const tokenUsage = typeof usage === "number" ? { usedTokens: usage, maxTokens, ...claudeAutoCompactThreshold( adapterOptions.settings.autoCompactWindow, maxTokens, ), updatedAt: DateTime.formatIso(now), } : claudeProviderTurnTokenUsage( usage, maxTokens, DateTime.formatIso(now), adapterOptions.settings.autoCompactWindow, context.streamUsage?.requestedAt, ); if (typeof usage !== "number") { if (claudeMessageInputTokens(usage) <= 0) return; context.compactionTokens = undefined; } yield* emitProviderEvent({ type: "provider_turn.updated", driver: CLAUDE_PROVIDER, threadId: context.input.threadId, providerTurn: { id: context.providerTurnId, providerThreadId: context.input.providerThread.id, nodeId: context.input.rootNodeId, runAttemptId: context.input.attemptId, nativeTurnRef: { driver: CLAUDE_PROVIDER, nativeId: context.nativeTurnId, strength: "strong", }, ordinal: context.providerTurnOrdinal, status: "running", startedAt: context.startedAt, completedAt: null, tokenUsage, }, }); }); // Claude has no recovered frame; the next assistant message proves recovery. // Runs on every model output frame and is almost always a no-op; only a pending retry gets a span. const completeProviderRetry = Effect.fnUntraced(function* ( context: ActiveClaudeTurnContext, updatedAt: DateTime.Utc, ) { const providerRetry = yield* Ref.modify(providerRetries, (current) => { const retry = current.get(context.providerTurnId); if (retry === undefined) { return [undefined, current] as const; } const updated = new Map(current); updated.delete(context.providerTurnId); return [retry, updated] as const; }); if (providerRetry === undefined) { return; } yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: makeProviderRetryTurnItem({ driver: CLAUDE_PROVIDER, threadId: context.input.threadId, runId: context.input.runId, nodeId: context.input.rootNodeId, providerThreadId: context.input.providerThread.id, providerTurnId: context.providerTurnId, itemOrdinal: providerRetry.itemOrdinal, failure: providerRetry.failure, retry: providerRetry.retry, status: "completed", startedAt: providerRetry.startedAt, updatedAt, }), }).pipe(Effect.withSpan("ClaudeAdapterV2.completeProviderRetry")); }); const rememberProviderThread = /* Provider events include streaming deltas; avoid a span for each update. */ Effect.fnUntraced(function* (providerThread: OrchestrationV2ProviderThread) { const nativeThreadId = providerThread.nativeThreadRef?.nativeId; if (nativeThreadId === undefined || nativeThreadId === null) { return; } yield* Ref.update(lastProviderThreadByNativeThread, (current) => new Map(current).set(nativeThreadId, providerThread), ); }); const rosterForNativeThread = ( all: ReadonlyMap>, nativeThreadId: string, ): Map => all.get(nativeThreadId) ?? new Map(); const hasPendingBackgroundTaskOnNativeThread = (nativeThreadId: string, taskId: string) => Ref.get(pendingBackgroundTasksByNativeThread).pipe( Effect.map((all) => rosterForNativeThread(all, nativeThreadId).has(taskId)), ); const taskIdSetForNativeThread = ( all: ReadonlyMap>, nativeThreadId: string, ): Set => all.get(nativeThreadId) ?? new Set(); const addTaskIdsToNativeThreadSet = ( ref: Ref.Ref>>, nativeThreadId: string, taskIds: ReadonlyArray, ) => Ref.update(ref, (current) => { if (taskIds.length === 0) { return current; } const next = new Set(taskIdSetForNativeThread(current, nativeThreadId)); let changed = false; for (const taskId of taskIds) { if (!next.has(taskId)) { next.add(taskId); changed = true; } } return changed ? new Map(current).set(nativeThreadId, next) : current; }); const clearTaskIdFromNativeThreadSet = ( ref: Ref.Ref>>, nativeThreadId: string, taskId: string, ) => Ref.update(ref, (current) => { const existing = taskIdSetForNativeThread(current, nativeThreadId); if (!existing.has(taskId)) { return current; } const next = new Set(existing); next.delete(taskId); const updated = new Map(current); if (next.size === 0) { updated.delete(nativeThreadId); } else { updated.set(nativeThreadId, next); } return updated; }); const clearNativeThreadTaskIdSet = ( ref: Ref.Ref>>, nativeThreadId: string, ) => Ref.update(ref, (current) => { if (!current.has(nativeThreadId)) { return current; } const updated = new Map(current); updated.delete(nativeThreadId); return updated; }); const isWakeEligibleOpaqueBackgroundTaskOnNativeThread = ( nativeThreadId: string, taskId: string, ) => Ref.get(wakeEligibleBackgroundTasksByNativeThread).pipe( Effect.map((all) => taskIdSetForNativeThread(all, nativeThreadId).has(taskId)), ); const hasOpaqueBackgroundTaskReplayTombstoneOnNativeThread = ( nativeThreadId: string, taskId: string, ) => Ref.get(opaqueBackgroundTaskReplayTombstonesByNativeThread).pipe( Effect.map((all) => taskIdSetForNativeThread(all, nativeThreadId).has(taskId)), ); const isKnownOpaqueBackgroundTaskOnNativeThread = /* Provider events include streaming deltas; avoid a span for each update. */ Effect.fnUntraced(function* (nativeThreadId: string, taskId: string) { if (yield* hasPendingBackgroundTaskOnNativeThread(nativeThreadId, taskId)) { return true; } if (yield* isWakeEligibleOpaqueBackgroundTaskOnNativeThread(nativeThreadId, taskId)) { return true; } return yield* hasOpaqueBackgroundTaskReplayTombstoneOnNativeThread( nativeThreadId, taskId, ); }); const markWakeEligibleOpaqueBackgroundTasks = ( nativeThreadId: string, taskIds: ReadonlyArray, ) => addTaskIdsToNativeThreadSet( wakeEligibleBackgroundTasksByNativeThread, nativeThreadId, taskIds, ); const consumeWakeEligibilityForBufferedNotification = /* Provider events include streaming deltas; avoid a span for each update. */ Effect.fnUntraced(function* (nativeThreadId: string, taskId: string) { yield* clearTaskIdFromNativeThreadSet( wakeEligibleBackgroundTasksByNativeThread, nativeThreadId, taskId, ); yield* addTaskIdsToNativeThreadSet( opaqueBackgroundTaskReplayTombstonesByNativeThread, nativeThreadId, [taskId], ); }); const clearOpaqueBackgroundTaskReplayTombstone = (nativeThreadId: string, taskId: string) => clearTaskIdFromNativeThreadSet( opaqueBackgroundTaskReplayTombstonesByNativeThread, nativeThreadId, taskId, ); const emitProviderThreadRoster = Effect.fnUntraced(function* (input: { readonly nativeThreadId: string; readonly providerThread: OrchestrationV2ProviderThread; readonly status?: OrchestrationV2ProviderThread["status"]; }) { const roster = rosterForNativeThread( yield* Ref.get(pendingBackgroundTasksByNativeThread), input.nativeThreadId, ); const awaitedTaskIds = taskIdSetForNativeThread( yield* Ref.get(awaitedBackgroundTasksByNativeThread), input.nativeThreadId, ); const now = yield* DateTime.now; const providerThread: OrchestrationV2ProviderThread = { ...input.providerThread, providerSessionId: session.id, ...(input.status === undefined ? {} : { status: input.status }), pendingBackgroundTasks: claudePendingBackgroundTasksFromRoster(roster, awaitedTaskIds), ...(goalsByNativeThread.has(input.nativeThreadId) ? { goal: goalsByNativeThread.get(input.nativeThreadId) ?? null } : {}), updatedAt: now, }; yield* rememberProviderThread(providerThread); yield* emitProviderEvent({ type: "provider_thread.updated", driver: CLAUDE_PROVIDER, providerThread, }); }); const trackClaudeGoal = Effect.fnUntraced(function* (input: { readonly nativeThreadId: string; readonly context: ActiveClaudeTurnContext; readonly message: SDKMessage; }) { const current = goalsByNativeThread.get(input.nativeThreadId) ?? null; if ( current?.status === "active" && input.message.type === "assistant" && input.message.parent_tool_use_id === null && input.message.message.model !== "" ) { goalCheckedTurns.add(input.context.providerTurnId); } const next = nextClaudeGoal(current, input.message); if (next === undefined || providerGoalsEqual(current, next)) return; goalsByNativeThread.set(input.nativeThreadId, next); const providerThread = (yield* Ref.get(lastProviderThreadByNativeThread)).get( input.nativeThreadId, ); if (providerThread === undefined) return; const updated = { ...providerThread, goal: next, updatedAt: yield* DateTime.now }; yield* rememberProviderThread(updated); yield* emitProviderEvent({ type: "provider_thread.updated", driver: CLAUDE_PROVIDER, providerThread: updated, }); }); const rememberOpaqueTasks = (tasks: ReadonlyArray) => tasks.length === 0 ? Effect.void : Ref.update(lastKnownOpaqueTasks, (current) => { const updated = new Map(current); for (const task of tasks) updated.set(task.taskId, task); for (const oldest of updated.keys()) { if (updated.size <= 64) break; updated.delete(oldest); } return updated; }); const replacePendingBackgroundTasks = /* Provider events include streaming deltas; avoid a span for each update. */ Effect.fnUntraced(function* ( nativeThreadId: string, tasks: ReadonlyArray, ) { yield* Ref.update(pendingBackgroundTasksByNativeThread, (current) => { const updated = new Map(current); if (tasks.length === 0) { updated.delete(nativeThreadId); } else { updated.set( nativeThreadId, new Map(tasks.map((task) => [task.taskId, task] as const)), ); } return updated; }); yield* rememberOpaqueTasks(tasks); if (tasks.length > 0) { yield* markWakeEligibleOpaqueBackgroundTasks( nativeThreadId, tasks.map((task) => task.taskId), ); } const listed = new Set(tasks.map((task) => task.taskId)); yield* Ref.update(awaitedBackgroundTasksByNativeThread, (current) => { const awaited = current.get(nativeThreadId); if (awaited === undefined) return current; const kept = new Set([...awaited].filter((taskId) => listed.has(taskId))); if (kept.size === awaited.size) return current; const updated = new Map(current); if (kept.size === 0) updated.delete(nativeThreadId); else updated.set(nativeThreadId, kept); return updated; }); }); const upsertPendingBackgroundTask = /* Provider events include streaming deltas; avoid a span for each update. */ Effect.fnUntraced(function* ( nativeThreadId: string, task: OrchestrationV2PendingBackgroundTask, ) { yield* Ref.update(pendingBackgroundTasksByNativeThread, (current) => { const roster = new Map(rosterForNativeThread(current, nativeThreadId)); roster.set(task.taskId, task); return new Map(current).set(nativeThreadId, roster); }); yield* rememberOpaqueTasks([task]); yield* markWakeEligibleOpaqueBackgroundTasks(nativeThreadId, [task.taskId]); }); const clearPendingBackgroundTask = (nativeThreadId: string, taskId: string) => Ref.modify(pendingBackgroundTasksByNativeThread, (current) => { const roster = rosterForNativeThread(current, nativeThreadId); if (!roster.has(taskId)) { return [false, current] as const; } const nextRoster = new Map(roster); nextRoster.delete(taskId); const updated = new Map(current); if (nextRoster.size === 0) { updated.delete(nativeThreadId); } else { updated.set(nativeThreadId, nextRoster); } return [true, updated] as const; }).pipe( Effect.tap(() => clearTaskIdFromNativeThreadSet( awaitedBackgroundTasksByNativeThread, nativeThreadId, taskId, ), ), ); const clearPendingBackgroundTasksForNativeThread = /* Provider events include streaming deltas; avoid a span for each update. */ Effect.fnUntraced(function* (nativeThreadId: string) { yield* Ref.update(pendingBackgroundTasksByNativeThread, (current) => { if (!current.has(nativeThreadId)) { return current; } const updated = new Map(current); updated.delete(nativeThreadId); return updated; }); yield* clearNativeThreadTaskIdSet(awaitedBackgroundTasksByNativeThread, nativeThreadId); yield* endClaudeMonitorTasks((_taskId, task) => task.nativeThreadId === nativeThreadId); }); const forgetAbsorbedWake = (nativeThreadId: string) => Ref.update(absorbedWakes, (current) => { if (!current.has(nativeThreadId)) return current; const updated = new Set(current); updated.delete(nativeThreadId); return updated; }); const clearWakeStateForNativeThread = /* Provider events include streaming deltas; avoid a span for each update. */ Effect.fnUntraced(function* (nativeThreadId: string) { yield* clearWakeReports(nativeThreadId); yield* Ref.update(wakeBuffers, (current) => { if (!current.has(nativeThreadId)) { return current; } const updated = new Map(current); updated.delete(nativeThreadId); return updated; }); yield* Ref.update(requestedContinuations, (current) => { if (!current.has(nativeThreadId)) { return current; } const updated = new Set(current); updated.delete(nativeThreadId); return updated; }); yield* forgetAbsorbedWake(nativeThreadId); }); // Claude runs one root turn at a time, so a user turn's result also ends a wake turn it // folded in. The continuation replays that wake's buffered frames but gets no result of its own. const markAbsorbedWake = Effect.fnUntraced(function* (nativeThreadId: string) { const messages = (yield* Ref.get(wakeBuffers)).get(nativeThreadId)?.messages ?? []; const lastResult = messages.findLastIndex((entry) => entry.type === "result"); if (!messages.slice(lastResult + 1).some(isClaudeRootTurnFrame)) return; yield* Ref.update(absorbedWakes, (current) => new Set(current).add(nativeThreadId)); }); // The SDK emits no initial roster; reset roster and wake eligibility when opening a live query. const resetBackgroundTaskStateForNativeThreadProcess = Effect.fnUntraced(function* ( nativeThreadId: string, options?: { readonly status?: OrchestrationV2ProviderThread["status"]; }, ) { const hadRoster = rosterForNativeThread( yield* Ref.get(pendingBackgroundTasksByNativeThread), nativeThreadId, ).size > 0; const remembered = (yield* Ref.get(lastProviderThreadByNativeThread)).get(nativeThreadId); const hadPersistedRoster = (remembered?.pendingBackgroundTasks?.length ?? 0) > 0; const priorOpaqueTombstones = taskIdSetForNativeThread( yield* Ref.get(opaqueBackgroundTaskReplayTombstonesByNativeThread), nativeThreadId, ); const bufferedTaskNotificationIds = new Set(); const buffered = (yield* Ref.get(wakeBuffers)).get(nativeThreadId); if (buffered !== undefined) { for (const message of buffered.messages) { if (message.type === "system" && message.subtype === "task_notification") { bufferedTaskNotificationIds.add(message.task_id); } } } const preservedOpaqueTombstones = [...priorOpaqueTombstones].filter((taskId) => bufferedTaskNotificationIds.has(taskId), ); yield* clearPendingBackgroundTasksForNativeThread(nativeThreadId); yield* clearNativeThreadTaskIdSet( wakeEligibleBackgroundTasksByNativeThread, nativeThreadId, ); yield* clearNativeThreadTaskIdSet( opaqueBackgroundTaskReplayTombstonesByNativeThread, nativeThreadId, ); if (preservedOpaqueTombstones.length > 0) { yield* addTaskIdsToNativeThreadSet( opaqueBackgroundTaskReplayTombstonesByNativeThread, nativeThreadId, preservedOpaqueTombstones, ); } if (!hadRoster && !hadPersistedRoster) { return; } if (remembered === undefined) { return; } const activeContext = yield* Ref.get(activeTurn); const status = options?.status ?? (activeContext === null ? ("idle" as const) : remembered.status === "idle" ? ("active" as const) : remembered.status); yield* emitProviderThreadRoster({ nativeThreadId, providerThread: remembered, status, }); }); const claudeSubagentIds = (context: ActiveClaudeTurnContext, taskId: string) => ({ nodeId: idAllocator.derive.nodeFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: `task:${taskId}`, }), childRootNodeId: idAllocator.derive.nodeFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: `task:${taskId}:thread-root`, }), childThreadId: idAllocator.derive.threadFromProviderThread({ driver: CLAUDE_PROVIDER, nativeThreadId: `${context.input.providerThread.id}:${taskId}`, }), turnItemId: idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: `task:${taskId}:subagent`, }), }); const resolveItemOrdinal = (context: ActiveClaudeTurnContext, nativeItemId: string) => Effect.sync(() => { const existing = context.itemOrdinals.get(nativeItemId); if (existing !== undefined) { return existing; } const ordinal = context.input.providerTurnOrdinal * 100 + context.itemOrdinals.size + 1; context.itemOrdinals.set(nativeItemId, ordinal); return ordinal; }); const providerTurnPayload = (input: { readonly context: ActiveClaudeTurnContext; readonly status: OrchestrationV2ProviderTurn["status"]; readonly completedAt: DateTime.Utc | null; }): OrchestrationV2ProviderTurn => ({ id: input.context.providerTurnId, providerThreadId: input.context.input.providerThread.id, nodeId: input.context.input.rootNodeId, runAttemptId: input.context.input.attemptId, nativeTurnRef: { driver: CLAUDE_PROVIDER, nativeId: input.context.nativeMessageCursor ?? input.context.nativeTurnId, strength: "weak", }, ordinal: input.context.providerTurnOrdinal, status: input.status, startedAt: input.context.startedAt, completedAt: input.completedAt, }); const buildToolCallArtifacts = (input: { readonly context: ActiveClaudeTurnContext | null; readonly nativeItemId: string; readonly toolName: string; readonly classification: ClaudeToolClassification; readonly toolInput: ClaudeNativeToolInput; readonly threadId: ThreadId; readonly runId: ProviderAdapter.ProviderAdapterV2TurnInput["runId"] | null; readonly rootNodeId: OrchestrationV2ExecutionNode["id"]; readonly parentNodeId: OrchestrationV2ExecutionNode["id"]; readonly ordinal: number; readonly output: ClaudeNativeToolOutput; readonly status: Extract< OrchestrationV2TurnItem["status"], "running" | "completed" | "failed" | "interrupted" | "cancelled" >; readonly startedAt: DateTime.Utc; readonly updatedAt: DateTime.Utc; readonly presentation: ClaudeToolPresentation | undefined; readonly toolNonExecutionKind?: string; }) => { const completedAt = input.status === "running" ? null : input.updatedAt; const nodeId = idAllocator.derive.nodeFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: input.nativeItemId, }); const turnItemId = idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: input.nativeItemId, }); const nativeItemRef = { driver: CLAUDE_PROVIDER, nativeId: input.nativeItemId, strength: "strong" as const, }; const node: OrchestrationV2ExecutionNode = { id: nodeId, threadId: input.threadId, runId: input.runId, parentNodeId: input.parentNodeId, rootNodeId: input.rootNodeId, kind: "tool_call", status: input.status, countsForRun: false, providerThreadId: input.runId === null ? null : (input.context?.input.providerThread.id ?? null), providerTurnId: input.runId === null ? null : (input.context?.providerTurnId ?? null), nativeItemRef, runtimeRequestId: null, checkpointScopeId: null, startedAt: input.startedAt, completedAt, }; const readPath = ["read", "read file"].includes(input.classification.normalizedName) ? firstStringInputField(input.toolInput, ["file_path", "path"])?.trim() : undefined; const nativeToolInput = claudeNativeToolInputValue(input.toolInput); const searchTitle = ["grep", "glob", "ls"].includes(input.classification.normalizedName) ? formatSearchToolLabel({ input: nativeToolInput !== null && typeof nativeToolInput === "object" && !Array.isArray(nativeToolInput) ? (nativeToolInput as Record) : undefined, }) : undefined; const itemBase = { ...(input.toolNonExecutionKind === undefined ? {} : { toolNonExecutionKind: input.toolNonExecutionKind }), id: turnItemId, threadId: input.threadId, runId: input.runId, nodeId, providerThreadId: input.runId === null ? null : (input.context?.input.providerThread.id ?? null), providerTurnId: input.runId === null ? null : (input.context?.providerTurnId ?? null), nativeItemRef, parentItemId: null, ordinal: input.ordinal, status: input.status, title: readPath !== undefined ? formatReadToolLabel(readPath) : (searchTitle ?? dynamicToolTitle(input.toolName, nativeToolInput) ?? input.presentation?.title ?? null), startedAt: input.startedAt, completedAt, updatedAt: input.updatedAt, } satisfies Pick< OrchestrationV2TurnItem, | "id" | "threadId" | "runId" | "nodeId" | "providerThreadId" | "providerTurnId" | "nativeItemRef" | "parentItemId" | "ordinal" | "status" | "title" | "startedAt" | "completedAt" | "updatedAt" >; const viewedImagePath = readPath && readPath.length <= 4096 && !/[\r\n]/.test(readPath) && isWorkspaceImagePreviewPath(readPath) ? readPath : undefined; const itemType = input.classification.itemType; const webSearchPatterns = webSearchPatternsFromClaudeTool({ toolInput: input.toolInput, output: input.output, }); const webSearchResults = webSearchResultsFromClaudeOutput(input.output); const webFetchUrl = firstStringInputField(input.toolInput, ["url"])?.trim(); const outputValue = claudeNativeToolOutputValue(input.output); const outputText = itemType === "command_execution" ? claudeCommandOutputText(input.output) : itemType === "file_change" ? claudeFileChangeText(input.output) : claudeNativeToolOutputText(input.output); const turnItem: OrchestrationV2TurnItem = itemType === "command_execution" ? { ...itemBase, type: "command_execution", input: commandInputFromClaudeTool(input.toolName, input.toolInput), ...(outputText.length === 0 ? {} : { output: outputText }), } : itemType === "file_change" ? { ...itemBase, type: "file_change", fileName: fileNameFromClaudeTool(input.toolName, input.toolInput), ...(outputText.length === 0 ? {} : { diffStr: outputText }), } : itemType === "web_search" ? { ...itemBase, type: "web_search", ...(webSearchPatterns.length === 0 ? {} : { patterns: [...webSearchPatterns] }), ...(webSearchResults.length > 0 ? { results: [...webSearchResults] } : input.classification.normalizedName === "webfetch" && outputText.trim().length > 0 ? { results: [ { ...(webFetchUrl === undefined ? {} : { url: webFetchUrl }), snippet: outputText.slice(0, WEB_FETCH_SNIPPET_MAX_CHARS), }, ], } : {}), } : { ...itemBase, type: "dynamic_tool", ...(input.presentation?.toolIcon === undefined ? {} : { toolIcon: input.presentation.toolIcon }), ...(input.presentation?.toolSource === undefined ? {} : { toolSource: input.presentation.toolSource }), toolName: input.toolName, ...(viewedImagePath === undefined ? {} : { viewedImagePath }), input: claudeNativeToolInputValue(input.toolInput), ...(outputValue === undefined ? {} : { output: outputValue }), }; return { node, turnItem }; }; const emitToolCallArtifacts = Effect.fnUntraced(function* (artifacts: { readonly node: OrchestrationV2ExecutionNode; readonly turnItem: OrchestrationV2TurnItem; }) { yield* emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node: artifacts.node, }); yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: artifacts.turnItem, }); }); // Resume frames reference the launch call, but task_started names SendMessage; recover the mapping from CLI storage. const recoverResumedClaudeSubagent = Effect.fnUntraced(function* (resume: { readonly context: ActiveClaudeTurnContext; readonly nativeThreadId: string; readonly taskId: string; readonly toolUseId: string | undefined; readonly title: string; }) { if ( resume.toolUseId === undefined || findToolCall(resume.context, resume.toolUseId) === undefined || resume.context.subagentsByTaskId.has(resume.taskId) || (yield* Ref.get(sessionSubagentsByTaskId)).has(resume.taskId) ) { return; } const launchToolUseId = yield* queryRunner .subagentLaunchToolUseId({ sessionId: resume.nativeThreadId, agentId: resume.taskId, dir: resume.context.input.runtimePolicy.cwd, threadId: resume.context.input.threadId, providerSessionId: input.providerSessionId, }) .pipe( Effect.catch(() => Effect.logWarning("orchestration-v2.claude-subagent-launch-lookup-failed", { taskId: resume.taskId, }).pipe(Effect.as(null)), ), ); if (launchToolUseId === null || launchToolUseId === resume.toolUseId) { return; } const ids = claudeSubagentIds(resume.context, resume.taskId); const now = yield* DateTime.now; const subagent: ActiveClaudeSubagent = { nativeTaskType: undefined, confirmedOutcome: undefined, childThread: null, task: { id: ids.nodeId, threadId: resume.context.input.threadId, runId: resume.context.input.runId, parentNodeId: resume.context.input.rootNodeId, origin: "provider_native", createdBy: "agent", driver: CLAUDE_PROVIDER, providerInstanceId: resume.context.input.modelSelection.instanceId, providerThreadId: null, childThreadId: ids.childThreadId, nativeTaskRef: { driver: CLAUDE_PROVIDER, nativeId: resume.taskId, strength: "strong", }, prompt: "", title: resume.title, model: null, result: null, status: "completed", startedAt: now, completedAt: now, updatedAt: now, }, rootNodeId: resume.context.input.rootNodeId, childThreadId: ids.childThreadId, childRootNodeId: ids.childRootNodeId, turnItemId: ids.turnItemId, turnItemOrdinal: yield* resolveItemOrdinal( resume.context, `task:${resume.taskId}:subagent`, ), runToolUseId: launchToolUseId, effort: undefined, nextChildItemOrdinal: 100, resultItemOrdinal: null, lastAssistantText: null, lastAssistantMessageId: null, }; yield* Ref.update(sessionSubagentsByTaskId, (current) => new Map(current).set(resume.taskId, subagent), ); yield* Ref.update(sessionSubagentTaskIdsByToolUseId, (current) => new Map(current).set(launchToolUseId, resume.taskId), ); }); const withWorkflowMemberThreadIds = ( workflow: OrchestrationV2SubagentWorkflow, coordinatorChildThreadId: ThreadId, ): OrchestrationV2SubagentWorkflow => ({ ...workflow, agents: workflow.agents.map((member) => ({ ...member, childThreadId: idAllocator.derive.threadFromProviderThread({ driver: CLAUDE_PROVIDER, nativeThreadId: `${coordinatorChildThreadId}:agent:${member.index}`, }), })), }); const emitSubagentConversationMessage = Effect.fnUntraced(function* (input: { readonly nativeItemId: string; readonly threadId: ThreadId; readonly senderThreadId?: ThreadId; readonly rootNodeId: OrchestrationV2ExecutionNode["id"]; readonly role: "user" | "assistant"; readonly text: string; readonly workflowScript?: boolean; readonly ordinal: number; readonly now: DateTime.Utc; }) { const artifacts = makeSubagentConversationArtifacts({ messageId: idAllocator.derive.messageFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: input.nativeItemId, }), turnItemId: idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: input.nativeItemId, }), threadId: input.threadId, ...(input.senderThreadId === undefined ? {} : { senderThreadId: input.senderThreadId }), rootNodeId: input.rootNodeId, providerThreadId: null, providerTurnId: null, nativeItemRef: { driver: CLAUDE_PROVIDER, nativeId: input.nativeItemId, strength: "strong", }, role: input.role, text: input.text, ...(input.workflowScript === undefined ? {} : { workflowScript: input.workflowScript }), ordinal: input.ordinal, now: input.now, }); yield* emitProviderEvent({ type: "message.updated", driver: CLAUDE_PROVIDER, message: artifacts.message, }); yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: artifacts.turnItem, }); }); const projectClaudeWorkflowMembers = Effect.fnUntraced(function* (input: { readonly coordinator: ActiveClaudeSubagent; readonly workflow: OrchestrationV2SubagentWorkflow; }) { const parentThread = input.coordinator.childThread; const taskId = input.coordinator.task.nativeTaskRef?.nativeId; if (parentThread === null || taskId === undefined || taskId === null) return; const now = yield* DateTime.now; const seen = yield* Ref.get(workflowMemberStates); const transcriptDir = input.workflow.runHandles?.transcriptDir; const membersWithMetadata = input.workflow.agents.slice(); for (const [memberIndex, member] of input.workflow.agents.entries()) { if ((yield* Ref.get(sessionSubagentsByTaskId)).get(taskId) !== input.coordinator) return; const memberKey = `${input.coordinator.childThreadId}:agent:${member.index}`; const previous = seen.get(memberKey); const status = (member.state === "queued" || member.state === "running") && input.coordinator.task.status !== "running" ? input.coordinator.task.status : member.state === "queued" ? "pending" : member.state; const settled = status !== "pending" && status !== "running"; const prompt = member.prompt ?? member.label; const model = member.model ?? parentThread.modelSelection.model; const attempt = member.attempt ?? previous?.attempt ?? 1; const restarted = previous !== undefined && attempt > previous.attempt; const createdAt = previous?.createdAt ?? now; const startedAt = status === "pending" ? null : member.startedAt !== undefined ? DateTime.makeUnsafe(member.startedAt) : ((restarted ? null : previous?.startedAt) ?? (status === "running" ? now : null)); const completedAt = settled ? member.completedAt !== undefined ? DateTime.makeUnsafe(member.completedAt) : ((restarted ? null : previous?.completedAt) ?? now) : null; const changed = restarted || previous?.status !== status || !Equal.equals(previous?.startedAt, startedAt) || !Equal.equals(previous?.completedAt, completedAt) || previous?.result !== member.result || previous?.label !== member.label || previous?.prompt !== prompt || previous?.model !== model; const agentId = member.agentId; const readTranscript = settled && transcriptDir !== undefined && agentId !== undefined && (input.coordinator.task.status === "running" ? (restarted ? 0 : (previous?.transcriptReadAttempts ?? 0)) < 3 : restarted || previous?.finalTranscriptRead !== true); const readLiveTranscript = status === "running" && transcriptDir !== undefined && agentId !== undefined; if (!changed && !readTranscript && !readLiveTranscript) continue; const nodeId = idAllocator.derive.nodeFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: memberKey, }); const childRootNodeId = idAllocator.derive.nodeFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: `${memberKey}:thread-root`, }); const childThreadId = member.childThreadId; if (childThreadId === undefined) continue; if ( previous === undefined || previous.label !== member.label || previous.model !== model ) { yield* emitProviderEvent({ type: "app_thread.created", driver: CLAUDE_PROVIDER, appThread: makeSubagentChildThread({ parentThread, childThreadId, parentNodeId: nodeId, activeProviderThreadId: null, providerInstanceId: parentThread.providerInstanceId, modelSelection: { instanceId: parentThread.providerInstanceId, model }, title: member.label, now: createdAt, createdBy: "agent", creationSource: "provider", }), }); } if (previous === undefined || previous.prompt !== prompt) { yield* emitSubagentConversationMessage({ nativeItemId: `${memberKey}:prompt`, threadId: childThreadId, rootNodeId: childRootNodeId, role: "user", text: prompt, ordinal: 100, now, }); } const task = { id: nodeId, threadId: parentThread.id, runId: null, parentNodeId: input.coordinator.childRootNodeId, origin: "provider_native" as const, createdBy: "agent" as const, driver: CLAUDE_PROVIDER, providerInstanceId: parentThread.providerInstanceId, providerThreadId: null, childThreadId, nativeTaskRef: { driver: CLAUDE_PROVIDER, nativeId: memberKey, strength: "strong" as const, }, prompt, title: member.label, model, status, result: member.result ?? null, startedAt, completedAt, updatedAt: now, } satisfies OrchestrationV2Subagent; const nodeBase = { runId: null, status, countsForRun: false, providerThreadId: null, providerTurnId: null, nativeItemRef: task.nativeTaskRef, runtimeRequestId: null, checkpointScopeId: null, startedAt, completedAt, }; if (changed) { for (const node of [ { ...nodeBase, id: nodeId, threadId: parentThread.id, parentNodeId: input.coordinator.childRootNodeId, rootNodeId: input.coordinator.childRootNodeId, kind: "subagent" as const, }, { ...nodeBase, id: childRootNodeId, threadId: childThreadId, parentNodeId: null, rootNodeId: childRootNodeId, kind: "root_turn" as const, }, ]) { yield* emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node }); } yield* emitProviderEvent({ type: "subagent.updated", driver: CLAUDE_PROVIDER, subagent: task, }); } const toolCalls = previous?.toolCalls ?? new Map(); let nextItemOrdinal = previous?.nextItemOrdinal ?? 100; const target: ClaudeSubagentProjectionTarget = { childThreadId, childRootNodeId, nextOrdinal: () => ++nextItemOrdinal, toolCalls, }; const endOpenToolCalls = Effect.fnUntraced(function* () { if (toolCalls.size === 0) return; yield* endToolCalls({ context: null, toolCalls: [...toolCalls.values()], status: "interrupted", completedAt: now, }); toolCalls.clear(); }); if (restarted) yield* endOpenToolCalls(); // Retries can rewrite transcripts past the cursor; rereading from zero repeats only already shown items. let transcriptCursor = previous === undefined || restarted || previous.agentId !== agentId ? initialWorkflowAgentTranscriptCursor : previous.transcriptCursor; let transcriptMetadata = previous === undefined || restarted || previous.agentId !== agentId ? {} : previous.transcriptMetadata; let textShown = !restarted && previous?.textShown === true; let resultShown = !restarted && previous?.resultShown === true; let turnEnded = !restarted && previous?.turnEnded === true; let transcriptRead = false; let lastReadAt = now; // Pre-tool text is not the reply; wait for turn end. const awaitingReply = () => settled && transcriptRead && !turnEnded && !resultShown; const flushWindowLeft = (at: DateTime.Utc) => completedAt === null ? 0 : WORKFLOW_MEMBER_TRANSCRIPT_FLUSH_MS - (DateTime.toEpochMillis(at) - DateTime.toEpochMillis(completedAt)); // Drain capped reads; settled replies can lie beyond the cap. let transcriptDrained = true; if ( (readTranscript || readLiveTranscript) && transcriptDir !== undefined && agentId !== undefined ) { const readNext = (cursor: WorkflowAgentTranscriptCursor) => readWorkflowAgentTranscript({ transcriptDir, agentId, cursor, ...(adapterOptions.environment.CLAUDE_CONFIG_DIR === undefined ? {} : { configDir: path.resolve( session.cwd, adapterOptions.environment.CLAUDE_CONFIG_DIR, ), }), }).pipe(Effect.option); const drain = Effect.fnUntraced(function* () { lastReadAt = yield* DateTime.now; transcriptDrained = false; while (!transcriptDrained) { const read = yield* readNext(transcriptCursor); if ((yield* Ref.get(sessionSubagentsByTaskId)).get(taskId) !== input.coordinator) return false; if (Option.isNone(read)) break; transcriptRead = true; const advanced = read.value.cursor.offset !== transcriptCursor.offset; transcriptCursor = read.value.cursor; transcriptMetadata = mergeWorkflowAgentMetadata( read.value.lines, transcriptMetadata, ); for (const line of read.value.lines) { if (yield* projectSubagentTranscriptLine(target, line)) textShown = true; turnEnded = isClaudeTranscriptTurnEnd(line); } // A line longer than the cap can't advance; stop rather than spin. transcriptDrained = !read.value.truncated || !advanced; } return true; }); if (!(yield* drain())) return; // The coordinator sends no more frames; wait out the transcript flush before using the excerpt. const waitLeft = flushWindowLeft(lastReadAt); if ( input.coordinator.task.status !== "running" && awaitingReply() && member.result !== undefined && waitLeft > 0 ) { yield* Effect.sleep(waitLeft); if (!(yield* drain())) return; } } if ( transcriptRead && (transcriptMetadata.effort !== member.effort || transcriptMetadata.speed !== member.speed) ) { membersWithMetadata[memberIndex] = { ...member, effort: transcriptMetadata.effort, speed: transcriptMetadata.speed, }; } if (settled) { // Open calls receive no results after their member settles. yield* endOpenToolCalls(); if ( !textShown && !resultShown && member.result !== undefined && (!transcriptRead || flushWindowLeft(lastReadAt) <= 0) ) { yield* emitSubagentConversationMessage({ nativeItemId: `${memberKey}:result:${attempt}`, threadId: childThreadId, rootNodeId: childRootNodeId, role: "assistant", text: `Partial answer from workflow progress:\n\n${member.result}`, ordinal: target.nextOrdinal(), now, }); resultShown = true; } } yield* Ref.update(workflowMemberStates, (current) => new Map(current).set(memberKey, { status, attempt, agentId, result: member.result, label: member.label, prompt, model, transcriptReadAttempts: (restarted ? 0 : (previous?.transcriptReadAttempts ?? 0)) + (readTranscript ? 1 : 0), finalTranscriptRead: (!restarted && previous?.finalTranscriptRead === true) || (readTranscript && transcriptRead && transcriptDrained && textShown && input.coordinator.task.status !== "running"), transcriptCursor, transcriptMetadata, nextItemOrdinal, toolCalls, textShown, turnEnded, resultShown, createdAt, startedAt, completedAt, }), ); } if ( membersWithMetadata.some((member, index) => member !== input.workflow.agents[index]) ) { if ((yield* Ref.get(sessionSubagentsByTaskId)).get(taskId) !== input.coordinator) return; input.coordinator.task = { ...input.coordinator.task, workflow: { ...input.workflow, agents: membersWithMetadata }, }; yield* emitProviderEvent({ type: "subagent.updated", driver: CLAUDE_PROVIDER, subagent: input.coordinator.task, }); } }); const confirmedSubagentOutcome = (task: OrchestrationV2Subagent | undefined) => { if ( task === undefined || task.completedAt === null || (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") ) return; return { status: task.status, completedAt: task.completedAt, result: task.result }; }; const discardBufferedSubagentStarts = ( nativeThreadId: string, taskId: string, outcome: SDKMessage, ) => Ref.update(wakeBuffers, (current) => { const entry = current.get(nativeThreadId); if (entry === undefined) return current; const outcomeIndex = entry.messages.indexOf(outcome); const messages = entry.messages.filter( (buffered, index) => !( (outcomeIndex === -1 || index < outcomeIndex) && buffered.type === "system" && (buffered.subtype === "task_started" || buffered.subtype === "task_progress") && buffered.task_id === taskId ), ); return messages.length === entry.messages.length ? current : new Map(current).set(nativeThreadId, { ...entry, messages }); }); const updateClaudeSubagentNode = Effect.fnUntraced(function* (input: { readonly context: ActiveClaudeTurnContext; readonly taskId: string; readonly toolUseId?: string; readonly prompt?: string; readonly title?: string; readonly model?: string; readonly effort?: string; readonly owner?: ActiveClaudeSubagent; readonly nativeTaskType?: string; readonly progress?: string; readonly workflowFrame?: unknown; readonly workflowName?: string; readonly workflowRunHandles?: OrchestrationV2WorkflowRunHandles; readonly result?: string; readonly status: Extract< OrchestrationV2ExecutionNode["status"], "running" | "completed" | "failed" | "cancelled" >; readonly reopen?: boolean; readonly nativeOutcome?: Extract< SDKMessage, { readonly type: "system"; readonly subtype: "task_notification" } >; // The process that ran it exited, which confirms the outcome as a native one would. readonly processExited?: true; readonly onlyIfRunningInRun?: RunId | null; }) { const registeredSubagent = (yield* Ref.get(sessionSubagentsByTaskId)).get(input.taskId); const existingSubagent = input.onlyIfRunningInRun !== undefined ? registeredSubagent : (input.context.subagentsByTaskId.get(input.taskId) ?? (input.toolUseId === undefined ? undefined : input.context.subagentsByToolUseId.get(input.toolUseId)) ?? registeredSubagent); if (existingSubagent === undefined && input.status !== "running") { return; } if ( input.onlyIfRunningInRun !== undefined && (existingSubagent?.task.status !== "running" || existingSubagent.task.runId !== input.onlyIfRunningInRun) ) return; // Only task_started may reopen a terminal task id on resume; late task_progress must not. const isReopen = input.reopen === true && existingSubagent !== undefined && existingSubagent.task.status !== "running" && input.status === "running"; if ( existingSubagent !== undefined && existingSubagent.task.status !== "running" && input.status === "running" && !isReopen ) { return; } // Resume task_started uses the SendMessage tool_use_id and sent message as its prompt. const resumeToolUseId = input.reopen === true && existingSubagent !== undefined && input.toolUseId !== undefined && input.toolUseId !== existingSubagent.runToolUseId && input.prompt !== undefined ? input.toolUseId : null; const lifecycleChanged = existingSubagent === undefined || existingSubagent.task.status !== input.status || (input.reopen === true && input.status === "running"); const now = yield* DateTime.now; const nativeItemId = `task:${input.taskId}`; const derivedIds = claudeSubagentIds(input.context, input.taskId); const nodeId = existingSubagent?.task.id ?? derivedIds.nodeId; const childRootNodeId = existingSubagent?.childRootNodeId ?? derivedIds.childRootNodeId; const childThreadId = existingSubagent?.childThreadId ?? derivedIds.childThreadId; const turnItemOrdinal = existingSubagent?.turnItemOrdinal ?? (yield* resolveItemOrdinal(input.context, `${nativeItemId}:subagent`)); const priorTask = existingSubagent === undefined ? undefined : isReopen ? (({ progress: _staleProgress, ...rest }) => ({ ...rest, result: null }))( existingSubagent.task, ) : existingSubagent.task; const workflow = mergeClaudeWorkflowProgress({ previous: priorTask?.workflow, ...(input.workflowName === undefined ? {} : { name: input.workflowName }), ...(input.workflowRunHandles === undefined ? {} : { runHandles: input.workflowRunHandles }), message: input.workflowFrame, }); const workflowWithThreads = workflow === undefined ? undefined : withWorkflowMemberThreadIds(workflow, childThreadId); const effort = existingSubagent === undefined ? (input.effort ?? input.owner?.effort ?? compileClaudeModelSelection(input.context.input.modelSelection).effort) : existingSubagent.effort; const model = input.model ?? priorTask?.model ?? null; const task = { ...(priorTask ?? { id: nodeId, threadId: input.context.input.threadId, runId: input.context.input.runId, parentNodeId: input.owner?.task.id ?? input.context.input.rootNodeId, origin: "provider_native" as const, createdBy: "agent" as const, driver: CLAUDE_PROVIDER, providerInstanceId: input.context.input.modelSelection.instanceId, providerThreadId: null, childThreadId, nativeTaskRef: { driver: CLAUDE_PROVIDER, nativeId: input.taskId, strength: "strong" as const, }, prompt: input.prompt ?? "", title: input.title ?? null, model: input.model ?? null, result: null, startedAt: now, }), status: input.status, ...(input.reopen === true && input.status === "running" && existingSubagent !== undefined ? { runId: input.context.input.runId } : {}), ...(input.prompt === undefined ? {} : { prompt: input.prompt }), ...(input.title === undefined ? {} : { title: input.title }), model, ...(existingSubagent === undefined || existingSubagent.task.modelSelection !== undefined ? { modelSelection: claudeSubagentModelSelection( subagentModelCatalog, input.context.input.modelSelection.instanceId, model || input.context.input.modelSelection.model, effort, ), } : {}), ...(input.progress === undefined ? {} : { progress: input.progress }), ...(workflowWithThreads === undefined ? {} : { workflow: workflowWithThreads }), ...(input.result === undefined ? {} : { result: input.result }), ...(isReopen ? { startedAt: now } : {}), completedAt: input.status === "running" ? null : (priorTask?.completedAt ?? now), updatedAt: now, } satisfies OrchestrationV2Subagent; const subagent = { task, nativeTaskType: input.nativeTaskType ?? existingSubagent?.nativeTaskType, confirmedOutcome: input.nativeOutcome !== undefined || input.processExited === true ? confirmedSubagentOutcome(task) : isReopen ? undefined : existingSubagent?.confirmedOutcome, rootNodeId: existingSubagent?.rootNodeId ?? input.context.input.rootNodeId, childThreadId, childRootNodeId, turnItemId: existingSubagent?.turnItemId ?? derivedIds.turnItemId, turnItemOrdinal, runToolUseId: existingSubagent === undefined ? (input.toolUseId ?? null) : (resumeToolUseId ?? existingSubagent.runToolUseId), childThread: existingSubagent?.childThread ?? null, effort, nextChildItemOrdinal: existingSubagent?.nextChildItemOrdinal ?? 100, resultItemOrdinal: existingSubagent?.resultItemOrdinal ?? null, lastAssistantText: input.reopen === true ? null : (existingSubagent?.lastAssistantText ?? null), lastAssistantMessageId: input.reopen === true ? null : (existingSubagent?.lastAssistantMessageId ?? null), } satisfies ActiveClaudeSubagent; // Ingestion can terminalize after lookup; reopen only that generation to preserve newer results. const accepted = yield* Ref.modify(sessionSubagentsByTaskId, (current) => { const registered = current.get(input.taskId); if ( (input.onlyIfRunningInRun !== undefined && (registered?.task.status !== "running" || registered.task.runId !== input.onlyIfRunningInRun)) || (registered !== undefined && registered.task.status !== "running" && input.status === "running" && !(isReopen && registered === existingSubagent)) ) { return [false, current] as const; } return [true, new Map(current).set(input.taskId, subagent)] as const; }); if (!accepted) return; if (input.nativeOutcome !== undefined) appliedSubagentOutcomes.add(input.nativeOutcome); if (existingSubagent !== undefined && input.status !== "running") { const openCalls = [...subagentToolCalls.values()].filter( (toolCall) => toolCall.threadId === existingSubagent.childThreadId, ); yield* endToolCalls({ context: input.context, toolCalls: openCalls, status: input.status === "cancelled" ? "interrupted" : "failed", completedAt: yield* DateTime.now, }); for (const toolCall of openCalls) { subagentToolCalls.delete(toolCall.nativeItemId); } } input.context.subagentsByTaskId.set(input.taskId, subagent); if (input.toolUseId !== undefined) { input.context.subagentsByToolUseId.set(input.toolUseId, subagent); const toolUseId = input.toolUseId; yield* Ref.update(sessionSubagentTaskIdsByToolUseId, (current) => current.get(toolUseId) === input.taskId ? current : new Map(current).set(toolUseId, input.taskId), ); } if (subagent.childThread === null) { const childThread = makeSubagentChildThread({ parentThread: input.context.input.appThread, childThreadId, parentNodeId: nodeId, activeProviderThreadId: null, providerInstanceId: input.context.input.modelSelection.instanceId, modelSelection: task.modelSelection ?? input.context.input.modelSelection, title: subagentThreadTitle({ parentTitle: input.context.input.appThread.title, prompt: task.prompt, title: workflowWithThreads === undefined ? task.title : `Workflow · ${workflowWithThreads.name ?? task.title ?? "Untitled"}`, ordinal: input.context.subagentsByTaskId.size, }), now, createdBy: "agent", creationSource: "provider", }); // Recovery cannot tell whether the child thread was already persisted. yield* emitProviderEvent({ type: "app_thread.created", driver: CLAUDE_PROVIDER, appThread: childThread, ...(existingSubagent === undefined ? {} : { createOnly: true }), }); subagent.childThread = childThread; } if (lifecycleChanged) { yield* emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node: { id: nodeId, threadId: task.threadId, runId: task.runId, parentNodeId: task.parentNodeId, rootNodeId: subagent.rootNodeId, kind: "subagent", status: input.status, countsForRun: false, providerThreadId: input.context.input.providerThread.id, providerTurnId: input.context.providerTurnId, nativeItemRef: { driver: CLAUDE_PROVIDER, nativeId: input.taskId, strength: "strong", }, runtimeRequestId: null, checkpointScopeId: null, startedAt: task.startedAt, completedAt: task.completedAt, }, }); } // Emit before child records so the resumed run can adopt the child thread. yield* emitProviderEvent({ type: "subagent.updated", driver: CLAUDE_PROVIDER, subagent: task, }); if (lifecycleChanged) { yield* emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node: { id: childRootNodeId, threadId: childThreadId, runId: null, parentNodeId: null, rootNodeId: childRootNodeId, kind: "root_turn", status: input.status, countsForRun: false, providerThreadId: null, providerTurnId: null, nativeItemRef: task.nativeTaskRef, runtimeRequestId: null, checkpointScopeId: null, startedAt: task.startedAt, completedAt: task.completedAt, }, }); } const promptNativeItemId = existingSubagent === undefined ? `${nativeItemId}:prompt` : resumeToolUseId === null ? null : `${nativeItemId}:prompt:${resumeToolUseId}`; if (promptNativeItemId !== null) { yield* emitSubagentConversationMessage({ nativeItemId: promptNativeItemId, senderThreadId: input.context.input.threadId, threadId: childThreadId, rootNodeId: childRootNodeId, role: "user", text: task.prompt, // Workflow prompts are collapsed scripts; clients open with the plan. A brief must not be mistaken for a script. ...(existingSubagent === undefined ? { workflowScript: task.workflow !== undefined } : {}), ordinal: existingSubagent === undefined ? 100 : ++subagent.nextChildItemOrdinal, now, }); } if (workflowWithThreads !== undefined) { yield* projectClaudeWorkflowMembers({ coordinator: subagent, workflow: workflowWithThreads, }); if ((yield* Ref.get(sessionSubagentsByTaskId)).get(input.taskId) !== subagent) return; } yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: { id: subagent.turnItemId, threadId: task.threadId, runId: task.runId, nodeId: task.id, providerThreadId: input.context.input.providerThread.id, providerTurnId: input.context.providerTurnId, nativeItemRef: task.nativeTaskRef, parentItemId: null, ordinal: subagent.turnItemOrdinal, status: task.status, title: task.title, startedAt: task.startedAt, completedAt: task.completedAt, updatedAt: task.updatedAt, type: "subagent", subagentId: task.id, origin: task.origin, driver: task.driver, providerInstanceId: task.providerInstanceId, childThreadId: task.childThreadId, prompt: task.prompt, ...(task.progress === undefined ? {} : { progress: task.progress }), result: task.result, }, }); if (input.nativeOutcome !== undefined && subagent.confirmedOutcome !== undefined) { const outcome = subagentStopOutcomes.get(input.taskId); if (outcome !== undefined) yield* Deferred.succeed(outcome, subagent.confirmedOutcome); } const resultAlreadyShown = input.status === "completed" && input.result !== undefined && normalizeClaudeResultText(subagent.lastAssistantText ?? "") === normalizeClaudeResultText(input.result); if ( input.result !== undefined && input.result.trim().length > 0 && input.status !== "running" && !resultAlreadyShown ) { const resultNativeItemId = `${nativeItemId}:result`; const resultItemOrdinal = subagent.resultItemOrdinal ?? ++subagent.nextChildItemOrdinal; subagent.resultItemOrdinal = resultItemOrdinal; yield* emitSubagentConversationMessage({ nativeItemId: resultNativeItemId, threadId: childThreadId, rootNodeId: childRootNodeId, role: "assistant", text: input.result, ordinal: resultItemOrdinal, now, }); } }); const emitClaudePlanProjection = Effect.fnUntraced(function* (input: { readonly context: ActiveClaudeTurnContext; readonly nativeItemId: string; readonly kind: "todo_list" | "proposed_plan"; readonly steps?: ReadonlyArray; readonly markdown?: string; }) { const updatedAt = yield* DateTime.now; const projectionNativeItemId = `${input.kind}:${input.nativeItemId}`; const planKey = `${input.context.input.threadId}:${projectionNativeItemId}`; const existingPlanId = (yield* Ref.get(planIdsByNativeItem)).get(planKey); const planId = existingPlanId ?? (yield* idAllocator.allocate.plan({ threadId: input.context.input.threadId, runId: input.context.input.runId, driver: CLAUDE_PROVIDER, })); if (existingPlanId === undefined) { yield* Ref.update(planIdsByNativeItem, (current) => { const updated = new Map(current); updated.set(planKey, planId); return updated; }); } const nodeId = idAllocator.derive.nodeFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: projectionNativeItemId, }); const nativeItemRef = { driver: CLAUDE_PROVIDER, nativeId: input.nativeItemId, strength: "strong" as const, }; const ordinal = yield* resolveItemOrdinal(input.context, projectionNativeItemId); const steps = [...(input.steps ?? [])]; const todoStatus = steps.every((step) => step.status === "completed") ? "completed" : "active"; const plan: OrchestrationV2PlanArtifact = input.kind === "todo_list" ? { id: planId, threadId: input.context.input.threadId, runId: input.context.input.runId, nodeId, kind: "todo_list", status: todoStatus, steps, } : { id: planId, threadId: input.context.input.threadId, runId: input.context.input.runId, nodeId, kind: "proposed_plan", status: "active", markdown: input.markdown ?? "", }; const node: OrchestrationV2ExecutionNode = { id: nodeId, threadId: input.context.input.threadId, runId: input.context.input.runId, parentNodeId: input.context.input.rootNodeId, rootNodeId: input.context.input.rootNodeId, kind: input.kind === "todo_list" ? "todo_list" : "plan", status: "completed", countsForRun: false, providerThreadId: input.context.input.providerThread.id, providerTurnId: input.context.providerTurnId, nativeItemRef, runtimeRequestId: null, checkpointScopeId: null, startedAt: updatedAt, completedAt: updatedAt, }; const common = { id: idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: projectionNativeItemId, }), threadId: input.context.input.threadId, runId: input.context.input.runId, nodeId, providerThreadId: input.context.input.providerThread.id, providerTurnId: input.context.providerTurnId, nativeItemRef, parentItemId: null, ordinal, status: "completed" as const, title: null, startedAt: updatedAt, completedAt: updatedAt, updatedAt, }; const turnItem: OrchestrationV2TurnItem = input.kind === "todo_list" ? { ...common, type: "todo_list", planId, steps } : { ...common, type: "proposed_plan", planId, markdown: input.markdown ?? "", streaming: false, }; const latestPlanKey = `${input.context.input.threadId}:${input.kind}`; const previousPlan = (yield* Ref.get(latestPlanByKind)).get(latestPlanKey); yield* Effect.all( [ ...(previousPlan === undefined || previousPlan.id === plan.id || (previousPlan.kind !== "todo_list" && previousPlan.status === "completed") ? [] : [ emitProviderEvent({ type: "plan.updated" as const, driver: CLAUDE_PROVIDER, plan: { ...previousPlan, status: "superseded" as const }, }), ]), emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node }), emitProviderEvent({ type: "plan.updated", driver: CLAUDE_PROVIDER, plan }), emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem }), ], { concurrency: 1 }, ); yield* Ref.update(latestPlanByKind, (current) => { const updated = new Map(current); updated.set(latestPlanKey, plan); return updated; }); }); const resolveSubagentByToolUseId = Effect.fnUntraced(function* ( context: ActiveClaudeTurnContext, toolUseId: string, ) { const taskId = (yield* Ref.get(sessionSubagentTaskIdsByToolUseId)).get(toolUseId); const registered = taskId === undefined ? undefined : (yield* Ref.get(sessionSubagentsByTaskId)).get(taskId); return registered ?? context.subagentsByToolUseId.get(toolUseId); }); const findToolCall = (context: ActiveClaudeTurnContext, nativeItemId: string) => context.toolCalls.get(nativeItemId) ?? subagentToolCalls.get(nativeItemId); const toolCallsFor = (context: ActiveClaudeTurnContext, toolCall: ActiveClaudeToolCall) => toolCall.runId === null ? subagentToolCalls : context.toolCalls; const ensureToolCallStarted = Effect.fnUntraced(function* (input: { readonly context: ActiveClaudeTurnContext; readonly nativeItemId: string; readonly toolName: string; readonly toolInput: ClaudeNativeToolInput; readonly parentToolUseId: string | null; readonly presentation?: ClaudeToolPresentation | undefined; }) { const existing = findToolCall(input.context, input.nativeItemId); if (existing !== undefined) { // Permission callbacks can start calls before assistant frames provide their display name and icon. if (input.presentation === undefined || existing.presentation !== undefined) { return existing; } const presented = { ...existing, presentation: input.presentation }; toolCallsFor(input.context, presented).set(input.nativeItemId, presented); const updatedAt = yield* DateTime.now; yield* emitToolCallArtifacts( buildToolCallArtifacts({ context: input.context, nativeItemId: presented.nativeItemId, toolName: presented.toolName, classification: presented.classification, toolInput: presented.input, threadId: presented.threadId, runId: presented.runId, rootNodeId: presented.rootNodeId, parentNodeId: presented.parentNodeId, ordinal: presented.ordinal, output: NO_CLAUDE_NATIVE_TOOL_OUTPUT, status: "running", startedAt: presented.startedAt, updatedAt, presentation: input.presentation, }), ); return presented; } const subagent = input.parentToolUseId === null ? undefined : yield* resolveSubagentByToolUseId(input.context, input.parentToolUseId); const call = { nativeItemId: input.nativeItemId, toolName: input.toolName, toolInput: input.toolInput, presentation: input.presentation, startedAt: yield* DateTime.now, }; if (subagent !== undefined) { return yield* startSubagentToolCall(subagentProjectionTarget(subagent), call); } return yield* startToolCall({ ...call, context: input.context, threadId: input.context.input.threadId, runId: input.context.input.runId, rootNodeId: input.context.input.rootNodeId, ordinal: yield* resolveItemOrdinal(input.context, input.nativeItemId), registry: input.context.toolCalls, }); }); const startToolCall = Effect.fnUntraced(function* (input: { readonly context: ActiveClaudeTurnContext | null; readonly nativeItemId: string; readonly toolName: string; readonly toolInput: ClaudeNativeToolInput; readonly presentation: ClaudeToolPresentation | undefined; readonly threadId: ThreadId; readonly runId: ProviderAdapter.ProviderAdapterV2TurnInput["runId"] | null; readonly rootNodeId: OrchestrationV2ExecutionNode["id"]; readonly ordinal: number; readonly startedAt: DateTime.Utc; readonly registry: Map; }) { const toolCall: ActiveClaudeToolCall = { nativeItemId: input.nativeItemId, toolName: input.toolName, classification: classifyClaudeNativeTool(input.toolName), input: input.toolInput, threadId: input.threadId, runId: input.runId, rootNodeId: input.rootNodeId, parentNodeId: input.rootNodeId, ordinal: input.ordinal, startedAt: input.startedAt, ...(input.presentation === undefined ? {} : { presentation: input.presentation }), }; input.registry.set(input.nativeItemId, toolCall); yield* emitToolCallArtifacts( buildToolCallArtifacts({ context: input.context, nativeItemId: toolCall.nativeItemId, toolName: toolCall.toolName, classification: toolCall.classification, toolInput: toolCall.input, threadId: toolCall.threadId, runId: toolCall.runId, rootNodeId: toolCall.rootNodeId, parentNodeId: toolCall.parentNodeId, ordinal: toolCall.ordinal, output: NO_CLAUDE_NATIVE_TOOL_OUTPUT, status: "running", startedAt: toolCall.startedAt, updatedAt: toolCall.startedAt, presentation: input.presentation, }), ); return toolCall; }); const subagentProjectionTarget = ( subagent: ActiveClaudeSubagent, ): ClaudeSubagentProjectionTarget => ({ childThreadId: subagent.childThreadId, childRootNodeId: subagent.childRootNodeId, nextOrdinal: () => ++subagent.nextChildItemOrdinal, toolCalls: subagentToolCalls, }); const startSubagentToolCall = ( target: ClaudeSubagentProjectionTarget, call: { readonly nativeItemId: string; readonly toolName: string; readonly toolInput: ClaudeNativeToolInput; readonly presentation: ClaudeToolPresentation | undefined; readonly startedAt: DateTime.Utc; }, ) => startToolCall({ ...call, context: null, threadId: target.childThreadId, runId: null, rootNodeId: target.childRootNodeId, ordinal: target.nextOrdinal(), registry: target.toolCalls, }); const settleToolCall = Effect.fnUntraced(function* (input: { readonly context: ActiveClaudeTurnContext | null; readonly toolCall: ActiveClaudeToolCall; readonly registry: Map; readonly message: SDKMessage; readonly toolResult: ClaudeToolResultContentBlock; readonly output: ClaudeNativeToolOutput; readonly completedAt: DateTime.Utc; }) { const { toolCall } = input; const toolNonExecutionKind = claudeToolNonExecutionKind( input.message, input.toolResult.tool_use_id, ); yield* stopTaskOutputTail(toolCall.nativeItemId); yield* emitToolCallArtifacts( buildToolCallArtifacts({ context: input.context, nativeItemId: toolCall.nativeItemId, toolName: toolCall.toolName, classification: toolCall.classification, toolInput: toolCall.input, threadId: toolCall.threadId, runId: toolCall.runId, rootNodeId: toolCall.rootNodeId, parentNodeId: toolCall.parentNodeId, ordinal: toolCall.ordinal, output: input.output, status: toolNonExecutionKind === "cancelled" ? "cancelled" : isClaudeToolResultError(input.toolResult) ? "failed" : "completed", ...(toolNonExecutionKind === undefined ? {} : { toolNonExecutionKind }), startedAt: toolCall.startedAt, updatedAt: input.completedAt, presentation: toolCall.presentation, }), ); input.registry.delete(toolCall.nativeItemId); }); const emitSubagentThinking = Effect.fnUntraced(function* ( target: ClaudeSubagentProjectionTarget, message: SDKAssistantMessage, at: DateTime.Utc, ) { const thinking = message.message.content.flatMap((block) => block.type === "thinking" && block.thinking.trim().length > 0 ? [block.thinking] : [], ); for (const [index, text] of thinking.entries()) { const nativeItemId = `${message.uuid}:thinking:${index}`; yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: { id: idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId, }), threadId: target.childThreadId, runId: null, nodeId: target.childRootNodeId, providerThreadId: null, providerTurnId: null, nativeItemRef: { driver: CLAUDE_PROVIDER, nativeId: nativeItemId, strength: "strong", }, parentItemId: null, ordinal: target.nextOrdinal(), type: "reasoning", title: "Thinking", text, streaming: false, status: "completed", startedAt: at, completedAt: at, updatedAt: at, }, }); } }); const projectSubagentTranscriptLine = Effect.fnUntraced(function* ( target: ClaudeSubagentProjectionTarget, line: unknown, ) { if (!isClaudeTranscriptConversationLine(line)) return false; const at = Option.getOrUndefined(DateTime.make(text(field(line, "timestamp")) ?? "")) ?? (yield* DateTime.now); const presentations = claudeToolPresentationsFromAssistantMessage(line); for (const toolUse of claudeToolUseBlocksFromAssistantMessage(line)) { if (target.toolCalls.has(toolUse.id)) continue; yield* startSubagentToolCall(target, { nativeItemId: toolUse.id, toolName: toolUse.name, toolInput: claudeNativeToolInputFromUnknown(toolUse.input), presentation: presentations.get(toolUse.id) ?? mcpToolPresentation({ toolName: toolUse.name.replace(/^mcp__claude_ai_/u, "mcp__"), }), startedAt: at, }); } for (const { toolResult, output } of claudeToolResultEntriesFromMessage(line)) { const toolCall = target.toolCalls.get(toolResult.tool_use_id) ?? (yield* startSubagentToolCall(target, { nativeItemId: toolResult.tool_use_id, toolName: toolNameFromClaudeToolResult(toolResult), toolInput: EMPTY_CLAUDE_NATIVE_TOOL_INPUT, presentation: undefined, startedAt: at, })); yield* settleToolCall({ context: null, toolCall, registry: target.toolCalls, message: line, toolResult, output, completedAt: at, }); } if (line.type !== "assistant") return false; yield* emitSubagentThinking(target, line, at); const assistantText = textFromClaudeContent(line.message.content); if (assistantText.length === 0) return false; yield* emitSubagentConversationMessage({ nativeItemId: line.uuid, threadId: target.childThreadId, rootNodeId: target.childRootNodeId, role: "assistant", text: assistantText, ordinal: target.nextOrdinal(), now: at, }); return true; }); const buildApprovalRequestArtifacts = Effect.fnUntraced(function* (input: { readonly context: ActiveClaudeTurnContext; readonly nativeItemId: string; readonly nativeRequestId: string; readonly parentNodeId?: OrchestrationV2ExecutionNode["id"]; readonly requestKind: OrchestrationV2RuntimeRequest["kind"]; readonly prompt?: string; readonly appName?: string; readonly options?: ReadonlyArray; readonly presentation?: ReturnType; readonly questions?: ReadonlyArray; }) { const createdAt = yield* DateTime.now; const requestId = yield* idAllocator.allocate.runtimeRequest({ driver: CLAUDE_PROVIDER, providerTurnId: input.context.providerTurnId, nativeRequestId: input.nativeRequestId, }); const nodeId = idAllocator.derive.approvalNode({ requestId }); const providerSessionId = input.context.input.providerThread.providerSessionId; if (providerSessionId === null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Provider thread ${input.context.input.providerThread.id} is missing a provider session id.`, }); } const ordinal = yield* resolveItemOrdinal( input.context, `${input.nativeItemId}:approval:${input.nativeRequestId}`, ); const nativeItemRef = { driver: CLAUDE_PROVIDER, nativeId: input.nativeRequestId, strength: "strong" as const, }; const node: OrchestrationV2ExecutionNode = { id: nodeId, threadId: input.context.input.threadId, runId: input.context.input.runId, parentNodeId: input.parentNodeId ?? idAllocator.derive.nodeFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: input.nativeItemId, }), rootNodeId: input.context.input.rootNodeId, kind: input.questions === undefined ? "approval_request" : "user_input_request", status: "waiting", countsForRun: false, providerThreadId: input.context.input.providerThread.id, providerTurnId: input.context.providerTurnId, nativeItemRef, runtimeRequestId: requestId, checkpointScopeId: null, startedAt: createdAt, completedAt: null, }; const request: OrchestrationV2RuntimeRequest = { id: requestId, nodeId, providerTurnId: input.context.providerTurnId, nativeRequestRef: { driver: CLAUDE_PROVIDER, nativeId: input.nativeRequestId, strength: "strong", }, kind: input.requestKind, status: "pending", responseCapability: { type: "live", providerSessionId, ...(input.questions === undefined && input.requestKind !== "mcp-elicitation" ? { supportsFeedback: true } : {}), }, createdAt, resolvedAt: null, }; const turnItem: OrchestrationV2TurnItem = { id: idAllocator.derive.approvalTurnItem({ requestId }), threadId: input.context.input.threadId, runId: input.context.input.runId, nodeId, providerThreadId: input.context.input.providerThread.id, providerTurnId: input.context.providerTurnId, nativeItemRef, parentItemId: null, ordinal, status: "waiting", title: null, startedAt: createdAt, completedAt: null, updatedAt: createdAt, ...(input.questions === undefined ? { type: "approval_request" as const, requestId, requestKind: input.requestKind as ProviderRequestKind, ...(input.prompt === undefined ? {} : { prompt: input.prompt }), ...(input.appName === undefined ? {} : { appName: input.appName }), ...(input.options === undefined ? {} : { options: input.options }), ...input.presentation, } : { type: "user_input_request" as const, requestId, questions: [...input.questions], }), }; return { node, request, turnItem }; }); // Claude withdraws a permission request it no longer needs, for example // when a PermissionRequest hook answered it. Close the request and its // card, which would otherwise wait for an answer nothing reads. const cancelWithdrawnRequest = Effect.fnUntraced(function* ( artifacts: Effect.Success>, ) { const now = yield* DateTime.now; yield* Effect.all( [ emitProviderEvent({ type: "runtime_request.updated", driver: CLAUDE_PROVIDER, runtimeRequest: { ...artifacts.request, status: "cancelled", resolvedAt: now }, }), emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node: { ...artifacts.node, status: "cancelled", completedAt: now }, }), emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: { ...artifacts.turnItem, status: "cancelled", completedAt: now, updatedAt: now, }, }), ], { concurrency: 1 }, ); }); const ensureReasoningBlock = Effect.fnUntraced(function* ( context: ActiveClaudeTurnContext, itemId: string, ) { if (!context.reasoning.blocks.has(itemId)) { context.reasoning.blocks.set(itemId, { startedAt: yield* DateTime.now, ordinal: yield* resolveItemOrdinal(context, itemId), text: "", }); } }); const reasoningDeltas = yield* makeProviderTextDeltaCoalescer({ flushIntervalMs: 50, emit: /* Provider events include streaming deltas; avoid a span for each update. */ Effect.fnUntraced(function* (update) { const context = yield* Ref.get(activeTurn); if (context === null || context.nativeTurnId !== update.turnId) return; const block = context.reasoning.blocks.get(update.itemId); if (block === undefined || update.text.length === 0) return; block.text = update.text; const now = yield* DateTime.now; yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: { id: idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: update.itemId, }), threadId: context.input.threadId, runId: context.input.runId, nodeId: context.input.rootNodeId, providerThreadId: context.input.providerThread.id, providerTurnId: context.providerTurnId, nativeItemRef: { driver: CLAUDE_PROVIDER, nativeId: update.itemId, strength: "strong", }, parentItemId: null, ordinal: block.ordinal, type: "reasoning", title: "Thinking", text: update.text, streaming: !update.completed, status: update.completed ? "completed" : "running", startedAt: block.startedAt, completedAt: update.completed ? now : null, updatedAt: now, }, }); }), }); const endToolCalls = Effect.fnUntraced(function* (input: { readonly context: ActiveClaudeTurnContext | null; readonly toolCalls: ReadonlyArray; readonly status: Extract< OrchestrationV2ProviderTurn["status"], "interrupted" | "failed" | "cancelled" >; readonly completedAt: DateTime.Utc; }) { for (const toolCall of input.toolCalls) { yield* stopTaskOutputTail(toolCall.nativeItemId); yield* emitToolCallArtifacts( buildToolCallArtifacts({ context: input.context, nativeItemId: toolCall.nativeItemId, toolName: toolCall.toolName, classification: toolCall.classification, toolInput: toolCall.input, threadId: toolCall.threadId, runId: toolCall.runId, rootNodeId: toolCall.rootNodeId, parentNodeId: toolCall.parentNodeId, ordinal: toolCall.ordinal, output: NO_CLAUDE_NATIVE_TOOL_OUTPUT, status: input.status, startedAt: toolCall.startedAt, updatedAt: input.completedAt, presentation: toolCall.presentation, }), ); } }); const endSubagentCallsOfClosedQuery = Effect.fnUntraced(function* ( nativeThreadId: string, status: "interrupted" | "failed", ) { const settled = settledTurnByNativeThread.get(nativeThreadId); if (settled === undefined || subagentToolCalls.size === 0) { return; } yield* endToolCalls({ context: settled, toolCalls: [...subagentToolCalls.values()], status, completedAt: yield* DateTime.now, }); subagentToolCalls.clear(); }); const finalizeActiveTurn = Effect.fnUntraced(function* (input: { readonly context: ActiveClaudeTurnContext; readonly status: Extract< OrchestrationV2ProviderTurn["status"], "completed" | "interrupted" | "failed" | "cancelled" >; readonly completedAt: DateTime.Utc; readonly failure?: OrchestrationV2ProviderFailure; readonly threadDisposition?: "reusable" | "broken"; readonly result?: SDKResultMessage; }) { yield* reasoningDeltas.flushTurn(input.context.nativeTurnId); if (input.context.compaction?.status === "running") { const reason = input.result?.subtype === "success" ? input.result.result.trim() : ""; yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: { ...input.context.compaction, status: input.status === "completed" ? "failed" : input.status, title: input.status === "completed" || input.status === "failed" ? "Context compaction failed" : "Context compaction stopped", ...(reason === "" ? {} : { summary: reason }), completedAt: input.completedAt, updatedAt: input.completedAt, }, }); } input.context.compaction = null; if (input.status !== "completed") { const liveQuery = yield* Ref.get(queryContext); for (const [taskId, subagent] of yield* Ref.get(sessionSubagentsByTaskId)) { if ( subagent.task.workflow === undefined || subagent.task.status !== "running" || subagent.task.runId !== input.context.input.runId ) continue; if (liveQuery !== null && !liveQuery.stopping) { // An unanswered stop must not delay turn settlement. yield* liveQuery.query .stopTask(taskId) .pipe(Effect.timeout("15 seconds"), Effect.ignore, Effect.forkIn(sessionScope)); } yield* updateClaudeSubagentNode({ context: input.context, taskId, status: input.status === "failed" ? "failed" : "cancelled", onlyIfRunningInRun: input.context.input.runId, }); } } const liveQuery = yield* Ref.get(queryContext); const runningChildThreadIds = new Set( [...(yield* Ref.get(sessionSubagentsByTaskId)).values()].flatMap((subagent) => subagent.task.status === "running" && (input.status === "completed" || (liveQuery !== null && subagent.task.runId !== input.context.input.runId)) && liveQuery?.subagentsFromEarlierProcesses.has(subagent) !== true ? [subagent.childThreadId] : [], ), ); const endingSubagentCalls = [...subagentToolCalls.values()].filter( (toolCall) => !runningChildThreadIds.has(toolCall.threadId), ); yield* endToolCalls({ context: input.context, toolCalls: [...input.context.toolCalls.values(), ...endingSubagentCalls], status: input.status === "completed" ? "failed" : input.status, completedAt: input.completedAt, }); input.context.toolCalls.clear(); for (const toolCall of endingSubagentCalls) { subagentToolCalls.delete(toolCall.nativeItemId); } if ( input.context.assistant.emittedNativeItemIds.size === 0 && input.context.assistant.fallbackText.length > 0 ) { const ordinal = yield* resolveItemOrdinal( input.context, input.context.assistant.fallbackNativeItemId, ); const artifacts = buildAssistantArtifacts({ idAllocator, turnInput: input.context.input, providerTurnId: input.context.providerTurnId, nativeItemId: input.context.assistant.fallbackNativeItemId, text: input.context.assistant.fallbackText, ordinal, startedAt: input.context.startedAt, completedAt: input.completedAt, }); yield* Effect.all( [ emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node: artifacts.node, }), emitProviderEvent({ type: "message.updated", driver: CLAUDE_PROVIDER, message: artifacts.message, }), emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: artifacts.turnItem, }), ], { concurrency: 1 }, ); } const providerRetry = yield* Ref.modify(providerRetries, (current) => { const retry = current.get(input.context.providerTurnId); if (retry === undefined) { return [undefined, current] as const; } const updated = new Map(current); updated.delete(input.context.providerTurnId); return [retry, updated] as const; }); if (providerRetry !== undefined && input.status !== "failed") { yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: makeProviderRetryTurnItem({ driver: CLAUDE_PROVIDER, threadId: input.context.input.threadId, runId: input.context.input.runId, nodeId: input.context.input.rootNodeId, providerThreadId: input.context.input.providerThread.id, providerTurnId: input.context.providerTurnId, itemOrdinal: providerRetry.itemOrdinal, failure: providerRetry.failure, retry: providerRetry.retry, status: input.status, startedAt: providerRetry.startedAt, updatedAt: input.completedAt, }), }); } const threadDisposition = input.threadDisposition ?? "reusable"; const terminalEvent: ProviderAdapter.ProviderAdapterV2Event = input.status === "failed" ? { type: "turn.terminal", driver: CLAUDE_PROVIDER, providerThreadId: input.context.input.providerThread.id, providerTurnId: input.context.providerTurnId, runOrdinal: input.context.input.runOrdinal, failureItemOrdinal: yield* resolveItemOrdinal( input.context, `terminal-failure:${input.context.providerTurnId}`, ), status: input.status, failure: input.failure ?? makeProviderFailure({ class: "provider_error" }), ...(providerRetry === undefined ? {} : { retry: providerRetry.retry, retryStartedAt: providerRetry.startedAt, }), threadDisposition, } : { type: "turn.terminal", driver: CLAUDE_PROVIDER, providerThreadId: input.context.input.providerThread.id, providerTurnId: input.context.providerTurnId, runOrdinal: input.context.input.runOrdinal, status: input.status, failure: null, threadDisposition, }; yield* Effect.all( [ emitProviderEvent({ type: "provider_turn.updated", driver: CLAUDE_PROVIDER, providerTurn: { ...providerTurnPayload({ context: input.context, status: input.status, completedAt: input.completedAt, }), turnTokenUsage: normalizeClaudeTurnTokenUsage( input.result, input.context.subagentsByTaskId.size > 0 || input.context.subagentsByToolUseId.size > 0, input.status, input.context.steeredSegmentUsage, { rootModels: input.context.rootModels, ...(input.result === undefined ? {} : { ledger: addClaudeLedgerUsage( input.context.steeredLedgerUsage, resultLedgerUsage.get(input.result), ), }), }, ), }, }), // Emit the roster before root settlement so writeFinalRunEvents preserves it. Effect.gen(function* () { const nativeThreadId = input.context.input.providerThread.nativeThreadRef?.nativeId ?? null; if (nativeThreadId !== null) { if (input.status !== "completed") { yield* clearPendingBackgroundTasksForNativeThread(nativeThreadId); yield* clearNativeThreadTaskIdSet( wakeEligibleBackgroundTasksByNativeThread, nativeThreadId, ); yield* clearNativeThreadTaskIdSet( opaqueBackgroundTaskReplayTombstonesByNativeThread, nativeThreadId, ); } } const roster = nativeThreadId === null ? new Map() : rosterForNativeThread( yield* Ref.get(pendingBackgroundTasksByNativeThread), nativeThreadId, ); const awaitedTaskIds = nativeThreadId === null ? new Set() : taskIdSetForNativeThread( yield* Ref.get(awaitedBackgroundTasksByNativeThread), nativeThreadId, ); // A stopped turn that wrote to the transcript moved past the pinned head. const clearConversationHead = (input.status === "completed" || input.context.nativeMessageCursor !== null) && input.context.input.providerThread.nativeConversationHeadRef !== null; const goal = nativeThreadId === null ? undefined : goalsByNativeThread.get(nativeThreadId); const goalChecked = goalCheckedTurns.delete(input.context.providerTurnId); const terminalReason = input.result?.terminal_reason; const liveQuery = yield* Ref.get(queryContext); if ( nativeThreadId !== null && goal?.status === "active" && goalChecked && input.status === "completed" && (terminalReason === undefined || terminalReason === "completed") && roster.size === 0 && !( liveQuery?.nativeThreadId === nativeThreadId && (yield* liveProcessRunsBackgroundWork(liveQuery, { forGoalCheck: true })) ) ) { goalsByNativeThread.set(nativeThreadId, { objective: goal.objective, status: "complete", ...(goal.checks === undefined ? {} : { checks: goal.checks }), }); } const providerThread: OrchestrationV2ProviderThread = { ...input.context.input.providerThread, providerSessionId: session.id, ...(clearConversationHead ? { nativeConversationHeadRef: null } : {}), ...(nativeThreadId !== null && goalsByNativeThread.has(nativeThreadId) ? { goal: goalsByNativeThread.get(nativeThreadId) ?? null } : {}), firstRunOrdinal: input.context.input.providerThread.firstRunOrdinal ?? input.context.input.runOrdinal, lastRunOrdinal: input.context.input.runOrdinal, pendingBackgroundTasks: claudePendingBackgroundTasksFromRoster( roster, awaitedTaskIds, ), status: input.status === "completed" ? "active" : "idle", updatedAt: input.completedAt, }; yield* rememberProviderThread(providerThread); yield* emitProviderEvent({ type: "provider_thread.updated" as const, driver: CLAUDE_PROVIDER, providerThread, }); }), emitProviderEvent(terminalEvent), ], { concurrency: 1 }, ); yield* Ref.update(activeTurn, (current) => current?.providerTurnId === input.context.providerTurnId ? null : current, ); if (input.status === "completed") { settledTurnByNativeThread.set(input.context.nativeThreadId, input.context); } else { settledTurnByNativeThread.delete(input.context.nativeThreadId); } yield* Ref.update(interruptedTurns, (current) => { const next = new Set(current); next.delete(input.context.providerTurnId); return next; }); }); const emitAssistantTextArtifacts = Effect.fnUntraced(function* (input: { readonly context: ActiveClaudeTurnContext; readonly nativeItemId: string; readonly text: string; }) { if (input.context.assistant.emittedNativeItemIds.has(input.nativeItemId)) { return; } input.context.assistant.emittedNativeItemIds.add(input.nativeItemId); const now = yield* DateTime.now; const ordinal = yield* resolveItemOrdinal(input.context, input.nativeItemId); const artifacts = buildAssistantArtifacts({ idAllocator, turnInput: input.context.input, providerTurnId: input.context.providerTurnId, nativeItemId: input.nativeItemId, text: input.text, ordinal, startedAt: now, completedAt: now, }); yield* Effect.all( [ emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node: artifacts.node, }), emitProviderEvent({ type: "message.updated", driver: CLAUDE_PROVIDER, message: artifacts.message, }), emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: artifacts.turnItem, }), ], { concurrency: 1 }, ); }); const endSubagentsOfClosedQuery = Effect.fnUntraced(function* (input: { readonly closedQuery: ClaudeLiveQueryContext; readonly context: ActiveClaudeTurnContext; readonly subagents: Iterable; readonly buffered: ReadonlyArray | undefined; readonly interrupted: boolean; }) { for (const [taskId, subagent] of input.subagents) { if ( subagent.task.status !== "running" || input.closedQuery.subagentsFromEarlierProcesses.has(subagent) ) continue; const latestLifecycle = input.buffered?.findLast( ( message, ): message is Extract< SDKMessage, { readonly subtype: "task_started" | "task_notification" } > => message.type === "system" && (message.subtype === "task_started" || message.subtype === "task_notification") && message.task_id === taskId, ); const completion = latestLifecycle?.subtype === "task_notification" ? latestLifecycle : undefined; yield* updateClaudeSubagentNode({ context: input.context, taskId, status: completion ? claudeTaskOutcome(completion.status) : input.interrupted ? "cancelled" : "failed", ...(completion === undefined ? {} : { workflowFrame: completion, result: completion.summary }), processExited: true, onlyIfRunningInRun: subagent.task.runId, }); } }); // A query opened while this one was closing inherited the subagents it ran, so // nothing else ends them. Leave the live query's own turn, roster and subagents alone. const endSubagentsOfReplacedQuery = Effect.fnUntraced(function* ( closedQuery: ClaudeLiveQueryContext, ) { const context = closedQuery.lastTurn; if (context === null) return; yield* endSubagentsOfClosedQuery({ closedQuery, context, subagents: [...(yield* Ref.get(sessionSubagentsByTaskId))].filter(([, subagent]) => closedQuery.handedOffSubagents.has(subagent), ), buffered: (yield* Ref.get(wakeBuffers)).get(closedQuery.nativeThreadId)?.messages, interrupted: closedQuery.stopping || (yield* Ref.get(interruptedTurns)).has(context.providerTurnId), }); }); const finalizeActiveTurnAfterQueryExit = Effect.fnUntraced(function* ( closedQuery: ClaudeLiveQueryContext, cause?: Cause.Cause, ) { const current = yield* Ref.get(activeTurn); // A replacement query can reuse the native thread id; only this query's turn is ours. const context = closedQuery.lastTurn; if (context === null) return; const interrupted = closedQuery.stopping || (yield* Ref.get(interruptedTurns)).has(context.providerTurnId); // Finalization can yield to a replacement process; capture ownership and outcomes first. const subagents = yield* Ref.get(sessionSubagentsByTaskId); const buffered = (yield* Ref.get(wakeBuffers)).get(closedQuery.nativeThreadId)?.messages; if (current === context) { yield* finalizeActiveTurn({ context, status: interrupted ? "interrupted" : "failed", completedAt: yield* DateTime.now, ...(interrupted ? {} : { failure: makeProviderFailure({ cause: cause === undefined ? undefined : Cause.squash(cause), class: "transport_error", }), }), }); } else { yield* resetBackgroundTaskStateForNativeThreadProcess(closedQuery.nativeThreadId); } // Preserve buffered outcomes before Stop clears the wake buffer. yield* endSubagentsOfClosedQuery({ closedQuery, context, subagents, buffered, interrupted, }); yield* Ref.update(interruptedTurns, (current) => { const next = new Set(current); next.delete(context.providerTurnId); return next; }); if (cause !== undefined) { yield* Effect.logWarning("orchestration-v2.claude-query-stream-failed", { providerSessionId: input.providerSessionId, providerThreadId: context.input.providerThread.id, providerTurnId: context.providerTurnId, cause, }); } }); const applyWorkflowProgressWithoutTurn = Effect.fnUntraced(function* (message: SDKMessage) { if (message.type !== "system" || message.subtype !== "task_progress") return; if (!hasClaudeWorkflowSnapshot(message)) return; const taskId = message.task_id; const registered = (yield* Ref.get(sessionSubagentsByTaskId)).get(taskId); if ( registered === undefined || registered.task.workflow === undefined || registered.task.status !== "running" ) return; const workflow = mergeClaudeWorkflowProgress({ previous: registered.task.workflow, message, }); if (workflow === undefined) return; const now = yield* DateTime.now; const progress = text(message.description); const workflowWithThreads = withWorkflowMemberThreadIds( workflow, registered.childThreadId, ); const task = { ...registered.task, ...(progress === undefined ? {} : { progress }), workflow: workflowWithThreads, updatedAt: now, } satisfies OrchestrationV2Subagent; const coordinator = { ...registered, task }; const accepted = yield* Ref.modify(sessionSubagentsByTaskId, (current) => { const latest = current.get(taskId); if (latest !== registered || latest.task.status !== "running") { return [false, current] as const; } return [true, new Map(current).set(taskId, coordinator)] as const; }); if (!accepted) return; yield* emitProviderEvent({ type: "subagent.updated", driver: CLAUDE_PROVIDER, subagent: task, }); yield* projectClaudeWorkflowMembers({ coordinator, workflow: workflowWithThreads, }); }); const bufferWakeMessage = Effect.fnUntraced(function* (wakeInput: { readonly nativeThreadId: string; readonly message: SDKMessage; }) { const message = wakeInput.message; const isNotification = message.type === "system" && message.subtype === "task_notification"; const isPendingTaskNotification = isNotification && (yield* isWakeEligibleOpaqueBackgroundTaskOnNativeThread( wakeInput.nativeThreadId, message.task_id, )); const bufferedMessages = (yield* Ref.get(wakeBuffers)).get(wakeInput.nativeThreadId)?.messages ?? []; const isNestedSubagentNotification = isNotification && !isPendingTaskNotification && (yield* isNestedSubagentTask(message.task_id)); const isPendingSubagentNotification = isNotification && !isPendingTaskNotification && !isNestedSubagentNotification && ((yield* Ref.get(sessionSubagentsByTaskId)).get(message.task_id)?.task.status === "running" || bufferedMessages.some( (entry) => entry.type === "system" && entry.subtype === "task_started" && entry.task_id === message.task_id, )); // Idle-wake task_started must reach the continuation drain before child frames can find their owner. const isNewSubagentTaskStarted = message.type === "system" && message.subtype === "task_started" && !isClaudeNonSubagentTask(message); const isKnownSubagentTaskStarted = message.type === "system" && message.subtype === "task_started" && (yield* Ref.get(sessionSubagentsByTaskId)).has(message.task_id); if ( isKnownSubagentTaskStarted && message.type === "system" && message.subtype === "task_started" ) { const now = yield* DateTime.now; yield* Ref.update(sessionSubagentsByTaskId, (current) => { const registered = current.get(message.task_id); if (registered === undefined || registered.task.status === "running") { return current; } const { progress: _staleProgress, ...priorTask } = registered.task; return new Map(current).set(message.task_id, { ...registered, confirmedOutcome: undefined, task: { ...priorTask, status: "running", result: null, startedAt: now, completedAt: null, updatedAt: now, }, }); }); } const isWakeTurnStart = isClaudeTurnStartMessage(message); const isWakeEvidence = isPendingTaskNotification || isPendingSubagentNotification || isNestedSubagentNotification || isKnownSubagentTaskStarted || isNewSubagentTaskStarted || isWakeTurnStart || message.type === "assistant" || message.type === "user" || message.type === "result" || message.type === "rate_limit_event"; if (!isWakeEvidence) { return; } const notificationSummary = isNotification && typeof message.summary === "string" && message.summary.length > 0 ? message.summary : null; if (isPendingTaskNotification) { yield* recordWakeReport( wakeInput.nativeThreadId, message.task_id, yield* opaqueTaskWakeReport(wakeInput.nativeThreadId, message), ); } else if (isPendingSubagentNotification) { const registered = (yield* Ref.get(sessionSubagentsByTaskId)).get(message.task_id); const started = bufferedMessages.find( (entry) => entry.type === "system" && entry.subtype === "task_started" && entry.task_id === message.task_id, ); yield* recordWakeReport(wakeInput.nativeThreadId, message.task_id, { kind: "subagent", label: registered?.task.title ?? (started?.type === "system" && started.subtype === "task_started" ? started.description : undefined), outcome: claudeTaskOutcome(message.status), childThreadId: registered?.childThreadId, }); } if (message.type === "result") { bufferedResultArrivals.set(message, yield* DateTime.now); } if (isClaudeRootTurnFrame(message)) { yield* forgetAbsorbedWake(wakeInput.nativeThreadId); } yield* Ref.update(wakeBuffers, (current) => { const existing = current.get(wakeInput.nativeThreadId); const updated = new Map(current); updated.set(wakeInput.nativeThreadId, { messages: [...(existing?.messages ?? []), message], detail: notificationSummary ?? existing?.detail ?? null, }); return updated; }); if (isPendingTaskNotification) { yield* consumeWakeEligibilityForBufferedNotification( wakeInput.nativeThreadId, message.task_id, ); } // Open continuations only after root init, user, assistant, or result frames prove a wake began. // Background child frames can stream while the root is idle. const buffered = (yield* Ref.get(wakeBuffers)).get(wakeInput.nativeThreadId); const hasBufferedNotification = buffered?.messages.some( (entry) => entry.type === "system" && entry.subtype === "task_notification", ) ?? false; const isNativeOpaqueWakeFrame = hasBufferedNotification && (message.type === "assistant" || message.type === "user") && parentToolUseIdFromSdkMessage(message) === null; if ( !isPendingSubagentNotification && !isNativeOpaqueWakeFrame && !isWakeTurnStart && message.type !== "result" ) { return; } const route = (yield* Ref.get(lastTurnRouteByNativeThread)).get(wakeInput.nativeThreadId); if (route === undefined) { yield* Effect.logWarning("orchestration-v2.claude-wake-turn-unroutable", { providerSessionId: input.providerSessionId, nativeThreadId: wakeInput.nativeThreadId, }); return; } const shouldOffer = yield* Ref.modify(requestedContinuations, (current) => { if (current.has(wakeInput.nativeThreadId)) { return [false, current] as const; } const updated = new Set(current); updated.add(wakeInput.nativeThreadId); return [true, updated] as const; }); if (!shouldOffer) { return; } const detail = (yield* Ref.get(wakeBuffers)).get(wakeInput.nativeThreadId)?.detail ?? null; const reports = yield* takeWakeReports(wakeInput.nativeThreadId); const notification = backgroundWorkNotification( [...(reports?.values() ?? [])].map((entry) => entry.report), ); yield* Effect.logInfo("orchestration-v2.claude-wake-turn-detected", { providerSessionId: input.providerSessionId, threadId: route.threadId, providerThreadId: route.providerThreadId, }); yield* continuationRequests.offer({ threadId: route.threadId, providerThreadId: route.providerThreadId, driver: CLAUDE_PROVIDER, detail, ...(notification === null ? {} : { notification }), }); }); // Untraced like the roster message handling: it runs for every streamed roster change. const emitRosterUpdate = Effect.fnUntraced(function* ( nativeThreadId: string, activeContext: ActiveClaudeTurnContext | null, ) { const baseThread = activeContext?.input.providerThread ?? (yield* Ref.get(lastProviderThreadByNativeThread)).get(nativeThreadId); if (baseThread === undefined) { return; } const status = activeContext === null ? ("idle" as const) : baseThread.status === "idle" ? ("active" as const) : baseThread.status; yield* emitProviderThreadRoster({ nativeThreadId, providerThread: baseThread, status }); }); const applyBackgroundTaskRosterMessage = Effect.fnUntraced(function* (input: { readonly nativeThreadId: string; readonly message: SDKMessage; readonly activeContext: ActiveClaudeTurnContext | null; }) { const message = input.message; let rosterChanged = false; if (isClaudeBackgroundTasksChangedMessage(message)) { const roster = Reflect.get(message, "tasks"); if (!Array.isArray(roster)) { return false; } const nextTasks: OrchestrationV2PendingBackgroundTask[] = []; const listedTaskIds = new Set( roster.flatMap((entry) => { const taskId = entry !== null && typeof entry === "object" ? Reflect.get(entry, "task_id") : undefined; return typeof taskId === "string" ? [taskId] : []; }), ); const monitorTasks = yield* endClaudeMonitorTasks( (taskId, task) => task.nativeThreadId === input.nativeThreadId && !listedTaskIds.has(taskId), ); for (const entry of roster) { const task = parseClaudeBackgroundTaskEntry(entry, monitorTasks); if (task !== null) { nextTasks.push(task); } } yield* replacePendingBackgroundTasks(input.nativeThreadId, nextTasks); rosterChanged = true; } else if (message.type === "system" && message.subtype === "task_started") { if (!isClaudeNonSubagentTask(message) || message.is_backgrounded === false) { return false; } yield* upsertPendingBackgroundTask( input.nativeThreadId, claudePendingBackgroundTask({ taskId: message.task_id, taskType: claudeTaskTypeFromSdkMessage(message), startedByMonitor: yield* isClaudeMonitorTask({ nativeThreadId: input.nativeThreadId, taskId: message.task_id, toolUseId: message.tool_use_id, }), description: typeof message.description === "string" ? message.description : undefined, parentTaskId: Reflect.get(message, "parent_task_id"), }), ); rosterChanged = true; } else if (message.type === "system" && message.subtype === "task_notification") { yield* endClaudeMonitorTasks((taskId) => taskId === message.task_id); const removed = yield* clearPendingBackgroundTask( input.nativeThreadId, message.task_id, ); yield* clearTaskIdFromNativeThreadSet( wakeEligibleBackgroundTasksByNativeThread, input.nativeThreadId, message.task_id, ); rosterChanged = removed; } if (!rosterChanged) { return false; } yield* emitRosterUpdate(input.nativeThreadId, input.activeContext); return true; }); const settledTurnForSubagentFrame = Effect.fnUntraced(function* ( nativeThreadId: string, message: SDKMessage, ) { const settled = settledTurnByNativeThread.get(nativeThreadId); if (settled === undefined) { return null; } const userResults = claudeToolResultBlocksFromUserMessage(message); if ( message.type === "user" && userResults.length > 0 && userResults.length === message.message.content.length && userResults.every( (toolResult) => findToolCall(settled, toolResult.tool_use_id) !== undefined, ) ) { return settled; } const bufferedStarts = ( (yield* Ref.get(wakeBuffers)).get(nativeThreadId)?.messages ?? [] ).flatMap((entry) => entry.type === "system" && entry.subtype === "task_started" && !isClaudeNonSubagentTask(entry) ? [entry] : [], ); const parentToolUseId = parentToolUseIdFromSdkMessage(message); const taskId = message.type === "system" && message.subtype === "task_progress" ? message.task_id : parentToolUseId === null ? undefined : (yield* Ref.get(sessionSubagentTaskIdsByToolUseId)).get(parentToolUseId); const returnedToolUseIds = new Set( [...claudeToolResultBlocksFromAssistantMessage(message), ...userResults].map( (toolResult) => toolResult.tool_use_id, ), ); if ( bufferedStarts.some( (start) => start.task_id === taskId || (start.tool_use_id !== undefined && (start.tool_use_id === parentToolUseId || returnedToolUseIds.has(start.tool_use_id))), ) ) { return null; } if (taskId === undefined) { return parentToolUseId === null ? null : settled; } if ( returnedToolUseIds.size > 0 && bufferedStarts.some((start) => start.tool_use_id === undefined) ) { return null; } const subagent = (yield* Ref.get(sessionSubagentsByTaskId)).get(taskId); return subagent?.task.status === "running" ? settled : null; }); const handleSdkMessageFrame = Effect.fnUntraced(function* (input: { readonly query: ClaudeAgentSdkQuerySession; readonly message: SDKMessage; readonly replayed?: boolean; }) { const liveQuery = yield* Ref.get(queryContext); if (liveQuery?.query !== input.query) { return; } const message = input.message; // Track before routing: idle-wake Monitors report tasks before replaying their tool calls. yield* trackClaudeMonitorCalls(message); // Update before routing because the idle wake gate reads this. if ( message.type === "system" && message.subtype === "task_started" && !isClaudeNonSubagentTask(message) && (message.spawn_depth ?? 1) > 1 ) { yield* Ref.update(nestedSubagentTaskIds, (current) => current.has(message.task_id) ? current : new Set(current).add(message.task_id), ); } if (message.type === "rate_limit_event") { const rateLimitInfo = message.rate_limit_info; if (!rateLimitInfo) return; const names = adapterOptions.scopedLimitNames ? yield* Ref.get(adapterOptions.scopedLimitNames) : { overageIncluded: undefined }; const update = claudeRateLimitEventToUpdate(rateLimitInfo, names); const now = yield* DateTime.now; if (update && adapterOptions.onUsageLimits) { yield* adapterOptions.onUsageLimits({ ...update, checkedAt: DateTime.formatIso(now), }); } const context = yield* Ref.get(activeTurn); if (context === null) { yield* bufferWakeMessage({ nativeThreadId: liveQuery.nativeThreadId, message, }); return; } const overageAllowed = rateLimitInfo.overageStatus === "allowed" || rateLimitInfo.overageStatus === "allowed_warning" || rateLimitInfo.isUsingOverage === true || rateLimitInfo.overageInUse === true; const blocked = rateLimitInfo.status === "rejected" && !overageAllowed; const limitType = rateLimitInfo.rateLimitType ?? "unknown"; if (blocked) { context.rejectedRateLimitTypes.add(limitType); context.rateLimitResetTimes.set( limitType, claudeUsageLimitResetAt(rateLimitInfo.resetsAt), ); } else if ( rateLimitInfo.status === "allowed" || rateLimitInfo.status === "allowed_warning" || overageAllowed ) { context.rejectedRateLimitTypes.delete(limitType); context.rateLimitResetTimes.delete(limitType); } if (blocked) { const limitKey = `${limitType}:${rateLimitInfo.resetsAt ?? "unknown"}`; if (!context.announcedUsageLimits.has(limitKey)) { context.announcedUsageLimits.add(limitKey); const nativeItemId = `usage-limit:${context.providerTurnId}:${limitKey}`; const notice = describeClaudeUsageLimit( rateLimitInfo, DateTime.toEpochMillis(now), names, ); yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: { id: idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId, }), threadId: context.input.threadId, runId: context.input.runId, nodeId: context.input.rootNodeId, providerThreadId: context.input.providerThread.id, providerTurnId: context.providerTurnId, nativeItemRef: { driver: CLAUDE_PROVIDER, nativeId: nativeItemId, strength: "weak", }, parentItemId: null, ordinal: yield* resolveItemOrdinal(context, nativeItemId), type: "system_notice", status: "completed", title: notice, message: notice, startedAt: now, completedAt: now, updatedAt: now, }, }); } } return; } const context = (yield* Ref.get(activeTurn)) ?? (yield* settledTurnForSubagentFrame(liveQuery.nativeThreadId, message)); if (context === null) { yield* applyWorkflowProgressWithoutTurn(message); // Buffer wake evidence before clearing the task from the roster. if (message.type === "system" && message.subtype === "task_notification") { yield* bufferWakeMessage({ nativeThreadId: liveQuery.nativeThreadId, message }); yield* applyBackgroundTaskRosterMessage({ nativeThreadId: liveQuery.nativeThreadId, message, activeContext: null, }); const registered = (yield* Ref.get(sessionSubagentsByTaskId)).get(message.task_id); const settled = settledTurnByNativeThread.get(liveQuery.nativeThreadId); const hasBufferedStart = (yield* Ref.get(wakeBuffers)) .get(liveQuery.nativeThreadId) ?.messages.some( (entry) => entry.type === "system" && entry.subtype === "task_started" && entry.task_id === message.task_id, ) ?? false; const canApplyOutcome = subagentStopOutcomes.has(message.task_id) || (!hasBufferedStart && registered?.task.workflow === undefined); if (registered !== undefined && settled !== undefined && canApplyOutcome) { yield* discardBufferedSubagentStarts( liveQuery.nativeThreadId, message.task_id, message, ); yield* updateClaudeSubagentNode({ context: settled, taskId: message.task_id, ...(message.tool_use_id === undefined ? {} : { toolUseId: message.tool_use_id }), workflowFrame: message, nativeOutcome: message, result: message.summary, status: claudeTaskOutcome(message.status), onlyIfRunningInRun: registered.task.runId, }); } } else { yield* applyBackgroundTaskRosterMessage({ nativeThreadId: liveQuery.nativeThreadId, message, activeContext: null, }); yield* bufferWakeMessage({ nativeThreadId: liveQuery.nativeThreadId, message }); } return; } const focusedPlugin = claudeFocusRequestedPanePlugin(message); if (focusedPlugin !== null && context === (yield* Ref.get(activeTurn))) { yield* Effect.logWarning("orchestration-v2.claude-mod-pane-focus-unsupported", { providerTurnId: context.providerTurnId, plugin: focusedPlugin, }); liveQuery.stopping = true; yield* finalizeActiveTurn({ context, status: "failed", completedAt: yield* DateTime.now, failure: makeProviderFailure({ class: "provider_error", code: "claude_mod_ui_unsupported", message: `The Claude Code mod "${focusedPlugin}" is waiting for input in its terminal pane, which T3 Code cannot show. Disable the mod for sessions run from T3 Code, or run the task in the Claude Code terminal.`, }), }); // Closing ends the CLI process and abandons the held tool call // without answering it. Not interrupt: the held CLI may never ack it. yield* liveQuery.query.close.pipe(Effect.ignore); return; } yield* trackClaudeGoal({ nativeThreadId: liveQuery.nativeThreadId, context, message }); if (message.type === "stream_event" && !message.parent_tool_use_id) { const event = message.event; const reasoning = context.reasoning; if (event.type === "message_start") { reasoning.messageId = event.message.id; reasoning.streamBlocks.clear(); const usage: ClaudeMessageUsage | undefined = event.message.usage; context.streamUsage = usage === undefined ? null : { messageId: event.message.id, usage }; if (usage !== undefined) yield* emitContextUsage(context, usage, true); } else if (event.type === "message_delta") { const usage: typeof event.usage | undefined = event.usage; const stream = context.streamUsage; if (stream?.usage && usage !== undefined) { stream.usage = mergeClaudeStreamUsage(stream.usage, usage); yield* emitContextUsage(context, stream.usage); } } else if ( event.type === "content_block_start" && event.content_block.type === "thinking" && reasoning.messageId !== null ) { const index = reasoning.nextBlockIndex.get(reasoning.messageId) ?? 0; reasoning.nextBlockIndex.set(reasoning.messageId, index + 1); const itemId = `${reasoning.messageId}:thinking:${index}`; reasoning.streamBlocks.set(event.index, itemId); yield* ensureReasoningBlock(context, itemId); yield* reasoningDeltas.append({ turnId: context.nativeTurnId, itemId, delta: event.content_block.thinking, }); } else if ( event.type === "content_block_delta" && event.delta.type === "thinking_delta" ) { const itemId = reasoning.streamBlocks.get(event.index); if (itemId !== undefined) { yield* reasoningDeltas.append({ turnId: context.nativeTurnId, itemId, delta: event.delta.thinking, }); } } else if (event.type === "content_block_stop") { const itemId = reasoning.streamBlocks.get(event.index); if (itemId !== undefined) { yield* reasoningDeltas.complete({ turnId: context.nativeTurnId, itemId, emitEmpty: false, }); reasoning.streamBlocks.delete(event.index); } } return; } if ( message.type === "assistant" && !message.parent_tool_use_id && !context.reasoning.snapshots.has(message.uuid) ) { context.reasoning.snapshots.add(message.uuid); // The SDK snapshots each completed block; count thinking blocks separately to preserve streamed ids. for (const block of message.message.content) { if (block.type !== "thinking") { const update = context.reasoning.lastThinking; context.reasoning.lastThinking = null; if ( block.type === "tool_use" && update !== null && update.messageId === message.message.id && update.followsThinking && claudeWritesUpdatesAsThinking(message.message.model) ) { yield* emitAssistantTextArtifacts({ context, nativeItemId: update.itemId, text: update.text, }); } continue; } const index = context.reasoning.snapshotBlockIndex.get(message.message.id) ?? 0; context.reasoning.snapshotBlockIndex.set(message.message.id, index + 1); const itemId = `${message.message.id}:thinking:${index}`; yield* ensureReasoningBlock(context, itemId); const finalText = block.thinking || context.reasoning.blocks.get(itemId)?.text; const completedText = yield* reasoningDeltas.complete({ turnId: context.nativeTurnId, itemId, ...(finalText ? { finalText } : {}), emitEmpty: false, }); const previous = context.reasoning.lastThinking; context.reasoning.lastThinking = completedText.trim() ? { messageId: message.message.id, itemId, text: completedText, followsThinking: previous !== null && previous.messageId === message.message.id, } : null; } } if (message.type === "assistant" && input.replayed !== true) { if (message.parent_tool_use_id === null) { // CLI notices and API errors arrive as assistant messages from "". if (message.message.model && message.message.model !== "") { context.rootModels.add(message.message.model); } // Subagent uuids exist only in the sidechain transcript, so resume and fork cannot target them. context.nativeMessageCursor = message.uuid; context.latestAssistantRateLimit = message.error === "rate_limit" ? { type: null, resetAt: null } : null; if (message.error === "rate_limit" && isClaudeBlockedAssistantRateLimit(message)) { const info = message.api_error_params.rate_limit_info; const overageAllowed = info.overageStatus === "allowed" || info.overageStatus === "allowed_warning" || info.isUsingOverage === true || info.overageInUse === true; if (!overageAllowed) { context.latestAssistantRateLimit = { type: typeof info.rateLimitType === "string" ? info.rateLimitType : "unknown", resetAt: claudeUsageLimitResetAt(info.resetsAt), }; } } if (message.error === "authentication_failed") { context.authenticationFailureMessage = claudeSignedOutMessage({ configDir: adapterOptions.environment.CLAUDE_CONFIG_DIR, cwd: path.resolve(context.input.runtimePolicy.cwd ?? "."), }); } } } if ( message.type === "system" && message.subtype === "status" && (message.compact_result !== undefined || (message.status === null && message.permissionMode === undefined)) ) { if (message.compact_result !== undefined) { context.compactionFailure = message.compact_result === "failed" ? { message: message.compact_error } : null; } if (context.compaction?.status === "running") { const now = yield* DateTime.now; context.compaction = { ...context.compaction, status: message.compact_result === "success" ? "completed" : message.compact_result === "failed" ? "failed" : "interrupted", title: message.compact_result === "success" ? "Context compacted" : message.compact_result === "failed" ? "Context compaction failed" : "Context compaction stopped", ...(message.compact_result === "failed" && message.compact_error ? { summary: message.compact_error } : {}), completedAt: now, updatedAt: now, }; yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: context.compaction, }); if (message.compact_result !== "success") { context.compaction = null; const budget = pdfBudgets.get(context.nativeThreadId); if (budget !== undefined) budget.compacting = null; } } } if ( message.type === "system" && (message.subtype === "compact_boundary" || (message.subtype === "status" && message.status === "compacting")) ) { if (message.subtype === "status" && context.compaction?.status === "running") return; const now = yield* DateTime.now; const nativeItemId = message.uuid; const compaction = (message.subtype === "compact_boundary" ? context.compaction : null) ?? ({ id: idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId, }), threadId: context.input.threadId, runId: context.input.runId, nodeId: context.input.rootNodeId, providerThreadId: context.input.providerThread.id, providerTurnId: context.providerTurnId, nativeItemRef: { driver: CLAUDE_PROVIDER, nativeId: nativeItemId, strength: "strong", }, parentItemId: null, ordinal: yield* resolveItemOrdinal(context, nativeItemId), type: "compaction", driver: CLAUDE_PROVIDER, status: "running", title: "Compacting context", startedAt: now, completedAt: null, updatedAt: now, } satisfies OrchestrationV2TurnItem); if (message.subtype === "status") { const budget = pdfBudgets.get(context.nativeThreadId); // Offers made while compaction runs are outside the history it // removes. Pending reservations must survive its boundary too. if (budget !== undefined) budget.compacting = { ...budget.committed }; context.compaction = compaction; yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: compaction, }); return; } const budget = pdfBudgets.get(context.nativeThreadId); if (budget !== undefined) { // Aggregate persisted usage cannot identify attachments in a // preserved segment. Retain it conservatively on partial compacts. if ( message.compact_metadata.preserved_segment === undefined && message.compact_metadata.preserved_messages === undefined ) { const compacted = budget.compacting ?? { ...budget.committed }; budget.committed.pages -= compacted.pages; budget.committed.bytes -= compacted.bytes; } budget.compacting = null; yield* persistAttachmentBudget(context.nativeThreadId); } if (context.streamUsage !== null) context.streamUsage.usage = null; const postTokens = message.compact_metadata.post_tokens; const afterTokenCount = typeof postTokens === "number" && Number.isFinite(postTokens) && postTokens > 0 ? Math.round(postTokens) : undefined; context.compactionTokens = afterTokenCount; if (afterTokenCount !== undefined) yield* emitContextUsage(context, afterTokenCount); yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: { ...compaction, status: "completed", title: "Context compacted", completedAt: compaction.completedAt ?? now, updatedAt: now, ...(message.compact_metadata.pre_tokens === undefined ? {} : { beforeTokenCount: message.compact_metadata.pre_tokens }), ...(afterTokenCount === undefined ? {} : { afterTokenCount }), }, }); context.compaction = null; return; } 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, turnItem: { id: idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId, }), threadId: context.input.threadId, runId: context.input.runId, nodeId: context.input.rootNodeId, providerThreadId: context.input.providerThread.id, providerTurnId: context.providerTurnId, nativeItemRef: { driver: CLAUDE_PROVIDER, nativeId: nativeItemId, strength: "strong", }, parentItemId: null, ordinal: yield* resolveItemOrdinal(context, nativeItemId), type: "system_notice", status: "completed", title: message.content, message: message.content, startedAt: now, completedAt: now, updatedAt: now, }, }); return; } if (message.type === "system" && message.subtype === "api_retry") { const updatedAt = yield* DateTime.now; const previous = (yield* Ref.get(providerRetries)).get(context.providerTurnId); const retry: OrchestrationV2ProviderRetry = { attempt: Math.max(1, Math.trunc(message.attempt)), maxAttempts: Math.max(1, Math.trunc(message.max_retries)), retryDelayMs: Math.max(0, Math.trunc(message.retry_delay_ms)), }; const failure = providerFailureFromApiRetry(message); const itemOrdinal = previous?.itemOrdinal ?? (yield* resolveItemOrdinal(context, `terminal-failure:${context.providerTurnId}`)); const state: ActiveClaudeProviderRetry = { retry, failure, startedAt: previous?.startedAt ?? updatedAt, itemOrdinal, }; yield* Ref.update(providerRetries, (current) => { const updated = new Map(current); updated.set(context.providerTurnId, state); return updated; }); yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: makeProviderRetryTurnItem({ driver: CLAUDE_PROVIDER, threadId: context.input.threadId, runId: context.input.runId, nodeId: context.input.rootNodeId, providerThreadId: context.input.providerThread.id, providerTurnId: context.providerTurnId, itemOrdinal, failure, retry, status: "running", startedAt: state.startedAt, updatedAt, }), }); return; } if (message.type === "assistant" && input.replayed !== true) { const now = yield* DateTime.now; yield* completeProviderRetry(context, now); // CLI-generated frames report zero usage, not context usage. if ( message.parent_tool_use_id === null && message.message.usage !== undefined && message.message.model !== "" ) { const snapshotUsage = message.message.usage; const stream = context.streamUsage; if (stream === null || stream.messageId !== message.message.id) { context.streamUsage = { messageId: message.message.id, usage: snapshotUsage }; yield* emitContextUsage(context, snapshotUsage); } else if ( stream.usage !== null && claudeMessageInputTokens(stream.usage) <= 0 && claudeMessageInputTokens(snapshotUsage) > 0 ) { stream.usage = { ...snapshotUsage, output_tokens: Math.max(snapshotUsage.output_tokens, stream.usage.output_tokens), }; yield* emitContextUsage(context, stream.usage); } } const parentToolUseId = message.parent_tool_use_id; const snapshotModel = typeof message.message.model === "string" ? message.message.model.trim() : ""; // "" identifies CLI notices, not the subagent model. const model = snapshotModel.length === 0 || snapshotModel === "" ? undefined : canonicalClaudeSubagentModel(subagentModelCatalog, snapshotModel); if (parentToolUseId !== null && model !== undefined) { const subagent = yield* resolveSubagentByToolUseId(context, parentToolUseId); if (subagent === undefined) { rememberPendingClaudeSubagentLaunch( pendingSubagentLaunchesByToolUseId, parentToolUseId, { model }, ); } else if (subagent.task.status === "running" && subagent.task.model !== model) { yield* updateClaudeSubagentNode({ context, taskId: subagent.task.nativeTaskRef?.nativeId ?? String(subagent.task.id), toolUseId: parentToolUseId, model, status: subagent.task.status, }); } } } const frameParentToolUseId = parentToolUseIdFromSdkMessage(message); if ( frameParentToolUseId !== null && (yield* resolveSubagentByToolUseId(context, frameParentToolUseId)) === undefined ) { const droppedToolUseId = yield* Ref.modify( pendingSubagentFramesByToolUseId, (current) => { const frames = current.get(frameParentToolUseId) ?? []; if (frames.length >= PENDING_CLAUDE_SUBAGENT_FRAME_CAP) { return [frameParentToolUseId, current] as const; } const next = new Map(current).set(frameParentToolUseId, [...frames, message]); const oldest = next.keys().next(); if (next.size > PENDING_CLAUDE_SUBAGENT_CAP && !oldest.done) { next.delete(oldest.value); return [oldest.value, next] as const; } return [null, next] as const; }, ); if (droppedToolUseId !== null) { yield* Effect.logWarning("orchestration-v2.claude-subagent-frames-dropped", { providerTurnId: context.providerTurnId, parentToolUseId: droppedToolUseId, }); } return; } if (isClaudeBackgroundTasksChangedMessage(message)) { yield* applyBackgroundTaskRosterMessage({ nativeThreadId: liveQuery.nativeThreadId, message, activeContext: context, }); return; } if (message.type === "system" && message.subtype === "task_started") { if (isClaudeNonSubagentTask(message)) { const tailedCall = message.tool_use_id === undefined ? undefined : findToolCall(context, message.tool_use_id); if ( liveQuery.taskOutputRoot !== undefined && message.task_type === "local_bash" && // Older Claude Code builds omit the flag on foreground calls. message.is_backgrounded !== true && tailedCall?.classification.itemType === "command_execution" && !taskOutputTails.has(tailedCall.nativeItemId) ) { const itemId = idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId: tailedCall.nativeItemId, }); const stop = yield* Deferred.make(); const fiber = yield* tailClaudeTaskOutput({ root: liveQuery.taskOutputRoot, sessionId: message.session_id, taskId: message.task_id, stop, onChunk: (chunk) => emitProviderEvent({ type: "command_output.delta", driver: CLAUDE_PROVIDER, threadId: tailedCall.threadId, itemId, chunk, }), }).pipe( Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), Effect.forkIn(sessionScope), ); taskOutputTails.set(tailedCall.nativeItemId, { fiber, stop }); } context.ignoredTaskIds.add(message.task_id); yield* applyBackgroundTaskRosterMessage({ nativeThreadId: liveQuery.nativeThreadId, message, activeContext: context, }); } else { const launch = message.tool_use_id === undefined ? undefined : pendingSubagentLaunchesByToolUseId.get(message.tool_use_id); if (message.tool_use_id !== undefined) { pendingSubagentLaunchesByToolUseId.delete(message.tool_use_id); } const owner = launch?.ownerToolUseId === undefined ? undefined : yield* resolveSubagentByToolUseId(context, launch.ownerToolUseId); // Nested subagents default to their owner's model. const model = launch?.model ?? owner?.task.model ?? undefined; if ( message.is_backgrounded === true && !(yield* isNestedSubagentTask(message.task_id)) ) { yield* Ref.update(backgroundedSubagentTaskIds, (current) => new Set(current).add(message.task_id), ); } yield* recoverResumedClaudeSubagent({ context, nativeThreadId: liveQuery.nativeThreadId, taskId: message.task_id, toolUseId: message.tool_use_id, title: message.description, }); const isWorkflow = claudeTaskTypeFromSdkMessage(message) === CLAUDE_WORKFLOW_TASK_TYPE; const workflowName = isWorkflow ? text(field(message, "workflow_name")) : undefined; yield* updateClaudeSubagentNode({ context, taskId: message.task_id, ...(message.task_type === undefined ? {} : { nativeTaskType: message.task_type }), ...(message.tool_use_id === undefined ? {} : { toolUseId: message.tool_use_id }), ...(message.prompt === undefined ? {} : { prompt: message.prompt }), ...(model === undefined ? {} : { model }), ...(launch?.effort === undefined ? {} : { effort: launch.effort }), ...(owner === undefined ? {} : { owner }), ...(isWorkflow ? { workflowFrame: message, ...(workflowName === undefined ? {} : { workflowName }), } : {}), title: message.description, status: "running", reopen: true, }); yield* Ref.update(wakeBuffers, (current) => { const entry = current.get(liveQuery.nativeThreadId); if (entry === undefined) return current; const replayIndex = entry.messages.indexOf(message); const messages = entry.messages.filter( (buffered, index) => !( (replayIndex === -1 || index < replayIndex) && buffered.type === "system" && (buffered.subtype === "task_started" || buffered.subtype === "task_notification") && buffered.task_id === message.task_id ), ); return messages.length === entry.messages.length ? current : new Map(current).set(liveQuery.nativeThreadId, { ...entry, messages }); }); } } if (message.type === "system" && message.subtype === "task_progress") { const progress = message.description.trim(); const isBackgroundTask = yield* hasPendingBackgroundTaskOnNativeThread( liveQuery.nativeThreadId, message.task_id, ); const carriesWorkflowTelemetry = hasClaudeWorkflowSnapshot(message); const isWorkflowCoordinator = (yield* Ref.get(sessionSubagentsByTaskId)).get(message.task_id)?.task.workflow !== undefined; if ( (carriesWorkflowTelemetry || (progress.length > 0 && !isWorkflowCoordinator)) && !context.ignoredTaskIds.has(message.task_id) && !isBackgroundTask ) { yield* updateClaudeSubagentNode({ context, taskId: message.task_id, ...(message.tool_use_id === undefined ? {} : { toolUseId: message.tool_use_id }), ...(progress.length === 0 ? {} : { progress }), workflowFrame: message, status: "running", }); } } if (message.type === "system" && message.subtype === "task_notification") { const isRegistered = (yield* Ref.get(sessionSubagentsByTaskId)).has(message.task_id); // A task whose start is still held for the next wake drain has no // node to finish yet; hold its outcome behind that start, or the // drain replays the start alone and the task stays running. if (!isRegistered) { yield* Ref.update(wakeBuffers, (current) => { const entry = current.get(liveQuery.nativeThreadId); if ( entry === undefined || entry.messages.includes(message) || !entry.messages.some( (buffered) => buffered.type === "system" && buffered.subtype === "task_started" && buffered.task_id === message.task_id, ) ) { return current; } return new Map(current).set(liveQuery.nativeThreadId, { ...entry, messages: [...entry.messages, message], }); }); } if (!appliedSubagentOutcomes.has(message) && isRegistered) { yield* discardBufferedSubagentStarts( liveQuery.nativeThreadId, message.task_id, message, ); } const wasBackgroundTask = yield* isKnownOpaqueBackgroundTaskOnNativeThread( liveQuery.nativeThreadId, message.task_id, ); if (!isClaudeProviderContinuationTurn(context.input)) { if (wasBackgroundTask) { yield* recordWakeReport( liveQuery.nativeThreadId, message.task_id, yield* opaqueTaskWakeReport(liveQuery.nativeThreadId, message), ); } else if ( !wasBackgroundTask && (yield* Ref.get(backgroundedSubagentTaskIds)).has(message.task_id) ) { const registered = (yield* Ref.get(sessionSubagentsByTaskId)).get(message.task_id); yield* recordWakeReport(liveQuery.nativeThreadId, message.task_id, { kind: "subagent", label: registered?.task.title ?? undefined, outcome: claudeTaskOutcome(message.status), childThreadId: registered?.childThreadId, }); } } yield* Ref.update(backgroundedSubagentTaskIds, (current) => { if (!current.has(message.task_id)) return current; const updated = new Set(current); updated.delete(message.task_id); return updated; }); yield* applyBackgroundTaskRosterMessage({ nativeThreadId: liveQuery.nativeThreadId, message, activeContext: context, }); if ( !wasBackgroundTask && !context.ignoredTaskIds.has(message.task_id) && !appliedSubagentOutcomes.has(message) ) { yield* updateClaudeSubagentNode({ context, taskId: message.task_id, ...(message.tool_use_id === undefined ? {} : { toolUseId: message.tool_use_id }), workflowFrame: message, nativeOutcome: message, result: message.summary, status: message.status === "completed" ? "completed" : message.status === "stopped" ? "cancelled" : "failed", }); } if (wasBackgroundTask) { yield* clearOpaqueBackgroundTaskReplayTombstone( liveQuery.nativeThreadId, message.task_id, ); } } const toolPresentations = claudeToolPresentationsFromAssistantMessage(message); for (const toolUse of claudeToolUseBlocksFromAssistantMessage(message)) { const nativeToolInput = claudeNativeToolInputFromUnknown(toolUse.input); if (toolUse.name === "Agent") { rememberClaudeSubagentLaunch( subagentModelCatalog, context, pendingSubagentLaunchesByToolUseId, toolUse.id, nativeToolInput, parentToolUseIdFromSdkMessage(message), ); continue; } if (toolUse.name === "TodoWrite" && parentToolUseIdFromSdkMessage(message) === null) { yield* emitClaudePlanProjection({ context, nativeItemId: toolUse.id, kind: "todo_list", steps: claudeTodoSteps(nativeToolInput), }).pipe(Effect.orDie); } yield* ensureToolCallStarted({ context, nativeItemId: toolUse.id, toolName: toolUse.name, toolInput: nativeToolInput, parentToolUseId: parentToolUseIdFromSdkMessage(message), presentation: toolPresentations.get(toolUse.id) ?? mcpToolPresentation({ toolName: toolUse.name.replace(/^mcp__claude_ai_/u, "mcp__"), }), }); const heldPlan = heldProposedPlansByToolUseId.get(toolUse.id); if (heldPlan !== undefined) { heldProposedPlansByToolUseId.delete(toolUse.id); yield* emitClaudePlanProjection({ context, nativeItemId: toolUse.id, kind: "proposed_plan", markdown: heldPlan, }).pipe(Effect.orDie); } } for (const { toolResult, output } of claudeToolResultEntriesFromMessage(message)) { // A foreground fork's stream opens with a placeholder result for its own launch call. if (parentToolUseIdFromSdkMessage(message) === toolResult.tool_use_id) continue; const subagent = yield* resolveSubagentByToolUseId(context, toolResult.tool_use_id); const runHandles = parseClaudeWorkflowRunHandles(claudeNativeToolOutputValue(output)); if (subagent !== undefined && runHandles !== undefined) { yield* updateClaudeSubagentNode({ context, taskId: subagent.task.nativeTaskRef?.nativeId ?? String(subagent.task.id), toolUseId: toolResult.tool_use_id, workflowRunHandles: runHandles, status: "running", }); } // Resume tool results only acknowledge delivery; only Agent launch results may settle subagents. // Agent tool_uses project as subagent rows and never enter toolCalls. if ( subagent !== undefined && findToolCall(context, toolResult.tool_use_id) === undefined ) { // Background Agent launches ACK immediately; only task_notification settles them. if ( !isClaudeToolResultError(toolResult) && (runHandles !== undefined || isClaudeSubagentAsyncLaunchAck(output)) ) { continue; } const result = claudeSubagentResultText(output); yield* updateClaudeSubagentNode({ context, taskId: subagent.task.nativeTaskRef?.nativeId ?? String(subagent.task.id), toolUseId: toolResult.tool_use_id, ...(result.length === 0 ? {} : { result }), status: isClaudeToolResultError(toolResult) ? "failed" : "completed", }); continue; } const parentToolUseId = parentToolUseIdFromSdkMessage(message); const toolCall = findToolCall(context, toolResult.tool_use_id) ?? (yield* ensureToolCallStarted({ context, nativeItemId: toolResult.tool_use_id, toolName: toolNameFromClaudeToolResult(toolResult), toolInput: EMPTY_CLAUDE_NATIVE_TOOL_INPUT, parentToolUseId, })); yield* settleToolCall({ context, toolCall, registry: toolCallsFor(context, toolCall), message, toolResult, output, completedAt: yield* DateTime.now, }); // Subagents write to the session's shared task store, so their // task tools update the parent turn's list. Failed or never-run // calls changed nothing. if ( claudeToolNonExecutionKind(message, toolResult.tool_use_id) === undefined && !isClaudeToolResultError(toolResult) && CLAUDE_TASK_TOOL_NAMES.has(toolCall.toolName) ) { const taskLists = yield* Ref.get(taskListsByNativeThread); const knownTaskList = taskLists.get(liveQuery.nativeThreadId); const outputValue = claudeNativeToolOutputValue(output); // A structured TaskList already supplies a snapshot. Anything else, including // a subagent's text-only TaskList, needs Claude's persisted list once; // afterward the cache also records an unavailable store. const storedTaskList = !taskLists.has(liveQuery.nativeThreadId) && !( toolCall.toolName === "TaskList" && Array.isArray(claudeTaskField(outputValue, "tasks")) ) ? yield* readClaudeTaskList({ environment: adapterOptions.environment, cwd: context.input.runtimePolicy.cwd, nativeThreadId: liveQuery.nativeThreadId, }).pipe( Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), ) : undefined; const taskList = applyClaudeTaskTool( knownTaskList ?? storedTaskList ?? new Map(), toolCall.toolName, claudeNativeToolInputValue(toolCall.input), outputValue, ) ?? storedTaskList; yield* Ref.set( taskListsByNativeThread, new Map(taskLists).set(liveQuery.nativeThreadId, taskList ?? knownTaskList), ); if (taskList !== undefined) { yield* emitClaudePlanProjection({ context, nativeItemId: `claude-tasks:${context.providerTurnId}`, kind: "todo_list", steps: claudeTaskSteps(taskList), }).pipe(Effect.orDie); } } } const assistantParentToolUseId = parentToolUseIdFromSdkMessage(message); if (message.type === "assistant" && assistantParentToolUseId !== null) { const thinking = message.message.content.flatMap((block) => block.type === "thinking" && block.thinking.trim().length > 0 ? [block.thinking] : [], ); const subagent = thinking.length === 0 ? undefined : yield* resolveSubagentByToolUseId(context, assistantParentToolUseId); if (subagent !== undefined) { yield* emitSubagentThinking( subagentProjectionTarget(subagent), message, yield* DateTime.now, ); } } const assistantText = assistantTextFromSdkMessage(message); if ( assistantText !== null && assistantText.text.length > 0 && assistantParentToolUseId !== null ) { const subagent = yield* resolveSubagentByToolUseId(context, assistantParentToolUseId); if (subagent === undefined) { return; } const now = yield* DateTime.now; const nativeMessageId = message.type === "assistant" && typeof message.message.id === "string" ? message.message.id : null; subagent.lastAssistantText = nativeMessageId !== null && nativeMessageId === subagent.lastAssistantMessageId ? `${subagent.lastAssistantText ?? ""}\n${assistantText.text}` : assistantText.text; subagent.lastAssistantMessageId = nativeMessageId; yield* emitSubagentConversationMessage({ nativeItemId: assistantText.nativeItemId, threadId: subagent.childThreadId, rootNodeId: subagent.childRootNodeId, role: "assistant", text: assistantText.text, ordinal: ++subagent.nextChildItemOrdinal, now, }); return; } if (isClaudeGoalAcknowledgement(message)) { // The CLI repeats a local command's output as the turn's result text. if (assistantText !== null) { context.assistant.noticeTexts.add(normalizeClaudeResultText(assistantText.text)); } return; } // Claude Code echoes /compact's stdout ("Not enough messages to compact.") as a // synthetic assistant message. The compaction item reports that outcome; it is not a // reply. if ( message.type === "assistant" && "local_command_source" in message && isCompactionCommandMessage({ role: "user", ...context.input.message }) ) { return; } if (assistantText !== null && assistantText.text.length > 0) { yield* emitAssistantTextArtifacts({ context, nativeItemId: assistantText.nativeItemId, text: assistantText.text, }); return; } if ( isClaudeTaskNotificationOriginResult(message) && !isClaudeProviderContinuationTurn(context.input) && message.num_turns === 0 ) { yield* Effect.logDebug("orchestration-v2.claude-task-notification-result-dropped", { providerTurnId: context.providerTurnId, num_turns: message.num_turns, stop_reason: message.stop_reason, terminal_reason: message.terminal_reason, uuid: message.uuid, session_id: message.session_id, createdBy: context.input.message.createdBy, creationSource: context.input.message.creationSource, }); return; } if ( isClaudeTaskNotificationOriginResult(message) && !isClaudeProviderContinuationTurn(context.input) ) { yield* clearWakeReports(liveQuery.nativeThreadId); yield* Effect.logInfo("orchestration-v2.claude-task-notification-result-accepted", { providerTurnId: context.providerTurnId, num_turns: message.num_turns, stop_reason: message.stop_reason, terminal_reason: message.terminal_reason, uuid: message.uuid, session_id: message.session_id, createdBy: context.input.message.createdBy, creationSource: context.input.message.creationSource, }); } const resultText = message.type === "result" && ((message.subtype === "success" && message.is_error) || terminalStatusFromResult(message) === "failed") ? null : resultTextFromSdkMessage(message); if ( // A /compact's result text reports the compaction ("Not enough messages to // compact."), which its compaction item already carries; it is not a reply. !( message.type === "result" && message.subtype === "success" && message.local_command === "compact" ) && context.assistant.emittedNativeItemIds.size === 0 && context.assistant.fallbackText.length === 0 && resultText !== null && resultText.text.length > 0 && !context.assistant.noticeTexts.has(resultText.text) && !context.assistant.noticeTexts.has(normalizeClaudeResultText(resultText.text)) ) { context.assistant.fallbackText = resultText.text; context.assistant.fallbackNativeItemId = resultText.nativeItemId; } if (message.type === "result") { const contextUsage = context.streamUsage?.usage ?? context.compactionTokens; if (resultsReportingNewContextWindow.has(message) && contextUsage !== undefined) { yield* emitContextUsage(context, contextUsage); } const completedAt = yield* DateTime.now; const interrupted = (yield* Ref.get(interruptedTurns)).has(context.providerTurnId); const wasSteered = (yield* Ref.get(steeredTurns)).has(context.providerTurnId); if (!interrupted && wasSteered && isClaudeActiveSteeringAbortResult(message)) { context.steeredSegmentUsage.push(message.usage); context.steeredLedgerUsage = addClaudeLedgerUsage( context.steeredLedgerUsage, resultLedgerUsage.get(message), ); return; } yield* Ref.update(steeredTurns, (current) => { const next = new Set(current); next.delete(context.providerTurnId); return next; }); if (!isClaudeProviderContinuationTurn(context.input)) { yield* markAbsorbedWake(liveQuery.nativeThreadId); } const usageLimited = context.authenticationFailureMessage === undefined && (context.rejectedRateLimitTypes.size > 0 || context.latestAssistantRateLimit !== null) && (message.subtype !== "success" || message.api_error_status == null || message.api_error_status === 429) && (message.terminal_reason == null || message.terminal_reason === "api_error" || message.terminal_reason === "blocking_limit"); // The CLI also labels 403 authentication_failed; show its text because login cannot grant access. const permissionDenied = message.subtype === "success" && message.api_error_status === 403; const failureHint = permissionDenied ? message.result : (context.authenticationFailureMessage ?? (usageLimited ? "Claude usage limit reached. Send the message again once the limit resets." : undefined)); // Usage-limited /compact reports success; fail it so queued messages wait for the limit to clear. const limitedCompaction = usageLimited && message.subtype === "success" && message.local_command === "compact" ? context.compactionFailure : null; const resetTimes = Array.from(context.rateLimitResetTimes.values()); const assistantLimit = context.latestAssistantRateLimit; if ( assistantLimit?.type != null && !context.rateLimitResetTimes.has(assistantLimit.type) ) { resetTimes.push(assistantLimit.resetAt); } const resetAt = resetTimes.length > 0 && resetTimes.every((time) => time !== null) ? resetTimes.reduce((latest, time) => (time! > latest ? time! : latest), "") : null; const resultFailure = interrupted ? null : limitedCompaction !== null ? makeProviderFailure({ class: "usage_limit", message: limitedCompaction.message ?? failureHint, code: "compaction_failed", }) : providerFailureFromResult( message, bufferedResultArrivals.get(message) ?? completedAt, failureHint, usageLimited, ); const terminalFailure = resultFailure?.class === "usage_limit" ? { ...resultFailure, resetAt: resetAt ?? resultFailure.resetAt ?? null } : resultFailure; yield* finalizeActiveTurn({ context, status: interrupted ? "interrupted" : limitedCompaction !== null ? "failed" : terminalStatusFromResult(message, failureHint), completedAt, result: message, ...(terminalFailure === null ? {} : { failure: terminalFailure }), }); } }); const takeReleasableSubagentFrames = Effect.fnUntraced(function* (message: SDKMessage) { if ( message.type !== "system" || (message.subtype !== "task_started" && message.subtype !== "task_progress" && message.subtype !== "task_notification") || message.tool_use_id === undefined || !(yield* Ref.get(sessionSubagentsByTaskId)).has(message.task_id) ) { return []; } const toolUseId = message.tool_use_id; const taskId = message.task_id; yield* Ref.update(sessionSubagentTaskIdsByToolUseId, (current) => current.get(toolUseId) === taskId ? current : new Map(current).set(toolUseId, taskId), ); return yield* Ref.modify(pendingSubagentFramesByToolUseId, (current) => { const frames = current.get(toolUseId); if (frames === undefined) { return [[] as ReadonlyArray, current] as const; } const next = new Map(current); next.delete(toolUseId); return [frames, next] as const; }); }); const handleRoutedSdkMessage = Effect.fnUntraced(function* (input: { readonly query: ClaudeAgentSdkQuerySession; readonly message: SDKMessage; }) { if (input.message.type !== "system" || input.message.subtype !== "task_started") { for (const message of yield* takeReleasableSubagentFrames(input.message)) { yield* handleSdkMessageFrame({ query: input.query, message, replayed: true }); } } yield* handleSdkMessageFrame(input); for (const message of yield* takeReleasableSubagentFrames(input.message)) { yield* handleSdkMessageFrame({ query: input.query, message, replayed: true }); } }); const heldToolUseIds = (context: ActiveClaudeTurnContext): ReadonlySet => new Set( context.heldRootFrames.flatMap((frame) => parentToolUseIdFromSdkMessage(frame) === null ? claudeToolUseBlocksFromAssistantMessage(frame).map((toolUse) => toolUse.id) : [], ), ); const releaseHeldRootFrames = Effect.fnUntraced(function* ( context: ActiveClaudeTurnContext, ) { context.promptEcho = "confirmed"; const liveQuery = yield* Ref.get(queryContext); const held = context.heldRootFrames.splice(0); if (liveQuery === null) { return; } for (const message of held) { yield* handleRoutedSdkMessage({ query: liveQuery.query, message }); } }); const handleSdkMessage = Effect.fnUntraced(function* (input: { readonly query: ClaudeAgentSdkQuerySession; readonly message: SDKMessage; }) { const message = input.message; const context = yield* Ref.get(activeTurn); const liveQuery = yield* Ref.get(queryContext); if ( context?.promptUuid === null && liveQuery?.query === input.query && isClaudeRootTurnFrame(message) ) { context.nativeWakeStarted = true; } if ( context === null || context.promptUuid === null || context.promptEcho === "confirmed" || liveQuery?.query !== input.query ) { yield* handleRoutedSdkMessage(input); return; } if (claudeEchoedPromptUuids(message).includes(context.promptUuid)) { if ( liveQuery.promptEchoMode === "unknown" || liveQuery.promptEchoMode === "acknowledged" ) { liveQuery.promptEchoMode = message.type !== "result" && context.gatedFramesBeforeEcho === 0 ? "early" : "result_only"; } yield* releaseHeldRootFrames(context); yield* handleRoutedSdkMessage(input); return; } const lifecycle = claudePromptLifecycle(message); if (lifecycle?.uuid === context.promptUuid) { if (liveQuery.promptEchoMode === "unknown") { liveQuery.promptEchoMode = "acknowledged"; } if (lifecycle.state === "started") { context.promptStarted = true; } // Previously recorded prompt uuids complete without starting a turn. if (lifecycle.state === "completed" && !context.promptStarted) { yield* Effect.logWarning("orchestration-v2.claude-prompt-completed-without-turn", { providerTurnId: context.providerTurnId, promptUuid: context.promptUuid, }); yield* finalizeActiveTurn({ context, status: "failed", completedAt: yield* DateTime.now, failure: makeProviderFailure({ class: "provider_error", message: "Claude acknowledged the message but never started a turn for it.", }), }); return; } } if (!isClaudePromptEchoGatedFrame(message)) { if ( context.heldRootFrames.length > 0 && claudeFrameToolUseIds(message).some((id) => heldToolUseIds(context).has(id)) ) { context.heldRootFrames.push(message); return; } yield* handleRoutedSdkMessage(input); return; } context.gatedFramesBeforeEcho += 1; if ( message.type === "result" && isClaudeResultForOtherTurn({ message, promptUuid: context.promptUuid, promptEchoMode: liveQuery.promptEchoMode, }) ) { const held = context.heldRootFrames.splice(0); context.gatedFramesBeforeEcho = 0; if (message.num_turns === 0) { context.heldRootFrames.push(...held); yield* Effect.logDebug("orchestration-v2.claude-result-for-other-turn-dropped", { providerTurnId: context.providerTurnId, origin: message.origin?.kind, uuid: message.uuid, }); return; } if (liveQuery.promptEchoMode === "early") { yield* Effect.logInfo("orchestration-v2.claude-wake-turn-before-prompt", { providerTurnId: context.providerTurnId, origin: message.origin?.kind, heldFrames: held.length, }); for (const frame of [...held, message]) { yield* bufferWakeMessage({ nativeThreadId: liveQuery.nativeThreadId, message: frame, }); } return; } // Foreign output already streamed; exclude its result so only the prompt's own echoing result settles the turn. if (isClaudeTaskNotificationOriginResult(message)) { yield* clearWakeReports(liveQuery.nativeThreadId); } yield* Effect.logInfo("orchestration-v2.claude-result-for-other-turn", { providerTurnId: context.providerTurnId, origin: message.origin?.kind, num_turns: message.num_turns, uuid: message.uuid, }); return; } if (liveQuery.promptEchoMode !== "early") { yield* handleRoutedSdkMessage(input); return; } if (message.type !== "result") { context.heldRootFrames.push(message); return; } yield* releaseHeldRootFrames(context); yield* handleRoutedSdkMessage(input); }); const canUseToolEffect = Effect.fn("ClaudeAdapterV2.canUseTool")(function* ( toolName: Parameters[0], toolInput: Parameters[1], callbackOptions: Parameters[2], options?: { readonly startToolCall?: boolean }, ) { const context = yield* Ref.get(activeTurn); if (context === null) { return { behavior: "deny", message: "Claude V2 adapter has no active turn for this tool request.", toolUseID: callbackOptions.toolUseID, } satisfies PermissionResult; } const nativeRequestId = callbackOptions.toolUseID; const nativeToolInput = claudeNativeToolInputFromRecord(toolInput); const heldForEcho = context.heldRootFrames.length > 0; const startToolCall = toolName !== "Agent" && options?.startToolCall !== false; if (toolName === "Agent") { rememberClaudeSubagentLaunch( subagentModelCatalog, context, pendingSubagentLaunchesByToolUseId, nativeRequestId, nativeToolInput, null, ); } else if (startToolCall && !heldForEcho) { yield* ensureToolCallStarted({ context, nativeItemId: nativeRequestId, toolName, toolInput: nativeToolInput, parentToolUseId: null, }); } if ( heldForEcho && toolName !== "ExitPlanMode" && (toolName === "AskUserQuestion" || requiresClaudeApproval(context)) ) { // The SDK blocks prompt echoes on permission answers; release held output except for ExitPlanMode. yield* releaseHeldRootFrames(context); if (startToolCall) { yield* ensureToolCallStarted({ context, nativeItemId: nativeRequestId, toolName, toolInput: nativeToolInput, parentToolUseId: null, }); } } if (toolName === "AskUserQuestion") { const questions = claudeUserInputQuestions(nativeToolInput); const artifacts = yield* buildApprovalRequestArtifacts({ context, nativeItemId: nativeRequestId, nativeRequestId, // No tool node exists for a control request to hang under. ...(options?.startToolCall === false ? { parentNodeId: context.input.rootNodeId } : {}), requestKind: "user_input", questions, }); const answers = yield* Deferred.make(); yield* Ref.update(pendingRuntimeRequests, (current) => { const updated = new Map(current); updated.set(String(artifacts.request.id), { type: "user_input", answers, }); return updated; }); yield* Effect.all( [ emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node: artifacts.node, }), emitProviderEvent({ type: "runtime_request.updated", driver: CLAUDE_PROVIDER, runtimeRequest: artifacts.request, }), emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: artifacts.turnItem, }), ], { concurrency: 1 }, ); const resolvedAnswers = yield* awaitClaudeUserInputAnswers( answers, callbackOptions.signal, ).pipe( Effect.ensuring( Ref.update(pendingRuntimeRequests, (current) => { const updated = new Map(current); updated.delete(String(artifacts.request.id)); return updated; }), ), ); if (!(yield* Deferred.isDone(answers))) yield* cancelWithdrawnRequest(artifacts); return callbackOptions.signal.aborted ? ({ behavior: "deny", message: "User cancelled tool execution.", } satisfies PermissionResult) : ({ behavior: "allow", updatedInput: { questions: toolInput.questions, answers: claudeSdkUserInputAnswers(resolvedAnswers), }, toolUseID: callbackOptions.toolUseID, } satisfies PermissionResult); } if (toolName === "ExitPlanMode") { const markdown = claudeProposedPlan(nativeToolInput); if (markdown !== null && heldToolUseIds(context).has(nativeRequestId)) { heldProposedPlansByToolUseId.set(nativeRequestId, markdown); } else if (markdown !== null) { yield* emitClaudePlanProjection({ context, nativeItemId: nativeRequestId, kind: "proposed_plan", markdown, }); } return { behavior: "deny", message: "The client captured your proposed plan. Stop here and wait for the user's feedback or implementation request in a later turn.", toolUseID: callbackOptions.toolUseID, } satisfies PermissionResult; } if ( context.input.runtimePolicy.interactionMode === "plan" && context.input.runtimePolicy.approvalPolicy === "never" ) { return { behavior: "deny", message: "This tool requires approval in Plan mode, but approvals are disabled for this thread.", toolUseID: callbackOptions.toolUseID, } satisfies PermissionResult; } if (!requiresClaudeApproval(context)) { return { behavior: "allow", updatedInput: toolInput, toolUseID: callbackOptions.toolUseID, } satisfies PermissionResult; } const activePermissionMode = (yield* Ref.get(queryContext))?.permissionMode ?? context.queryPolicy.permissionMode; const safetyReason = NodeUtil.stripVTControlCharacters( callbackOptions.decisionReason ?? "", ).trim(); // The SDK omits the CLI's dangerousRemoval discriminator, so match its removal warnings. if ( toolName === "Bash" && activePermissionMode === "bypassPermissions" && callbackOptions.matchedAskRule === undefined && (/^Dangerous rm(?:dir)?\b/i.test(safetyReason) || safetyReason === "This shell -c script runs rm and could not be checked" || safetyReason === "A permission rule denies a command in this shell -c script") ) { return { behavior: "deny", message: `Claude Code's removal safety check denied this command; the user did not reject it. The command did not run. Do not bypass the check by splitting the removal, putting it in a script, or using another tool or shell. If the warning below supplies a safe rewrite, use it and let the same check evaluate it. Otherwise continue the rest of the task without the removal and tell the user what you needed to delete and why. Warning: ${safetyReason}`, toolUseID: callbackOptions.toolUseID, } satisfies PermissionResult; } const requestKind = providerRequestKindFromClaudeTool(toolName); // The user approves the command itself; Claude's title and description only summarize it. const command = inputRecordValue(nativeToolInput, "command"); const prompt = (typeof command === "string" && command.trim() !== "" ? command : undefined) ?? callbackOptions.title ?? callbackOptions.description ?? callbackOptions.decisionReason ?? summarizeClaudeToolRequest(toolName, nativeToolInput); const artifacts = yield* buildApprovalRequestArtifacts({ context, nativeItemId: nativeRequestId, nativeRequestId, requestKind, prompt, presentation: claudeApprovalPresentation({ permissionMode: context.queryPolicy.permissionMode, prompt, callbackOptions, }), }); const decision = yield* Deferred.make(); yield* Ref.update(pendingRuntimeRequests, (current) => { const updated = new Map(current); updated.set(String(artifacts.request.id), { type: "approval", requestKind, decision, }); return updated; }); yield* Effect.all( [ emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node: artifacts.node, }), emitProviderEvent({ type: "runtime_request.updated", driver: CLAUDE_PROVIDER, runtimeRequest: artifacts.request, }), emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: artifacts.turnItem, }), ], { concurrency: 1 }, ); const resolvedDecision = yield* awaitClaudeApprovalDecision( decision, callbackOptions.signal, ).pipe( Effect.ensuring( Ref.update(pendingRuntimeRequests, (current) => { const updated = new Map(current); updated.delete(String(artifacts.request.id)); return updated; }), ), ); if (!(yield* Deferred.isDone(decision))) yield* cancelWithdrawnRequest(artifacts); return permissionResultFromDecision({ toolName, ...resolvedDecision, toolInput, toolUseID: callbackOptions.toolUseID, managedCeiling: context.input.runtimePolicy.maxRuntimeMode !== undefined, ...(callbackOptions.suggestions === undefined ? {} : { suggestions: callbackOptions.suggestions }), }); }); const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) => runPromise(canUseToolEffect(toolName, toolInput, callbackOptions)); const onElicitationEffect = Effect.fn("ClaudeAdapterV2.onElicitation")(function* ( request: ElicitationRequest, callbackOptions: Parameters>[1], ) { if (callbackOptions.signal.aborted) return { action: "cancel" } satisfies ElicitationResult; const acceptance = claudeMcpElicitationAcceptance(request); const context = yield* Ref.get(activeTurn); if (acceptance === undefined || context === null) { yield* Effect.logWarning( "Declined an MCP elicitation the approval card cannot answer.", { serverName: request.serverName, mode: request.mode, activeTurn: context !== null, }, ); return { action: "decline" } satisfies ElicitationResult; } // The CLI waits on this answer, so a turn held for its prompt echo // must show its output now, as canUseTool does. if (context.heldRootFrames.length > 0) { yield* releaseHeldRootFrames(context); } // The SDK's requestId names this control request; T3 allocates its own // runtime request id from it. const nativeRequestId = `mcp-elicitation:${callbackOptions.requestId}`; const artifacts = yield* buildApprovalRequestArtifacts({ context, nativeItemId: nativeRequestId, nativeRequestId, requestKind: "mcp-elicitation", prompt: request.message, appName: request.displayName ?? request.title ?? request.serverName, options: CLAUDE_MCP_ELICITATION_OPTIONS, }); const decision = yield* Deferred.make(); yield* Ref.update(pendingRuntimeRequests, (current) => { const updated = new Map(current); updated.set(String(artifacts.request.id), { type: "approval", requestKind: "mcp-elicitation", decision, }); return updated; }); yield* Effect.all( [ emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node: artifacts.node, }), emitProviderEvent({ type: "runtime_request.updated", driver: CLAUDE_PROVIDER, runtimeRequest: artifacts.request, }), emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: artifacts.turnItem, }), ], { concurrency: 1 }, ); // Closing the query aborts the signal, so a stopped session settles // this as cancel and a late response finds no pending request. const { decision: resolved } = yield* awaitClaudeApprovalDecision( decision, callbackOptions.signal, ).pipe( Effect.ensuring( Ref.update(pendingRuntimeRequests, (current) => { const updated = new Map(current); updated.delete(String(artifacts.request.id)); return updated; }), ), ); if (resolved === "cancel" && callbackOptions.signal.aborted) { const now = yield* DateTime.now; yield* emitProviderEvent({ type: "node.updated", driver: CLAUDE_PROVIDER, node: { ...artifacts.node, status: "cancelled", completedAt: now }, }); yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: { ...artifacts.turnItem, status: "cancelled", completedAt: now, updatedAt: now, }, }); yield* emitProviderEvent({ type: "runtime_request.updated", driver: CLAUDE_PROVIDER, runtimeRequest: { ...artifacts.request, status: "cancelled", resolvedAt: now }, }); } // Never null: the SDK reads null as answered out of band and leaves // the server waiting. Every accept is one-time; no grant is offered. return resolved === "accept" ? acceptance : ({ action: resolved === "cancel" ? "cancel" : "decline", } satisfies ElicitationResult); }); const onElicitation: NonNullable = ( request, callbackOptions, ) => runPromise(onElicitationEffect(request, callbackOptions)); const onUserDialog: NonNullable = ( request, callbackOptions, ) => runPromise( Effect.gen(function* () { if (request.dialogKind !== "resume_return") { return { behavior: "cancelled" as const }; } const idleCompaction = Option.isSome(serverSettings) ? yield* serverSettings.value.getSettings.pipe( Effect.map((settings) => resolveIdleCompaction(settings, adapterOptions.instanceId), ), Effect.orElseSucceed(() => adapterOptions.settings.idleCompaction), ) : adapterOptions.settings.idleCompaction; if (idleCompaction === "off") { return { behavior: "completed" as const, result: "continue" as const }; } const ageMinutes = typeof request.payload.sessionAgeMinutes === "number" && Number.isFinite(request.payload.sessionAgeMinutes) ? Math.max(0, Math.floor(request.payload.sessionAgeMinutes)) : 0; const estimatedTokens = typeof request.payload.estimatedTokens === "number" && Number.isFinite(request.payload.estimatedTokens) ? Math.max(0, Math.floor(request.payload.estimatedTokens)) : 0; const question = formatClaudeResumeCompactionQuestion({ ageMinutes, estimatedTokens, }); const result = yield* canUseToolEffect( "AskUserQuestion", { questions: [ { header: "Resume session", question, options: [ { label: CLAUDE_RESUME_COMPACTION_COMPACT_ANSWER, description: "Resume with a summary and use fewer tokens.", }, { label: "Keep full history", description: "Resume without changing the conversation.", }, { label: CLAUDE_RESUME_COMPACTION_NEVER_ANSWER, description: "Keep full history and skip future resume prompts.", }, ], multiSelect: false, }, ], }, { signal: callbackOptions.signal, requestId: callbackOptions.requestId, toolUseID: request.toolUseID ?? callbackOptions.requestId, }, // The dialog is a control request, not a tool call, so no // tool_result would ever settle a tool row started for it. { startToolCall: false }, ); if (result.behavior !== "allow") return { behavior: "cancelled" as const }; const answers = result.updatedInput === undefined ? undefined : Reflect.get(result.updatedInput, "answers"); const selection = typeof answers === "object" && answers !== null ? Reflect.get(answers, question) : undefined; return { behavior: "completed" as const, result: selection === CLAUDE_RESUME_COMPACTION_COMPACT_ANSWER ? ("compact" as const) : selection === CLAUDE_RESUME_COMPACTION_NEVER_ANSWER ? ("never" as const) : ("continue" as const), }; }), ); const liveProcessRunsBackgroundWork = Effect.fnUntraced(function* ( live: ClaudeLiveQueryContext, options?: { readonly forGoalCheck?: boolean }, ) { if ( rosterForNativeThread( yield* Ref.get(pendingBackgroundTasksByNativeThread), live.nativeThreadId, ).size > 0 ) { return true; } const buffered = (yield* Ref.get(wakeBuffers)).get(live.nativeThreadId)?.messages ?? []; for (const [taskId, subagent] of yield* Ref.get(sessionSubagentsByTaskId)) { if ( subagent.task.status === "running" && !live.subagentsFromEarlierProcesses.has(subagent) && (options?.forGoalCheck !== true || subagent.nativeTaskType === undefined || CLAUDE_GOAL_DEFERRING_TASK_TYPES.has(subagent.nativeTaskType)) && !buffered.some( (message) => message.type === "system" && message.subtype === "task_notification" && message.task_id === taskId, ) ) { return true; } } return false; }); const openQuery = Effect.fnUntraced(function* ( turnInput: ProviderAdapter.ProviderAdapterV2TurnInput, nativeThreadId: string, ) { const queryPolicy = yield* queryPolicyForTurn(turnInput); const mcpSession = yield* mcpSessions.read(turnInput.threadId); const mcpOverrides = claudeMcpQueryOverrides({ mcpSession, readOnlySandbox: sandboxPolicyKindForClaudeRuntimePolicy(turnInput.runtimePolicy) === "readOnly", ...(queryPolicy.allowedTools === undefined ? {} : { allowedTools: queryPolicy.allowedTools }), }); const queryPolicyKey = claudeEffectiveQueryPolicyKey(queryPolicy, mcpOverrides); const compiledSelection = compileClaudeModelSelection(turnInput.modelSelection); const resumeSessionAt = yield* getNativeConversationHeadId(turnInput.providerThread); const existing = yield* Ref.get(queryContext); // Continuations drain existing output without prompting; retain the live process regardless of selection. if ( existing !== null && existing.nativeThreadId === nativeThreadId && !existing.stopping && isClaudeProviderContinuationTurn(turnInput) ) { return existing; } if ( existing !== null && existing.nativeThreadId === nativeThreadId && existing.queryPolicyKey === queryPolicyKey && !existing.stopping && existing.selection.queryIdentity !== compiledSelection.queryIdentity && canApplyClaudeSelectionLive(existing.selection, compiledSelection) && (existing.selection.apiModelId === compiledSelection.apiModelId || existing.openedPermissionMode === queryPolicy.permissionMode) ) { const live = existing; // Preserve launcher-selected models unless the model changed or a failed update left it unknown. yield* ( live.selection.queryIdentity === "" || live.selection.apiModelId !== compiledSelection.apiModelId ? live.query.setModel(compiledSelection.apiModelId) : Effect.void ).pipe( Effect.andThen( live.query.applyFlagSettings(claudeLiveSelectionFlagSettings(compiledSelection)), ), Effect.tapError(() => Effect.sync(() => { live.selection = { ...live.selection, queryIdentity: "" }; }), ), ); live.selection = compiledSelection; } if ( existing !== null && existing.nativeThreadId === nativeThreadId && existing.queryPolicyKey === queryPolicyKey && !existing.stopping && existing.selection.queryIdentity === compiledSelection.queryIdentity ) { // A denied ExitPlanMode leaves Claude in plan mode. Restore only from plan: // Auto/default may reflect a policy restriction that forbids restoring bypass. if (existing.permissionMode === "plan" && existing.openedPermissionMode !== "plan") { existing.permissionMode = yield* existing.query .setPermissionMode(existing.openedPermissionMode) .pipe( Effect.as(existing.openedPermissionMode), Effect.catchTags({ ClaudeAgentSdkQueryRunnerError: (error) => existing.openedPermissionMode === "bypassPermissions" ? existing.query .setPermissionMode("default") .pipe(Effect.as("default" as const)) : Effect.fail(error), }), ); } return existing; } // Replacing this thread's process kills its background work, even with a new native identity. if ( existing !== null && (existing.nativeThreadId === nativeThreadId || existing.lastTurn?.input.threadId === turnInput.threadId) && !existing.stopping && (yield* liveProcessRunsBackgroundWork(existing)) ) { return yield* new ClaudeBackgroundWorkBlocksQueryReplacementError(); } // Read before closing the live process so a bad path leaves it running. const appendSystemPromptFileText = yield* readClaudeAppendSystemPromptFile({ settings: adapterOptions.settings, cwd: turnInput.runtimePolicy.cwd, fileSystem, path, }); const launchArgSettings = yield* readClaudeLaunchArgSettings({ settings: adapterOptions.settings, cwd: turnInput.runtimePolicy.cwd, fileSystem, path, }); const closedExistingNativeThreadId = existing !== null ? existing.nativeThreadId : null; if (existing !== null) { yield* existing.query.close.pipe(Effect.ignore); if (existing.nativeThreadId !== nativeThreadId) { yield* clearWakeStateForNativeThread(existing.nativeThreadId); yield* resetBackgroundTaskStateForNativeThreadProcess(existing.nativeThreadId, { status: "idle", }); } } const openedWithResume = (yield* Ref.get(openedNativeThreads)).has(nativeThreadId); // Idle release loses openedNativeThreads; persisted turns still require resume to avoid "already in use". const hasPersistedProviderTurn = turnInput.nativeThreadHasTurns ?? turnInput.providerTurnOrdinal > 1; const shouldResume = resumeSessionAt !== undefined || openedWithResume || hasPersistedProviderTurn; const toolSearchOverride = yield* resolveToolSearchOverride( turnInput.runtimePolicy.cwd, launchArgSettings, ); const queryOptions = makeClaudeQueryOptions({ modelSelection: turnInput.modelSelection, nativeThreadId, title: turnInput.appThread.title, resume: shouldResume, ...(resumeSessionAt === undefined ? {} : { resumeSessionAt }), cwd: turnInput.runtimePolicy.cwd, attachmentsDir, settings: adapterOptions.settings, ...(launchArgSettings === undefined ? {} : { sdkSettings: claudeSdkLaunchArgSettings(launchArgSettings) }), environment: claudeQueryEnvironment( adapterOptions.environment, turnInput.appThread, turnInput.appThread.lineage.relationshipToParent === "subagent" ? yield* resolveSubagentPromptCacheTtl(turnInput.runtimePolicy.cwd) : undefined, ), tools: queryPolicy.tools ?? CLAUDE_CODE_PRESET_TOOLS, ...(mcpOverrides.allowedTools === undefined ? {} : { allowedTools: mcpOverrides.allowedTools }), ...(mcpOverrides.mcpServers === undefined ? {} : { mcpServers: mcpOverrides.mcpServers }), deviceToolsAvailable: mcpSession?.capabilities?.has("device") === true, permissionMode: queryPolicy.permissionMode, ...(queryPolicy.allowDangerouslySkipPermissions === undefined ? {} : { allowDangerouslySkipPermissions: queryPolicy.allowDangerouslySkipPermissions, }), canUseTool, onUserDialog, onElicitation, supportedDialogKinds: ["resume_return"], ...(appendSystemPromptFileText === undefined ? {} : { appendSystemPromptFileText }), ...(toolSearchOverride === undefined ? {} : { toolSearchOverride }), }); const resolvedTurnCwd = yield* fileSystem .realPath(turnInput.runtimePolicy.cwd ?? ".") .pipe(Effect.orElseSucceed(() => turnInput.runtimePolicy.cwd)); const querySession = yield* queryRunner .open({ threadId: turnInput.threadId, providerSessionId: input.providerSessionId, options: queryOptions, }) .pipe( Effect.onError(() => closedExistingNativeThreadId === nativeThreadId ? Effect.gen(function* () { yield* clearWakeStateForNativeThread(nativeThreadId); yield* resetBackgroundTaskStateForNativeThreadProcess(nativeThreadId, { status: "idle", }); }) : Effect.void, ), ); // Mark only successful opens; retries must not resume sessions that failed to create. yield* Ref.update(openedNativeThreads, (current) => { if (current.has(nativeThreadId)) { return current; } const updated = new Set(current); updated.add(nativeThreadId); return updated; }); yield* resetBackgroundTaskStateForNativeThreadProcess(nativeThreadId, { status: "active", }); const closed = yield* Deferred.make(); const context: ClaudeLiveQueryContext = { queryPolicy, nativeThreadId, query: querySession, queryPolicyKey, selection: compiledSelection, closed, lastTurn: null, promptEchoMode: "unknown", openedPermissionMode: queryOptions.permissionMode, receivedInit: false, permissionMode: queryOptions.permissionMode, stopping: false, handedOffSubagents: new Set(), subagentsFromEarlierProcesses: new Set( [...(yield* Ref.get(sessionSubagentsByTaskId)).values()].filter( (subagent) => subagent.task.status === "running", ), ), modelLedger: new Map(), taskOutputRoot: claudeTaskOutputRoot( adapterOptions.environment, path, resolvedTurnCwd, hostPlatform, ), }; // One step, so the replaced process cannot exit between the install and the handoff. yield* Ref.update(queryContext, (replaced) => { if (replaced !== null) { replaced.handedOffSubagents = new Set( [...context.subagentsFromEarlierProcesses].filter( (subagent) => !replaced.subagentsFromEarlierProcesses.has(subagent), ), ); } return context; }); yield* querySession.messages.pipe( Stream.runForEach((message) => sdkMessageLock.withPermits(1)( Effect.suspend(() => { if ( message.type === "system" && message.subtype === "init" && !context.receivedInit ) { context.openedPermissionMode = message.permissionMode ?? context.openedPermissionMode; context.receivedInit = true; } if ( message.type === "system" && (message.subtype === "init" || message.subtype === "status") && message.permissionMode !== undefined ) { context.permissionMode = message.permissionMode; } if (message.type === "result") { const ledger = claudeModelLedger(message.modelUsage); const growth = claudeLedgerDelta(context.modelLedger, ledger); if (growth !== undefined) resultLedgerUsage.set(message, growth); // A ledger that went backwards cannot anchor the next turn's growth. context.modelLedger = growth === undefined && context.modelLedger !== undefined ? undefined : ledger; for (const [model, window] of claudeReportedContextWindows( message.modelUsage, )) { if (reportedContextWindows.get(model) === window) continue; reportedContextWindows.set(model, window); resultsReportingNewContextWindow.add(message); } } return handleSdkMessage({ query: querySession, message }); }), ), ), Effect.exit, Effect.flatMap( Effect.fnUntraced(function* (exit: ClaudeQueryStreamExit) { const heldContext = yield* Ref.get(activeTurn); if ( (yield* Ref.get(queryContext))?.query === querySession && heldContext !== null && heldContext.heldRootFrames.length > 0 ) { yield* releaseHeldRootFrames(heldContext); } const ownsLiveQuery = yield* Ref.modify(queryContext, (current) => current?.query === querySession ? [true, null] : [false, current], ); if (ownsLiveQuery) { yield* finalizeActiveTurnAfterQueryExit( context, exit._tag === "Failure" ? exit.cause : undefined, ); yield* endSubagentCallsOfClosedQuery( context.nativeThreadId, context.stopping ? "interrupted" : "failed", ); } else { yield* endSubagentsOfReplacedQuery(context); } }), ), Effect.ensuring(Deferred.succeed(closed, undefined)), Effect.forkIn(sessionScope), ); return context; }); const startTurn = Effect.fn("ClaudeAdapterV2.startTurn")( function* (turnInput: ProviderAdapter.ProviderAdapterV2TurnInput) { const startedAt = yield* DateTime.now; const nativeThreadId = yield* getNativeThreadId(turnInput.providerThread); const nativeTurnId = `turn:${turnInput.attemptId}`; // Use fresh uuids: native sessions can outlive this database, and Claude skips previously recorded prompts. const promptUuid = isClaudeProviderContinuationTurn(turnInput) ? null : yield* random.uuidV4().pipe( Effect.filterOrFail(isClaudePromptUuid), Effect.mapError( (cause) => new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: "Failed to allocate a Claude prompt uuid.", payload: cause, }), ), ); const providerTurnId = idAllocator.derive.providerTurn({ driver: CLAUDE_PROVIDER, nativeTurnId, }); const providerTurnOrdinal = turnInput.providerTurnOrdinal; const currentTurn = yield* Ref.get(activeTurn); if (currentTurn !== null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Claude provider turn ${currentTurn.providerTurnId} is still active.`, reason: "previous_turn_active", }); } yield* Ref.update(lastTurnRouteByNativeThread, (current) => { const updated = new Map(current); updated.set(nativeThreadId, { threadId: turnInput.threadId, providerThreadId: turnInput.providerThread.id, }); return updated; }); yield* rememberProviderThread(turnInput.providerThread); if (!goalsByNativeThread.has(nativeThreadId)) { goalsByNativeThread.set(nativeThreadId, turnInput.providerThread.goal ?? null); } if (!pdfBudgets.has(nativeThreadId)) { // Older sessions did not record retained attachments. Keep their // existing native-image allowance rather than disabling images. const used = turnInput.providerThread.nativeMetadata?.claudeAttachmentBudget; pdfBudgets.set(nativeThreadId, makeClaudeAttachmentBudget(used)); } const pdfReservation = yield* reserveClaudePdfBudget( pdfBudgets.get(nativeThreadId)!, persistAttachmentBudget(nativeThreadId), ); const userMessage = promptUuid === null ? null : yield* makeClaudeUserMessageWithAttachments({ text: applyClaudePromptEffortPrefix( turnInput.message.text, compileClaudeModelSelection(turnInput.modelSelection).promptEffort, ), attachments: turnInput.message.attachments, attachmentsDir, fileSystem, pdfReservation, skills: yield* userInvocableSkills(turnInput.runtimePolicy.cwd), uuid: promptUuid, }).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(Path.Path, path), ); const querySession = yield* openQuery(turnInput, nativeThreadId); const context: ActiveClaudeTurnContext = { input: turnInput, queryPolicy: querySession.queryPolicy, nativeThreadId, nativeTurnId, nativeMessageCursor: null, providerTurnId, providerTurnOrdinal, startedAt, itemOrdinals: new Map(), assistant: { fallbackText: "", fallbackNativeItemId: `assistant:${turnInput.runId}`, emittedNativeItemIds: new Set(), noticeTexts: new Set(), }, reasoning: { messageId: null, streamBlocks: new Map(), nextBlockIndex: new Map(), snapshotBlockIndex: new Map(), lastThinking: null, snapshots: new Set(), blocks: new Map(), }, streamUsage: null, compactionTokens: undefined, toolCalls: new Map(), compaction: null, ignoredTaskIds: new Set(), announcedUsageLimits: new Set(), authenticationFailureMessage: undefined, rejectedRateLimitTypes: new Set(), rateLimitResetTimes: new Map(), latestAssistantRateLimit: null, compactionFailure: null, steeredSegmentUsage: [], steeredLedgerUsage: { inputTokens: 0, outputTokens: 0 }, rootModels: new Set(), subagentsByTaskId: new Map(), subagentsByToolUseId: new Map(), promptUuid, promptEcho: isClaudeProviderContinuationTurn(turnInput) ? "confirmed" : "pending", promptStarted: false, nativeWakeStarted: false, gatedFramesBeforeEcho: 0, heldRootFrames: [], }; const attachTurn = Effect.gen(function* () { querySession.lastTurn = context; yield* Ref.set(activeTurn, context); if ( !isClaudeProviderContinuationTurn(turnInput) && permissionModeForClaudeRuntimePolicy(turnInput.runtimePolicy) === "bypassPermissions" && context.queryPolicy.permissionMode !== "bypassPermissions" && !announcedBypassDowngrades.has(turnInput.threadId) ) { announcedBypassDowngrades.add(turnInput.threadId); if (announcedBypassDowngrades.size > 1000) { const oldest = announcedBypassDowngrades.values().next().value; if (oldest !== undefined) announcedBypassDowngrades.delete(oldest); } const nativeItemId = `bypass-policy-downgrade:${context.providerTurnId}`; const notice = "T3 could not confirm Claude bypass permissions here, so Full access is running with approval checks."; yield* emitProviderEvent({ type: "turn_item.updated", driver: CLAUDE_PROVIDER, turnItem: { id: idAllocator.derive.turnItemFromProviderItem({ driver: CLAUDE_PROVIDER, nativeItemId, }), threadId: turnInput.threadId, runId: turnInput.runId, nodeId: turnInput.rootNodeId, providerThreadId: turnInput.providerThread.id, providerTurnId: context.providerTurnId, nativeItemRef: { driver: CLAUDE_PROVIDER, nativeId: nativeItemId, strength: "weak", }, parentItemId: null, ordinal: yield* resolveItemOrdinal(context, nativeItemId), type: "system_notice", status: "completed", title: notice, message: notice, startedAt, completedAt: startedAt, updatedAt: startedAt, }, }); } yield* emitProviderEvent({ type: "provider_turn.updated", driver: CLAUDE_PROVIDER, providerTurn: providerTurnPayload({ context, status: "running", completedAt: null, }), }); }); if (userMessage !== null) { yield* attachTurn; yield* startUserTurnForWakeReports(nativeThreadId); yield* Effect.uninterruptibleMask((restore) => restore( querySession.query.offer(userMessage, () => { pdfReservation.commit(); }), ).pipe( Effect.tap(() => Effect.sync(() => { pdfReservation.commit(); }), ), ), ); return; } // Transcript reads suspend replay; hold SDK frames until the buffer is applied to preserve order. yield* sdkMessageLock.withPermits(1)( Effect.gen(function* () { if ( (yield* Ref.get(queryContext))?.query !== querySession.query || querySession.stopping ) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: "Claude provider query ended before the continuation could start.", }); } yield* attachTurn; const drained = (yield* Ref.get(wakeBuffers)).get(nativeThreadId)?.messages ?? []; yield* Ref.update(requestedContinuations, (current) => { const updated = new Set(current); updated.delete(nativeThreadId); return updated; }); const wakeAbsorbed = (yield* Ref.get(absorbedWakes)).has(nativeThreadId); yield* forgetAbsorbedWake(nativeThreadId); if (drained.length === 0) { const completedAt = yield* DateTime.now; yield* finalizeActiveTurn({ context, status: "completed", completedAt }); return; } // Replay results last to avoid rebuffering later frames. An empty turn's result must not settle a newer running turn. const lastResultIndex = drained.findLastIndex((entry) => entry.type === "result"); const tailResult = drained[lastResultIndex]; const emptyTurnBeforeNextTurn = tailResult?.type === "result" && tailResult.num_turns === 0 && !tailResult.is_error && terminalStatusFromResult(tailResult) === "completed" && drained .slice(lastResultIndex + 1) .some((entry) => isClaudeTurnStartMessage(entry)); // The CLI can append empty task successes after a failure. Filter // those for replay without changing the raw-tail successor guard. const isEmptyTaskNotificationSuccess = (entry: SDKMessage) => isClaudeTaskNotificationOriginResult(entry) && entry.subtype === "success" && !entry.is_error && entry.num_turns === 0 && entry.result.length === 0 && terminalStatusFromResult(entry) === "completed"; const replayResultIndex = drained.findLastIndex( (entry) => entry.type === "result" && !isEmptyTaskNotificationSuccess(entry), ); const lastResult = drained[replayResultIndex === -1 ? lastResultIndex : replayResultIndex]; const opaqueReplayTombstones = taskIdSetForNativeThread( yield* Ref.get(opaqueBackgroundTaskReplayTombstonesByNativeThread), nativeThreadId, ); const hasOpaqueTaskNotification = drained.some( (entry) => entry.type === "system" && entry.subtype === "task_notification" && opaqueReplayTombstones.has(entry.task_id), ); for (const entry of drained) { if ( !(yield* Ref.get(wakeBuffers)).get(nativeThreadId)?.messages.includes(entry) ) { continue; } if (entry.type !== "result") { yield* handleSdkMessage({ query: querySession.query, message: entry }); } yield* Ref.update(wakeBuffers, (current) => { const buffered = current.get(nativeThreadId); if (buffered === undefined) return current; const index = buffered.messages.indexOf(entry); if (index === -1) return current; const messages = [ ...buffered.messages.slice(0, index), ...buffered.messages.slice(index + 1), ]; const next = new Map(current); if (messages.length === 0) next.delete(nativeThreadId); else next.set(nativeThreadId, { ...buffered, messages }); return next; }); } if (lastResult !== undefined && !emptyTurnBeforeNextTurn) { yield* handleSdkMessage({ query: querySession.query, message: lastResult }); return; } if (wakeAbsorbed || (hasOpaqueTaskNotification && !context.nativeWakeStarted)) { const completedAt = yield* DateTime.now; yield* finalizeActiveTurn({ context, status: "completed", completedAt }); } else if (!context.nativeWakeStarted) { yield* Effect.sleep(CLAUDE_WAKE_TURN_START_DEADLINE).pipe( Effect.andThen( sdkMessageLock.withPermits(1)( Effect.gen(function* () { if ( (yield* Ref.get(activeTurn)) !== context || (yield* Ref.get(queryContext)) !== querySession || querySession.stopping || context.nativeWakeStarted || (yield* Ref.get(interruptedTurns)).has(context.providerTurnId) ) return; yield* Effect.logWarning( "orchestration-v2.claude-wake-turn-never-opened", { providerTurnId: context.providerTurnId, nativeThreadId, }, ); const completedAt = yield* DateTime.now; yield* finalizeActiveTurn({ context, status: "completed", completedAt }); }), ), ), Effect.forkIn(sessionScope), ); } }), ); }, (effect, turnInput) => effect.pipe( Effect.scoped, Effect.mapError( (cause) => new ProviderAdapter.ProviderAdapterTurnStartError({ driver: CLAUDE_PROVIDER, threadId: turnInput.threadId, providerThreadId: turnInput.providerThread.id, runId: turnInput.runId, cause, }), ), ), ); const interruptTurn = Effect.fn("ClaudeAdapterV2.interruptTurn")( function* (turnInput: ProviderAdapter.ProviderAdapterV2InterruptInput) { const existing = yield* Ref.get(queryContext); const currentTurn = yield* Ref.get(activeTurn); const nativeThreadId = turnInput.providerThread.nativeThreadRef?.nativeId ?? null; if (currentTurn === null && turnInput.requestRuntimeRestart === true) { if (nativeThreadId === null) return; if (existing === null || existing.nativeThreadId !== nativeThreadId) { yield* clearWakeStateForNativeThread(nativeThreadId); yield* resetBackgroundTaskStateForNativeThreadProcess(nativeThreadId, { status: "idle", }); return; } yield* closeLiveQueryForNativeThread(nativeThreadId); const current = yield* Ref.get(queryContext); if (current === null || current.query === existing.query) { yield* clearWakeStateForNativeThread(nativeThreadId); yield* resetBackgroundTaskStateForNativeThreadProcess(nativeThreadId, { status: "idle", }); } return; } if (existing === null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Claude provider thread ${turnInput.providerThread.id} has no live query.`, }); } if (currentTurn?.providerTurnId !== turnInput.providerTurnId) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Claude provider turn ${turnInput.providerTurnId} is not the active turn.`, }); } yield* Ref.update(interruptedTurns, (current) => { const next = new Set(current); next.add(turnInput.providerTurnId); return next; }); yield* existing.query.interrupt.pipe(Effect.timeoutOption("10 seconds")); yield* existing.query.close.pipe(Effect.ignore); const closed = yield* Deferred.await(existing.closed).pipe( Effect.timeoutOption(CLAUDE_STOP_CONFIRM_TIMEOUT_MS), ); if (Option.isSome(closed)) { return; } // The turn stays open: the CLI may still be working, and the user decides // whether to Force stop it. yield* Effect.logWarning("orchestration-v2.claude-query-interrupt-timeout", { providerSessionId: input.providerSessionId, providerThreadId: turnInput.providerThread.id, providerTurnId: turnInput.providerTurnId, }); return yield* new ProviderAdapter.ProviderAdapterInterruptTimeoutError({ driver: CLAUDE_PROVIDER, providerThreadId: turnInput.providerThread.id, providerTurnId: turnInput.providerTurnId, timeoutMs: CLAUDE_STOP_CONFIRM_TIMEOUT_MS, }); }, (effect, turnInput) => effect.pipe( Effect.mapError((cause) => cause._tag === "ProviderAdapterInterruptTimeoutError" ? cause : new ProviderAdapter.ProviderAdapterInterruptError({ driver: CLAUDE_PROVIDER, providerThreadId: turnInput.providerThread.id, providerTurnId: turnInput.providerTurnId, cause, }), ), ), ); // Move subagents to the background before priority "now" steers cancel foreground tools. const backgroundForegroundSubagents = Effect.fnUntraced(function* ( live: ClaudeLiveQueryContext, deadline: number, ) { const foreground: Array<{ readonly taskId: string; readonly toolUseId: string; }> = []; for (const [taskId, subagent] of yield* Ref.get(sessionSubagentsByTaskId)) { const toolUseId = subagent.runToolUseId; if ( toolUseId === null || subagent.task.status !== "running" || subagent.task.parentNodeId !== subagent.rootNodeId || live.subagentsFromEarlierProcesses.has(subagent) || (yield* isNestedSubagentTask(taskId)) ) { continue; } foreground.push({ taskId, toolUseId }); } for (const [toolUseId, move] of backgroundMovesByToolUseId) { if (move.pollUnsafe() !== undefined) backgroundMovesByToolUseId.delete(toolUseId); } const moves: Array> = []; for (const { taskId, toolUseId } of foreground) { const inFlight = backgroundMovesByToolUseId.get(toolUseId); if (inFlight !== undefined) { moves.push(inFlight); continue; } const move = yield* live.query.backgroundTasks(toolUseId).pipe( Effect.flatMap((moved) => moved ? Ref.get(sessionSubagentsByTaskId).pipe( Effect.flatMap((registry) => registry.get(taskId)?.task.status === "running" && registry.get(taskId)?.runToolUseId === toolUseId ? Ref.update(backgroundedSubagentTaskIds, (current) => new Set(current).add(taskId), ) : Effect.void, ), ) : Effect.void, ), Effect.catch((cause) => Effect.logWarning("orchestration-v2.claude-steer-background-subagent-failed", { taskId, toolUseId, cause, }), ), Effect.forkIn(sessionScope), ); backgroundMovesByToolUseId.set(toolUseId, move); moves.push(move); } const remaining = deadline - (yield* Clock.currentTimeMillis); yield* Fiber.joinAll(moves).pipe(Effect.timeoutOption(Math.max(0, remaining))); }); const steerTurn = Effect.fn("ClaudeAdapterV2.steerTurn")( function* (turnInput: ProviderAdapter.ProviderAdapterV2SteerInput) { // Measured from the request, not from when the moves are forked: // building the message and the moves can take clock time a timer // started later would not count, and the steer would wait past it. const backgroundDeadline = (yield* Clock.currentTimeMillis) + BACKGROUND_MOVE_TIMEOUT_MS; const existing = yield* Ref.get(queryContext); if (existing === null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Claude provider thread ${turnInput.providerThread.id} has no live query.`, }); } const currentTurn = yield* Ref.get(activeTurn); if (currentTurn?.providerTurnId !== turnInput.providerTurnId) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Claude provider turn ${turnInput.providerTurnId} is not the active turn.`, }); } const pdfReservation = yield* reserveClaudePdfBudget( pdfBudgets.get(existing.nativeThreadId) ?? makeClaudeAttachmentBudget({ pagesUsed: CLAUDE_ATTACHMENT_MAX_PAGES, bytesUsed: CLAUDE_ATTACHMENT_MAX_BYTES, }), persistAttachmentBudget(existing.nativeThreadId), ); const userMessage = yield* makeClaudeUserMessageWithAttachments({ text: applyClaudePromptEffortPrefix( turnInput.message.text, compileClaudeModelSelection(currentTurn.input.modelSelection).promptEffort, ), attachments: turnInput.message.attachments, priority: "now", attachmentsDir, fileSystem, pdfReservation, skills: yield* userInvocableSkills(currentTurn.input.runtimePolicy.cwd), }).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(Path.Path, path), ); yield* backgroundForegroundSubagents(existing, backgroundDeadline); // The turn can end while the moves are answered. A steer offered // then would start a new turn the orchestrator never asked for, so // the steer fails and the orchestrator starts the follow-up turn. 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* Ref.update(steeredTurns, (current) => { const next = new Set(current); next.add(turnInput.providerTurnId); return next; }); yield* Effect.uninterruptibleMask((restore) => restore( existing.query.offer(userMessage, () => { pdfReservation.commit(); }), ).pipe( Effect.tap(() => Effect.sync(() => { pdfReservation.commit(); }), ), ), ); }, (effect, turnInput) => effect.pipe( Effect.scoped, Effect.mapError( (cause) => new ProviderAdapter.ProviderAdapterSteerRunError({ driver: CLAUDE_PROVIDER, providerThreadId: turnInput.providerThread.id, providerTurnId: turnInput.providerTurnId, cause, }), ), ), ); const closeSession = Effect.fnUntraced(function* () { const existing = yield* Ref.get(queryContext); if (existing !== null) { yield* existing.query.close.pipe(Effect.ignore); } yield* Effect.yieldNow; yield* queryRunner.assertComplete.pipe( Effect.catchCause((cause) => Effect.logWarning("orchestration-v2.claude-query-runner-incomplete", { providerSessionId: input.providerSessionId, cause, }), ), ); }); const closeLiveQueryForNativeThread = Effect.fnUntraced(function* (nativeThreadId: string) { const existing = yield* Ref.get(queryContext); if (existing === null || existing.nativeThreadId !== nativeThreadId) { return; } existing.stopping = true; yield* existing.query.close.pipe(Effect.ignore); const closed = yield* Deferred.await(existing.closed).pipe( Effect.timeoutOption("10 seconds"), ); if (Option.isSome(closed)) { return; } yield* Effect.logWarning("orchestration-v2.claude-query-close-timeout-before-fork", { providerSessionId: input.providerSessionId, nativeThreadId, }); const ownedLiveQuery = yield* Ref.modify(queryContext, (current) => current?.query === existing.query ? [true, null] : [false, current], ); if (ownedLiveQuery) { yield* finalizeActiveTurnAfterQueryExit(existing); yield* endSubagentCallsOfClosedQuery(nativeThreadId, "interrupted"); } else { yield* endSubagentsOfReplacedQuery(existing); } yield* Deferred.succeed(existing.closed, undefined); }); yield* Effect.addFinalizer(() => closeSession()); // Metadata-only transcripts cannot be resumed or recreated; fail resumeThread to trigger a fresh session and handoff. const claudeTranscriptState = Effect.fn("ClaudeAdapterV2.claudeTranscriptState")(function* ( nativeThreadId: string, ) { const configuredDir = adapterOptions.environment.CLAUDE_CONFIG_DIR?.trim() ?? ""; const projectsDir = path.join( configuredDir.length > 0 ? path.resolve(session.cwd, configuredDir) : path.join(yield* HostProcess.HomeDirectory, ".claude"), "projects", ); // Any message in any transcript copy means Claude may still resume it. let found = false; let unreadable = false; for (const project of yield* fileSystem.readDirectory(projectsDir)) { const transcriptPath = path.join(projectsDir, project, `${nativeThreadId}.jsonl`); // Stray files beside the project dirs fail with ENOTDIR. if ( !(yield* fileSystem.exists(transcriptPath).pipe(Effect.orElseSucceed(() => false))) ) { continue; } const conversationLine = yield* fileSystem .stream(transcriptPath) .pipe( Stream.decodeText, Stream.splitLines, Stream.filter(isClaudeConversationLine), Stream.runHead, Effect.option, ); if (Option.isNone(conversationLine)) { unreadable = true; continue; } if (Option.isSome(conversationLine.value)) { return "conversation" as const; } found = true; } return found && !unreadable ? ("empty" as const) : ("missing" as const); }); const runtime: ProviderAdapter.ProviderAdapterV2SessionRuntime = { instanceId: adapterOptions.instanceId, driver: CLAUDE_PROVIDER, providerSessionId: input.providerSessionId, providerSession: session, getModelContextWindow: (selection) => claudeContextWindow(selection) ?? undefined, events: Stream.fromEffectRepeat(Queue.take(events)), backgroundTasksEndWithProcess: true, hasPendingBackgroundWork: Effect.gen(function* () { for (const roster of (yield* Ref.get(pendingBackgroundTasksByNativeThread)).values()) { if (roster.size > 0) { return true; } } for (const subagent of (yield* Ref.get(sessionSubagentsByTaskId)).values()) { if (subagent.task.status === "running") { return true; } } const buffers = yield* Ref.get(wakeBuffers); for (const entry of buffers.values()) { if ( entry.messages.some( (message) => message.type === "user" || message.type === "assistant" || message.type === "result", ) ) { return true; } } return false; }), hasPendingBackgroundWorkForThread: Effect.fn( "ClaudeAdapterV2.hasPendingBackgroundWorkForThread", )(function* (providerThread, options) { const nativeThreadId = providerThread.nativeThreadRef?.nativeId; if (nativeThreadId === undefined || nativeThreadId === null) { return false; } if (options?.includeSubagents) { const live = yield* Ref.get(queryContext); if ( live?.nativeThreadId === nativeThreadId && !live.stopping && (yield* liveProcessRunsBackgroundWork(live)) ) return true; } return ( rosterForNativeThread( yield* Ref.get(pendingBackgroundTasksByNativeThread), nativeThreadId, ).size > 0 ); }), ensureThread: Effect.fn("ClaudeAdapterV2.ensureThread")( function* (threadInput: ProviderAdapter.ProviderAdapterV2EnsureThreadInput) { const createdAt = yield* DateTime.now; const nativeThreadId = yield* queryRunner.allocateSessionId; return makeProviderThread({ idAllocator, providerInstanceId: adapterOptions.instanceId, appThreadId: threadInput.threadId, providerSessionId: input.providerSessionId, nativeThreadId, now: createdAt, }); }, (effect, threadInput) => effect.pipe( Effect.mapError( (cause) => new ProviderAdapter.ProviderAdapterEnsureThreadError({ driver: CLAUDE_PROVIDER, threadId: threadInput.threadId, cause, }), ), ), ), resumeThread: Effect.fn("ClaudeAdapterV2.resumeThread")( function* (threadInput: { readonly providerThread: OrchestrationV2ProviderThread }) { const nativeThreadId = threadInput.providerThread.nativeThreadRef?.nativeId; if ( nativeThreadId !== undefined && nativeThreadId !== null && // A live process holds the conversation even before saving it. (yield* Ref.get(queryContext))?.nativeThreadId !== nativeThreadId ) { // An unreadable transcript leaves the resume to the CLI. const transcript = yield* claudeTranscriptState(nativeThreadId).pipe( Effect.orElseSucceed(() => "missing" as const), ); if (transcript === "empty") { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Claude session ${nativeThreadId} has no conversation to resume.`, }); } // A session imported from the terminal has history but no T3 // provider turns; opening it with sessionId fails "already in use". if (transcript === "conversation") { yield* Ref.update(openedNativeThreads, (current) => current.has(nativeThreadId) ? current : new Set(current).add(nativeThreadId), ); } } const updatedAt = yield* DateTime.now; return { ...threadInput.providerThread, providerSessionId: input.providerSessionId, status: "idle" as const, updatedAt, }; }, (effect, threadInput) => effect.pipe( Effect.mapError( (cause) => new ProviderAdapter.ProviderAdapterResumeThreadError({ driver: CLAUDE_PROVIDER, providerSessionId: input.providerSessionId, providerThreadId: threadInput.providerThread.id, cause, }), ), ), ), startTurn, compactThread: (turnInput) => startTurn({ ...turnInput, message: { ...turnInput.message, text: "/compact" }, }), steerTurn, stopTask: Effect.fn("ClaudeAdapterV2.stopTask")(function* ({ providerThread, taskId }) { const liveQuery = yield* Ref.get(queryContext); const task = (yield* Ref.get(sessionSubagentsByTaskId)).get(taskId); if ( liveQuery === null || liveQuery.stopping || liveQuery.nativeThreadId !== providerThread.nativeThreadRef?.nativeId || task === undefined || task.task.threadId !== providerThread.appThreadId || task.task.workflow === undefined || task.task.status !== "running" || liveQuery.subagentsFromEarlierProcesses.has(task) ) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: "No running workflow task belongs to this provider thread", }); } yield* liveQuery.query.stopTask(taskId).pipe( Effect.mapError( (cause) => new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: "Workflow stop request failed", cause, }), ), ); }), awaitBackgroundTasks: Effect.fn("ClaudeAdapterV2.awaitBackgroundTasks")( function* ({ providerThread, taskIds, awaited }) { const nativeThreadId = providerThread.nativeThreadRef?.nativeId; if (nativeThreadId == null) return { matched: [] }; const roster = rosterForNativeThread( yield* Ref.get(pendingBackgroundTasksByNativeThread), nativeThreadId, ); const matched = [...new Set(taskIds)].filter((taskId) => roster.has(taskId)); if (matched.length === 0) return { matched }; if (awaited) { yield* addTaskIdsToNativeThreadSet( awaitedBackgroundTasksByNativeThread, nativeThreadId, matched, ); } else { for (const taskId of matched) { yield* clearTaskIdFromNativeThreadSet( awaitedBackgroundTasksByNativeThread, nativeThreadId, taskId, ); } } const active = yield* Ref.get(activeTurn); yield* emitRosterUpdate( nativeThreadId, active?.nativeThreadId === nativeThreadId ? active : null, ); return { matched }; }, (effect) => sdkMessageLock.withPermits(1)(effect), ), interruptTurn, stopSubagent: Effect.fn("ClaudeAdapterV2.stopSubagent")(function* (input) { const existing = yield* Ref.get(queryContext); const nativeThreadId = input.providerThread.nativeThreadRef?.nativeId; const subagent = (yield* Ref.get(sessionSubagentsByTaskId)).get(input.nativeTaskId); if (subagent?.task.threadId !== input.providerThread.appThreadId) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: "Claude subagent has no known owning task to confirm its outcome.", }); } if ( input.requestedAt !== undefined && subagent.task.startedAt !== null && DateTime.toEpochMillis(subagent.task.startedAt) > DateTime.toEpochMillis(input.requestedAt) ) return; const confirmed = subagent.confirmedOutcome; if (confirmed !== undefined) return confirmed; if ( existing === null || existing.nativeThreadId !== nativeThreadId || existing.subagentsFromEarlierProcesses.has(subagent) ) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: "Claude subagent has no live owning query with task stopping support.", }); } const outcome = subagentStopOutcomes.get(input.nativeTaskId) ?? (yield* Deferred.make()); subagentStopOutcomes.set(input.nativeTaskId, outcome); const recorded = (yield* Ref.get(sessionSubagentsByTaskId)).get(input.nativeTaskId); if ( recorded?.task.startedAt === subagent.task.startedAt && recorded.confirmedOutcome !== undefined ) { if (subagentStopOutcomes.get(input.nativeTaskId) === outcome) subagentStopOutcomes.delete(input.nativeTaskId); return recorded.confirmedOutcome; } const confirmedOutcome = yield* existing.query.stopTask(input.nativeTaskId).pipe( Effect.andThen(Deferred.await(outcome)), Effect.timeout("2 seconds"), Effect.mapError( (cause) => new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: "Claude subagent Stop was not confirmed by a native outcome.", cause, }), ), Effect.ensuring( Effect.sync(() => { if (subagentStopOutcomes.get(input.nativeTaskId) === outcome) subagentStopOutcomes.delete(input.nativeTaskId); }), ), ); const current = (yield* Ref.get(sessionSubagentsByTaskId)).get(input.nativeTaskId); return current?.task.startedAt === subagent.task.startedAt ? confirmedOutcome : undefined; }), respondToRuntimeRequest: Effect.fn("ClaudeAdapterV2.respondToRuntimeRequest")( function* (requestInput) { const pending = (yield* Ref.get(pendingRuntimeRequests)).get( String(requestInput.requestId), ); if (pending === undefined) { return yield* new ProviderAdapter.ProviderAdapterRuntimeRequestResponseError({ driver: CLAUDE_PROVIDER, requestId: requestInput.requestId, cause: new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `No pending Claude runtime request ${requestInput.requestId}.`, }), }); } if (pending.type === "user_input") { yield* Deferred.succeed(pending.answers, requestInput.answers ?? {}); return; } if (requestInput.decision === undefined) { return yield* new ProviderAdapter.ProviderAdapterRuntimeRequestResponseError({ driver: CLAUDE_PROVIDER, requestId: requestInput.requestId, cause: new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Claude ${pending.requestKind} request ${requestInput.requestId} requires an approval decision.`, }), }); } yield* Deferred.succeed(pending.decision, { decision: requestInput.decision, ...(requestInput.feedback === undefined ? {} : { feedback: requestInput.feedback }), }); }, (effect, requestInput) => effect.pipe( Effect.mapError((cause) => isProviderAdapterRuntimeRequestResponseError(cause) ? cause : new ProviderAdapter.ProviderAdapterRuntimeRequestResponseError({ driver: CLAUDE_PROVIDER, requestId: requestInput.requestId, cause, }), ), ), ), readThreadSnapshot: (snapshotInput) => Effect.fail( new ProviderAdapter.ProviderAdapterReadThreadSnapshotError({ driver: CLAUDE_PROVIDER, providerThreadId: snapshotInput.providerThread.id, cause: "Claude V2 adapter does not implement snapshots.", }), ), rollbackThread: Effect.fn("ClaudeAdapterV2.rollbackThread")( function* (rollbackInput) { const currentTurn = yield* Ref.get(activeTurn); if (currentTurn !== null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Cannot roll back Claude provider thread ${rollbackInput.providerThread.id} while provider turn ${currentTurn.providerTurnId} is active.`, }); } const nativeThreadId = yield* getNativeThreadId(rollbackInput.providerThread); yield* closeLiveQueryForNativeThread(nativeThreadId); const now = yield* DateTime.now; const resumeSessionAt = yield* resolveClaudeRollbackResumeSessionAt(rollbackInput); if (rollbackInput.target.type === "thread_start" && resumeSessionAt === null) { const resetNativeThreadId = yield* queryRunner.allocateSessionId; return { providerThread: { ...makeProviderThread({ idAllocator, providerInstanceId: adapterOptions.instanceId, appThreadId: rollbackInput.providerThread.appThreadId, ...(rollbackInput.providerThread.ownerNodeId === null ? {} : { ownerNodeId: rollbackInput.providerThread.ownerNodeId }), providerSessionId: input.providerSessionId, nativeThreadId: resetNativeThreadId, ...(rollbackInput.providerThread.forkedFrom === null ? {} : { forkedFrom: rollbackInput.providerThread.forkedFrom }), now, }), handoffIds: rollbackInput.providerThread.handoffIds, }, providerTurns: [], messages: [], runtimeRequests: [], }; } return { providerThread: { ...rollbackInput.providerThread, providerSessionId: input.providerSessionId, nativeConversationHeadRef: resumeSessionAt === null ? null : { driver: CLAUDE_PROVIDER, nativeId: resumeSessionAt, strength: "weak" as const, }, status: "idle" as const, lastRunOrdinal: rollbackInput.target.appRunOrdinal, updatedAt: now, }, providerTurns: [], messages: [], runtimeRequests: [], }; }, (effect, rollbackInput) => effect.pipe( Effect.mapError((cause) => isProviderAdapterRollbackThreadError(cause) ? cause : new ProviderAdapter.ProviderAdapterRollbackThreadError({ driver: CLAUDE_PROVIDER, providerThreadId: rollbackInput.providerThread.id, cause, }), ), ), ), forkThread: Effect.fn("ClaudeAdapterV2.forkThread")( function* (forkInput) { const currentTurn = yield* Ref.get(activeTurn); if (currentTurn !== null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CLAUDE_PROVIDER, detail: `Cannot fork Claude provider thread ${forkInput.sourceProviderThread.id} while provider turn ${currentTurn.providerTurnId} is active.`, }); } const sourceNativeThreadId = yield* getNativeThreadId(forkInput.sourceProviderThread); yield* closeLiveQueryForNativeThread(sourceNativeThreadId); const upToMessageId = yield* resolveClaudeForkUpToMessageId(forkInput); const forkOptions: ForkSessionOptions = { ...(input.runtimePolicy.cwd === null ? {} : { dir: input.runtimePolicy.cwd }), ...(upToMessageId === undefined ? {} : { upToMessageId }), }; const forked = yield* queryRunner.forkSession({ sessionId: sourceNativeThreadId, options: forkOptions, environment: adapterOptions.environment, threadId: forkInput.targetThreadId, providerSessionId: input.providerSessionId, }); yield* Ref.update(openedNativeThreads, (current) => { const updated = new Set(current); updated.add(forked.sessionId); return updated; }); const now = yield* DateTime.now; // Native forks retain document/image history. Inherit full current // usage even at an earlier fork point; this is conservative. const claudeAttachmentBudget = forkInput.sourceProviderThread.nativeMetadata?.claudeAttachmentBudget; return makeProviderThread({ idAllocator, providerInstanceId: adapterOptions.instanceId, appThreadId: forkInput.targetThreadId, ownerNodeId: forkInput.ownerNodeId ?? null, providerSessionId: input.providerSessionId, nativeThreadId: forked.sessionId, forkedFrom: { providerThreadId: forkInput.sourceProviderThread.id, ...(forkInput.providerTurnId === undefined ? {} : { providerTurnId: forkInput.providerTurnId }), }, ...(forked.headMessageId === undefined && claudeAttachmentBudget === undefined ? {} : { nativeMetadata: { ...(forked.headMessageId === undefined ? {} : { forkHeadMessageId: forked.headMessageId }), ...(claudeAttachmentBudget === undefined ? {} : { claudeAttachmentBudget }), }, }), now, }); }, (effect, forkInput) => effect.pipe( Effect.mapError( (cause) => new ProviderAdapter.ProviderAdapterForkThreadError({ driver: CLAUDE_PROVIDER, providerThreadId: forkInput.sourceProviderThread.id, cause, }), ), ), ), }; return runtime; }, (effect, input) => effect.pipe( Effect.mapError( (cause) => new ProviderAdapter.ProviderAdapterOpenSessionError({ driver: CLAUDE_PROVIDER, providerSessionId: input.providerSessionId, cause, }), ), ), ), }); }); export type ClaudeAdapterV2DriverEnv = | ChildProcessSpawner.ChildProcessSpawner | ClaudeAgentSdkQueryRunner | SecureRandom.SecureRandom | FileSystem.FileSystem | IdAllocator.IdAllocatorV2 | McpProviderSessions.McpProviderSessions | Path.Path | ServerConfig.ServerConfig; export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( function* ( input: ProviderAdapterDriverCreateInput, hooks: Pick = {}, ) { const { instanceId, environment, enabled, config } = input; const fileSystem = yield* FileSystem.FileSystem; const hostEnvironment = yield* HostProcess.Environment; const idAllocator = yield* IdAllocator.IdAllocatorV2; const queryRunner = yield* ClaudeAgentSdkQueryRunner; const serverConfig = yield* ServerConfig.ServerConfig; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; const baseEnvironment = yield* mergeClaudeProcessEnvironment( config, environment, hostEnvironment, ); const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment); const path = yield* Path.Path; const random = yield* SecureRandom.SecureRandom; const binaryPath = yield* resolveClaudeSdkExecutablePath( expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory), claudeEnvironment, ); return yield* makeClaudeAdapterV2({ instanceId, settings: { ...config, enabled, binaryPath }, environment: claudeEnvironment, attachmentsDir: serverConfig.attachmentsDir, fileSystem, path, random, idAllocator, queryRunner, continuationRequests, ...hooks, }); }, (effect, input, _hooks) => effect.pipe( Effect.mapError( (cause) => new ProviderAdapterDriverCreateError({ driver: CLAUDE_PROVIDER, instanceId: input.instanceId, detail: "Failed to create Claude Agent SDK adapter.", cause, }), ), ), );