fix(types): replace all as any with proper type assertions

Eliminate unsafe `as any` casts across 21 non-test source files,
replacing them with specific type annotations:

- Bridge transport: use StdoutMessage type for write/writeBatch calls
- print.ts: type msg.request as Record<string, unknown> for unknown
  SDK control subtypes; use StdoutMessage for output.enqueue()
- API providers (openai/grok/gemini): import ChatCompletion types,
  type streams as AsyncIterable<ChatCompletionChunk>, type request
  bodies as ChatCompletionCreateParamsStreaming
- Computer use executor: use Partial<ResolvePrepareCaptureResult>
  for cross-platform screenshot result
- Components: replace Ink color string casts with proper typing
- Win32 bridge: type stdin as Writable after null check

All 2453 tests pass with 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-09 23:51:33 +08:00
parent a14d3dc8f0
commit 34bbc1d403
19 changed files with 317 additions and 231 deletions

View File

@@ -69,8 +69,19 @@ import type {
SDKControlRequest, SDKControlRequest,
SDKControlResponse, SDKControlResponse,
} from '../entrypoints/sdk/controlTypes.js' } 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' 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' const ANTHROPIC_VERSION = '2023-06-01'
// Telemetry discriminator for ws_connected. 'initial' is the default and // Telemetry discriminator for ws_connected. 'initial' is the default and
@@ -608,7 +619,7 @@ export async function initEnvLessBridgeCore(
const msgs = flushGate.end() const msgs = flushGate.end()
if (msgs.length === 0) return if (msgs.length === 0) return
for (const msg of msgs) recentPostedUUIDs.add(msg.uuid) for (const msg of msgs) recentPostedUUIDs.add(msg.uuid)
const events = toSDKMessages(msgs).map(m => ({ const events: StdoutMessageWithSession[] = toSDKMessages(msgs).map(m => ({
...m, ...m,
session_id: sessionId, session_id: sessionId,
})) }))
@@ -636,7 +647,7 @@ export async function initEnvLessBridgeCore(
`[remote-bridge] Capped initial flush: ${eligible.length} -> ${capped.length} (cap=${initialHistoryCap})`, `[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, ...m,
session_id: sessionId, session_id: sessionId,
})) }))
@@ -675,8 +686,11 @@ export async function initEnvLessBridgeCore(
// explicit sleep. close() sets closed=true which interrupts drain at the // explicit sleep. close() sets closed=true which interrupts drain at the
// next while-check, so close-before-archive drops the result. // next while-check, so close-before-archive drops the result.
transport.reportState('idle') transport.reportState('idle')
void transport.write(makeResultMessage(sessionId)) const resultMsg: StdoutMessageWithSession = {
...makeResultMessage(sessionId),
session_id: sessionId,
}
void transport.write(resultMsg)
let token = getAccessToken() let token = getAccessToken()
let status = await archiveSession( let status = await archiveSession(
sessionId, sessionId,
@@ -795,7 +809,7 @@ export async function initEnvLessBridgeCore(
} }
for (const msg of filtered) recentPostedUUIDs.add(msg.uuid) for (const msg of filtered) recentPostedUUIDs.add(msg.uuid)
const events = toSDKMessages(filtered).map(m => ({ const events: StdoutMessageWithSession[] = toSDKMessages(filtered).map(m => ({
...m, ...m,
session_id: sessionId, session_id: sessionId,
})) }))
@@ -818,7 +832,7 @@ export async function initEnvLessBridgeCore(
for (const msg of filtered) { for (const msg of filtered) {
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string) 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) void transport.writeBatch(events)
}, },
sendControlRequest(request: SDKControlRequest) { sendControlRequest(request: SDKControlRequest) {
@@ -828,7 +842,7 @@ export async function initEnvLessBridgeCore(
) )
return 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') { if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') {
transport.reportState('requires_action') transport.reportState('requires_action')
} }
@@ -844,7 +858,7 @@ export async function initEnvLessBridgeCore(
) )
return return
} }
const event = { ...response, session_id: sessionId } const event: StdoutMessageWithSession = { ...response, session_id: sessionId }
transport.reportState('running') transport.reportState('running')
void transport.write(event) void transport.write(event)
logForDebugging('[remote-bridge] Sent control_response') logForDebugging('[remote-bridge] Sent control_response')
@@ -856,7 +870,7 @@ export async function initEnvLessBridgeCore(
) )
return return
} }
const event = { const event: StdoutMessageWithSession = {
type: 'control_cancel_request' as const, type: 'control_cancel_request' as const,
request_id: requestId, request_id: requestId,
session_id: sessionId, session_id: sessionId,
@@ -876,7 +890,11 @@ export async function initEnvLessBridgeCore(
return return
} }
transport.reportState('idle') 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`) logForDebugging(`[remote-bridge] Sent result`)
}, },
async teardown() { async teardown() {

View File

@@ -53,6 +53,17 @@ import type {
SDKControlRequest, SDKControlRequest,
SDKControlResponse, SDKControlResponse,
} from '../entrypoints/sdk/controlTypes.js' } 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 { createCapacityWake, type CapacitySignal } from './capacityWake.js'
import { FlushGate } from './flushGate.js' import { FlushGate } from './flushGate.js'
import { import {
@@ -865,7 +876,7 @@ export async function initBridgeCore(
recentPostedUUIDs.add(msg.uuid) recentPostedUUIDs.add(msg.uuid)
} }
const sdkMessages = toSDKMessages(msgs) const sdkMessages = toSDKMessages(msgs)
const events = sdkMessages.map(sdkMsg => ({ const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({
...sdkMsg, ...sdkMsg,
session_id: currentSessionId, session_id: currentSessionId,
})) }))
@@ -1285,7 +1296,7 @@ export async function initBridgeCore(
logForDebugging( logForDebugging(
`[bridge:repl] Flushing ${sdkMessages.length} initial message(s) via transport`, `[bridge:repl] Flushing ${sdkMessages.length} initial message(s) via transport`,
) )
const events = sdkMessages.map(sdkMsg => ({ const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({
...sdkMsg, ...sdkMsg,
session_id: currentSessionId, session_id: currentSessionId,
})) }))
@@ -1655,7 +1666,11 @@ export async function initBridgeCore(
transport = null transport = null
flushGate.drop() flushGate.drop()
if (teardownTransport) { if (teardownTransport) {
void teardownTransport.write(makeResultMessage(currentSessionId)) const resultMsg: StdoutMessageWithSession = {
...makeResultMessage(currentSessionId),
session_id: currentSessionId,
}
void teardownTransport.write(resultMsg)
} }
const stopWorkP = currentWorkId const stopWorkP = currentWorkId
@@ -1778,7 +1793,7 @@ export async function initBridgeCore(
// Convert to SDK format and send via HTTP POST (HybridTransport). // Convert to SDK format and send via HTTP POST (HybridTransport).
// The web UI receives them via the subscribe WebSocket. // The web UI receives them via the subscribe WebSocket.
const sdkMessages = toSDKMessages(filtered) const sdkMessages = toSDKMessages(filtered)
const events = sdkMessages.map(sdkMsg => ({ const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({
...sdkMsg, ...sdkMsg,
session_id: currentSessionId, session_id: currentSessionId,
})) }))
@@ -1803,7 +1818,7 @@ export async function initBridgeCore(
for (const msg of filtered) { for (const msg of filtered) {
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string) 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) void transport.writeBatch(events)
}, },
sendControlRequest(request: SDKControlRequest) { sendControlRequest(request: SDKControlRequest) {
@@ -1813,7 +1828,7 @@ export async function initBridgeCore(
) )
return return
} }
const event = { ...request, session_id: currentSessionId } const event: StdoutMessageWithSession = { ...request, session_id: currentSessionId }
void transport.write(event) void transport.write(event)
logForDebugging( logForDebugging(
`[bridge:repl] Sent control_request request_id=${request.request_id}`, `[bridge:repl] Sent control_request request_id=${request.request_id}`,
@@ -1826,7 +1841,7 @@ export async function initBridgeCore(
) )
return return
} }
const event = { ...response, session_id: currentSessionId } const event: StdoutMessageWithSession = { ...response, session_id: currentSessionId }
void transport.write(event) void transport.write(event)
logForDebugging('[bridge:repl] Sent control_response') logForDebugging('[bridge:repl] Sent control_response')
}, },
@@ -1837,7 +1852,7 @@ export async function initBridgeCore(
) )
return return
} }
const event = { const event: StdoutMessageWithSession = {
type: 'control_cancel_request' as const, type: 'control_cancel_request' as const,
request_id: requestId, request_id: requestId,
session_id: currentSessionId, session_id: currentSessionId,
@@ -1854,7 +1869,11 @@ export async function initBridgeCore(
) )
return return
} }
void transport.write(makeResultMessage(currentSessionId)) const resultMsg: StdoutMessageWithSession = {
...makeResultMessage(currentSessionId),
session_id: currentSessionId,
}
void transport.write(resultMsg)
logForDebugging( logForDebugging(
`[bridge:repl] Sent result for session=${currentSessionId}`, `[bridge:repl] Sent result for session=${currentSessionId}`,
) )

