Skip to content

Commit 637531f

Browse files
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>
1 parent 875510e commit 637531f

2 files changed

Lines changed: 62 additions & 60 deletions

File tree

src/bridge/remoteBridgeCore.ts

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,15 @@ import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
7474
import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
7575

7676
/**
77-
* StdoutMessage with session_id added. The transport layer adds session_id
78-
* to messages at runtime, but the Zod schemas don't include it. This type
79-
* makes it explicit that we're adding session_id to each message variant.
77+
* StdoutMessage with optional session_id. The transport layer accepts
78+
* StdoutMessage but we add session_id at runtime. Using optional because
79+
* the type system can't verify that adding session_id to a union type
80+
* is always valid, even though it is at runtime.
81+
*
82+
* We need to use 'as StdoutMessage' when passing to transport because
83+
* TypeScript can't verify that objects with session_id are valid StdoutMessage.
8084
*/
81-
type StdoutMessageWithSession = StdoutMessage extends infer T
82-
? T & { session_id: string }
83-
: never
85+
type TransportMessage = StdoutMessage & { session_id?: string }
8486

8587
const ANTHROPIC_VERSION = '2023-06-01'
8688

@@ -619,17 +621,17 @@ export async function initEnvLessBridgeCore(
619621
const msgs = flushGate.end()
620622
if (msgs.length === 0) return
621623
for (const msg of msgs) recentPostedUUIDs.add(msg.uuid)
622-
const events: StdoutMessageWithSession[] = toSDKMessages(msgs).map(m => ({
624+
const events: TransportMessage[] = toSDKMessages(msgs).map(m => ({
623625
...m,
624626
session_id: sessionId,
625-
}))
627+
})) as TransportMessage[]
626628
if (msgs.some(m => m.type === 'user')) {
627629
transport.reportState('running')
628630
}
629631
logForDebugging(
630632
`[remote-bridge] Drained ${msgs.length} queued message(s) after flush`,
631633
)
632-
void transport.writeBatch(events)
634+
void transport.writeBatch(events as StdoutMessage[])
633635
}
634636

635637
async function flushHistory(msgs: Message[]): Promise<void> {
@@ -647,10 +649,10 @@ export async function initEnvLessBridgeCore(
647649
`[remote-bridge] Capped initial flush: ${eligible.length} -> ${capped.length} (cap=${initialHistoryCap})`,
648650
)
649651
}
650-
const events: StdoutMessageWithSession[] = toSDKMessages(capped).map(m => ({
652+
const events: TransportMessage[] = toSDKMessages(capped).map(m => ({
651653
...m,
652654
session_id: sessionId,
653-
}))
655+
})) as TransportMessage[]
654656
if (events.length === 0) return
655657
// Mid-turn init: if Remote Control is enabled while a query is running,
656658
// the last eligible message is a user prompt or tool_result (both 'user'
@@ -663,7 +665,7 @@ export async function initEnvLessBridgeCore(
663665
transport.reportState('running')
664666
}
665667
logForDebugging(`[remote-bridge] Flushing ${events.length} history events`)
666-
await transport.writeBatch(events)
668+
await transport.writeBatch(events as StdoutMessage[])
667669
}
668670

