diff --git a/src/bridge/remoteBridgeCore.ts b/src/bridge/remoteBridgeCore.ts index d1886cc83..884f5eed4 100644 --- a/src/bridge/remoteBridgeCore.ts +++ b/src/bridge/remoteBridgeCore.ts @@ -69,8 +69,19 @@ import type { SDKControlRequest, SDKControlResponse, } from '../entrypoints/sdk/controlTypes.js' +import type { StdoutMessage } from '../entrypoints/sdk/controlTypes.js' +import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js' import type { PermissionMode } from '../utils/permissions/PermissionMode.js' +/** + * StdoutMessage with session_id added. The transport layer adds session_id + * to messages at runtime, but the Zod schemas don't include it. This type + * makes it explicit that we're adding session_id to each message variant. + */ +type StdoutMessageWithSession = StdoutMessage extends infer T + ? T & { session_id: string } + : never + const ANTHROPIC_VERSION = '2023-06-01' // Telemetry discriminator for ws_connected. 'initial' is the default and @@ -608,7 +619,7 @@ export async function initEnvLessBridgeCore( const msgs = flushGate.end() if (msgs.length === 0) return for (const msg of msgs) recentPostedUUIDs.add(msg.uuid) - const events = toSDKMessages(msgs).map(m => ({ + const events: StdoutMessageWithSession[] = toSDKMessages(msgs).map(m => ({ ...m, session_id: sessionId, })) @@ -636,7 +647,7 @@ export async function initEnvLessBridgeCore( `[remote-bridge] Capped initial flush: ${eligible.length} -> ${capped.length} (cap=${initialHistoryCap})`, ) } - const events = toSDKMessages(capped).map(m => ({ + const events: StdoutMessageWithSession[] = toSDKMessages(capped).map(m => ({ ...m, session_id: sessionId, })) @@ -675,8 +686,11 @@ export async function initEnvLessBridgeCore( // explicit sleep. close() sets closed=true which interrupts drain at the // next while-check, so close-before-archive drops the result. transport.reportState('idle') - void transport.write(makeResultMessage(sessionId)) - + const resultMsg: StdoutMessageWithSession = { + ...makeResultMessage(sessionId), + session_id: sessionId, + } + void transport.write(resultMsg) let token = getAccessToken() let status = await archiveSession( sessionId, @@ -795,7 +809,7 @@ export async function initEnvLessBridgeCore( } for (const msg of filtered) recentPostedUUIDs.add(msg.uuid) - const events = toSDKMessages(filtered).map(m => ({ + const events: StdoutMessageWithSession[] = toSDKMessages(filtered).map(m => ({ ...m, session_id: sessionId, })) @@ -818,7 +832,7 @@ export async function initEnvLessBridgeCore( for (const msg of filtered) { if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string) } - const events = filtered.map(m => ({ ...m, session_id: sessionId })) + const events = filtered.map(m => ({ ...m, session_id: sessionId })) as StdoutMessage[] void transport.writeBatch(events) }, sendControlRequest(request: SDKControlRequest) { @@ -828,7 +842,7 @@ export async function initEnvLessBridgeCore( ) return } - const event = { ...request, session_id: sessionId } + const event: StdoutMessageWithSession = { ...request, session_id: sessionId } if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') { transport.reportState('requires_action') } @@ -844,7 +858,7 @@ export async function initEnvLessBridgeCore( ) return } - const event = { ...response, session_id: sessionId } + const event: StdoutMessageWithSession = { ...response, session_id: sessionId } transport.reportState('running') void transport.write(event) logForDebugging('[remote-bridge] Sent control_response') @@ -856,7 +870,7 @@ export async function initEnvLessBridgeCore( ) return } - const event = { + const event: StdoutMessageWithSession = { type: 'control_cancel_request' as const, request_id: requestId, session_id: sessionId, @@ -876,7 +890,11 @@ export async function initEnvLessBridgeCore( return } transport.reportState('idle') - void transport.write(makeResultMessage(sessionId)) + const resultMsg: StdoutMessageWithSession = { + ...makeResultMessage(sessionId), + session_id: sessionId, + } + void transport.write(resultMsg) logForDebugging(`[remote-bridge] Sent result`) }, async teardown() { diff --git a/src/bridge/replBridge.ts b/src/bridge/replBridge.ts index 7406bdde8..afab1be2e 100644 --- a/src/bridge/replBridge.ts +++ b/src/bridge/replBridge.ts @@ -53,6 +53,17 @@ import type { SDKControlRequest, SDKControlResponse, } from '../entrypoints/sdk/controlTypes.js' +import type { StdoutMessage } from '../entrypoints/sdk/controlTypes.js' +import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js' + +/** + * StdoutMessage with session_id added. The transport layer adds session_id + * to messages at runtime, but the Zod schemas don't include it. This type + * makes it explicit that we're adding session_id to each message variant. + */ +type StdoutMessageWithSession = StdoutMessage extends infer T + ? T & { session_id: string } + : never import { createCapacityWake, type CapacitySignal } from './capacityWake.js' import { FlushGate } from './flushGate.js' import { @@ -865,7 +876,7 @@ export async function initBridgeCore( recentPostedUUIDs.add(msg.uuid) } const sdkMessages = toSDKMessages(msgs) - const events = sdkMessages.map(sdkMsg => ({ + const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({ ...sdkMsg, session_id: currentSessionId, })) @@ -1285,7 +1296,7 @@ export async function initBridgeCore( logForDebugging( `[bridge:repl] Flushing ${sdkMessages.length} initial message(s) via transport`, ) - const events = sdkMessages.map(sdkMsg => ({ + const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({ ...sdkMsg, session_id: currentSessionId, })) @@ -1655,7 +1666,11 @@ export async function initBridgeCore( transport = null flushGate.drop() if (teardownTransport) { - void teardownTransport.write(makeResultMessage(currentSessionId)) + const resultMsg: StdoutMessageWithSession = { + ...makeResultMessage(currentSessionId), + session_id: currentSessionId, + } + void teardownTransport.write(resultMsg) } const stopWorkP = currentWorkId @@ -1778,7 +1793,7 @@ export async function initBridgeCore( // Convert to SDK format and send via HTTP POST (HybridTransport). // The web UI receives them via the subscribe WebSocket. const sdkMessages = toSDKMessages(filtered) - const events = sdkMessages.map(sdkMsg => ({ + const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({ ...sdkMsg, session_id: currentSessionId, })) @@ -1803,7 +1818,7 @@ export async function initBridgeCore( for (const msg of filtered) { if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string) } - const events = filtered.map(m => ({ ...m, session_id: currentSessionId })) + const events: StdoutMessageWithSession[] = filtered.map(m => ({ ...m, session_id: currentSessionId })) void transport.writeBatch(events) }, sendControlRequest(request: SDKControlRequest) { @@ -1813,7 +1828,7 @@ export async function initBridgeCore( ) return } - const event = { ...request, session_id: currentSessionId } + const event: StdoutMessageWithSession = { ...request, session_id: currentSessionId } void transport.write(event) logForDebugging( `[bridge:repl] Sent control_request request_id=${request.request_id}`, @@ -1826,7 +1841,7 @@ export async function initBridgeCore( ) return } - const event = { ...response, session_id: currentSessionId } + const event: StdoutMessageWithSession = { ...response, session_id: currentSessionId } void transport.write(event) logForDebugging('[bridge:repl] Sent control_response') }, @@ -1837,7 +1852,7 @@ export async function initBridgeCore( ) return } - const event = { + const event: StdoutMessageWithSession = { type: 'control_cancel_request' as const, request_id: requestId, session_id: currentSessionId, @@ -1854,7 +1869,11 @@ export async function initBridgeCore( ) return } - void transport.write(makeResultMessage(currentSessionId)) + const resultMsg: StdoutMessageWithSession = { + ...makeResultMessage(currentSessionId), + session_id: currentSessionId, + } + void transport.write(resultMsg) logForDebugging( `[bridge:repl] Sent result for session=${currentSessionId}`, ) diff --git a/src/cli/print.ts b/src/cli/print.ts index 644753119..43d255637 100644 --- a/src/cli/print.ts +++ b/src/cli/print.ts @@ -1128,7 +1128,7 @@ function runHeadlessStreaming( rate_limit_info: rateLimitInfo, uuid: randomUUID(), session_id: getSessionId(), - }) + } as unknown as Parameters[0]) } } statusListeners.add(rateLimitListener) @@ -1237,7 +1237,7 @@ function runHeadlessStreaming( uuid: crumb.uuid, timestamp: crumb.timestamp, isReplay: true, - } as SDKUserMessageReplay) + } as SDKUserMessageReplay as StdoutMessage) } } } @@ -1974,7 +1974,7 @@ function runHeadlessStreaming( parent_tool_use_id: null, uuid: c.uuid as string, isReplay: true, - } as SDKUserMessageReplay) + } as SDKUserMessageReplay as StdoutMessage) } } } @@ -2200,7 +2200,7 @@ function runHeadlessStreaming( output.enqueue({ type: 'system', subtype: 'status', - status, + status: status as 'compacting' | null, session_id: getSessionId(), uuid: randomUUID(), }) @@ -2227,10 +2227,10 @@ function runHeadlessStreaming( isBackgroundTask(t), ) ) { - heldBackResult = message + heldBackResult = message as StdoutMessage } else { heldBackResult = null - output.enqueue(message) + output.enqueue(message as StdoutMessage) } } else { // Flush SDK events (task_started, task_progress) so background @@ -2238,7 +2238,7 @@ function runHeadlessStreaming( for (const event of drainSdkEvents()) { output.enqueue(event) } - output.enqueue(message) + output.enqueue(message as StdoutMessage) } } }) // end runWithWorkload @@ -2256,11 +2256,12 @@ function runHeadlessStreaming( { turnStartTime } as import('src/utils/filePersistence/types.js').TurnStartTime, abortController.signal, result => { + const filesResult = result as { persistedFiles: { filename: string; file_id: string }[]; failedFiles: { filename: string; error: string }[] } output.enqueue({ type: 'system' as const, subtype: 'files_persisted' as const, - files: (result as any).persistedFiles, - failed: (result as any).failedFiles, + files: filesResult.persistedFiles, + failed: filesResult.failedFiles, processed_at: new Date().toISOString(), uuid: randomUUID(), session_id: getSessionId(), @@ -2730,7 +2731,7 @@ function runHeadlessStreaming( } const sendControlResponseSuccess = function ( - message: SDKControlRequest, + message: { request_id: string } | SDKControlRequest, response?: Record, ) { output.enqueue({ @@ -2744,7 +2745,7 @@ function runHeadlessStreaming( } const sendControlResponseError = function ( - message: SDKControlRequest, + message: { request_id: string } | SDKControlRequest, errorMessage: string, ) { output.enqueue({ @@ -2820,11 +2821,21 @@ function runHeadlessStreaming( message.type !== 'user' && message.type !== 'control_response' ) { - notifyCommandLifecycle(eventId, 'completed') + notifyCommandLifecycle(eventId as string, 'completed') } if (message.type === 'control_request') { - if (message.request.subtype === 'interrupt') { + // Type assertion: structuredInput yields StdinMessage | SDKMessage, but + // when type === 'control_request' the object has request_id and request. + // The union with SDKMessage (typed as `any`) causes request to be `unknown`. + // Cast to SDKControlRequest (via unknown) for type safety on known subtypes, + // and use Record for subtypes not in the zod schema union. + const msg = message as unknown as SDKControlRequest + // Wider-typed alias for request properties on subtypes not in the zod schema. + // The schema union doesn't include end_session, channel_enable, mcp_authenticate, + // claude_authenticate, etc. so accessing their properties narrows to `never`. + const req = msg.request as Record + if (msg.request.subtype === 'interrupt') { // Track escapes for attribution (ant-only feature) if (feature('COMMIT_ATTRIBUTION')) { setAppState(prev => ({ @@ -2842,10 +2853,10 @@ function runHeadlessStreaming( suggestionState.abortController = null suggestionState.lastEmitted = null suggestionState.pendingSuggestion = null - sendControlResponseSuccess(message) - } else if (message.request.subtype === 'end_session') { + sendControlResponseSuccess(msg) + } else if (req.subtype === 'end_session') { logForDebugging( - `[print.ts] end_session received, reason=${message.request.reason ?? 'unspecified'}`, + `[print.ts] end_session received, reason=${req.reason ?? 'unspecified'}`, ) if (abortController) { abortController.abort() @@ -2854,16 +2865,16 @@ function runHeadlessStreaming( suggestionState.abortController = null suggestionState.lastEmitted = null suggestionState.pendingSuggestion = null - sendControlResponseSuccess(message) + sendControlResponseSuccess(msg) break // exits for-await → falls through to inputClosed=true drain below - } else if (message.request.subtype === 'initialize') { + } else if (msg.request.subtype === 'initialize') { // SDK MCP server names from the initialize message // Populated by both browser and ProcessTransport sessions if ( - message.request.sdkMcpServers && - message.request.sdkMcpServers.length > 0 + msg.request.sdkMcpServers && + msg.request.sdkMcpServers.length > 0 ) { - for (const serverName of message.request.sdkMcpServers) { + for (const serverName of msg.request.sdkMcpServers) { // Create placeholder config for SDK MCP servers // The actual server connection is managed by the SDK Query class sdkMcpConfigs[serverName] = { @@ -2874,8 +2885,8 @@ function runHeadlessStreaming( } await handleInitializeRequest( - message.request, - message.request_id, + msg.request, + msg.request_id, initialized, output, commands, @@ -2890,7 +2901,7 @@ function runHeadlessStreaming( // Enable prompt suggestions in AppState when SDK consumer opts in. // shouldEnablePromptSuggestion() returns false for non-interactive // sessions, but the SDK consumer explicitly requested suggestions. - if (message.request.promptSuggestions) { + if (msg.request.promptSuggestions) { setAppState(prev => { if (prev.promptSuggestionEnabled) return prev return { ...prev, promptSuggestionEnabled: true } @@ -2898,7 +2909,7 @@ function runHeadlessStreaming( } if ( - message.request.agentProgressSummaries && + msg.request.agentProgressSummaries && getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_prism', true) ) { setSdkAgentProgressSummariesEnabled(true) @@ -2911,13 +2922,13 @@ function runHeadlessStreaming( if (hasCommandsInQueue()) { void run() } - } else if (message.request.subtype === 'set_permission_mode') { - const m = message.request // for typescript (TODO: use readonly types to avoid this) + } else if (msg.request.subtype === 'set_permission_mode') { + const m = msg.request // for typescript (TODO: use readonly types to avoid this) setAppState(prev => ({ ...prev, toolPermissionContext: handleSetPermissionMode( m, - message.request_id, + msg.request_id, prev.toolPermissionContext, output, ), @@ -2926,8 +2937,8 @@ function runHeadlessStreaming( // handleSetPermissionMode sends the control_response; the // notifySessionMetadataChanged that used to follow here is // now fired by onChangeAppState (with externalized mode name). - } else if (message.request.subtype === 'set_model') { - const requestedModel = message.request.model ?? 'default' + } else if (msg.request.subtype === 'set_model') { + const requestedModel = msg.request.model ?? 'default' const model = requestedModel === 'default' ? getDefaultMainLoopModel() @@ -2937,24 +2948,24 @@ function runHeadlessStreaming( notifySessionMetadataChanged({ model }) injectModelSwitchBreadcrumbs(requestedModel, model) - sendControlResponseSuccess(message) - } else if (message.request.subtype === 'set_max_thinking_tokens') { - if (message.request.max_thinking_tokens === null) { + sendControlResponseSuccess(msg) + } else if (msg.request.subtype === 'set_max_thinking_tokens') { + if (msg.request.max_thinking_tokens === null) { options.thinkingConfig = undefined - } else if (message.request.max_thinking_tokens === 0) { + } else if (msg.request.max_thinking_tokens === 0) { options.thinkingConfig = { type: 'disabled' } } else { options.thinkingConfig = { type: 'enabled', - budgetTokens: message.request.max_thinking_tokens, + budgetTokens: msg.request.max_thinking_tokens, } } - sendControlResponseSuccess(message) - } else if (message.request.subtype === 'mcp_status') { - sendControlResponseSuccess(message, { + sendControlResponseSuccess(msg) + } else if (msg.request.subtype === 'mcp_status') { + sendControlResponseSuccess(msg, { mcpServers: buildMcpServerStatuses(), }) - } else if (message.request.subtype === 'get_context_usage') { + } else if (msg.request.subtype === 'get_context_usage') { try { const appState = getAppState() const data = await collectContextData({ @@ -2968,13 +2979,13 @@ function runHeadlessStreaming( appendSystemPrompt: options.appendSystemPrompt, }, }) - sendControlResponseSuccess(message, { ...data }) + sendControlResponseSuccess(msg, { ...data }) } catch (error) { - sendControlResponseError(message, errorMessage(error)) + sendControlResponseError(msg, errorMessage(error)) } - } else if (message.request.subtype === 'mcp_message') { + } else if (msg.request.subtype === 'mcp_message') { // Handle MCP notifications from SDK servers - const mcpRequest = message.request + const mcpRequest = msg.request as Record const sdkClient = sdkClients.find( client => client.name === mcpRequest.server_name, ) @@ -2985,32 +2996,32 @@ function runHeadlessStreaming( sdkClient.type === 'connected' && sdkClient.client?.transport?.onmessage ) { - sdkClient.client.transport.onmessage(mcpRequest.message) + sdkClient.client.transport.onmessage(mcpRequest.message as import('@modelcontextprotocol/sdk/types.js').JSONRPCMessage) } - sendControlResponseSuccess(message) - } else if (message.request.subtype === 'rewind_files') { + sendControlResponseSuccess(msg) + } else if (msg.request.subtype === 'rewind_files') { const appState = getAppState() const result = await handleRewindFiles( - message.request.user_message_id as UUID, + msg.request.user_message_id as UUID, appState, setAppState, - message.request.dry_run ?? false, + msg.request.dry_run ?? false, ) - if (result.canRewind || message.request.dry_run) { - sendControlResponseSuccess(message, result) + if (result.canRewind || msg.request.dry_run) { + sendControlResponseSuccess(msg, result) } else { sendControlResponseError( - message, + msg, (result.error as string) ?? 'Unexpected error', ) } - } else if (message.request.subtype === 'cancel_async_message') { - const targetUuid = message.request.message_uuid + } else if (msg.request.subtype === 'cancel_async_message') { + const targetUuid = msg.request.message_uuid const removed = dequeueAllMatching(cmd => cmd.uuid === targetUuid) - sendControlResponseSuccess(message, { + sendControlResponseSuccess(msg, { cancelled: removed.length > 0, }) - } else if (message.request.subtype === 'seed_read_state') { + } else if (msg.request.subtype === 'seed_read_state') { // Client observed a Read that was later removed from context (e.g. // by snip), so transcript-based seeding missed it. Queued into // pendingSeeds; applied at the next clone-replace boundary. @@ -3018,7 +3029,7 @@ function runHeadlessStreaming( // expandPath: all other readFileState writers normalize (~, relative, // session cwd vs process cwd). FileEditTool looks up by expandPath'd // key — a verbatim client path would miss. - const normalizedPath = expandPath(message.request.path) + const normalizedPath = expandPath(msg.request.path) // Check disk mtime before reading content. If the file changed // since the client's observation, readFile would return C_current // but we'd store it with the client's M_observed — getChangedFiles @@ -3028,7 +3039,7 @@ function runHeadlessStreaming( // makes Edit fail "file not read yet" → forces a fresh Read. // Math.floor matches FileReadTool and getFileModificationTime. const diskMtime = Math.floor((await stat(normalizedPath)).mtimeMs) - if (diskMtime <= message.request.mtime) { + if (diskMtime <= msg.request.mtime) { const raw = await readFile(normalizedPath, 'utf-8') // Strip BOM + normalize CRLF→LF to match readFileInRange and // readFileSyncWithMetadata. FileEditTool's content-compare @@ -3047,18 +3058,18 @@ function runHeadlessStreaming( } catch { // ENOENT etc — skip seeding but still succeed } - sendControlResponseSuccess(message) - } else if (message.request.subtype === 'mcp_set_servers') { + sendControlResponseSuccess(msg) + } else if (msg.request.subtype === 'mcp_set_servers') { const { response, sdkServersChanged } = await applyMcpServerChanges( - message.request.servers, + msg.request.servers as Record, ) - sendControlResponseSuccess(message, response) + sendControlResponseSuccess(msg, response) // Connect SDK servers AFTER response to avoid deadlock if (sdkServersChanged) { void updateSdkMcp() } - } else if (message.request.subtype === 'reload_plugins') { + } else if (msg.request.subtype === 'reload_plugins') { try { if ( feature('DOWNLOAD_USER_SETTINGS') && @@ -3106,7 +3117,7 @@ function runHeadlessStreaming( logError(pluginsR.reason) } - sendControlResponseSuccess(message, { + sendControlResponseSuccess(msg, { commands: currentCommands .filter(cmd => cmd.userInvocable !== false) .map(cmd => ({ @@ -3120,15 +3131,15 @@ function runHeadlessStreaming( model: a.model === 'inherit' ? undefined : a.model, })), plugins, - mcpServers: buildMcpServerStatuses(), + mcpServers: buildMcpServerStatuses() as SDKControlReloadPluginsResponse['mcpServers'], error_count: r.error_count, } satisfies SDKControlReloadPluginsResponse) } catch (error) { - sendControlResponseError(message, errorMessage(error)) + sendControlResponseError(msg, errorMessage(error)) } - } else if (message.request.subtype === 'mcp_reconnect') { + } else if (msg.request.subtype === 'mcp_reconnect') { const currentAppState = getAppState() - const { serverName } = message.request + const { serverName } = msg.request elicitationRegistered.delete(serverName) // Config-existence gate must cover the SAME sources as the // operations below. SDK-injected servers (query({mcpServers:{...}})) @@ -3144,7 +3155,7 @@ function runHeadlessStreaming( ?.config ?? null if (!config) { - sendControlResponseError(message, `Server not found: ${serverName}`) + sendControlResponseError(msg, `Server not found: ${serverName}`) } else { const result = await reconnectMcpServerImpl(serverName, config) // Update appState.mcp with the new client, tools, commands, and resources @@ -3190,18 +3201,18 @@ function runHeadlessStreaming( if (result.client.type === 'connected') { registerElicitationHandlers([result.client]) reregisterChannelHandlerAfterReconnect(result.client) - sendControlResponseSuccess(message) + sendControlResponseSuccess(msg) } else { const errorMessage = result.client.type === 'failed' ? (result.client.error ?? 'Connection failed') : `Server status: ${result.client.type}` - sendControlResponseError(message, errorMessage) + sendControlResponseError(msg, errorMessage) } } - } else if (message.request.subtype === 'mcp_toggle') { + } else if (msg.request.subtype === 'mcp_toggle') { const currentAppState = getAppState() - const { serverName, enabled } = message.request + const { serverName, enabled } = msg.request elicitationRegistered.delete(serverName) // Gate must match the client-lookup spread below (which // includes sdkClients and dynamicMcpState.clients). Same fix as @@ -3216,7 +3227,7 @@ function runHeadlessStreaming( null if (!config) { - sendControlResponseError(message, `Server not found: ${serverName}`) + sendControlResponseError(msg, `Server not found: ${serverName}`) } else if (!enabled) { // Disabling: persist + disconnect (matches TUI toggleMcpServer behavior) setMcpServerEnabled(serverName, false) @@ -3247,7 +3258,7 @@ function runHeadlessStreaming( resources: omit(prev.mcp.resources, serverName), }, })) - sendControlResponseSuccess(message) + sendControlResponseSuccess(msg) } else { // Enabling: persist + reconnect setMcpServerEnabled(serverName, true) @@ -3281,20 +3292,20 @@ function runHeadlessStreaming( if (result.client.type === 'connected') { registerElicitationHandlers([result.client]) reregisterChannelHandlerAfterReconnect(result.client) - sendControlResponseSuccess(message) + sendControlResponseSuccess(msg) } else { const errorMessage = result.client.type === 'failed' ? (result.client.error ?? 'Connection failed') : `Server status: ${result.client.type}` - sendControlResponseError(message, errorMessage) + sendControlResponseError(msg, errorMessage) } } - } else if (message.request.subtype === 'channel_enable') { + } else if (req.subtype === 'channel_enable') { const currentAppState = getAppState() handleChannelEnable( - message.request_id, - message.request.serverName, + msg.request_id, + req.serverName as string, // Pool spread matches mcp_status — all three client sources. [ ...currentAppState.mcp.clients, @@ -3303,8 +3314,8 @@ function runHeadlessStreaming( ], output, ) - } else if (message.request.subtype === 'mcp_authenticate') { - const { serverName } = message.request + } else if (req.subtype === 'mcp_authenticate') { + const { serverName } = req const currentAppState = getAppState() const config = getMcpConfigByName(serverName) ?? @@ -3313,10 +3324,10 @@ function runHeadlessStreaming( ?.config ?? null if (!config) { - sendControlResponseError(message, `Server not found: ${serverName}`) + sendControlResponseError(msg, `Server not found: ${serverName}`) } else if (config.type !== 'sse' && config.type !== 'http') { sendControlResponseError( - message, + msg, `Server type "${config.type}" does not support OAuth authentication`, ) } else { @@ -3353,12 +3364,12 @@ function runHeadlessStreaming( ]) if (authUrl) { - sendControlResponseSuccess(message, { + sendControlResponseSuccess(msg, { authUrl, requiresUserAction: true, }) } else { - sendControlResponseSuccess(message, { + sendControlResponseSuccess(msg, { requiresUserAction: false, }) } @@ -3453,11 +3464,11 @@ function runHeadlessStreaming( }) void fullFlowPromise } catch (error) { - sendControlResponseError(message, errorMessage(error)) + sendControlResponseError(msg, errorMessage(error)) } } - } else if (message.request.subtype === 'mcp_oauth_callback_url') { - const { serverName, callbackUrl } = message.request + } else if (req.subtype === 'mcp_oauth_callback_url') { + const { serverName, callbackUrl } = req const submit = oauthCallbackSubmitters.get(serverName) if (submit) { // Validate the callback URL before submitting. The submit @@ -3475,7 +3486,7 @@ function runHeadlessStreaming( } if (!hasCodeOrError) { sendControlResponseError( - message, + msg, 'Invalid callback URL: missing authorization code. Please paste the full redirect URL including the code parameter.', ) } else { @@ -3488,32 +3499,32 @@ function runHeadlessStreaming( if (authPromise) { try { await authPromise - sendControlResponseSuccess(message) + sendControlResponseSuccess(msg) } catch (error) { sendControlResponseError( - message, + msg, error instanceof Error ? error.message : 'OAuth authentication failed', ) } } else { - sendControlResponseSuccess(message) + sendControlResponseSuccess(msg) } } } else { sendControlResponseError( - message, + msg, `No active OAuth flow for server: ${serverName}`, ) } - } else if (message.request.subtype === 'claude_authenticate') { + } else if (req.subtype === 'claude_authenticate') { // Anthropic OAuth over the control channel. The SDK client owns // the user's browser (we're headless in -p mode); we hand back // both URLs and wait. Automatic URL → localhost listener catches // the redirect if the browser is on this host; manual URL → the // success page shows "code#state" for claude_oauth_callback. - const { loginWithClaudeAi } = message.request + const { loginWithClaudeAi } = req // Clean up any prior flow. cleanup() closes the localhost listener // and nulls the manual resolver. The prior `flow` promise is left @@ -3594,30 +3605,30 @@ function runHeadlessStreaming( ) }), ]) - sendControlResponseSuccess(message, { + sendControlResponseSuccess(msg, { manualUrl, automaticUrl, }) } catch (error) { - sendControlResponseError(message, errorMessage(error)) + sendControlResponseError(msg, errorMessage(error)) } } else if ( - message.request.subtype === 'claude_oauth_callback' || - message.request.subtype === 'claude_oauth_wait_for_completion' + req.subtype === 'claude_oauth_callback' || + req.subtype === 'claude_oauth_wait_for_completion' ) { if (!claudeOAuth) { sendControlResponseError( - message, + msg, 'No active claude_authenticate flow', ) } else { // Inject the manual code synchronously — must happen in stdin // message order so a subsequent claude_authenticate doesn't // replace the service before this code lands. - if (message.request.subtype === 'claude_oauth_callback') { + if (req.subtype === 'claude_oauth_callback') { claudeOAuth.service.handleManualAuthCodeInput({ - authorizationCode: message.request.authorizationCode, - state: message.request.state, + authorizationCode: req.authorizationCode as string, + state: req.state as string, }) } // Detach the await — the stdin reader is serial and blocking @@ -3629,7 +3640,7 @@ function runHeadlessStreaming( void flow.then( () => { const accountInfo = getAccountInformation() - sendControlResponseSuccess(message, { + sendControlResponseSuccess(msg, { account: { email: accountInfo?.email, organization: accountInfo?.organization, @@ -3641,11 +3652,11 @@ function runHeadlessStreaming( }) }, (error: unknown) => - sendControlResponseError(message, errorMessage(error)), + sendControlResponseError(msg, errorMessage(error)), ) } - } else if (message.request.subtype === 'mcp_clear_auth') { - const { serverName } = message.request + } else if (req.subtype === 'mcp_clear_auth') { + const { serverName } = req const currentAppState = getAppState() const config = getMcpConfigByName(serverName) ?? @@ -3654,10 +3665,10 @@ function runHeadlessStreaming( ?.config ?? null if (!config) { - sendControlResponseError(message, `Server not found: ${serverName}`) + sendControlResponseError(msg, `Server not found: ${serverName}`) } else if (config.type !== 'sse' && config.type !== 'http') { sendControlResponseError( - message, + msg, `Cannot clear auth for server type "${config.type}"`, ) } else { @@ -3690,16 +3701,16 @@ function runHeadlessStreaming( : omit(prev.mcp.resources, serverName), }, })) - sendControlResponseSuccess(message, {}) + sendControlResponseSuccess(msg, {}) } - } else if (message.request.subtype === 'apply_flag_settings') { + } else if (msg.request.subtype === 'apply_flag_settings') { // Snapshot the current model before applying — we need to detect // model switches so we can inject breadcrumbs and notify listeners. const prevModel = getMainLoopModel() // Merge the provided settings into the in-memory flag settings const existing = getFlagSettingsInline() ?? {} - const incoming = message.request.settings + const incoming = msg.request.settings // Shallow-merge top-level keys; getSettingsForSource handles // the deep merge with file-based flag settings via mergeWith. // JSON serialization drops `undefined`, so callers use `null` @@ -3748,8 +3759,8 @@ function runHeadlessStreaming( injectModelSwitchBreadcrumbs(modelArg, newModel) } - sendControlResponseSuccess(message) - } else if (message.request.subtype === 'get_settings') { + sendControlResponseSuccess(msg) + } else if (msg.request.subtype === 'get_settings') { const currentAppState = getAppState() const model = getMainLoopModel() // modelSupportsEffort gate matches claude.ts — applied.effort must @@ -3757,7 +3768,7 @@ function runHeadlessStreaming( const effort = modelSupportsEffort(model) ? resolveAppliedEffort(model, currentAppState.effortValue) : undefined - sendControlResponseSuccess(message, { + sendControlResponseSuccess(msg, { ...getSettingsWithSources(), applied: { model, @@ -3765,22 +3776,22 @@ function runHeadlessStreaming( effort: typeof effort === 'string' ? effort : null, }, }) - } else if (message.request.subtype === 'stop_task') { - const { task_id: taskId } = message.request + } else if (msg.request.subtype === 'stop_task') { + const { task_id: taskId } = msg.request try { await stopTask(taskId, { getAppState, setAppState, }) - sendControlResponseSuccess(message, {}) + sendControlResponseSuccess(msg, {}) } catch (error) { - sendControlResponseError(message, errorMessage(error)) + sendControlResponseError(msg, errorMessage(error)) } - } else if (message.request.subtype === 'generate_session_title') { + } else if (req.subtype === 'generate_session_title') { // Fire-and-forget so the Haiku call does not block the stdin loop // (which would delay processing of subsequent user messages / // interrupts for the duration of the API roundtrip). - const { description, persist } = message.request + const { description, persist } = req // Reuse the live controller only if it has not already been aborted // (e.g. by interrupt()); an aborted signal would cause queryHaiku to // immediately throw APIUserAbortError → {title: null}. @@ -3799,16 +3810,16 @@ function runHeadlessStreaming( logError(e) } } - sendControlResponseSuccess(message, { title }) + sendControlResponseSuccess(msg, { title }) } catch (e) { // Unreachable in practice — generateSessionTitle wraps its // own body and returns null, saveAiGeneratedTitle is wrapped // above. Propagate (not swallow) so unexpected failures are // visible to the SDK caller (hostComms.ts catches and logs). - sendControlResponseError(message, errorMessage(e)) + sendControlResponseError(msg, errorMessage(e)) } })() - } else if (message.request.subtype === 'side_question') { + } else if (req.subtype === 'side_question') { // Same fire-and-forget pattern as generate_session_title above — // the forked agent's API roundtrip must not block the stdin loop. // @@ -3824,7 +3835,7 @@ function runHeadlessStreaming( // matches in the common case. May still miss the cache for // coordinator mode or memory-mechanics extras — acceptable, the // alternative is the side question failing entirely. - const { question } = message.request + const { question } = req void (async () => { try { const saved = getLastCacheSafeParams() @@ -3863,16 +3874,16 @@ function runHeadlessStreaming( question, cacheSafeParams, }) - sendControlResponseSuccess(message, { response: result.response }) + sendControlResponseSuccess(msg, { response: result.response }) } catch (e) { - sendControlResponseError(message, errorMessage(e)) + sendControlResponseError(msg, errorMessage(e)) } })() } else if ( (feature('PROACTIVE') || feature('KAIROS')) && - (message.request as { subtype: string }).subtype === 'set_proactive' + (msg.request as { subtype: string }).subtype === 'set_proactive' ) { - const req = message.request as unknown as { + const req = msg.request as unknown as { subtype: string enabled: boolean } @@ -3884,12 +3895,12 @@ function runHeadlessStreaming( } else { proactiveModule!.deactivateProactive() } - sendControlResponseSuccess(message) - } else if (message.request.subtype === 'remote_control') { - if (message.request.enabled) { + sendControlResponseSuccess(msg) + } else if (req.subtype === 'remote_control') { + if (req.enabled as boolean) { if (bridgeHandle) { // Already connected - sendControlResponseSuccess(message, { + sendControlResponseSuccess(msg, { session_url: getRemoteSessionUrl( bridgeHandle.bridgeSessionId, bridgeHandle.sessionIngressUrl, @@ -3972,7 +3983,7 @@ function runHeadlessStreaming( }) if (!handle) { sendControlResponseError( - message, + msg, bridgeFailureDetail ?? 'Remote Control initialization failed', ) @@ -3988,7 +3999,7 @@ function runHeadlessStreaming( structuredIO.setOnControlRequestResolved(requestId => { handle.sendControlCancelRequest(requestId) }) - sendControlResponseSuccess(message, { + sendControlResponseSuccess(msg, { session_url: getRemoteSessionUrl( handle.bridgeSessionId, handle.sessionIngressUrl, @@ -4001,7 +4012,7 @@ function runHeadlessStreaming( }) } } catch (err) { - sendControlResponseError(message, errorMessage(err)) + sendControlResponseError(msg, errorMessage(err)) } } } else { @@ -4012,21 +4023,21 @@ function runHeadlessStreaming( await bridgeHandle.teardown() bridgeHandle = null } - sendControlResponseSuccess(message) + sendControlResponseSuccess(msg) } } else { // Unknown control request subtype — send an error response so // the caller doesn't hang waiting for a reply that never comes. sendControlResponseError( - message, - `Unsupported control request subtype: ${(message.request as { subtype: string }).subtype}`, + msg, + `Unsupported control request subtype: ${(msg.request as { subtype: string }).subtype}`, ) } continue } else if (message.type === 'control_response') { // Replay control_response messages when replay mode is enabled if (options.replayUserMessages) { - output.enqueue(message) + output.enqueue(message as StdoutMessage) } continue } else if (message.type === 'keep_alive') { @@ -4038,11 +4049,11 @@ function runHeadlessStreaming( } else if (message.type === 'assistant' || message.type === 'system') { // History replay from bridge: inject into mutableMessages as // conversation context so the model sees prior turns. - const internalMsgs = toInternalMessages([message]) + const internalMsgs = toInternalMessages([message as SDKMessage]) mutableMessages.push(...internalMsgs) // Echo assistant messages back so CCR displays them if (message.type === 'assistant' && options.replayUserMessages) { - output.enqueue(message) + output.enqueue(message as StdoutMessage) } continue } @@ -4051,58 +4062,61 @@ function runHeadlessStreaming( if (message.type !== 'user') { continue } + // Type assertion: after the type guard, message is a user message. + // The union with SDKMessage (any) prevents proper narrowing. + const userMsg = message as SDKUserMessage // First prompt message implicitly initializes if not already done. initialized = true // Check for duplicate user message - skip if already processed - if (message.uuid) { + if (userMsg.uuid) { const sessionId = getSessionId() as UUID const existsInSession = await doesMessageExistInSession( sessionId, - message.uuid, + userMsg.uuid as UUID, ) // Check both historical duplicates (from file) and runtime duplicates (this session) - if (existsInSession || receivedMessageUuids.has(message.uuid)) { - logForDebugging(`Skipping duplicate user message: ${message.uuid}`) + if (existsInSession || receivedMessageUuids.has(userMsg.uuid as UUID)) { + logForDebugging(`Skipping duplicate user message: ${userMsg.uuid}`) // Send acknowledgment for duplicate message if replay mode is enabled if (options.replayUserMessages) { logForDebugging( - `Sending acknowledgment for duplicate user message: ${message.uuid}`, + `Sending acknowledgment for duplicate user message: ${userMsg.uuid}`, ) output.enqueue({ type: 'user', - content: message.message?.content ?? '', - message: message.message, + content: (userMsg.message as { content?: string })?.content ?? '', + message: userMsg.message as { role: string; content: unknown }, session_id: sessionId, parent_tool_use_id: null, - uuid: message.uuid, - timestamp: message.timestamp, + uuid: userMsg.uuid as string, + timestamp: (userMsg as { timestamp?: string }).timestamp, isReplay: true, - } as unknown as SDKUserMessageReplay) + } as unknown as SDKUserMessageReplay as StdoutMessage) } // Historical dup = transcript already has this turn's output, so it // ran but its lifecycle was never closed (interrupted before ack). // Runtime dups don't need this — the original enqueue path closes them. if (existsInSession) { - notifyCommandLifecycle(message.uuid, 'completed') + notifyCommandLifecycle(userMsg.uuid as string, 'completed') } // Don't enqueue duplicate messages for execution continue } // Track this UUID to prevent runtime duplicates - trackReceivedMessageUuid(message.uuid) + trackReceivedMessageUuid(userMsg.uuid as UUID) } enqueue({ mode: 'prompt' as const, // file_attachments rides the protobuf catchall from the web composer. // Same-ref no-op when absent (no 'file_attachments' key). - value: await resolveAndPrepend(message, message.message.content), - uuid: message.uuid, - priority: message.priority, + value: await resolveAndPrepend(userMsg, (userMsg.message as { content: ContentBlockParam[] }).content), + uuid: userMsg.uuid as `${string}-${string}-${string}-${string}-${string}`, + priority: (userMsg as { priority?: string }).priority as import('src/types/textInputTypes.js').QueuePriority, }) // Increment prompt count for attribution tracking and save snapshot // The snapshot persists promptCount so it survives compaction @@ -4463,7 +4477,7 @@ async function handleInitializeRequest( })), output_style: outputStyle, available_output_styles: Object.keys(availableOutputStyles), - models: modelInfos, + models: modelInfos as unknown as SDKControlInitializeResponse['models'], account: { email: accountInfo?.email, organization: accountInfo?.organization, @@ -4473,7 +4487,7 @@ async function handleInitializeRequest( // getAccountInformation() returns undefined under 3P providers, so the // other fields are all absent. apiProvider disambiguates "not logged // in" (firstParty + tokenSource:none) from "3P, login not applicable". - apiProvider: getAPIProvider(), + apiProvider: getAPIProvider() as 'firstParty' | 'bedrock' | 'vertex' | 'foundry', }, pid: process.pid, } diff --git a/src/cli/structuredIO.ts b/src/cli/structuredIO.ts index d5cedfa2d..3f8d426e2 100644 --- a/src/cli/structuredIO.ts +++ b/src/cli/structuredIO.ts @@ -355,7 +355,7 @@ export class StructuredIO { // Used by bridge session runner for auth token refresh // (CLAUDE_CODE_SESSION_ACCESS_TOKEN) which must be readable // by the REPL process itself, not just child Bash commands. - const variables = message.variables as Record + const variables = message.variables ?? {} const keys = Object.keys(variables) for (const [key, value] of Object.entries(variables)) { process.env[key] = value @@ -377,7 +377,8 @@ export class StructuredIO { if (uuid) { notifyCommandLifecycle(uuid, 'completed') } - const request = this.pendingRequests.get(message.response.request_id) + const resp = message.response as { request_id: string; subtype: string; response?: Record; error?: string } + const request = this.pendingRequests.get(resp.request_id) if (!request) { // Check if this tool_use was already resolved through the normal // permission flow. Duplicate control_response deliveries (e.g. from @@ -385,8 +386,8 @@ export class StructuredIO { // re-processing them would push duplicate assistant messages into // the conversation, causing API 400 errors. const responsePayload = - message.response.subtype === 'success' - ? message.response.response + resp.subtype === 'success' + ? resp.response : undefined const toolUseID = responsePayload?.toolUseID if ( @@ -394,31 +395,31 @@ export class StructuredIO { this.resolvedToolUseIds.has(toolUseID) ) { logForDebugging( - `Ignoring duplicate control_response for already-resolved toolUseID=${toolUseID} request_id=${message.response.request_id}`, + `Ignoring duplicate control_response for already-resolved toolUseID=${toolUseID} request_id=${resp.request_id}`, ) return undefined } if (this.unexpectedResponseCallback) { - await this.unexpectedResponseCallback(message) + await this.unexpectedResponseCallback(message as SDKControlResponse & { uuid?: string }) } return undefined // Ignore responses for requests we don't know about } this.trackResolvedToolUseId(request.request) - this.pendingRequests.delete(message.response.request_id) + this.pendingRequests.delete(resp.request_id) // Notify the bridge when the SDK consumer resolves a can_use_tool // request, so it can cancel the stale permission prompt on claude.ai. if ( (request.request.request as { subtype?: string }).subtype === 'can_use_tool' && this.onControlRequestResolved ) { - this.onControlRequestResolved(message.response.request_id) + this.onControlRequestResolved(resp.request_id) } - if (message.response.subtype === 'error') { - request.reject(new Error(message.response.error)) + if (resp.subtype === 'error') { + request.reject(new Error(resp.error ?? 'Unknown error')) return undefined } - const result = message.response.response + const result = resp.response if (request.schema) { try { request.resolve(request.schema.parse(result)) @@ -454,9 +455,9 @@ export class StructuredIO { if (message.type === 'assistant' || message.type === 'system') { return message } - if (message.message.role !== 'user') { + if ((message as { message?: { role?: string } }).message?.role !== 'user') { exitWithMessage( - `Error: Expected message role 'user', got '${message.message.role}'`, + `Error: Expected message role 'user', got '${(message as { message?: { role?: string } }).message?.role}'`, ) } return message @@ -678,7 +679,7 @@ export class StructuredIO { { subtype: 'hook_callback', callback_id: callbackId, - input, + input: input as Parameters[0], tool_use_id: toolUseID || undefined, }, hookJSONOutputSchema(), diff --git a/src/commands/remoteControlServer/remoteControlServer.tsx b/src/commands/remoteControlServer/remoteControlServer.tsx index 0d350a7a0..dc08a9cbc 100644 --- a/src/commands/remoteControlServer/remoteControlServer.tsx +++ b/src/commands/remoteControlServer/remoteControlServer.tsx @@ -148,7 +148,7 @@ function ServerManagementDialog({ onDone }: Props): React.ReactNode { Remote Control Server is{' '} - + running {daemonProcess ? ` (PID: ${daemonProcess.pid})` : ''} @@ -233,10 +233,10 @@ function startDaemon(): void { } }); - child.on('exit', (code, signal) => { + child.on('exit', (code: number | null, signal: NodeJS.Signals | null) => { daemonProcess = null; daemonStatus = 'stopped'; - daemonLogs.push(`[daemon] exited (code=${code}, signal=${signal})`); + daemonLogs.push(`[daemon] exited (code=${code ?? 'unknown'}, signal=${signal})`); }); child.on('error', (err: Error) => { diff --git a/src/components/BuiltinStatusLine.tsx b/src/components/BuiltinStatusLine.tsx index fdbf2969d..45fe3805b 100644 --- a/src/components/BuiltinStatusLine.tsx +++ b/src/components/BuiltinStatusLine.tsx @@ -55,7 +55,7 @@ function BuiltinStatusLineInner({ // Force re-render every 60s so countdowns stay current const [tick, setTick] = useState(0); useEffect(() => { - const hasResetTime = rateLimits.five_hour?.resets_at || rateLimits.seven_day?.resets_at; + const hasResetTime = (rateLimits.five_hour?.resets_at ?? 0) || (rateLimits.seven_day?.resets_at ?? 0); if (!hasResetTime) return; const id = setInterval(() => setTick(t => t + 1), 60_000); return () => clearInterval(id); diff --git a/src/components/CompactSummary.tsx b/src/components/CompactSummary.tsx index e343a6591..5d582a299 100644 --- a/src/components/CompactSummary.tsx +++ b/src/components/CompactSummary.tsx @@ -15,7 +15,11 @@ type Props = { export function CompactSummary({ message, screen }: Props): React.ReactNode { const isTranscriptMode = screen === 'transcript' const textContent = getUserMessageText(message) || '' - const metadata = message.summarizeMetadata + const metadata = message.summarizeMetadata as { + messagesSummarized?: number + direction?: string + userContext?: string + } | undefined // "Summarize from here" with metadata if (metadata) { diff --git a/src/components/PromptInput/PromptInputFooterSuggestions.tsx b/src/components/PromptInput/PromptInputFooterSuggestions.tsx index 689123581..60dc39d57 100644 --- a/src/components/PromptInput/PromptInputFooterSuggestions.tsx +++ b/src/components/PromptInput/PromptInputFooterSuggestions.tsx @@ -163,7 +163,7 @@ const SuggestionItemRow = memo(function SuggestionItemRow({ {paddedDisplayText} {tagText ? ( - + {tagText} ) : null} diff --git a/src/components/skills/SkillsMenu.tsx b/src/components/skills/SkillsMenu.tsx index 4b33eee57..37a196d45 100644 --- a/src/components/skills/SkillsMenu.tsx +++ b/src/components/skills/SkillsMenu.tsx @@ -162,13 +162,13 @@ export function SkillsMenu({ onExit, commands }: Props): React.ReactNode { skill.source === 'plugin' ? skill.pluginInfo?.pluginManifest.name : undefined - const scopeTag = getScopeTag(skill.source as SkillSource) + const scopeTag = getScopeTag(skill.source) return ( {getCommandName(skill)} {scopeTag && ( - [{scopeTag.label}] + [{scopeTag.label}] )} {pluginName ? ` · ${pluginName}` : ''} · {tokenDisplay} description diff --git a/src/components/tasks/RemoteSessionDetailDialog.tsx b/src/components/tasks/RemoteSessionDetailDialog.tsx index ec2c43b42..1e2e2e46c 100644 --- a/src/components/tasks/RemoteSessionDetailDialog.tsx +++ b/src/components/tasks/RemoteSessionDetailDialog.tsx @@ -122,7 +122,8 @@ function UltraplanSessionDetail({ let lastBlock: { name: string; input: unknown } | null = null for (const msg of session.log) { if (msg.type !== 'assistant') continue - for (const block of msg.message.content) { + const content = msg.message?.content ?? [] + for (const block of content as Array<{type: string; name: string; input: unknown}>) { if (block.type !== 'tool_use') continue calls++ lastBlock = block diff --git a/src/hooks/useReplBridge.tsx b/src/hooks/useReplBridge.tsx index 7be394869..c19304ffe 100644 --- a/src/hooks/useReplBridge.tsx +++ b/src/hooks/useReplBridge.tsx @@ -225,13 +225,16 @@ export function useReplBridge( const { resolveAndPrepend } = await import( '../bridge/inboundAttachments.js' ) - let sanitized = fields.content + const rawContent = fields.content + let sanitized: string | Array<{ type: string; [key: string]: unknown }> = typeof rawContent === 'string' ? rawContent : rawContent as Array<{ type: string; [key: string]: unknown }> if (feature('KAIROS_GITHUB_WEBHOOKS')) { /* eslint-disable @typescript-eslint/no-require-imports */ const { sanitizeInboundWebhookContent } = require('../bridge/webhookSanitizer.js') as typeof import('../bridge/webhookSanitizer.js') /* eslint-enable @typescript-eslint/no-require-imports */ - sanitized = sanitizeInboundWebhookContent(fields.content) + if (typeof sanitized === 'string') { + sanitized = sanitizeInboundWebhookContent(sanitized) + } } const content = await resolveAndPrepend(msg, sanitized) diff --git a/src/services/api/gemini/index.ts b/src/services/api/gemini/index.ts index 64dff7458..6935ac1bc 100644 --- a/src/services/api/gemini/index.ts +++ b/src/services/api/gemini/index.ts @@ -56,7 +56,7 @@ export async function* queryModelGemini( const standardTools = toolSchemas.filter( (t): t is BetaToolUnion & { type: string } => { - const anyTool = t as Record + const anyTool = t as unknown as Record return ( anyTool.type !== 'advisor_20260301' && anyTool.type !== 'computer_20250124' @@ -186,7 +186,7 @@ export async function* queryModelGemini( yield createAssistantAPIErrorMessage({ content: `API Error: ${errorMessage}`, apiError: 'api_error', - error: error instanceof Error ? error : new Error(String(error)), + error: (error instanceof Error ? error : new Error(String(error))) as Error, }) } } diff --git a/src/services/api/grok/index.ts b/src/services/api/grok/index.ts index d8b429ded..8e4f934b4 100644 --- a/src/services/api/grok/index.ts +++ b/src/services/api/grok/index.ts @@ -2,6 +2,10 @@ import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/me import type { SystemPrompt } from '../../../utils/systemPromptType.js' import type { Message, StreamEvent, SystemAPIErrorMessage, AssistantMessage } from '../../../types/message.js' import type { Tools } from '../../../Tool.js' +import type { + ChatCompletionChunk, + ChatCompletionCreateParamsStreaming, +} from 'openai/resources/chat/completions/completions.mjs' import { getGrokClient } from './client.js' import { anthropicMessagesToOpenAI } from '../openai/convertMessages.js' import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../openai/convertTools.js' @@ -51,7 +55,7 @@ export async function* queryModelGrok( ) const standardTools = toolSchemas.filter( (t): t is BetaToolUnion & { type: string } => { - const anyT = t as Record + const anyT = t as unknown as Record return anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124' }, ) @@ -62,7 +66,7 @@ export async function* queryModelGrok( const client = getGrokClient({ maxRetries: 0, - fetchOverride: options.fetchOverride, + fetchOverride: options.fetchOverride as typeof fetch | undefined, source: options.querySource, }) @@ -81,13 +85,13 @@ export async function* queryModelGrok( ...(options.temperatureOverride !== undefined && { temperature: options.temperatureOverride, }), - }, + } as ChatCompletionCreateParamsStreaming, { signal, }, ) - const adaptedStream = adaptOpenAIStreamToAnthropic(stream, grokModel) + const adaptedStream = adaptOpenAIStreamToAnthropic(stream as AsyncIterable, grokModel) const contentBlocks: Record = {} let partialMessage: any = undefined @@ -186,7 +190,7 @@ export async function* queryModelGrok( yield createAssistantAPIErrorMessage({ content: `API Error: ${errorMessage}`, apiError: 'api_error', - error: error instanceof Error ? error : new Error(String(error)), + error: (error instanceof Error ? error : new Error(String(error))) as Error, }) } } diff --git a/src/services/api/openai/client.ts b/src/services/api/openai/client.ts index 111e8a330..eea96cb8e 100644 --- a/src/services/api/openai/client.ts +++ b/src/services/api/openai/client.ts @@ -1,6 +1,6 @@ import OpenAI from 'openai' import { getProxyFetchOptions } from 'src/utils/proxy.js' -import { isEnvTruthy } from '../../utils/envUtils.js' +import { isEnvTruthy } from 'src/utils/envUtils.js' /** * Environment variables: diff --git a/src/services/api/openai/index.ts b/src/services/api/openai/index.ts index 958f0b3d4..0f350ad3e 100644 --- a/src/services/api/openai/index.ts +++ b/src/services/api/openai/index.ts @@ -7,6 +7,11 @@ import type { AssistantMessage, } from '../../../types/message.js' import type { Tools } from '../../../Tool.js' +import type { Stream } from 'openai/streaming.mjs' +import type { + ChatCompletionChunk, + ChatCompletionCreateParamsStreaming, +} from 'openai/resources/chat/completions/completions.mjs' import { getOpenAIClient } from './client.js' import { anthropicMessagesToOpenAI } from './convertMessages.js' import { @@ -82,7 +87,7 @@ export function buildOpenAIRequestBody(params: { toolChoice: any enableThinking: boolean temperatureOverride?: number -}): Record { +}): ChatCompletionCreateParamsStreaming { const { model, messages, tools, toolChoice, enableThinking, temperatureOverride } = params return { model, @@ -183,7 +188,7 @@ export async function* queryModelOpenAI( // 7. Filter out non-standard tools (server tools like advisor) const standardTools = toolSchemas.filter( (t): t is BetaToolUnion & { type: string } => { - const anyT = t as Record + const anyT = t as unknown as Record return ( anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124' ) @@ -349,7 +354,7 @@ export async function* queryModelOpenAI( yield createAssistantAPIErrorMessage({ content: `API Error: ${errorMessage}`, apiError: 'api_error', - error: error instanceof Error ? error : new Error(String(error)), + error: (error instanceof Error ? error : new Error(String(error))) as Error, }) } } \ No newline at end of file diff --git a/src/tools/AgentTool/UI.tsx b/src/tools/AgentTool/UI.tsx index b6bc20da4..f1e762f57 100644 --- a/src/tools/AgentTool/UI.tsx +++ b/src/tools/AgentTool/UI.tsx @@ -125,7 +125,7 @@ function processProgressMessages( isAgentRunning: boolean, ): ProcessedMessage[] { // Only process for ants - if ("external" !== 'ant') { + if (process.env.USER_TYPE !== 'ant') { return messages .filter( (m): m is ProgressMessage => @@ -411,7 +411,7 @@ export function renderToolResultMessage( const finalAssistantMessage = createAssistantMessage({ content: completionMessage, - usage: { ...usage, inference_geo: null, iterations: null, speed: null }, + usage: { ...usage, inference_geo: null, iterations: null, speed: null } as typeof usage, }) return ( @@ -866,7 +866,7 @@ export function renderGroupedAgentToolUse( taskDescription = parsedInput.data.description // Use the custom agent definition's color on the type, not the name descriptionColor = isCustomSubagentType(subagentType) - ? (getAgentColor(subagentType) as keyof Theme | undefined) + ? getAgentColor(subagentType) : undefined } else { agentType = parsedInput.success @@ -1019,7 +1019,7 @@ export function userFacingNameBackgroundColor( } // Get the color for this agent - return getAgentColor(input.subagent_type) as keyof Theme | undefined + return getAgentColor(input.subagent_type) } export function extractLastToolInfo( diff --git a/src/utils/computerUse/executor.ts b/src/utils/computerUse/executor.ts index 346ac7d50..1c5ba247d 100644 --- a/src/utils/computerUse/executor.ts +++ b/src/utils/computerUse/executor.ts @@ -423,18 +423,21 @@ export function createCliExecutor(opts: { targetW, targetH, opts.preferredDisplayId, - opts.autoResolve, - opts.doHide, ), ) // Ensure the result has fields expected by toolCalls.ts (hidden, displayId). // macOS native returns these from Swift; our cross-platform ComputerUseAPI // returns {base64, width, height} — fill in the missing fields. + const baseResult = raw as Partial & { width?: number; height?: number } return { ...raw, - hidden: (raw as any).hidden ?? [], - displayId: (raw as any).displayId ?? opts.preferredDisplayId ?? d.displayId, - } + displayWidth: baseResult.displayWidth ?? baseResult.width, + displayHeight: baseResult.displayHeight ?? baseResult.height, + originX: baseResult.originX ?? 0, + originY: baseResult.originY ?? 0, + hidden: baseResult.hidden ?? [], + displayId: baseResult.displayId ?? opts.preferredDisplayId ?? d.displayId, + } as ResolvePrepareCaptureResult }, /** diff --git a/src/utils/computerUse/win32/bridgeClient.ts b/src/utils/computerUse/win32/bridgeClient.ts index b3f8a3749..70e747f5e 100644 --- a/src/utils/computerUse/win32/bridgeClient.ts +++ b/src/utils/computerUse/win32/bridgeClient.ts @@ -9,6 +9,7 @@ */ import * as path from 'path' +import type { Writable } from 'stream' interface BridgeRequest { id: number @@ -48,7 +49,7 @@ export function ensureBridge(): boolean { }) // Read stdout lines asynchronously - const reader = bridgeProc.stdout.getReader() + const reader = (bridgeProc.stdout as ReadableStream).getReader() const readLoop = async () => { try { while (true) { @@ -114,12 +115,12 @@ export async function call( }, timeoutMs) // Clear timeout on resolve/reject - const origResolve = resolve + const origResolve = resolve as (v: unknown) => void const origReject = reject pendingRequests.set(id, { resolve: v => { clearTimeout(timer) - ;(origResolve as any)(v) + origResolve(v) }, reject: e => { clearTimeout(timer) @@ -128,8 +129,14 @@ export async function call( }) try { - bridgeProc!.stdin.write(JSON.stringify(req) + '\n') - bridgeProc!.stdin.flush() + const stdin = bridgeProc!.stdin + if (stdin) { + const writable = stdin as Writable + writable.write(JSON.stringify(req) + '\n') + if (typeof writable.flush === 'function') { + writable.flush() + } + } } catch (err) { clearTimeout(timer) pendingRequests.delete(id) @@ -176,7 +183,13 @@ export function callSync( export function stopBridge(): void { if (bridgeProc) { try { - bridgeProc.stdin.end() + const stdin = bridgeProc.stdin + if (stdin) { + const writable = stdin as Writable + if (typeof writable.end === 'function') { + writable.end() + } + } bridgeProc.kill() } catch {} bridgeProc = null diff --git a/src/utils/streamlinedTransform.ts b/src/utils/streamlinedTransform.ts index 8b7aa4430..36245c389 100644 --- a/src/utils/streamlinedTransform.ts +++ b/src/utils/streamlinedTransform.ts @@ -137,13 +137,14 @@ export function createStreamlinedTransformer(): ( ): StdoutMessage | null { switch (message.type) { case 'assistant': { - const content = message.message.content + const messageContent = (message as SDKAssistantMessage).message + const content = messageContent?.content const text = Array.isArray(content) ? extractTextContent(content, '\n').trim() : '' // Accumulate tool counts from this message - accumulateToolUses(message, cumulativeCounts) + accumulateToolUses(message as SDKAssistantMessage, cumulativeCounts) if (text.length > 0) { // Text message: emit text only, reset counts