View File

@@ -1128,7 +1128,7 @@ function runHeadlessStreaming(
rate_limit_info: rateLimitInfo, rate_limit_info: rateLimitInfo,
uuid: randomUUID(), uuid: randomUUID(),
session_id: getSessionId(), session_id: getSessionId(),
}) } as unknown as Parameters<typeof output.enqueue>[0])
} }
} }
statusListeners.add(rateLimitListener) statusListeners.add(rateLimitListener)
@@ -1237,7 +1237,7 @@ function runHeadlessStreaming(
uuid: crumb.uuid, uuid: crumb.uuid,
timestamp: crumb.timestamp, timestamp: crumb.timestamp,
isReplay: true, isReplay: true,
} as SDKUserMessageReplay) } as SDKUserMessageReplay as StdoutMessage)
} }
} }
} }
@@ -1974,7 +1974,7 @@ function runHeadlessStreaming(
parent_tool_use_id: null, parent_tool_use_id: null,
uuid: c.uuid as string, uuid: c.uuid as string,
isReplay: true, isReplay: true,
} as SDKUserMessageReplay) } as SDKUserMessageReplay as StdoutMessage)
} }
} }
} }
@@ -2200,7 +2200,7 @@ function runHeadlessStreaming(
output.enqueue({ output.enqueue({
type: 'system', type: 'system',
subtype: 'status', subtype: 'status',
status, status: status as 'compacting' | null,
session_id: getSessionId(), session_id: getSessionId(),
uuid: randomUUID(), uuid: randomUUID(),
}) })
@@ -2227,10 +2227,10 @@ function runHeadlessStreaming(
isBackgroundTask(t), isBackgroundTask(t),
) )
) { ) {
heldBackResult = message heldBackResult = message as StdoutMessage
} else { } else {
heldBackResult = null heldBackResult = null
output.enqueue(message) output.enqueue(message as StdoutMessage)
} }
} else { } else {
// Flush SDK events (task_started, task_progress) so background // Flush SDK events (task_started, task_progress) so background
@@ -2238,7 +2238,7 @@ function runHeadlessStreaming(
for (const event of drainSdkEvents()) { for (const event of drainSdkEvents()) {
output.enqueue(event) output.enqueue(event)
} }
output.enqueue(message) output.enqueue(message as StdoutMessage)
} }
} }
}) // end runWithWorkload }) // end runWithWorkload
@@ -2256,11 +2256,12 @@ function runHeadlessStreaming(
{ turnStartTime } as import('src/utils/filePersistence/types.js').TurnStartTime, { turnStartTime } as import('src/utils/filePersistence/types.js').TurnStartTime,
abortController.signal, abortController.signal,
result => { result => {
const filesResult = result as { persistedFiles: { filename: string; file_id: string }[]; failedFiles: { filename: string; error: string }[] }
output.enqueue({ output.enqueue({
type: 'system' as const, type: 'system' as const,
subtype: 'files_persisted' as const, subtype: 'files_persisted' as const,
files: (result as any).persistedFiles, files: filesResult.persistedFiles,
failed: (result as any).failedFiles, failed: filesResult.failedFiles,
processed_at: new Date().toISOString(), processed_at: new Date().toISOString(),
uuid: randomUUID(), uuid: randomUUID(),
session_id: getSessionId(), session_id: getSessionId(),
@@ -2730,7 +2731,7 @@ function runHeadlessStreaming(
} }
const sendControlResponseSuccess = function ( const sendControlResponseSuccess = function (
message: SDKControlRequest, message: { request_id: string } | SDKControlRequest,
response?: Record<string, unknown>, response?: Record<string, unknown>,
) { ) {
output.enqueue({ output.enqueue({
@@ -2744,7 +2745,7 @@ function runHeadlessStreaming(
} }
const sendControlResponseError = function ( const sendControlResponseError = function (
message: SDKControlRequest, message: { request_id: string } | SDKControlRequest,
errorMessage: string, errorMessage: string,
) { ) {
output.enqueue({ output.enqueue({
@@ -2820,11 +2821,21 @@ function runHeadlessStreaming(
message.type !== 'user' && message.type !== 'user' &&
message.type !== 'control_response' message.type !== 'control_response'
) { ) {
notifyCommandLifecycle(eventId, 'completed') notifyCommandLifecycle(eventId as string, 'completed')
} }
if (message.type === 'control_request') { 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<string, unknown> 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<string, unknown>
if (msg.request.subtype === 'interrupt') {
// Track escapes for attribution (ant-only feature) // Track escapes for attribution (ant-only feature)
if (feature('COMMIT_ATTRIBUTION')) { if (feature('COMMIT_ATTRIBUTION')) {
setAppState(prev => ({ setAppState(prev => ({
@@ -2842,10 +2853,10 @@ function runHeadlessStreaming(
suggestionState.abortController = null suggestionState.abortController = null
suggestionState.lastEmitted = null suggestionState.lastEmitted = null
suggestionState.pendingSuggestion = null suggestionState.pendingSuggestion = null
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} else if (message.request.subtype === 'end_session') { } else if (req.subtype === 'end_session') {
logForDebugging( logForDebugging(
`[print.ts] end_session received, reason=${message.request.reason ?? 'unspecified'}`, `[print.ts] end_session received, reason=${req.reason ?? 'unspecified'}`,
) )
if (abortController) { if (abortController) {
abortController.abort() abortController.abort()
@@ -2854,16 +2865,16 @@ function runHeadlessStreaming(
suggestionState.abortController = null suggestionState.abortController = null
suggestionState.lastEmitted = null suggestionState.lastEmitted = null
suggestionState.pendingSuggestion = null suggestionState.pendingSuggestion = null
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
break // exits for-await → falls through to inputClosed=true drain below 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 // SDK MCP server names from the initialize message
// Populated by both browser and ProcessTransport sessions // Populated by both browser and ProcessTransport sessions
if ( if (
message.request.sdkMcpServers && msg.request.sdkMcpServers &&
message.request.sdkMcpServers.length > 0 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 // Create placeholder config for SDK MCP servers
// The actual server connection is managed by the SDK Query class // The actual server connection is managed by the SDK Query class
sdkMcpConfigs[serverName] = { sdkMcpConfigs[serverName] = {
@@ -2874,8 +2885,8 @@ function runHeadlessStreaming(
} }
await handleInitializeRequest( await handleInitializeRequest(
message.request, msg.request,
message.request_id, msg.request_id,
initialized, initialized,
output, output,
commands, commands,
@@ -2890,7 +2901,7 @@ function runHeadlessStreaming(
// Enable prompt suggestions in AppState when SDK consumer opts in. // Enable prompt suggestions in AppState when SDK consumer opts in.
// shouldEnablePromptSuggestion() returns false for non-interactive // shouldEnablePromptSuggestion() returns false for non-interactive
// sessions, but the SDK consumer explicitly requested suggestions. // sessions, but the SDK consumer explicitly requested suggestions.
if (message.request.promptSuggestions) { if (msg.request.promptSuggestions) {
setAppState(prev => { setAppState(prev => {
if (prev.promptSuggestionEnabled) return prev if (prev.promptSuggestionEnabled) return prev
return { ...prev, promptSuggestionEnabled: true } return { ...prev, promptSuggestionEnabled: true }
@@ -2898,7 +2909,7 @@ function runHeadlessStreaming(
} }
if ( if (
message.request.agentProgressSummaries && msg.request.agentProgressSummaries &&
getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_prism', true) getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_prism', true)
) { ) {
setSdkAgentProgressSummariesEnabled(true) setSdkAgentProgressSummariesEnabled(true)
@@ -2911,13 +2922,13 @@ function runHeadlessStreaming(
if (hasCommandsInQueue()) { if (hasCommandsInQueue()) {
void run() void run()
} }
} else if (message.request.subtype === 'set_permission_mode') { } else if (msg.request.subtype === 'set_permission_mode') {
const m = message.request // for typescript (TODO: use readonly types to avoid this) const m = msg.request // for typescript (TODO: use readonly types to avoid this)
setAppState(prev => ({ setAppState(prev => ({
...prev, ...prev,
toolPermissionContext: handleSetPermissionMode( toolPermissionContext: handleSetPermissionMode(
m, m,
message.request_id, msg.request_id,
prev.toolPermissionContext, prev.toolPermissionContext,
output, output,
), ),
@@ -2926,8 +2937,8 @@ function runHeadlessStreaming(
// handleSetPermissionMode sends the control_response; the // handleSetPermissionMode sends the control_response; the
// notifySessionMetadataChanged that used to follow here is // notifySessionMetadataChanged that used to follow here is
// now fired by onChangeAppState (with externalized mode name). // now fired by onChangeAppState (with externalized mode name).
} else if (message.request.subtype === 'set_model') { } else if (msg.request.subtype === 'set_model') {
const requestedModel = message.request.model ?? 'default' const requestedModel = msg.request.model ?? 'default'
const model = const model =
requestedModel === 'default' requestedModel === 'default'
? getDefaultMainLoopModel() ? getDefaultMainLoopModel()
@@ -2937,24 +2948,24 @@ function runHeadlessStreaming(
notifySessionMetadataChanged({ model }) notifySessionMetadataChanged({ model })
injectModelSwitchBreadcrumbs(requestedModel, model) injectModelSwitchBreadcrumbs(requestedModel, model)
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} else if (message.request.subtype === 'set_max_thinking_tokens') { } else if (msg.request.subtype === 'set_max_thinking_tokens') {
if (message.request.max_thinking_tokens === null) { if (msg.request.max_thinking_tokens === null) {
options.thinkingConfig = undefined options.thinkingConfig = undefined
} else if (message.request.max_thinking_tokens === 0) { } else if (msg.request.max_thinking_tokens === 0) {
options.thinkingConfig = { type: 'disabled' } options.thinkingConfig = { type: 'disabled' }
} else { } else {
options.thinkingConfig = { options.thinkingConfig = {
type: 'enabled', type: 'enabled',
budgetTokens: message.request.max_thinking_tokens, budgetTokens: msg.request.max_thinking_tokens,
} }
} }
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} else if (message.request.subtype === 'mcp_status') { } else if (msg.request.subtype === 'mcp_status') {
sendControlResponseSuccess(message, { sendControlResponseSuccess(msg, {
mcpServers: buildMcpServerStatuses(), mcpServers: buildMcpServerStatuses(),
}) })
} else if (message.request.subtype === 'get_context_usage') { } else if (msg.request.subtype === 'get_context_usage') {
try { try {
const appState = getAppState() const appState = getAppState()
const data = await collectContextData({ const data = await collectContextData({
@@ -2968,13 +2979,13 @@ function runHeadlessStreaming(
appendSystemPrompt: options.appendSystemPrompt, appendSystemPrompt: options.appendSystemPrompt,
}, },
}) })
sendControlResponseSuccess(message, { ...data }) sendControlResponseSuccess(msg, { ...data })
} catch (error) { } 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 // Handle MCP notifications from SDK servers
const mcpRequest = message.request const mcpRequest = msg.request as Record<string, unknown>
const sdkClient = sdkClients.find( const sdkClient = sdkClients.find(
client => client.name === mcpRequest.server_name, client => client.name === mcpRequest.server_name,
) )
@@ -2985,32 +2996,32 @@ function runHeadlessStreaming(
sdkClient.type === 'connected' && sdkClient.type === 'connected' &&
sdkClient.client?.transport?.onmessage 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) sendControlResponseSuccess(msg)
} else if (message.request.subtype === 'rewind_files') { } else if (msg.request.subtype === 'rewind_files') {
const appState = getAppState() const appState = getAppState()
const result = await handleRewindFiles( const result = await handleRewindFiles(
message.request.user_message_id as UUID, msg.request.user_message_id as UUID,
appState, appState,
setAppState, setAppState,
message.request.dry_run ?? false, msg.request.dry_run ?? false,
) )
if (result.canRewind || message.request.dry_run) { if (result.canRewind || msg.request.dry_run) {
sendControlResponseSuccess(message, result) sendControlResponseSuccess(msg, result)
} else { } else {
sendControlResponseError( sendControlResponseError(
message, msg,
(result.error as string) ?? 'Unexpected error', (result.error as string) ?? 'Unexpected error',
) )
} }
} else if (message.request.subtype === 'cancel_async_message') { } else if (msg.request.subtype === 'cancel_async_message') {
const targetUuid = message.request.message_uuid const targetUuid = msg.request.message_uuid
const removed = dequeueAllMatching(cmd => cmd.uuid === targetUuid) const removed = dequeueAllMatching(cmd => cmd.uuid === targetUuid)
sendControlResponseSuccess(message, { sendControlResponseSuccess(msg, {
cancelled: removed.length > 0, 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. // Client observed a Read that was later removed from context (e.g.
// by snip), so transcript-based seeding missed it. Queued into // by snip), so transcript-based seeding missed it. Queued into
// pendingSeeds; applied at the next clone-replace boundary. // pendingSeeds; applied at the next clone-replace boundary.
@@ -3018,7 +3029,7 @@ function runHeadlessStreaming(
// expandPath: all other readFileState writers normalize (~, relative, // expandPath: all other readFileState writers normalize (~, relative,
// session cwd vs process cwd). FileEditTool looks up by expandPath'd // session cwd vs process cwd). FileEditTool looks up by expandPath'd
// key — a verbatim client path would miss. // 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 // Check disk mtime before reading content. If the file changed
// since the client's observation, readFile would return C_current // since the client's observation, readFile would return C_current
// but we'd store it with the client's M_observed — getChangedFiles // 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. // makes Edit fail "file not read yet" → forces a fresh Read.
// Math.floor matches FileReadTool and getFileModificationTime. // Math.floor matches FileReadTool and getFileModificationTime.
const diskMtime = Math.floor((await stat(normalizedPath)).mtimeMs) 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') const raw = await readFile(normalizedPath, 'utf-8')
// Strip BOM + normalize CRLF→LF to match readFileInRange and // Strip BOM + normalize CRLF→LF to match readFileInRange and
// readFileSyncWithMetadata. FileEditTool's content-compare // readFileSyncWithMetadata. FileEditTool's content-compare
@@ -3047,18 +3058,18 @@ function runHeadlessStreaming(
} catch { } catch {
// ENOENT etc — skip seeding but still succeed // ENOENT etc — skip seeding but still succeed
} }
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} else if (message.request.subtype === 'mcp_set_servers') { } else if (msg.request.subtype === 'mcp_set_servers') {
const { response, sdkServersChanged } = await applyMcpServerChanges( const { response, sdkServersChanged } = await applyMcpServerChanges(
message.request.servers, msg.request.servers as Record<string, McpServerConfigForProcessTransport>,
) )
sendControlResponseSuccess(message, response) sendControlResponseSuccess(msg, response)
// Connect SDK servers AFTER response to avoid deadlock // Connect SDK servers AFTER response to avoid deadlock
if (sdkServersChanged) { if (sdkServersChanged) {
void updateSdkMcp() void updateSdkMcp()
} }
} else if (message.request.subtype === 'reload_plugins') { } else if (msg.request.subtype === 'reload_plugins') {
try { try {
if ( if (
feature('DOWNLOAD_USER_SETTINGS') && feature('DOWNLOAD_USER_SETTINGS') &&
@@ -3106,7 +3117,7 @@ function runHeadlessStreaming(
logError(pluginsR.reason) logError(pluginsR.reason)
} }
sendControlResponseSuccess(message, { sendControlResponseSuccess(msg, {
commands: currentCommands commands: currentCommands
.filter(cmd => cmd.userInvocable !== false) .filter(cmd => cmd.userInvocable !== false)
.map(cmd => ({ .map(cmd => ({
@@ -3120,15 +3131,15 @@ function runHeadlessStreaming(
model: a.model === 'inherit' ? undefined : a.model, model: a.model === 'inherit' ? undefined : a.model,
})), })),
plugins, plugins,
mcpServers: buildMcpServerStatuses(), mcpServers: buildMcpServerStatuses() as SDKControlReloadPluginsResponse['mcpServers'],
error_count: r.error_count, error_count: r.error_count,
} satisfies SDKControlReloadPluginsResponse) } satisfies SDKControlReloadPluginsResponse)
} catch (error) { } 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 currentAppState = getAppState()
const { serverName } = message.request const { serverName } = msg.request
elicitationRegistered.delete(serverName) elicitationRegistered.delete(serverName)
// Config-existence gate must cover the SAME sources as the // Config-existence gate must cover the SAME sources as the
// operations below. SDK-injected servers (query({mcpServers:{...}})) // operations below. SDK-injected servers (query({mcpServers:{...}}))
@@ -3144,7 +3155,7 @@ function runHeadlessStreaming(
?.config ?? ?.config ??
null null
if (!config) { if (!config) {
sendControlResponseError(message, `Server not found: ${serverName}`) sendControlResponseError(msg, `Server not found: ${serverName}`)
} else { } else {
const result = await reconnectMcpServerImpl(serverName, config) const result = await reconnectMcpServerImpl(serverName, config)
// Update appState.mcp with the new client, tools, commands, and resources // Update appState.mcp with the new client, tools, commands, and resources
@@ -3190,18 +3201,18 @@ function runHeadlessStreaming(
if (result.client.type === 'connected') { if (result.client.type === 'connected') {
registerElicitationHandlers([result.client]) registerElicitationHandlers([result.client])
reregisterChannelHandlerAfterReconnect(result.client) reregisterChannelHandlerAfterReconnect(result.client)
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} else { } else {
const errorMessage = const errorMessage =
result.client.type === 'failed' result.client.type === 'failed'
? (result.client.error ?? 'Connection failed') ? (result.client.error ?? 'Connection failed')
: `Server status: ${result.client.type}` : `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 currentAppState = getAppState()
const { serverName, enabled } = message.request const { serverName, enabled } = msg.request
elicitationRegistered.delete(serverName) elicitationRegistered.delete(serverName)
// Gate must match the client-lookup spread below (which // Gate must match the client-lookup spread below (which
// includes sdkClients and dynamicMcpState.clients). Same fix as // includes sdkClients and dynamicMcpState.clients). Same fix as
@@ -3216,7 +3227,7 @@ function runHeadlessStreaming(
null null
if (!config) { if (!config) {
sendControlResponseError(message, `Server not found: ${serverName}`) sendControlResponseError(msg, `Server not found: ${serverName}`)
} else if (!enabled) { } else if (!enabled) {
// Disabling: persist + disconnect (matches TUI toggleMcpServer behavior) // Disabling: persist + disconnect (matches TUI toggleMcpServer behavior)
setMcpServerEnabled(serverName, false) setMcpServerEnabled(serverName, false)
@@ -3247,7 +3258,7 @@ function runHeadlessStreaming(
resources: omit(prev.mcp.resources, serverName), resources: omit(prev.mcp.resources, serverName),
}, },
})) }))
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} else { } else {
// Enabling: persist + reconnect // Enabling: persist + reconnect
setMcpServerEnabled(serverName, true) setMcpServerEnabled(serverName, true)
@@ -3281,20 +3292,20 @@ function runHeadlessStreaming(
if (result.client.type === 'connected') { if (result.client.type === 'connected') {
registerElicitationHandlers([result.client]) registerElicitationHandlers([result.client])
reregisterChannelHandlerAfterReconnect(result.client) reregisterChannelHandlerAfterReconnect(result.client)
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} else { } else {
const errorMessage = const errorMessage =
result.client.type === 'failed' result.client.type === 'failed'
? (result.client.error ?? 'Connection failed') ? (result.client.error ?? 'Connection failed')
: `Server status: ${result.client.type}` : `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() const currentAppState = getAppState()
handleChannelEnable( handleChannelEnable(
message.request_id, msg.request_id,
message.request.serverName, req.serverName as string,
// Pool spread matches mcp_status — all three client sources. // Pool spread matches mcp_status — all three client sources.
[ [
...currentAppState.mcp.clients, ...currentAppState.mcp.clients,
@@ -3303,8 +3314,8 @@ function runHeadlessStreaming(
], ],
output, output,
) )
} else if (message.request.subtype === 'mcp_authenticate') { } else if (req.subtype === 'mcp_authenticate') {
const { serverName } = message.request const { serverName } = req
const currentAppState = getAppState() const currentAppState = getAppState()
const config = const config =
getMcpConfigByName(serverName) ?? getMcpConfigByName(serverName) ??
@@ -3313,10 +3324,10 @@ function runHeadlessStreaming(
?.config ?? ?.config ??
null null
if (!config) { if (!config) {
sendControlResponseError(message, `Server not found: ${serverName}`) sendControlResponseError(msg, `Server not found: ${serverName}`)
} else if (config.type !== 'sse' && config.type !== 'http') { } else if (config.type !== 'sse' && config.type !== 'http') {
sendControlResponseError( sendControlResponseError(
message, msg,
`Server type "${config.type}" does not support OAuth authentication`, `Server type "${config.type}" does not support OAuth authentication`,
) )
} else { } else {
@@ -3353,12 +3364,12 @@ function runHeadlessStreaming(
]) ])
if (authUrl) { if (authUrl) {
sendControlResponseSuccess(message, { sendControlResponseSuccess(msg, {
authUrl, authUrl,
requiresUserAction: true, requiresUserAction: true,
}) })
} else { } else {
sendControlResponseSuccess(message, { sendControlResponseSuccess(msg, {
requiresUserAction: false, requiresUserAction: false,
}) })
} }
@@ -3453,11 +3464,11 @@ function runHeadlessStreaming(
}) })
void fullFlowPromise void fullFlowPromise
} catch (error) { } catch (error) {
sendControlResponseError(message, errorMessage(error)) sendControlResponseError(msg, errorMessage(error))
} }
} }
} else if (message.request.subtype === 'mcp_oauth_callback_url') { } else if (req.subtype === 'mcp_oauth_callback_url') {
const { serverName, callbackUrl } = message.request const { serverName, callbackUrl } = req
const submit = oauthCallbackSubmitters.get(serverName) const submit = oauthCallbackSubmitters.get(serverName)
if (submit) { if (submit) {
// Validate the callback URL before submitting. The submit // Validate the callback URL before submitting. The submit
@@ -3475,7 +3486,7 @@ function runHeadlessStreaming(
} }
if (!hasCodeOrError) { if (!hasCodeOrError) {
sendControlResponseError( sendControlResponseError(
message, msg,
'Invalid callback URL: missing authorization code. Please paste the full redirect URL including the code parameter.', 'Invalid callback URL: missing authorization code. Please paste the full redirect URL including the code parameter.',
) )
} else { } else {
@@ -3488,32 +3499,32 @@ function runHeadlessStreaming(
if (authPromise) { if (authPromise) {
try { try {
await authPromise await authPromise
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} catch (error) { } catch (error) {
sendControlResponseError( sendControlResponseError(
message, msg,
error instanceof Error error instanceof Error
? error.message ? error.message
: 'OAuth authentication failed', : 'OAuth authentication failed',
) )
} }
} else { } else {
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} }
} }
} else { } else {
sendControlResponseError( sendControlResponseError(
message, msg,
`No active OAuth flow for server: ${serverName}`, `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 // Anthropic OAuth over the control channel. The SDK client owns
// the user's browser (we're headless in -p mode); we hand back // the user's browser (we're headless in -p mode); we hand back
// both URLs and wait. Automatic URL → localhost listener catches // both URLs and wait. Automatic URL → localhost listener catches
// the redirect if the browser is on this host; manual URL → the // the redirect if the browser is on this host; manual URL → the
// success page shows "code#state" for claude_oauth_callback. // 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 // Clean up any prior flow. cleanup() closes the localhost listener
// and nulls the manual resolver. The prior `flow` promise is left // and nulls the manual resolver. The prior `flow` promise is left
@@ -3594,30 +3605,30 @@ function runHeadlessStreaming(
) )
}), }),
]) ])
sendControlResponseSuccess(message, { sendControlResponseSuccess(msg, {
manualUrl, manualUrl,
automaticUrl, automaticUrl,
}) })
} catch (error) { } catch (error) {
sendControlResponseError(message, errorMessage(error)) sendControlResponseError(msg, errorMessage(error))
} }
} else if ( } else if (
message.request.subtype === 'claude_oauth_callback' || req.subtype === 'claude_oauth_callback' ||
message.request.subtype === 'claude_oauth_wait_for_completion' req.subtype === 'claude_oauth_wait_for_completion'
) { ) {
if (!claudeOAuth) { if (!claudeOAuth) {
sendControlResponseError( sendControlResponseError(
message, msg,
'No active claude_authenticate flow', 'No active claude_authenticate flow',
) )
} else { } else {
// Inject the manual code synchronously — must happen in stdin // Inject the manual code synchronously — must happen in stdin
// message order so a subsequent claude_authenticate doesn't // message order so a subsequent claude_authenticate doesn't
// replace the service before this code lands. // replace the service before this code lands.
if (message.request.subtype === 'claude_oauth_callback') { if (req.subtype === 'claude_oauth_callback') {
claudeOAuth.service.handleManualAuthCodeInput({ claudeOAuth.service.handleManualAuthCodeInput({
authorizationCode: message.request.authorizationCode, authorizationCode: req.authorizationCode as string,
state: message.request.state, state: req.state as string,
}) })
} }
// Detach the await — the stdin reader is serial and blocking // Detach the await — the stdin reader is serial and blocking
@@ -3629,7 +3640,7 @@ function runHeadlessStreaming(
void flow.then( void flow.then(
() => { () => {
const accountInfo = getAccountInformation() const accountInfo = getAccountInformation()
sendControlResponseSuccess(message, { sendControlResponseSuccess(msg, {
account: { account: {
email: accountInfo?.email, email: accountInfo?.email,
organization: accountInfo?.organization, organization: accountInfo?.organization,
@@ -3641,11 +3652,11 @@ function runHeadlessStreaming(
}) })
}, },
(error: unknown) => (error: unknown) =>
sendControlResponseError(message, errorMessage(error)), sendControlResponseError(msg, errorMessage(error)),
) )
} }
} else if (message.request.subtype === 'mcp_clear_auth') { } else if (req.subtype === 'mcp_clear_auth') {
const { serverName } = message.request const { serverName } = req
const currentAppState = getAppState() const currentAppState = getAppState()
const config = const config =
getMcpConfigByName(serverName) ?? getMcpConfigByName(serverName) ??
@@ -3654,10 +3665,10 @@ function runHeadlessStreaming(
?.config ?? ?.config ??
null null
if (!config) { if (!config) {
sendControlResponseError(message, `Server not found: ${serverName}`) sendControlResponseError(msg, `Server not found: ${serverName}`)
} else if (config.type !== 'sse' && config.type !== 'http') { } else if (config.type !== 'sse' && config.type !== 'http') {
sendControlResponseError( sendControlResponseError(
message, msg,
`Cannot clear auth for server type "${config.type}"`, `Cannot clear auth for server type "${config.type}"`,
) )
} else { } else {
@@ -3690,16 +3701,16 @@ function runHeadlessStreaming(
: omit(prev.mcp.resources, serverName), : 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 // Snapshot the current model before applying — we need to detect
// model switches so we can inject breadcrumbs and notify listeners. // model switches so we can inject breadcrumbs and notify listeners.
const prevModel = getMainLoopModel() const prevModel = getMainLoopModel()
// Merge the provided settings into the in-memory flag settings // Merge the provided settings into the in-memory flag settings
const existing = getFlagSettingsInline() ?? {} const existing = getFlagSettingsInline() ?? {}
const incoming = message.request.settings const incoming = msg.request.settings
// Shallow-merge top-level keys; getSettingsForSource handles // Shallow-merge top-level keys; getSettingsForSource handles
// the deep merge with file-based flag settings via mergeWith. // the deep merge with file-based flag settings via mergeWith.
// JSON serialization drops `undefined`, so callers use `null` // JSON serialization drops `undefined`, so callers use `null`
@@ -3748,8 +3759,8 @@ function runHeadlessStreaming(
injectModelSwitchBreadcrumbs(modelArg, newModel) injectModelSwitchBreadcrumbs(modelArg, newModel)
} }
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} else if (message.request.subtype === 'get_settings') { } else if (msg.request.subtype === 'get_settings') {
const currentAppState = getAppState() const currentAppState = getAppState()
const model = getMainLoopModel() const model = getMainLoopModel()
// modelSupportsEffort gate matches claude.ts — applied.effort must // modelSupportsEffort gate matches claude.ts — applied.effort must
@@ -3757,7 +3768,7 @@ function runHeadlessStreaming(
const effort = modelSupportsEffort(model) const effort = modelSupportsEffort(model)
? resolveAppliedEffort(model, currentAppState.effortValue) ? resolveAppliedEffort(model, currentAppState.effortValue)
: undefined : undefined
sendControlResponseSuccess(message, { sendControlResponseSuccess(msg, {
...getSettingsWithSources(), ...getSettingsWithSources(),
applied: { applied: {
model, model,
@@ -3765,22 +3776,22 @@ function runHeadlessStreaming(
effort: typeof effort === 'string' ? effort : null, effort: typeof effort === 'string' ? effort : null,
}, },
}) })
} else if (message.request.subtype === 'stop_task') { } else if (msg.request.subtype === 'stop_task') {
const { task_id: taskId } = message.request const { task_id: taskId } = msg.request
try { try {
await stopTask(taskId, { await stopTask(taskId, {
getAppState, getAppState,
setAppState, setAppState,
}) })
sendControlResponseSuccess(message, {}) sendControlResponseSuccess(msg, {})
} catch (error) { } 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 // Fire-and-forget so the Haiku call does not block the stdin loop
// (which would delay processing of subsequent user messages / // (which would delay processing of subsequent user messages /
// interrupts for the duration of the API roundtrip). // 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 // Reuse the live controller only if it has not already been aborted
// (e.g. by interrupt()); an aborted signal would cause queryHaiku to // (e.g. by interrupt()); an aborted signal would cause queryHaiku to
// immediately throw APIUserAbortError → {title: null}. // immediately throw APIUserAbortError → {title: null}.
@@ -3799,16 +3810,16 @@ function runHeadlessStreaming(
logError(e) logError(e)
} }
} }
sendControlResponseSuccess(message, { title }) sendControlResponseSuccess(msg, { title })
} catch (e) { } catch (e) {
// Unreachable in practice — generateSessionTitle wraps its // Unreachable in practice — generateSessionTitle wraps its
// own body and returns null, saveAiGeneratedTitle is wrapped // own body and returns null, saveAiGeneratedTitle is wrapped
// above. Propagate (not swallow) so unexpected failures are // above. Propagate (not swallow) so unexpected failures are
// visible to the SDK caller (hostComms.ts catches and logs). // 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 — // Same fire-and-forget pattern as generate_session_title above —
// the forked agent's API roundtrip must not block the stdin loop. // 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 // matches in the common case. May still miss the cache for
// coordinator mode or memory-mechanics extras — acceptable, the // coordinator mode or memory-mechanics extras — acceptable, the
// alternative is the side question failing entirely. // alternative is the side question failing entirely.
const { question } = message.request const { question } = req
void (async () => { void (async () => {
try { try {
const saved = getLastCacheSafeParams() const saved = getLastCacheSafeParams()
@@ -3863,16 +3874,16 @@ function runHeadlessStreaming(
question, question,
cacheSafeParams, cacheSafeParams,
}) })
sendControlResponseSuccess(message, { response: result.response }) sendControlResponseSuccess(msg, { response: result.response })
} catch (e) { } catch (e) {
sendControlResponseError(message, errorMessage(e)) sendControlResponseError(msg, errorMessage(e))
} }
})() })()
} else if ( } else if (
(feature('PROACTIVE') || feature('KAIROS')) && (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 subtype: string
enabled: boolean enabled: boolean
} }
@@ -3884,12 +3895,12 @@ function runHeadlessStreaming(
} else { } else {
proactiveModule!.deactivateProactive() proactiveModule!.deactivateProactive()
} }
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} else if (message.request.subtype === 'remote_control') { } else if (req.subtype === 'remote_control') {
if (message.request.enabled) { if (req.enabled as boolean) {
if (bridgeHandle) { if (bridgeHandle) {
// Already connected // Already connected
sendControlResponseSuccess(message, { sendControlResponseSuccess(msg, {
session_url: getRemoteSessionUrl( session_url: getRemoteSessionUrl(
bridgeHandle.bridgeSessionId, bridgeHandle.bridgeSessionId,
bridgeHandle.sessionIngressUrl, bridgeHandle.sessionIngressUrl,
@@ -3972,7 +3983,7 @@ function runHeadlessStreaming(
}) })
if (!handle) { if (!handle) {
sendControlResponseError( sendControlResponseError(
message, msg,
bridgeFailureDetail ?? bridgeFailureDetail ??
'Remote Control initialization failed', 'Remote Control initialization failed',
) )
@@ -3988,7 +3999,7 @@ function runHeadlessStreaming(
structuredIO.setOnControlRequestResolved(requestId => { structuredIO.setOnControlRequestResolved(requestId => {
handle.sendControlCancelRequest(requestId) handle.sendControlCancelRequest(requestId)
}) })
sendControlResponseSuccess(message, { sendControlResponseSuccess(msg, {
session_url: getRemoteSessionUrl( session_url: getRemoteSessionUrl(
handle.bridgeSessionId, handle.bridgeSessionId,
handle.sessionIngressUrl, handle.sessionIngressUrl,
@@ -4001,7 +4012,7 @@ function runHeadlessStreaming(
}) })
} }
} catch (err) { } catch (err) {
sendControlResponseError(message, errorMessage(err)) sendControlResponseError(msg, errorMessage(err))
} }
} }
} else { } else {
@@ -4012,21 +4023,21 @@ function runHeadlessStreaming(
await bridgeHandle.teardown() await bridgeHandle.teardown()
bridgeHandle = null bridgeHandle = null
} }
sendControlResponseSuccess(message) sendControlResponseSuccess(msg)
} }
} else { } else {
// Unknown control request subtype — send an error response so // Unknown control request subtype — send an error response so
// the caller doesn't hang waiting for a reply that never comes. // the caller doesn't hang waiting for a reply that never comes.
sendControlResponseError( sendControlResponseError(
message, msg,
`Unsupported control request subtype: ${(message.request as { subtype: string }).subtype}`, `Unsupported control request subtype: ${(msg.request as { subtype: string }).subtype}`,
) )
} }
continue continue
} else if (message.type === 'control_response') { } else if (message.type === 'control_response') {
// Replay control_response messages when replay mode is enabled // Replay control_response messages when replay mode is enabled
if (options.replayUserMessages) { if (options.replayUserMessages) {
output.enqueue(message) output.enqueue(message as StdoutMessage)
} }
continue continue
} else if (message.type === 'keep_alive') { } else if (message.type === 'keep_alive') {
@@ -4038,11 +4049,11 @@ function runHeadlessStreaming(
} else if (message.type === 'assistant' || message.type === 'system') { } else if (message.type === 'assistant' || message.type === 'system') {
// History replay from bridge: inject into mutableMessages as // History replay from bridge: inject into mutableMessages as
// conversation context so the model sees prior turns. // conversation context so the model sees prior turns.
const internalMsgs = toInternalMessages([message]) const internalMsgs = toInternalMessages([message as SDKMessage])
mutableMessages.push(...internalMsgs) mutableMessages.push(...internalMsgs)
// Echo assistant messages back so CCR displays them // Echo assistant messages back so CCR displays them
if (message.type === 'assistant' && options.replayUserMessages) { if (message.type === 'assistant' && options.replayUserMessages) {
output.enqueue(message) output.enqueue(message as StdoutMessage)
} }
continue continue
} }
@@ -4051,58 +4062,61 @@ function runHeadlessStreaming(
if (message.type !== 'user') { if (message.type !== 'user') {
continue 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. // First prompt message implicitly initializes if not already done.
initialized = true initialized = true
// Check for duplicate user message - skip if already processed // Check for duplicate user message - skip if already processed
if (message.uuid) { if (userMsg.uuid) {
const sessionId = getSessionId() as UUID const sessionId = getSessionId() as UUID
const existsInSession = await doesMessageExistInSession( const existsInSession = await doesMessageExistInSession(
sessionId, sessionId,
message.uuid, userMsg.uuid as UUID,
) )
// Check both historical duplicates (from file) and runtime duplicates (this session) // Check both historical duplicates (from file) and runtime duplicates (this session)
if (existsInSession || receivedMessageUuids.has(message.uuid)) { if (existsInSession || receivedMessageUuids.has(userMsg.uuid as UUID)) {
logForDebugging(`Skipping duplicate user message: ${message.uuid}`) logForDebugging(`Skipping duplicate user message: ${userMsg.uuid}`)
// Send acknowledgment for duplicate message if replay mode is enabled // Send acknowledgment for duplicate message if replay mode is enabled
if (options.replayUserMessages) { if (options.replayUserMessages) {
logForDebugging( logForDebugging(
`Sending acknowledgment for duplicate user message: ${message.uuid}`, `Sending acknowledgment for duplicate user message: ${userMsg.uuid}`,
) )
output.enqueue({ output.enqueue({
type: 'user', type: 'user',
content: message.message?.content ?? '', content: (userMsg.message as { content?: string })?.content ?? '',
message: message.message, message: userMsg.message as { role: string; content: unknown },
session_id: sessionId, session_id: sessionId,
parent_tool_use_id: null, parent_tool_use_id: null,
uuid: message.uuid, uuid: userMsg.uuid as string,
timestamp: message.timestamp, timestamp: (userMsg as { timestamp?: string }).timestamp,
isReplay: true, isReplay: true,
} as unknown as SDKUserMessageReplay) } as unknown as SDKUserMessageReplay as StdoutMessage)
} }
// Historical dup = transcript already has this turn's output, so it // Historical dup = transcript already has this turn's output, so it
// ran but its lifecycle was never closed (interrupted before ack). // ran but its lifecycle was never closed (interrupted before ack).
// Runtime dups don't need this — the original enqueue path closes them. // Runtime dups don't need this — the original enqueue path closes them.
if (existsInSession) { if (existsInSession) {
notifyCommandLifecycle(message.uuid, 'completed') notifyCommandLifecycle(userMsg.uuid as string, 'completed')
} }
// Don't enqueue duplicate messages for execution // Don't enqueue duplicate messages for execution
continue continue
} }
// Track this UUID to prevent runtime duplicates // Track this UUID to prevent runtime duplicates
trackReceivedMessageUuid(message.uuid) trackReceivedMessageUuid(userMsg.uuid as UUID)
} }
enqueue({ enqueue({
mode: 'prompt' as const, mode: 'prompt' as const,
// file_attachments rides the protobuf catchall from the web composer. // file_attachments rides the protobuf catchall from the web composer.
// Same-ref no-op when absent (no 'file_attachments' key). // Same-ref no-op when absent (no 'file_attachments' key).
value: await resolveAndPrepend(message, message.message.content), value: await resolveAndPrepend(userMsg, (userMsg.message as { content: ContentBlockParam[] }).content),
uuid: message.uuid, uuid: userMsg.uuid as `${string}-${string}-${string}-${string}-${string}`,
priority: message.priority, priority: (userMsg as { priority?: string }).priority as import('src/types/textInputTypes.js').QueuePriority,
}) })
// Increment prompt count for attribution tracking and save snapshot // Increment prompt count for attribution tracking and save snapshot
// The snapshot persists promptCount so it survives compaction // The snapshot persists promptCount so it survives compaction
@@ -4463,7 +4477,7 @@ async function handleInitializeRequest(
})), })),
output_style: outputStyle, output_style: outputStyle,
available_output_styles: Object.keys(availableOutputStyles), available_output_styles: Object.keys(availableOutputStyles),
models: modelInfos, models: modelInfos as unknown as SDKControlInitializeResponse['models'],
account: { account: {
email: accountInfo?.email, email: accountInfo?.email,
organization: accountInfo?.organization, organization: accountInfo?.organization,
@@ -4473,7 +4487,7 @@ async function handleInitializeRequest(
// getAccountInformation() returns undefined under 3P providers, so the // getAccountInformation() returns undefined under 3P providers, so the
// other fields are all absent. apiProvider disambiguates "not logged // other fields are all absent. apiProvider disambiguates "not logged
// in" (firstParty + tokenSource:none) from "3P, login not applicable". // in" (firstParty + tokenSource:none) from "3P, login not applicable".
apiProvider: getAPIProvider(), apiProvider: getAPIProvider() as 'firstParty' | 'bedrock' | 'vertex' | 'foundry',
}, },
pid: process.pid, pid: process.pid,
} }

View File

@@ -355,7 +355,7 @@ export class StructuredIO {
// Used by bridge session runner for auth token refresh // Used by bridge session runner for auth token refresh
// (CLAUDE_CODE_SESSION_ACCESS_TOKEN) which must be readable // (CLAUDE_CODE_SESSION_ACCESS_TOKEN) which must be readable
// by the REPL process itself, not just child Bash commands. // by the REPL process itself, not just child Bash commands.
const variables = message.variables as Record<string, string> const variables = message.variables ?? {}
const keys = Object.keys(variables) const keys = Object.keys(variables)
for (const [key, value] of Object.entries(variables)) { for (const [key, value] of Object.entries(variables)) {
process.env[key] = value process.env[key] = value
@@ -377,7 +377,8 @@ export class StructuredIO {
if (uuid) { if (uuid) {
notifyCommandLifecycle(uuid, 'completed') notifyCommandLifecycle(uuid, 'completed')
} }
const request = this.pendingRequests.get(message.response.request_id) const resp = message.response as { request_id: string; subtype: string; response?: Record<string, unknown>; error?: string }
const request = this.pendingRequests.get(resp.request_id)
if (!request) { if (!request) {
// Check if this tool_use was already resolved through the normal // Check if this tool_use was already resolved through the normal
// permission flow. Duplicate control_response deliveries (e.g. from // 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 // re-processing them would push duplicate assistant messages into
// the conversation, causing API 400 errors. // the conversation, causing API 400 errors.
const responsePayload = const responsePayload =
message.response.subtype === 'success' resp.subtype === 'success'
? message.response.response ? resp.response
: undefined : undefined
const toolUseID = responsePayload?.toolUseID const toolUseID = responsePayload?.toolUseID
if ( if (
@@ -394,31 +395,31 @@ export class StructuredIO {
this.resolvedToolUseIds.has(toolUseID) this.resolvedToolUseIds.has(toolUseID)
) { ) {
logForDebugging( 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 return undefined
} }
if (this.unexpectedResponseCallback) { 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 return undefined // Ignore responses for requests we don't know about
} }
this.trackResolvedToolUseId(request.request) 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 // Notify the bridge when the SDK consumer resolves a can_use_tool
// request, so it can cancel the stale permission prompt on claude.ai. // request, so it can cancel the stale permission prompt on claude.ai.
if ( if (
(request.request.request as { subtype?: string }).subtype === 'can_use_tool' && (request.request.request as { subtype?: string }).subtype === 'can_use_tool' &&
this.onControlRequestResolved this.onControlRequestResolved
) { ) {
this.onControlRequestResolved(message.response.request_id) this.onControlRequestResolved(resp.request_id)
} }
if (message.response.subtype === 'error') { if (resp.subtype === 'error') {
request.reject(new Error(message.response.error)) request.reject(new Error(resp.error ?? 'Unknown error'))
return undefined return undefined
} }
const result = message.response.response const result = resp.response
if (request.schema) { if (request.schema) {
try { try {
request.resolve(request.schema.parse(result)) request.resolve(request.schema.parse(result))
@@ -454,9 +455,9 @@ export class StructuredIO {
if (message.type === 'assistant' || message.type === 'system') { if (message.type === 'assistant' || message.type === 'system') {
return message return message
} }
if (message.message.role !== 'user') { if ((message as { message?: { role?: string } }).message?.role !== 'user') {
exitWithMessage( 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 return message
@@ -678,7 +679,7 @@ export class StructuredIO {
{ {
subtype: 'hook_callback', subtype: 'hook_callback',
callback_id: callbackId, callback_id: callbackId,
input, input: input as Parameters<HookCallback['callback']>[0],
tool_use_id: toolUseID || undefined, tool_use_id: toolUseID || undefined,
}, },
hookJSONOutputSchema(), hookJSONOutputSchema(),

View File

@@ -148,7 +148,7 @@ function ServerManagementDialog({ onDone }: Props): React.ReactNode {
<Box flexDirection="column" gap={1}> <Box flexDirection="column" gap={1}>
<Text> <Text>
Remote Control Server is{' '} Remote Control Server is{' '}
<Text bold color="green"> <Text bold color="success">
running running
</Text> </Text>
{daemonProcess ? ` (PID: ${daemonProcess.pid})` : ''} {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; daemonProcess = null;
daemonStatus = 'stopped'; daemonStatus = 'stopped';
daemonLogs.push(`[daemon] exited (code=${code}, signal=${signal})`); daemonLogs.push(`[daemon] exited (code=${code ?? 'unknown'}, signal=${signal})`);
}); });
child.on('error', (err: Error) => { child.on('error', (err: Error) => {

View File

@@ -55,7 +55,7 @@ function BuiltinStatusLineInner({
// Force re-render every 60s so countdowns stay current // Force re-render every 60s so countdowns stay current
const [tick, setTick] = useState(0); const [tick, setTick] = useState(0);
useEffect(() => { 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; if (!hasResetTime) return;
const id = setInterval(() => setTick(t => t + 1), 60_000); const id = setInterval(() => setTick(t => t + 1), 60_000);
return () => clearInterval(id); return () => clearInterval(id);

View File

@@ -15,7 +15,11 @@ type Props = {
export function CompactSummary({ message, screen }: Props): React.ReactNode { export function CompactSummary({ message, screen }: Props): React.ReactNode {
const isTranscriptMode = screen === 'transcript' const isTranscriptMode = screen === 'transcript'
const textContent = getUserMessageText(message) || '' 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 // "Summarize from here" with metadata
if (metadata) { if (metadata) {

View File

@@ -163,7 +163,7 @@ const SuggestionItemRow = memo(function SuggestionItemRow({
{paddedDisplayText} {paddedDisplayText}
</Text> </Text>
{tagText ? ( {tagText ? (
<Text color={item.tag === 'local' ? 'yellow' : undefined} dimColor={item.tag !== 'local'}> <Text color={item.tag === 'local' ? ('yellow' as const) : undefined} dimColor={item.tag !== 'local'}>
{tagText} {tagText}
</Text> </Text>
) : null} ) : null}

View File

@@ -162,13 +162,13 @@ export function SkillsMenu({ onExit, commands }: Props): React.ReactNode {
skill.source === 'plugin' skill.source === 'plugin'
? skill.pluginInfo?.pluginManifest.name ? skill.pluginInfo?.pluginManifest.name
: undefined : undefined
const scopeTag = getScopeTag(skill.source as SkillSource) const scopeTag = getScopeTag(skill.source)
return ( return (
<Box key={`${skill.name}-${skill.source}`}> <Box key={`${skill.name}-${skill.source}`}>
<Text>{getCommandName(skill)}</Text> <Text>{getCommandName(skill)}</Text>
{scopeTag && ( {scopeTag && (
<Text color={scopeTag.color}> [{scopeTag.label}]</Text> <Text color={scopeTag.color as keyof Theme}> [{scopeTag.label}]</Text>
)} )}
<Text dimColor> <Text dimColor>
{pluginName ? ` · ${pluginName}` : ''} · {tokenDisplay} description {pluginName ? ` · ${pluginName}` : ''} · {tokenDisplay} description

View File

@@ -122,7 +122,8 @@ function UltraplanSessionDetail({
let lastBlock: { name: string; input: unknown } | null = null let lastBlock: { name: string; input: unknown } | null = null
for (const msg of session.log) { for (const msg of session.log) {
if (msg.type !== 'assistant') continue 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 if (block.type !== 'tool_use') continue
calls++ calls++
lastBlock = block lastBlock = block

View File

@@ -225,13 +225,16 @@ export function useReplBridge(
const { resolveAndPrepend } = await import( const { resolveAndPrepend } = await import(
'../bridge/inboundAttachments.js' '../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')) { if (feature('KAIROS_GITHUB_WEBHOOKS')) {
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
const { sanitizeInboundWebhookContent } = const { sanitizeInboundWebhookContent } =
require('../bridge/webhookSanitizer.js') as typeof import('../bridge/webhookSanitizer.js') require('../bridge/webhookSanitizer.js') as typeof import('../bridge/webhookSanitizer.js')
/* eslint-enable @typescript-eslint/no-require-imports */ /* eslint-enable @typescript-eslint/no-require-imports */
sanitized = sanitizeInboundWebhookContent(fields.content) if (typeof sanitized === 'string') {
sanitized = sanitizeInboundWebhookContent(sanitized)
}
} }
const content = await resolveAndPrepend(msg, sanitized) const content = await resolveAndPrepend(msg, sanitized)

View File

@@ -56,7 +56,7 @@ export async function* queryModelGemini(
const standardTools = toolSchemas.filter( const standardTools = toolSchemas.filter(
(t): t is BetaToolUnion & { type: string } => { (t): t is BetaToolUnion & { type: string } => {
const anyTool = t as Record<string, unknown> const anyTool = t as unknown as Record<string, unknown>
return ( return (
anyTool.type !== 'advisor_20260301' && anyTool.type !== 'advisor_20260301' &&
anyTool.type !== 'computer_20250124' anyTool.type !== 'computer_20250124'
@@ -186,7 +186,7 @@ export async function* queryModelGemini(
yield createAssistantAPIErrorMessage({ yield createAssistantAPIErrorMessage({
content: `API Error: ${errorMessage}`, content: `API Error: ${errorMessage}`,
apiError: 'api_error', apiError: 'api_error',
error: error instanceof Error ? error : new Error(String(error)), error: (error instanceof Error ? error : new Error(String(error))) as Error,
}) })
} }
} }

View File

@@ -2,6 +2,10 @@ import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/me
import type { SystemPrompt } from '../../../utils/systemPromptType.js' import type { SystemPrompt } from '../../../utils/systemPromptType.js'
import type { Message, StreamEvent, SystemAPIErrorMessage, AssistantMessage } from '../../../types/message.js' import type { Message, StreamEvent, SystemAPIErrorMessage, AssistantMessage } from '../../../types/message.js'
import type { Tools } from '../../../Tool.js' import type { Tools } from '../../../Tool.js'
import type {
ChatCompletionChunk,
ChatCompletionCreateParamsStreaming,
} from 'openai/resources/chat/completions/completions.mjs'
import { getGrokClient } from './client.js' import { getGrokClient } from './client.js'
import { anthropicMessagesToOpenAI } from '../openai/convertMessages.js' import { anthropicMessagesToOpenAI } from '../openai/convertMessages.js'
import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../openai/convertTools.js' import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../openai/convertTools.js'
@@ -51,7 +55,7 @@ export async function* queryModelGrok(
) )
const standardTools = toolSchemas.filter( const standardTools = toolSchemas.filter(
(t): t is BetaToolUnion & { type: string } => { (t): t is BetaToolUnion & { type: string } => {
const anyT = t as Record<string, unknown> const anyT = t as unknown as Record<string, unknown>
return anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124' return anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
}, },
) )
@@ -62,7 +66,7 @@ export async function* queryModelGrok(
const client = getGrokClient({ const client = getGrokClient({
maxRetries: 0, maxRetries: 0,
fetchOverride: options.fetchOverride, fetchOverride: options.fetchOverride as typeof fetch | undefined,
source: options.querySource, source: options.querySource,
}) })
@@ -81,13 +85,13 @@ export async function* queryModelGrok(
...(options.temperatureOverride !== undefined && { ...(options.temperatureOverride !== undefined && {
temperature: options.temperatureOverride, temperature: options.temperatureOverride,
}), }),
}, } as ChatCompletionCreateParamsStreaming,
{ {
signal, signal,
}, },
) )
const adaptedStream = adaptOpenAIStreamToAnthropic(stream, grokModel) const adaptedStream = adaptOpenAIStreamToAnthropic(stream as AsyncIterable<ChatCompletionChunk>, grokModel)
const contentBlocks: Record<number, any> = {} const contentBlocks: Record<number, any> = {}
let partialMessage: any = undefined let partialMessage: any = undefined
@@ -186,7 +190,7 @@ export async function* queryModelGrok(
yield createAssistantAPIErrorMessage({ yield createAssistantAPIErrorMessage({
content: `API Error: ${errorMessage}`, content: `API Error: ${errorMessage}`,
apiError: 'api_error', apiError: 'api_error',
error: error instanceof Error ? error : new Error(String(error)), error: (error instanceof Error ? error : new Error(String(error))) as Error,
}) })
} }
} }

View File

@@ -1,6 +1,6 @@
import OpenAI from 'openai' import OpenAI from 'openai'
import { getProxyFetchOptions } from 'src/utils/proxy.js' import { getProxyFetchOptions } from 'src/utils/proxy.js'
import { isEnvTruthy } from '../../utils/envUtils.js' import { isEnvTruthy } from 'src/utils/envUtils.js'
/** /**
* Environment variables: * Environment variables:

View File

@@ -7,6 +7,11 @@ import type {
AssistantMessage, AssistantMessage,
} from '../../../types/message.js' } from '../../../types/message.js'
import type { Tools } from '../../../Tool.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 { getOpenAIClient } from './client.js'
import { anthropicMessagesToOpenAI } from './convertMessages.js' import { anthropicMessagesToOpenAI } from './convertMessages.js'
import { import {
@@ -82,7 +87,7 @@ export function buildOpenAIRequestBody(params: {
toolChoice: any toolChoice: any
enableThinking: boolean enableThinking: boolean
temperatureOverride?: number temperatureOverride?: number
}): Record<string, any> { }): ChatCompletionCreateParamsStreaming {
const { model, messages, tools, toolChoice, enableThinking, temperatureOverride } = params const { model, messages, tools, toolChoice, enableThinking, temperatureOverride } = params
return { return {
model, model,
@@ -183,7 +188,7 @@ export async function* queryModelOpenAI(
// 7. Filter out non-standard tools (server tools like advisor) // 7. Filter out non-standard tools (server tools like advisor)
const standardTools = toolSchemas.filter( const standardTools = toolSchemas.filter(
(t): t is BetaToolUnion & { type: string } => { (t): t is BetaToolUnion & { type: string } => {
const anyT = t as Record<string, unknown> const anyT = t as unknown as Record<string, unknown>
return ( return (
anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124' anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
) )
@@ -349,7 +354,7 @@ export async function* queryModelOpenAI(
yield createAssistantAPIErrorMessage({ yield createAssistantAPIErrorMessage({
content: `API Error: ${errorMessage}`, content: `API Error: ${errorMessage}`,
apiError: 'api_error', apiError: 'api_error',
error: error instanceof Error ? error : new Error(String(error)), error: (error instanceof Error ? error : new Error(String(error))) as Error,
}) })
} }
} }

View File

@@ -125,7 +125,7 @@ function processProgressMessages(
isAgentRunning: boolean, isAgentRunning: boolean,
): ProcessedMessage[] { ): ProcessedMessage[] {
// Only process for ants // Only process for ants
if ("external" !== 'ant') { if (process.env.USER_TYPE !== 'ant') {
return messages return messages
.filter( .filter(
(m): m is ProgressMessage<AgentToolProgress> => (m): m is ProgressMessage<AgentToolProgress> =>
@@ -411,7 +411,7 @@ export function renderToolResultMessage(
const finalAssistantMessage = createAssistantMessage({ const finalAssistantMessage = createAssistantMessage({
content: completionMessage, content: completionMessage,
usage: { ...usage, inference_geo: null, iterations: null, speed: null }, usage: { ...usage, inference_geo: null, iterations: null, speed: null } as typeof usage,
}) })
return ( return (
@@ -866,7 +866,7 @@ export function renderGroupedAgentToolUse(
taskDescription = parsedInput.data.description taskDescription = parsedInput.data.description
// Use the custom agent definition's color on the type, not the name // Use the custom agent definition's color on the type, not the name
descriptionColor = isCustomSubagentType(subagentType) descriptionColor = isCustomSubagentType(subagentType)
? (getAgentColor(subagentType) as keyof Theme | undefined) ? getAgentColor(subagentType)
: undefined : undefined
} else { } else {
agentType = parsedInput.success agentType = parsedInput.success
@@ -1019,7 +1019,7 @@ export function userFacingNameBackgroundColor(
} }
// Get the color for this agent // Get the color for this agent
return getAgentColor(input.subagent_type) as keyof Theme | undefined return getAgentColor(input.subagent_type)
} }
export function extractLastToolInfo( export function extractLastToolInfo(

View File

@@ -423,18 +423,21 @@ export function createCliExecutor(opts: {
targetW, targetW,
targetH, targetH,
opts.preferredDisplayId, opts.preferredDisplayId,
opts.autoResolve,
opts.doHide,
), ),
) )
// Ensure the result has fields expected by toolCalls.ts (hidden, displayId). // Ensure the result has fields expected by toolCalls.ts (hidden, displayId).
// macOS native returns these from Swift; our cross-platform ComputerUseAPI // macOS native returns these from Swift; our cross-platform ComputerUseAPI
// returns {base64, width, height} — fill in the missing fields. // returns {base64, width, height} — fill in the missing fields.
const baseResult = raw as Partial<ResolvePrepareCaptureResult> & { width?: number; height?: number }
return { return {
...raw, ...raw,
hidden: (raw as any).hidden ?? [], displayWidth: baseResult.displayWidth ?? baseResult.width,
displayId: (raw as any).displayId ?? opts.preferredDisplayId ?? d.displayId, 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
}, },
/** /**

View File

@@ -9,6 +9,7 @@
*/ */
import * as path from 'path' import * as path from 'path'
import type { Writable } from 'stream'
interface BridgeRequest { interface BridgeRequest {
id: number id: number
@@ -48,7 +49,7 @@ export function ensureBridge(): boolean {
}) })
// Read stdout lines asynchronously // Read stdout lines asynchronously
const reader = bridgeProc.stdout.getReader() const reader = (bridgeProc.stdout as ReadableStream<Uint8Array>).getReader()
const readLoop = async () => { const readLoop = async () => {
try { try {
while (true) { while (true) {
@@ -114,12 +115,12 @@ export async function call<T = unknown>(
}, timeoutMs) }, timeoutMs)
// Clear timeout on resolve/reject // Clear timeout on resolve/reject
const origResolve = resolve const origResolve = resolve as (v: unknown) => void
const origReject = reject const origReject = reject
pendingRequests.set(id, { pendingRequests.set(id, {
resolve: v => { resolve: v => {
clearTimeout(timer) clearTimeout(timer)
;(origResolve as any)(v) origResolve(v)
}, },
reject: e => { reject: e => {
clearTimeout(timer) clearTimeout(timer)
@@ -128,8 +129,14 @@ export async function call<T = unknown>(
}) })
try { try {
bridgeProc!.stdin.write(JSON.stringify(req) + '\n') const stdin = bridgeProc!.stdin
bridgeProc!.stdin.flush() if (stdin) {
const writable = stdin as Writable
writable.write(JSON.stringify(req) + '\n')
if (typeof writable.flush === 'function') {
writable.flush()
}
}
} catch (err) { } catch (err) {
clearTimeout(timer) clearTimeout(timer)
pendingRequests.delete(id) pendingRequests.delete(id)
@@ -176,7 +183,13 @@ export function callSync<T = unknown>(
export function stopBridge(): void { export function stopBridge(): void {
if (bridgeProc) { if (bridgeProc) {
try { try {
bridgeProc.stdin.end() const stdin = bridgeProc.stdin
if (stdin) {
const writable = stdin as Writable
if (typeof writable.end === 'function') {
writable.end()
}
}
bridgeProc.kill() bridgeProc.kill()
} catch {} } catch {}
bridgeProc = null bridgeProc = null

View File

@@ -137,13 +137,14 @@ export function createStreamlinedTransformer(): (
): StdoutMessage | null { ): StdoutMessage | null {
switch (message.type) { switch (message.type) {
case 'assistant': { case 'assistant': {
const content = message.message.content const messageContent = (message as SDKAssistantMessage).message
const content = messageContent?.content
const text = Array.isArray(content) const text = Array.isArray(content)
? extractTextContent(content, '\n').trim() ? extractTextContent(content, '\n').trim()
: '' : ''
// Accumulate tool counts from this message // Accumulate tool counts from this message
accumulateToolUses(message, cumulativeCounts) accumulateToolUses(message as SDKAssistantMessage, cumulativeCounts)
if (text.length > 0) { if (text.length > 0) {
// Text message: emit text only, reset counts // Text message: emit text only, reset counts