669671
// ── 9. Teardown ───────────────────────────────────────────────────────────
@@ -686,11 +688,11 @@ export async function initEnvLessBridgeCore(
686688
// explicit sleep. close() sets closed=true which interrupts drain at the
687689
// next while-check, so close-before-archive drops the result.
688690
transport.reportState('idle')
689-
const resultMsg: StdoutMessageWithSession = {
691+
const resultMsg = {
690692
...makeResultMessage(sessionId),
691693
session_id: sessionId,
692-
}
693-
void transport.write(resultMsg)
694+
} as unknown as TransportMessage
695+
void transport.write(resultMsg as StdoutMessage)
694696
let token = getAccessToken()
695697
let status = await archiveSession(
696698
sessionId,
@@ -809,10 +811,10 @@ export async function initEnvLessBridgeCore(
809811
}
810812

811813
for (const msg of filtered) recentPostedUUIDs.add(msg.uuid)
812-
const events: StdoutMessageWithSession[] = toSDKMessages(filtered).map(m => ({
814+
const events: TransportMessage[] = toSDKMessages(filtered).map(m => ({
813815
...m,
814816
session_id: sessionId,
815-
}))
817+
})) as TransportMessage[]
816818
// v2 does not derive worker_status from events server-side (unlike v1
817819
// session-ingress session_status_updater.go). Push it from here so the
818820
// CCR web session list shows Running instead of stuck on Idle. A user
@@ -822,7 +824,7 @@ export async function initEnvLessBridgeCore(
822824
transport.reportState('running')
823825
}
824826
logForDebugging(`[remote-bridge] Sending ${filtered.length} message(s)`)
825-
void transport.writeBatch(events)
827+
void transport.writeBatch(events as StdoutMessage[])
826828
},
827829
writeSdkMessages(messages: SDKMessage[]) {
828830
const filtered = messages.filter(
@@ -842,11 +844,11 @@ export async function initEnvLessBridgeCore(
842844
)
843845
return
844846
}
845-
const event: StdoutMessageWithSession = { ...request, session_id: sessionId }
847+
const event: TransportMessage = { ...request, session_id: sessionId } as TransportMessage
846848
if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') {
847849
transport.reportState('requires_action')
848850
}
849-
void transport.write(event)
851+
void transport.write(event as StdoutMessage)
850852
logForDebugging(
851853
`[remote-bridge] Sent control_request request_id=${request.request_id}`,
852854
)
@@ -858,9 +860,9 @@ export async function initEnvLessBridgeCore(
858860
)
859861
return
860862
}
861-
const event: StdoutMessageWithSession = { ...response, session_id: sessionId }
863+
const event: TransportMessage = { ...response, session_id: sessionId } as TransportMessage
862864
transport.reportState('running')
863-
void transport.write(event)
865+
void transport.write(event as StdoutMessage)
864866
logForDebugging('[remote-bridge] Sent control_response')
865867
},
866868
sendControlCancelRequest(requestId: string) {
@@ -870,16 +872,16 @@ export async function initEnvLessBridgeCore(
870872
)
871873
return
872874
}
873-
const event: StdoutMessageWithSession = {
875+
const event: TransportMessage = {
874876
type: 'control_cancel_request' as const,
875877
request_id: requestId,
876878
session_id: sessionId,
877-
}
879+
} as TransportMessage
878880
// Hook/classifier/channel/recheck resolved the permission locally —
879881
// interactiveHandler calls only cancelRequest (no sendResponse) on
880882
// those paths, so without this the server stays on requires_action.
881883
transport.reportState('running')
882-
void transport.write(event)
884+
void transport.write(event as StdoutMessage)
883885
logForDebugging(
884886
`[remote-bridge] Sent control_cancel_request request_id=${requestId}`,
885887
)
@@ -890,11 +892,11 @@ export async function initEnvLessBridgeCore(
890892
return
891893
}
892894
transport.reportState('idle')
893-
const resultMsg: StdoutMessageWithSession = {
895+
const resultMsg = {
894896
...makeResultMessage(sessionId),
895897
session_id: sessionId,
896-
}
897-
void transport.write(resultMsg)
898+
} as unknown as TransportMessage
899+
void transport.write(resultMsg as StdoutMessage)
898900
logForDebugging(`[remote-bridge] Sent result`)
899901
},
900902
async teardown() {

src/bridge/replBridge.ts

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,15 @@ import type { StdoutMessage } from '../entrypoints/sdk/controlTypes.js'
5757
import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
5858

5959
/**
60-
* StdoutMessage with session_id added. The transport layer adds session_id
61-
* to messages at runtime, but the Zod schemas don't include it. This type
62-
* makes it explicit that we're adding session_id to each message variant.
60+
* StdoutMessage with optional session_id. The transport layer accepts
61+
* StdoutMessage but we add session_id at runtime. Using optional because
62+
* the type system can't verify that adding session_id to a union type
63+
* is always valid, even though it is at runtime.
64+
*
65+
* We need to use 'as StdoutMessage' when passing to transport because
66+
* TypeScript can't verify that objects with session_id are valid StdoutMessage.
6367
*/
64-
type StdoutMessageWithSession = StdoutMessage extends infer T
65-
? T & { session_id: string }
66-
: never
68+
type TransportMessage = StdoutMessage & { session_id?: string }
6769
import { createCapacityWake, type CapacitySignal } from './capacityWake.js'
6870
import { FlushGate } from './flushGate.js'
6971
import {
@@ -876,14 +878,14 @@ export async function initBridgeCore(
876878
recentPostedUUIDs.add(msg.uuid)
877879
}
878880
const sdkMessages = toSDKMessages(msgs)
879-
const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({
881+
const events: TransportMessage[] = sdkMessages.map(sdkMsg => ({
880882
...sdkMsg,
881883
session_id: currentSessionId,
882-
}))
884+
})) as TransportMessage[]
883885
logForDebugging(
884886
`[bridge:repl] Drained ${msgs.length} pending message(s) after flush`,
885887
)
886-
void transport.writeBatch(events)
888+
void transport.writeBatch(events as StdoutMessage[])
887889
}
888890

