fix(types): simplify bridge transport message type

Replace StdoutMessageWithSession conditional type with simpler
TransportMessage intersection type. The conditional type was
over-engineered for what is just StdoutMessage & { session_id? }.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-09 23:55:54 +08:00
parent 875510e1eb
commit 637531f81f
2 changed files with 62 additions and 60 deletions

View File

@@ -74,13 +74,15 @@ 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 * StdoutMessage with optional session_id. The transport layer accepts
* to messages at runtime, but the Zod schemas don't include it. This type * StdoutMessage but we add session_id at runtime. Using optional because
* makes it explicit that we're adding session_id to each message variant. * 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 type TransportMessage = StdoutMessage & { session_id?: string }
? T & { session_id: string }
: never
const ANTHROPIC_VERSION = '2023-06-01' const ANTHROPIC_VERSION = '2023-06-01'
@@ -619,17 +621,17 @@ 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: StdoutMessageWithSession[] = toSDKMessages(msgs).map(m => ({ const events: TransportMessage[] = toSDKMessages(msgs).map(m => ({
...m, ...m,
session_id: sessionId, session_id: sessionId,
})) })) as TransportMessage[]
if (msgs.some(m => m.type === 'user')) { if (msgs.some(m => m.type === 'user')) {
transport.reportState('running') transport.reportState('running')
} }
logForDebugging( logForDebugging(
`[remote-bridge] Drained ${msgs.length} queued message(s) after flush`, `[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<void> { async function flushHistory(msgs: Message[]): Promise<void> {
@@ -647,10 +649,10 @@ 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: StdoutMessageWithSession[] = toSDKMessages(capped).map(m => ({ const events: TransportMessage[] = toSDKMessages(capped).map(m => ({
...m, ...m,
session_id: sessionId, session_id: sessionId,
})) })) as TransportMessage[]
if (events.length === 0) return if (events.length === 0) return
// Mid-turn init: if Remote Control is enabled while a query is running, // 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' // the last eligible message is a user prompt or tool_result (both 'user'
@@ -663,7 +665,7 @@ export async function initEnvLessBridgeCore(
transport.reportState('running') transport.reportState('running')
} }
logForDebugging(`[remote-bridge] Flushing ${events.length} history events`) logForDebugging(`[remote-bridge] Flushing ${events.length} history events`)
await transport.writeBatch(events) await transport.writeBatch(events as StdoutMessage[])
} }
// ── 9. Teardown ─────────────────────────────────────────────────────────── // ── 9. Teardown ───────────────────────────────────────────────────────────
@@ -686,11 +688,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')
const resultMsg: StdoutMessageWithSession = { const resultMsg = {
...makeResultMessage(sessionId), ...makeResultMessage(sessionId),
session_id: sessionId, session_id: sessionId,
} } as unknown as TransportMessage
void transport.write(resultMsg) void transport.write(resultMsg as StdoutMessage)
let token = getAccessToken() let token = getAccessToken()
let status = await archiveSession( let status = await archiveSession(
sessionId, sessionId,
@@ -809,10 +811,10 @@ export async function initEnvLessBridgeCore(
} }
for (const msg of filtered) recentPostedUUIDs.add(msg.uuid) for (const msg of filtered) recentPostedUUIDs.add(msg.uuid)
const events: StdoutMessageWithSession[] = toSDKMessages(filtered).map(m => ({ const events: TransportMessage[] = toSDKMessages(filtered).map(m => ({
...m, ...m,
session_id: sessionId, session_id: sessionId,
})) })) as TransportMessage[]
// v2 does not derive worker_status from events server-side (unlike v1 // v2 does not derive worker_status from events server-side (unlike v1
// session-ingress session_status_updater.go). Push it from here so the // 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 // CCR web session list shows Running instead of stuck on Idle. A user
@@ -822,7 +824,7 @@ export async function initEnvLessBridgeCore(
transport.reportState('running') transport.reportState('running')
} }
logForDebugging(`[remote-bridge] Sending ${filtered.length} message(s)`) logForDebugging(`[remote-bridge] Sending ${filtered.length} message(s)`)
void transport.writeBatch(events) void transport.writeBatch(events as StdoutMessage[])
}, },
writeSdkMessages(messages: SDKMessage[]) { writeSdkMessages(messages: SDKMessage[]) {
const filtered = messages.filter( const filtered = messages.filter(
@@ -842,11 +844,11 @@ export async function initEnvLessBridgeCore(
) )
return 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') { if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') {
transport.reportState('requires_action') transport.reportState('requires_action')
} }
void transport.write(event) void transport.write(event as StdoutMessage)
logForDebugging( logForDebugging(
`[remote-bridge] Sent control_request request_id=${request.request_id}`, `[remote-bridge] Sent control_request request_id=${request.request_id}`,
) )
@@ -858,9 +860,9 @@ export async function initEnvLessBridgeCore(
) )
return return
} }
const event: StdoutMessageWithSession = { ...response, session_id: sessionId } const event: TransportMessage = { ...response, session_id: sessionId } as TransportMessage
transport.reportState('running') transport.reportState('running')
void transport.write(event) void transport.write(event as StdoutMessage)
logForDebugging('[remote-bridge] Sent control_response') logForDebugging('[remote-bridge] Sent control_response')
}, },
sendControlCancelRequest(requestId: string) { sendControlCancelRequest(requestId: string) {
@@ -870,16 +872,16 @@ export async function initEnvLessBridgeCore(
) )
return return
} }
const event: StdoutMessageWithSession = { const event: TransportMessage = {
type: 'control_cancel_request' as const, type: 'control_cancel_request' as const,
request_id: requestId, request_id: requestId,
session_id: sessionId, session_id: sessionId,
} } as TransportMessage
// Hook/classifier/channel/recheck resolved the permission locally — // Hook/classifier/channel/recheck resolved the permission locally —
// interactiveHandler calls only cancelRequest (no sendResponse) on // interactiveHandler calls only cancelRequest (no sendResponse) on
// those paths, so without this the server stays on requires_action. // those paths, so without this the server stays on requires_action.
transport.reportState('running') transport.reportState('running')
void transport.write(event) void transport.write(event as StdoutMessage)
logForDebugging( logForDebugging(
`[remote-bridge] Sent control_cancel_request request_id=${requestId}`, `[remote-bridge] Sent control_cancel_request request_id=${requestId}`,
) )
@@ -890,11 +892,11 @@ export async function initEnvLessBridgeCore(
return return
} }
transport.reportState('idle') transport.reportState('idle')
const resultMsg: StdoutMessageWithSession = { const resultMsg = {
...makeResultMessage(sessionId), ...makeResultMessage(sessionId),
session_id: sessionId, session_id: sessionId,
} } as unknown as TransportMessage
void transport.write(resultMsg) void transport.write(resultMsg as StdoutMessage)
logForDebugging(`[remote-bridge] Sent result`) logForDebugging(`[remote-bridge] Sent result`)
}, },
async teardown() { async teardown() {

View File

@@ -57,13 +57,15 @@ import type { StdoutMessage } from '../entrypoints/sdk/controlTypes.js'
import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js' import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
/** /**
* StdoutMessage with session_id added. The transport layer adds session_id * StdoutMessage with optional session_id. The transport layer accepts
* to messages at runtime, but the Zod schemas don't include it. This type * StdoutMessage but we add session_id at runtime. Using optional because
* makes it explicit that we're adding session_id to each message variant. * 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 type TransportMessage = StdoutMessage & { session_id?: string }
? 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 {
@@ -876,14 +878,14 @@ export async function initBridgeCore(
recentPostedUUIDs.add(msg.uuid) recentPostedUUIDs.add(msg.uuid)
} }
const sdkMessages = toSDKMessages(msgs) const sdkMessages = toSDKMessages(msgs)
const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({ const events: TransportMessage[] = sdkMessages.map(sdkMsg => ({
...sdkMsg, ...sdkMsg,
session_id: currentSessionId, session_id: currentSessionId,
})) })) as TransportMessage[]
logForDebugging( logForDebugging(
`[bridge:repl] Drained ${msgs.length} pending message(s) after flush`, `[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 // Teardown reference — set after definition below. All callers are async
@@ -1296,14 +1298,12 @@ 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: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({ const events: TransportMessage[] = sdkMessages.map(sdkMsg => ({
...sdkMsg, ...sdkMsg,
session_id: currentSessionId, session_id: currentSessionId,
})) })) as TransportMessage[]
const dropsBefore = newTransport.droppedBatchCount const dropsBefore = newTransport.droppedBatchCount
void newTransport void newTransport.writeBatch(events as StdoutMessage[]).then(() => {
.writeBatch(events)
.then(() => {
// If any batch was dropped during this flush (SI down for // If any batch was dropped during this flush (SI down for
// maxConsecutiveFailures attempts), flush() still resolved // maxConsecutiveFailures attempts), flush() still resolved
// normally but the events were NOT delivered. Don't mark // normally but the events were NOT delivered. Don't mark
@@ -1666,11 +1666,11 @@ export async function initBridgeCore(
transport = null transport = null
flushGate.drop() flushGate.drop()
if (teardownTransport) { if (teardownTransport) {
const resultMsg: StdoutMessageWithSession = { const resultMsg = {
...makeResultMessage(currentSessionId), ...makeResultMessage(currentSessionId),
session_id: currentSessionId, session_id: currentSessionId,
} } as unknown as TransportMessage
void teardownTransport.write(resultMsg) void teardownTransport.write(resultMsg as StdoutMessage)
} }
const stopWorkP = currentWorkId const stopWorkP = currentWorkId
@@ -1793,11 +1793,11 @@ 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: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({ const events: TransportMessage[] = sdkMessages.map(sdkMsg => ({
...sdkMsg, ...sdkMsg,
session_id: currentSessionId, session_id: currentSessionId,
})) })) as TransportMessage[]
void transport.writeBatch(events) void transport.writeBatch(events as StdoutMessage[])
}, },
writeSdkMessages(messages) { writeSdkMessages(messages) {
// Daemon path: query() already yields SDKMessage, skip conversion. // Daemon path: query() already yields SDKMessage, skip conversion.
@@ -1818,8 +1818,8 @@ 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: StdoutMessageWithSession[] = filtered.map(m => ({ ...m, session_id: currentSessionId })) const events: TransportMessage[] = filtered.map(m => ({ ...m, session_id: currentSessionId })) as TransportMessage[]
void transport.writeBatch(events) void transport.writeBatch(events as StdoutMessage[])
}, },
sendControlRequest(request: SDKControlRequest) { sendControlRequest(request: SDKControlRequest) {
if (!transport) { if (!transport) {
@@ -1828,8 +1828,8 @@ export async function initBridgeCore(
) )
return return
} }
const event: StdoutMessageWithSession = { ...request, session_id: currentSessionId } const event: TransportMessage = { ...request, session_id: currentSessionId } as TransportMessage
void transport.write(event) void transport.write(event as StdoutMessage)
logForDebugging( logForDebugging(
`[bridge:repl] Sent control_request request_id=${request.request_id}`, `[bridge:repl] Sent control_request request_id=${request.request_id}`,
) )
@@ -1841,8 +1841,8 @@ export async function initBridgeCore(
) )
return return
} }
const event: StdoutMessageWithSession = { ...response, session_id: currentSessionId } const event: TransportMessage = { ...response, session_id: currentSessionId } as TransportMessage
void transport.write(event) void transport.write(event as StdoutMessage)
logForDebugging('[bridge:repl] Sent control_response') logForDebugging('[bridge:repl] Sent control_response')
}, },
sendControlCancelRequest(requestId: string) { sendControlCancelRequest(requestId: string) {
@@ -1852,12 +1852,12 @@ export async function initBridgeCore(
) )
return return
} }
const event: StdoutMessageWithSession = { const event: TransportMessage = {
type: 'control_cancel_request' as const, type: 'control_cancel_request' as const,
request_id: requestId, request_id: requestId,
session_id: currentSessionId, session_id: currentSessionId,
} } as TransportMessage
void transport.write(event) void transport.write(event as StdoutMessage)
logForDebugging( logForDebugging(
`[bridge:repl] Sent control_cancel_request request_id=${requestId}`, `[bridge:repl] Sent control_cancel_request request_id=${requestId}`,
) )
@@ -1869,11 +1869,11 @@ export async function initBridgeCore(
) )
return return
} }
const resultMsg: StdoutMessageWithSession = { const resultMsg = {
...makeResultMessage(currentSessionId), ...makeResultMessage(currentSessionId),
session_id: currentSessionId, session_id: currentSessionId,
} } as unknown as TransportMessage
void transport.write(resultMsg) void transport.write(resultMsg as StdoutMessage)
logForDebugging( logForDebugging(
`[bridge:repl] Sent result for session=${currentSessionId}`, `[bridge:repl] Sent result for session=${currentSessionId}`,
) )