/** * Claude Code SDK integration. * * Spawns `claude` as a child process with --output-format stream-json * and --input-format stream-json for persistent bidirectional communication. * * Supports multiple concurrent conversations, each with its own Claude process. * Conversations are keyed by conversationId (a UUID generated by the web client). * When no conversationId is provided (backward compat), uses 'default' as the key. * * Lifecycle: * 1. Web user sends first message → startQuery() spawns Claude process * 2. Each user message is enqueued into the inputStream * 3. processOutput() iterates Claude stdout, forwarding messages via sendFn * 4. On 'result' message → turn is complete, process stays alive for next turn * 5. On abort / process exit → cleanup */ import { type ChildProcess } from 'child_process'; import { Stream } from './stream.js'; import { type HistoryMessage } from './history.js'; export { buildControlResponse, handleResultMessage, handleAssistantMessage, isTaskNotification, handleUserMessage, } from './claude-message-helpers.js'; export interface ClaudeMessage { type: string; subtype?: string; [key: string]: unknown; } export interface ChatFile { name: string; mimeType: string; data: string; } export interface ConversationState { child: ChildProcess | null; inputStream: Stream | null; abortController: AbortController | null; claudeSessionId: string | null; workDir: string; turnActive: boolean; turnResultReceived: boolean; conversationId: string; lastClaudeSessionId: string | null; isCompacting: boolean; createdAt: number; planMode: boolean; planModeJustChanged: boolean; permissionMode: 'normal' | 'acceptEdits' | 'auto' | 'default' | 'plan'; prePlanPermissionMode?: 'normal' | 'acceptEdits' | 'auto' | 'default'; providerId: string; actionDraftId: string | null; recapId: string | null; briefingDate: string | null; devopsEntityType: string | null; devopsEntityId: string | null; devopsEntityTitle: string | null; projectName: string | null; icmId: string | null; sessionTitle: string; turnId: string | null; stateVersion: number; heartbeatTimer: ReturnType | null; /** Whether the current turn has sent any visible output to the frontend. */ hasVisibleOutput: boolean; /** Last backend activity used for idle process cleanup once turnActive is false. */ lastActivityAt: number; } export type SendFn = (msg: Record) => void; export declare const IDLE_CONVERSATION_TIMEOUT_MS: number; export declare const STALE_STARTUP_TIMEOUT_MS: number; export declare const MAX_CHAT_CONVERSATIONS = 15; export declare const MAX_LOOP_CONVERSATIONS = 3; /** @deprecated Use MAX_CHAT_CONVERSATIONS instead. Kept for backward compat. */ export declare const MAX_CONVERSATIONS = 15; type OutputObserverFn = (conversationId: string, msg: ClaudeMessage) => boolean | void; export declare function addOutputObserver(fn: OutputObserverFn): void; export declare function removeOutputObserver(fn: OutputObserverFn): void; /** @deprecated Use addOutputObserver() instead. Kept for backward compat (team.ts). */ export declare function setOutputObserver(fn: OutputObserverFn): void; /** @deprecated Use removeOutputObserver() instead. Clears ALL observers. */ export declare function clearOutputObserver(): void; type CloseObserverFn = (conversationId: string, exitCode: number | null, resultReceived: boolean) => void; export declare function addCloseObserver(fn: CloseObserverFn): void; export declare function removeCloseObserver(fn: CloseObserverFn): void; /** @deprecated Use addCloseObserver() instead. Kept for backward compat (team.ts). */ export declare function setCloseObserver(fn: CloseObserverFn): void; /** @deprecated Use removeCloseObserver() instead. Clears ALL close observers. */ export declare function clearCloseObserver(): void; /** * Subscribe a SendFn additively. Returns an unsubscribe function. Use this * when multiple subscribers should each receive every outbound frame (e.g. * the legacy connection.ts path and the ClaudeBackend adapter coexisting). */ export declare function addSendFn(fn: SendFn): () => void; /** * Legacy replace-one semantics: clear all existing subscribers then install * just this one. Returns an unsubscribe function. Existing call sites should * migrate to {@link addSendFn} when they need additive behavior. */ export declare function setSendFn(fn: SendFn): () => void; type SessionStartedFn = (conversationId: string, claudeSessionId: string) => void; /** * Subscribe a callback for every Claude session-started event additively. * Returns an unsubscribe function. */ export declare function addOnSessionStarted(fn: SessionStartedFn): () => void; /** * Legacy replace-one semantics for session-started subscriber. Existing call * sites should migrate to {@link addOnSessionStarted}. */ export declare function setOnSessionStarted(fn: SessionStartedFn): () => void; /** * Set the model override for a conversation. * If the conversation has a claudeSessionId, persists to disk. * Otherwise stores as a pending model for the next spawn. */ export declare function setModel(conversationId: string | undefined, model: string): void; /** * Get the effective model for a conversation (pending or persisted). */ export declare function getEffectiveModel(conversationId: string | undefined): string | null; /** * Get a conversation by its conversationId. * If no conversationId is provided, returns the 'default' conversation (backward compat). */ export declare function getConversation(conversationId?: string): ConversationState | null; /** Get all active conversations. */ export declare function getConversations(): Map; /** * Evict any idle conversation that holds the given claudeSessionId. * Returns true if a busy (turnActive) conversation blocked the eviction. */ export declare function evictByClaudeSessionId(claudeSessionId: string): boolean; /** * Rebind a running conversation to a new conversationId. * Finds the conversation by claudeSessionId and remaps its key in the Map. * Used when the web client reconnects (page refresh) with a new conversationId. */ export declare function rebindConversation(claudeSessionId: string, newConvId: string): boolean; /** Return all pending AskUserQuestion requests for a conversation. */ export declare function getPendingQuestions(conversationId: string): Array<{ requestId: string; questions: unknown[]; }>; /** Return all pending tool permission requests for a conversation (excludes AskUserQuestion). */ export declare function getPendingToolPermissions(conversationId: string): Array<{ requestId: string; toolName: string; displayName: string; input: unknown; decisionReason: string; }>; /** Whether context compaction is currently in progress for a conversation. */ export declare function getIsCompacting(conversationId?: string): boolean; /** * Clear the saved session ID for a conversation. * If no conversationId is provided, clears all (backward compat). */ export declare function clearSessionId(conversationId?: string): void; /** Options for handleChat() */ export interface HandleChatOptions { resumeSessionId?: string; providerId?: string; actionDraftId?: string; recapId?: string; briefingDate?: string; devopsEntityType?: string; devopsEntityId?: string; devopsEntityTitle?: string; projectName?: string; icmId?: string; extraArgs?: string[]; } export declare function buildActionBuilderPrompt(prompt: string, actionDraftId?: string): string; /** * Handle a chat message from the web client. * Lazily starts the Claude process on the first message. * If resumeSessionId is provided, resumes that Claude session. */ export declare function handleChat(conversationId: string | undefined, prompt: string, workDir: string, options?: HandleChatOptions, files?: ChatFile[]): void; /** * Abort a specific conversation's Claude process. * If no conversationId, aborts the 'default' conversation (backward compat). */ export declare function abort(conversationId?: string): void; /** * Abort all conversations (used on agent shutdown). */ export declare function abortAll(): void; /** * Cancel the current execution for a conversation (user pressed stop button). * Kills the process and notifies the web client. */ export declare function cancelExecution(conversationId?: string): void; /** * Handle the user's answer to an AskUserQuestion control request. * Writes a control_response back to Claude's stdin. */ export declare function handleUserAnswer(requestId: string, answers: Record): void; /** * Handle the user's response to a tool permission request. * Writes a control_response back to Claude's stdin with allow/deny behavior. */ export declare function handleToolPermissionResponse(requestId: string, behavior: 'allow' | 'deny'): void; /** * Handle a /btw side question. Spawns a lightweight, ephemeral Claude query * with inline conversation context (stripped-down dialogue) instead of * --resume. Streams btw_answer deltas back to the web client. */ export declare function handleBtwQuestion(question: string, conversationId: string | undefined, workDir: string, send: (msg: Record) => void, fallbackClaudeSessionId?: string): Promise; export interface RestartOptions { /** New plan mode. If undefined, preserves current value. */ planMode?: boolean; /** New provider ID. If undefined, preserves current value. */ providerId?: string; /** New permission mode. If undefined, derives from planMode or preserves current. */ permissionMode?: 'normal' | 'acceptEdits' | 'auto' | 'default' | 'plan'; /** If true, reads JSONL history and includes it in the returned result. */ reloadHistory?: boolean; } export interface RestartResult { /** The claudeSessionId that can be used to resume. */ claudeSessionId: string | null; /** Whether an active turn was interrupted. */ wasTurnActive: boolean; /** Reloaded history messages (only if reloadHistory was true). */ history?: HistoryMessage[]; } /** * Shared primitive for restarting a conversation. * Kills the current Claude process, preserves session ID, recreates state * with updated parameters. Used by plan mode toggle and (future) reload. */ export declare function restartConversation(conversationId: string | undefined, options?: RestartOptions): RestartResult; /** * Create a placeholder conversation state (for toggling plan mode before first message). */ export declare function createPlaceholderConversation(conversationId: string | undefined, options?: { planMode?: boolean; providerId?: string; permissionMode?: 'normal' | 'acceptEdits' | 'auto' | 'default' | 'plan'; }): void;