diff --git a/src/bridge/remoteBridgeCore.ts b/src/bridge/remoteBridgeCore.ts index 884f5eed4..c3b0547e4 100644 --- a/src/bridge/remoteBridgeCore.ts +++ b/src/bridge/remoteBridgeCore.ts @@ -74,13 +74,15 @@ 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. + * StdoutMessage with optional session_id. The transport layer accepts + * StdoutMessage but we add session_id at runtime. Using optional because + * the type system can't verify that adding session_id to a union type + * is always valid, even though it is at runtime. + * + * We need to use 'as StdoutMessage' when passing to transport because + * TypeScript can't verify that objects with session_id are valid StdoutMessage. */ -type StdoutMessageWithSession = StdoutMessage extends infer T - ? T & { session_id: string } - : never +type TransportMessage = StdoutMessage & { session_id?: string } const ANTHROPIC_VERSION = '2023-06-01' @@ -619,17 +621,17 @@ export async function initEnvLessBridgeCore( const msgs = flushGate.end() if (msgs.length === 0) return for (const msg of msgs) recentPostedUUIDs.add(msg.uuid) - const events: StdoutMessageWithSession[] = toSDKMessages(msgs).map(m => ({ + const events: TransportMessage[] = toSDKMessages(msgs).map(m => ({ ...m, session_id: sessionId, - })) + })) as TransportMessage[] if (msgs.some(m => m.type === 'user')) { transport.reportState('running') } logForDebugging( `[remote-bridge] Drained ${msgs.length} queued message(s) after flush`, ) - void transport.writeBatch(events) + void transport.writeBatch(events as StdoutMessage[]) } async function flushHistory(msgs: Message[]): Promise { @@ -647,10 +649,10 @@ export async function initEnvLessBridgeCore( `[remote-bridge] Capped initial flush: ${eligible.length} -> ${capped.length} (cap=${initialHistoryCap})`, ) } - const events: StdoutMessageWithSession[] = toSDKMessages(capped).map(m => ({ + const events: TransportMessage[] = toSDKMessages(capped).map(m => ({ ...m, session_id: sessionId, - })) + })) as TransportMessage[] if (events.length === 0) return // Mid-turn init: if Remote Control is enabled while a query is running, // the last eligible message is a user prompt or tool_result (both 'user' @@ -663,7 +665,7 @@ export async function initEnvLessBridgeCore( transport.reportState('running') } logForDebugging(`[remote-bridge] Flushing ${events.length} history events`) - await transport.writeBatch(events) + await transport.writeBatch(events as StdoutMessage[]) } // ── 9. Teardown ─────────────────────────────────────────────────────────── @@ -686,11 +688,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') - const resultMsg: StdoutMessageWithSession = { + const resultMsg = { ...makeResultMessage(sessionId), session_id: sessionId, - } - void transport.write(resultMsg) + } as unknown as TransportMessage + void transport.write(resultMsg as StdoutMessage) let token = getAccessToken() let status = await archiveSession( sessionId, @@ -809,10 +811,10 @@ export async function initEnvLessBridgeCore( } for (const msg of filtered) recentPostedUUIDs.add(msg.uuid) - const events: StdoutMessageWithSession[] = toSDKMessages(filtered).map(m => ({ + const events: TransportMessage[] = toSDKMessages(filtered).map(m => ({ ...m, session_id: sessionId, - })) + })) as TransportMessage[] // v2 does not derive worker_status from events server-side (unlike v1 // session-ingress session_status_updater.go). Push it from here so the // CCR web session list shows Running instead of stuck on Idle. A user @@ -822,7 +824,7 @@ export async function initEnvLessBridgeCore( transport.reportState('running') } logForDebugging(`[remote-bridge] Sending ${filtered.length} message(s)`) - void transport.writeBatch(events) + void transport.writeBatch(events as StdoutMessage[]) }, writeSdkMessages(messages: SDKMessage[]) { const filtered = messages.filter( @@ -842,11 +844,11 @@ export async function initEnvLessBridgeCore( ) return } - const event: StdoutMessageWithSession = { ...request, session_id: sessionId } + const event: TransportMessage = { ...request, session_id: sessionId } as TransportMessage if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') { transport.reportState('requires_action') } - void transport.write(event) + void transport.write(event as StdoutMessage) logForDebugging( `[remote-bridge] Sent control_request request_id=${request.request_id}`, ) @@ -858,9 +860,9 @@ export async function initEnvLessBridgeCore( ) return } - const event: StdoutMessageWithSession = { ...response, session_id: sessionId } + const event: TransportMessage = { ...response, session_id: sessionId } as TransportMessage transport.reportState('running') - void transport.write(event) + void transport.write(event as StdoutMessage) logForDebugging('[remote-bridge] Sent control_response') }, sendControlCancelRequest(requestId: string) { @@ -870,16 +872,16 @@ export async function initEnvLessBridgeCore( ) return } - const event: StdoutMessageWithSession = { + const event: TransportMessage = { type: 'control_cancel_request' as const, request_id: requestId, session_id: sessionId, - } + } as TransportMessage // Hook/classifier/channel/recheck resolved the permission locally — // interactiveHandler calls only cancelRequest (no sendResponse) on // those paths, so without this the server stays on requires_action. transport.reportState('running') - void transport.write(event) + void transport.write(event as StdoutMessage) logForDebugging( `[remote-bridge] Sent control_cancel_request request_id=${requestId}`, ) @@ -890,11 +892,11 @@ export async function initEnvLessBridgeCore( return } transport.reportState('idle') - const resultMsg: StdoutMessageWithSession = { + const resultMsg = { ...makeResultMessage(sessionId), session_id: sessionId, - } - void transport.write(resultMsg) + } as unknown as TransportMessage + void transport.write(resultMsg as StdoutMessage) logForDebugging(`[remote-bridge] Sent result`) }, async teardown() { diff --git a/src/bridge/replBridge.ts b/src/bridge/replBridge.ts index afab1be2e..1d91c9990 100644 --- a/src/bridge/replBridge.ts +++ b/src/bridge/replBridge.ts @@ -57,13 +57,15 @@ 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. + * StdoutMessage with optional session_id. The transport layer accepts + * StdoutMessage but we add session_id at runtime. Using optional because + * the type system can't verify that adding session_id to a union type + * is always valid, even though it is at runtime. + * + * We need to use 'as StdoutMessage' when passing to transport because + * TypeScript can't verify that objects with session_id are valid StdoutMessage. */ -type StdoutMessageWithSession = StdoutMessage extends infer T - ? T & { session_id: string } - : never +type TransportMessage = StdoutMessage & { session_id?: string } import { createCapacityWake, type CapacitySignal } from './capacityWake.js' import { FlushGate } from './flushGate.js' import { @@ -876,14 +878,14 @@ export async function initBridgeCore( recentPostedUUIDs.add(msg.uuid) } const sdkMessages = toSDKMessages(msgs) - const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({ + const events: TransportMessage[] = sdkMessages.map(sdkMsg => ({ ...sdkMsg, session_id: currentSessionId, - })) + })) as TransportMessage[] logForDebugging( `[bridge:repl] Drained ${msgs.length} pending message(s) after flush`, ) - void transport.writeBatch(events) + void transport.writeBatch(events as StdoutMessage[]) } // Teardown reference — set after definition below. All callers are async @@ -1296,14 +1298,12 @@ export async function initBridgeCore( logForDebugging( `[bridge:repl] Flushing ${sdkMessages.length} initial message(s) via transport`, ) - const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({ + const events: TransportMessage[] = sdkMessages.map(sdkMsg => ({ ...sdkMsg, session_id: currentSessionId, - })) + })) as TransportMessage[] const dropsBefore = newTransport.droppedBatchCount - void newTransport - .writeBatch(events) - .then(() => { + void newTransport.writeBatch(events as StdoutMessage[]).then(() => { // If any batch was dropped during this flush (SI down for // maxConsecutiveFailures attempts), flush() still resolved // normally but the events were NOT delivered. Don't mark @@ -1666,11 +1666,11 @@ export async function initBridgeCore( transport = null flushGate.drop() if (teardownTransport) { - const resultMsg: StdoutMessageWithSession = { + const resultMsg = { ...makeResultMessage(currentSessionId), session_id: currentSessionId, - } - void teardownTransport.write(resultMsg) + } as unknown as TransportMessage + void teardownTransport.write(resultMsg as StdoutMessage) } const stopWorkP = currentWorkId @@ -1793,11 +1793,11 @@ 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: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({ + const events: TransportMessage[] = sdkMessages.map(sdkMsg => ({ ...sdkMsg, session_id: currentSessionId, - })) - void transport.writeBatch(events) + })) as TransportMessage[] + void transport.writeBatch(events as StdoutMessage[]) }, writeSdkMessages(messages) { // Daemon path: query() already yields SDKMessage, skip conversion. @@ -1818,8 +1818,8 @@ export async function initBridgeCore( for (const msg of filtered) { if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string) } - const events: StdoutMessageWithSession[] = filtered.map(m => ({ ...m, session_id: currentSessionId })) - void transport.writeBatch(events) + const events: TransportMessage[] = filtered.map(m => ({ ...m, session_id: currentSessionId })) as TransportMessage[] + void transport.writeBatch(events as StdoutMessage[]) }, sendControlRequest(request: SDKControlRequest) { if (!transport) { @@ -1828,8 +1828,8 @@ export async function initBridgeCore( ) return } - const event: StdoutMessageWithSession = { ...request, session_id: currentSessionId } - void transport.write(event) + const event: TransportMessage = { ...request, session_id: currentSessionId } as TransportMessage + void transport.write(event as StdoutMessage) logForDebugging( `[bridge:repl] Sent control_request request_id=${request.request_id}`, ) @@ -1841,8 +1841,8 @@ export async function initBridgeCore( ) return } - const event: StdoutMessageWithSession = { ...response, session_id: currentSessionId } - void transport.write(event) + const event: TransportMessage = { ...response, session_id: currentSessionId } as TransportMessage + void transport.write(event as StdoutMessage) logForDebugging('[bridge:repl] Sent control_response') }, sendControlCancelRequest(requestId: string) { @@ -1852,12 +1852,12 @@ export async function initBridgeCore( ) return } - const event: StdoutMessageWithSession = { + const event: TransportMessage = { type: 'control_cancel_request' as const, request_id: requestId, session_id: currentSessionId, - } - void transport.write(event) + } as TransportMessage + void transport.write(event as StdoutMessage) logForDebugging( `[bridge:repl] Sent control_cancel_request request_id=${requestId}`, ) @@ -1869,11 +1869,11 @@ export async function initBridgeCore( ) return } - const resultMsg: StdoutMessageWithSession = { + const resultMsg = { ...makeResultMessage(currentSessionId), session_id: currentSessionId, - } - void transport.write(resultMsg) + } as unknown as TransportMessage + void transport.write(resultMsg as StdoutMessage) logForDebugging( `[bridge:repl] Sent result for session=${currentSessionId}`, )