889891
// Teardown reference — set after definition below. All callers are async
@@ -1296,14 +1298,12 @@ export async function initBridgeCore(
12961298
logForDebugging(
12971299
`[bridge:repl] Flushing ${sdkMessages.length} initial message(s) via transport`,
12981300
)
1299-
const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({
1301+
const events: TransportMessage[] = sdkMessages.map(sdkMsg => ({
13001302
...sdkMsg,
13011303
session_id: currentSessionId,
1302-
}))
1304+
})) as TransportMessage[]
13031305
const dropsBefore = newTransport.droppedBatchCount
1304-
void newTransport
1305-
.writeBatch(events)
1306-
.then(() => {
1306+
void newTransport.writeBatch(events as StdoutMessage[]).then(() => {
13071307
// If any batch was dropped during this flush (SI down for
13081308
// maxConsecutiveFailures attempts), flush() still resolved
13091309
// normally but the events were NOT delivered. Don't mark
@@ -1666,11 +1666,11 @@ export async function initBridgeCore(
16661666
transport = null
16671667
flushGate.drop()
16681668
if (teardownTransport) {
1669-
const resultMsg: StdoutMessageWithSession = {
1669+
const resultMsg = {
16701670
...makeResultMessage(currentSessionId),
16711671
session_id: currentSessionId,
1672-
}
1673-
void teardownTransport.write(resultMsg)
1672+
} as unknown as TransportMessage
1673+
void teardownTransport.write(resultMsg as StdoutMessage)
16741674
}
16751675

16761676
const stopWorkP = currentWorkId
@@ -1793,11 +1793,11 @@ export async function initBridgeCore(
17931793
// Convert to SDK format and send via HTTP POST (HybridTransport).
17941794
// The web UI receives them via the subscribe WebSocket.
17951795
const sdkMessages = toSDKMessages(filtered)
1796-
const events: StdoutMessageWithSession[] = sdkMessages.map(sdkMsg => ({
1796+
const events: TransportMessage[] = sdkMessages.map(sdkMsg => ({
17971797
...sdkMsg,
17981798
session_id: currentSessionId,
1799-
}))
1800-
void transport.writeBatch(events)
1799+
})) as TransportMessage[]
1800+
void transport.writeBatch(events as StdoutMessage[])
18011801
},
18021802
writeSdkMessages(messages) {
18031803
// Daemon path: query() already yields SDKMessage, skip conversion.
@@ -1818,8 +1818,8 @@ export async function initBridgeCore(
18181818
for (const msg of filtered) {
18191819
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string)
18201820
}
1821-
const events: StdoutMessageWithSession[] = filtered.map(m => ({ ...m, session_id: currentSessionId }))
1822-
void transport.writeBatch(events)
1821+
const events: TransportMessage[] = filtered.map(m => ({ ...m, session_id: currentSessionId })) as TransportMessage[]
1822+
void transport.writeBatch(events as StdoutMessage[])
18231823
},
18241824
sendControlRequest(request: SDKControlRequest) {
18251825
if (!transport) {
@@ -1828,8 +1828,8 @@ export async function initBridgeCore(
18281828
)
18291829
return
18301830
}
1831-
const event: StdoutMessageWithSession = { ...request, session_id: currentSessionId }
1832-
void transport.write(event)
1831+
const event: TransportMessage = { ...request, session_id: currentSessionId } as TransportMessage
1832+
void transport.write(event as StdoutMessage)
18331833
logForDebugging(
18341834
`[bridge:repl] Sent control_request request_id=${request.request_id}`,
18351835
)
@@ -1841,8 +1841,8 @@ export async function initBridgeCore(
18411841
)
18421842
return
18431843
}
1844-
const event: StdoutMessageWithSession = { ...response, session_id: currentSessionId }
1845-
void transport.write(event)
1844+
const event: TransportMessage = { ...response, session_id: currentSessionId } as TransportMessage
1845+
void transport.write(event as StdoutMessage)
18461846
logForDebugging('[bridge:repl] Sent control_response')
18471847
},
18481848
sendControlCancelRequest(requestId: string) {
@@ -1852,12 +1852,12 @@ export async function initBridgeCore(
18521852
)
18531853
return
18541854
}
1855-
const event: StdoutMessageWithSession = {
1855+
const event: TransportMessage = {
18561856
type: 'control_cancel_request' as const,
18571857
request_id: requestId,
18581858
session_id: currentSessionId,
1859-
}
1860-
void transport.write(event)
1859+
} as TransportMessage
1860+
void transport.write(event as StdoutMessage)
18611861
logForDebugging(
18621862
`[bridge:repl] Sent control_cancel_request request_id=${requestId}`,
18631863
)
@@ -1869,11 +1869,11 @@ export async function initBridgeCore(
18691869
)
18701870
return
18711871
}
1872-
const resultMsg: StdoutMessageWithSession = {
1872+
const resultMsg = {
18731873
...makeResultMessage(currentSessionId),
18741874
session_id: currentSessionId,
1875-
}
1876-
void transport.write(resultMsg)
1875+
} as unknown as TransportMessage
1876+
void transport.write(resultMsg as StdoutMessage)
18771877
logForDebugging(
18781878
`[bridge:repl] Sent result for session=${currentSessionId}`,
18791879
)

0 commit comments

Comments
 (0)