@ai-sdk/harness-claude-code 1.0.80 → 1.0.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,8 @@ import {
8
8
  runBridge,
9
9
  type BridgeEvent,
10
10
  type BridgeTurn,
11
+ type Experimental_BridgeUserMessage,
12
+ type Experimental_BridgeUserMessageQueue,
11
13
  } from '@ai-sdk/harness/bridge';
12
14
  import { createCompactionLatch } from './compaction-latch';
13
15
  import type { StartMessage } from '../claude-code-bridge-protocol';
@@ -314,7 +316,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
314
316
 
315
317
  const queryInput = createQueryInput({
316
318
  initialUserMessage: start.prompt,
317
- pendingUserMessages: turn.pendingUserMessages,
319
+ userMessages: turn.experimental_userMessages,
318
320
  abortSignal: abortCtl.signal,
319
321
  });
320
322
  const skillsOption = toClaudeSkillsOption(start.skills);
@@ -419,6 +421,10 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
419
421
 
420
422
  const type = msg.type;
421
423
 
424
+ if (type === 'command_lifecycle') {
425
+ queryInput.handleLifecycle(msg);
426
+ }
427
+
422
428
  emitStreamEvent(msg);
423
429
 
424
430
  if (type === 'result') {
@@ -430,7 +436,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
430
436
  }
431
437
  const usage = msg.usage ?? msg.message?.usage;
432
438
  const harnessUsage = mapUsage(usage);
433
- if (harnessUsage) turnUsage = harnessUsage;
439
+ if (harnessUsage) turnUsage = addUsage(turnUsage, harnessUsage);
434
440
  if (typeof msg.total_cost_usd === 'number') {
435
441
  totalCostUsd = (totalCostUsd ?? 0) + msg.total_cost_usd;
436
442
  }
@@ -455,8 +461,11 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
455
461
  usage: harnessUsage ?? streamEventState.pendingStepUsage,
456
462
  });
457
463
  }
458
- queryInput.close();
459
- break;
464
+ queryInput.observeResult();
465
+ if (!queryInput.hasActiveUserMessages()) {
466
+ queryInput.close();
467
+ break;
468
+ }
460
469
  } else {
461
470
  emitTerminalError(
462
471
  (Array.isArray(msg.errors) ? msg.errors.join('\n') : undefined) ||
@@ -467,6 +476,11 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
467
476
  }
468
477
  continue;
469
478
  }
479
+
480
+ if (queryInput.hasObservedResult && !queryInput.hasActiveUserMessages()) {
481
+ queryInput.close();
482
+ break;
483
+ }
470
484
  }
471
485
  } catch (err) {
472
486
  if (!(abortCtl.signal.aborted && emittedTerminalError)) {
@@ -492,59 +506,122 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
492
506
 
493
507
  function createQueryInput({
494
508
  initialUserMessage,
495
- pendingUserMessages,
509
+ userMessages,
496
510
  abortSignal,
497
511
  }: {
498
512
  initialUserMessage: string;
499
- pendingUserMessages: string[];
513
+ userMessages: Experimental_BridgeUserMessageQueue;
500
514
  abortSignal: AbortSignal;
501
515
  }): {
502
516
  input: AsyncIterable<unknown>;
503
- close(): void;
517
+ close(error?: unknown): void;
518
+ handleLifecycle(message: ClaudeMessage): void;
519
+ hasActiveUserMessages(): boolean;
520
+ observeResult(): void;
521
+ readonly hasObservedResult: boolean;
504
522
  } {
505
523
  let closed = false;
506
- const close = (): void => {
524
+ let observedResult = false;
525
+ const submittedMessages = new Map<string, Experimental_BridgeUserMessage>();
526
+ const close = (error?: unknown): void => {
527
+ if (closed) return;
507
528
  closed = true;
529
+ userMessages.close(error);
508
530
  };
509
531
  if (abortSignal.aborted) {
510
- close();
532
+ close(abortSignal.reason);
511
533
  } else {
512
- abortSignal.addEventListener('abort', close, { once: true });
534
+ abortSignal.addEventListener('abort', () => close(abortSignal.reason), {
535
+ once: true,
536
+ });
513
537
  }
514
538
 
515
- const toUserMessage = (text: string): unknown => ({
539
+ const toUserMessage = (options: {
540
+ text: string;
541
+ messageId: string;
542
+ priority?: 'next';
543
+ }): unknown => ({
516
544
  type: 'user',
517
545
  message: {
518
546
  role: 'user',
519
- content: [{ type: 'text', text }],
547
+ content: [{ type: 'text', text: options.text }],
520
548
  },
549
+ parent_tool_use_id: null,
550
+ uuid: options.messageId,
551
+ ...(options.priority == null ? {} : { priority: options.priority }),
521
552
  });
522
553
 
554
+ const messageIterator = userMessages[Symbol.asyncIterator]();
555
+
523
556
  return {
524
557
  close,
558
+ handleLifecycle: message => {
559
+ const lifecycle = message as ClaudeMessage & {
560
+ command_uuid?: string;
561
+ state?: 'queued' | 'started' | 'completed' | 'cancelled' | 'discarded';
562
+ };
563
+ if (lifecycle.command_uuid == null || lifecycle.state == null) return;
564
+ const submitted = submittedMessages.get(lifecycle.command_uuid);
565
+ if (submitted == null) return;
566
+ if (lifecycle.state === 'queued' || lifecycle.state === 'started') {
567
+ submitted.accept();
568
+ return;
569
+ }
570
+ if (lifecycle.state === 'cancelled' || lifecycle.state === 'discarded') {
571
+ submitted.reject(
572
+ new Error(`Claude Code ${lifecycle.state} the user message.`),
573
+ );
574
+ }
575
+ submittedMessages.delete(lifecycle.command_uuid);
576
+ },
577
+ hasActiveUserMessages: () =>
578
+ submittedMessages.size > 0 || userMessages.pendingCount > 0,
579
+ observeResult: () => {
580
+ observedResult = true;
581
+ },
582
+ get hasObservedResult() {
583
+ return observedResult;
584
+ },
525
585
  input: {
526
586
  [Symbol.asyncIterator]() {
527
587
  let sentInitial = false;
528
588
  return {
529
589
  async next() {
530
- // eslint-disable-next-line no-unmodified-loop-condition
531
- while (!closed && !abortSignal.aborted) {
532
- if (!sentInitial) {
533
- sentInitial = true;
534
- return {
535
- value: toUserMessage(initialUserMessage),
536
- done: false,
537
- };
538
- }
539
- if (pendingUserMessages.length > 0) {
540
- return {
541
- value: toUserMessage(pendingUserMessages.shift()!),
542
- done: false,
543
- };
544
- }
545
- await new Promise(resolve => setTimeout(resolve, 50));
590
+ if (closed || abortSignal.aborted) {
591
+ return {
592
+ value: undefined,
593
+ done: true,
594
+ } as IteratorResult<unknown>;
595
+ }
596
+ if (!sentInitial) {
597
+ sentInitial = true;
598
+ return {
599
+ value: toUserMessage({
600
+ text: initialUserMessage,
601
+ messageId: randomUUID(),
602
+ }),
603
+ done: false,
604
+ };
546
605
  }
547
- return { value: undefined, done: true } as IteratorResult<unknown>;
606
+ const nextMessage = await messageIterator.next();
607
+ if (nextMessage.done) {
608
+ return {
609
+ value: undefined,
610
+ done: true,
611
+ } as IteratorResult<unknown>;
612
+ }
613
+ submittedMessages.set(
614
+ nextMessage.value.messageId,
615
+ nextMessage.value,
616
+ );
617
+ return {
618
+ value: toUserMessage({
619
+ text: nextMessage.value.text,
620
+ messageId: nextMessage.value.messageId,
621
+ priority: 'next',
622
+ }),
623
+ done: false,
624
+ };
548
625
  },
549
626
  };
550
627
  },
@@ -552,6 +629,35 @@ function createQueryInput({
552
629
  };
553
630
  }
554
631
 
632
+ function addUsage(
633
+ total: Record<string, unknown> | undefined,
634
+ usage: Record<string, unknown>,
635
+ ): Record<string, unknown> {
636
+ if (total == null) return usage;
637
+ const result: Record<string, unknown> = { ...total };
638
+ for (const [key, value] of Object.entries(usage)) {
639
+ const previous = result[key];
640
+ if (typeof value === 'number' && typeof previous === 'number') {
641
+ result[key] = previous + value;
642
+ } else if (
643
+ value != null &&
644
+ previous != null &&
645
+ typeof value === 'object' &&
646
+ typeof previous === 'object' &&
647
+ !Array.isArray(value) &&
648
+ !Array.isArray(previous)
649
+ ) {
650
+ result[key] = addUsage(
651
+ previous as Record<string, unknown>,
652
+ value as Record<string, unknown>,
653
+ );
654
+ } else {
655
+ result[key] = value;
656
+ }
657
+ }
658
+ return result;
659
+ }
660
+
555
661
  function parseArgs(args: string[]): {
556
662
  workdir?: string;
557
663
  bridgeStateDir?: string;
@@ -21,12 +21,15 @@ import {
21
21
  } from '@ai-sdk/harness';
22
22
  import {
23
23
  classifyDiskLog,
24
+ experimental_createBridgeUserMessageSubmitter,
24
25
  createBridgeErrorHandler,
25
26
  createBridgeStartupError,
26
27
  drainBridgeProcessStream,
27
28
  forwardBridgeProcessStream,
29
+ getRestrictedSandboxSession,
28
30
  markBridgeStarting,
29
31
  maskSandboxCredentials,
32
+ resolveSandboxDefaultWorkingDirectory,
30
33
  resolveSandboxHomeDir,
31
34
  SandboxChannel,
32
35
  shellQuote,
@@ -37,7 +40,7 @@ import {
37
40
  import {
38
41
  safeParseJSON,
39
42
  tool,
40
- type Experimental_SandboxSession,
43
+ type Experimental_SandboxSession as SandboxSession,
41
44
  type Experimental_SandboxProcess,
42
45
  } from '@ai-sdk/provider-utils';
43
46
  import { WebSocket } from 'ws';
@@ -108,6 +111,11 @@ export type ClaudeCodeHarnessSettings = {
108
111
  * is reserved for something else.
109
112
  */
110
113
  readonly port?: number;
114
+ /**
115
+ * Override the host endpoint used to connect to the sandbox bridge. Required
116
+ * together with `port` when using a basic sandbox session.
117
+ */
118
+ readonly portEndpoint?: HarnessV1PortEndpoint;
111
119
  /** Maximum milliseconds to wait for the bridge to advertise its port. Defaults to 120000. */
112
120
  readonly startupTimeoutMs?: number;
113
121
  /**
@@ -813,13 +821,36 @@ export function createClaudeCode(
813
821
  getBootstrap: getClaudeCodeBootstrap,
814
822
  doStart: async startOpts => {
815
823
  const sandboxSession = startOpts.sandboxSession;
824
+ const toolSafeSandboxSession =
825
+ getRestrictedSandboxSession(sandboxSession);
826
+ const sandboxId = 'id' in sandboxSession ? sandboxSession.id : undefined;
827
+ validateBasicSandboxSettings({
828
+ sandboxSession,
829
+ port: settings.port,
830
+ portEndpoint: settings.portEndpoint,
831
+ });
832
+ if (settings.mintBridgeToken != null && sandboxId == null) {
833
+ throw new HarnessCapabilityUnsupportedError({
834
+ harnessId: 'claude-code',
835
+ message:
836
+ 'The Claude Code harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id.',
837
+ });
838
+ }
839
+ const defaultWorkingDirectory =
840
+ await resolveSandboxDefaultWorkingDirectory({
841
+ sandboxSession,
842
+ abortSignal: startOpts.abortSignal,
843
+ });
816
844
  const authenticationMode = resolveClaudeCodeAuthenticationMode(
817
845
  settings.auth,
818
846
  );
819
847
  const resolvedAuthEnvironment = resolveClaudeCodeEnv(settings.auth);
820
848
  let authEnv = resolvedAuthEnvironment;
821
849
  let sandboxTurnEnvironment = settings.env;
822
- if (sandboxSession.addRequestTransformations != null) {
850
+ if (
851
+ 'addRequestTransformations' in sandboxSession &&
852
+ sandboxSession.addRequestTransformations != null
853
+ ) {
823
854
  const requestTransformations = createClaudeCodeRequestTransformations(
824
855
  {
825
856
  ...resolvedAuthEnvironment,
@@ -845,10 +876,8 @@ export function createClaudeCode(
845
876
  } else {
846
877
  warnCredentialBrokeringUnavailable();
847
878
  }
848
- const session = sandboxSession.restricted();
849
- const sandboxId = sandboxSession.id;
850
879
  const bootstrapDir = posix.resolve(
851
- sandboxSession.defaultWorkingDirectory,
880
+ defaultWorkingDirectory,
852
881
  BOOTSTRAP_DIR,
853
882
  );
854
883
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
@@ -859,7 +888,7 @@ export function createClaudeCode(
859
888
  : undefined;
860
889
 
861
890
  const workDir = startOpts.sessionWorkDir;
862
- const sessionDataDir = `${sandboxSession.defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
891
+ const sessionDataDir = `${defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
863
892
  const bridgeStateDir = `${sessionDataDir}/bridge`;
864
893
  const timeoutMs = settings.startupTimeoutMs ?? 120_000;
865
894
 
@@ -880,13 +909,20 @@ export function createClaudeCode(
880
909
  harnessId: 'claude-code',
881
910
  sessionId: startOpts.sessionId,
882
911
  });
912
+ let supportsUserMessageResponses = false;
883
913
 
884
914
  // Builds the `connect` thunk a `SandboxChannel` re-invokes on every
885
915
  // (re)connect: open the socket, then wait for `bridge-hello` so the
886
916
  // end-to-end link is proven live before any frame is sent.
887
917
  const buildConnect =
888
918
  (endpoint: HarnessV1PortEndpoint) => async (): Promise<WebSocket> => {
889
- return openBridgeWebSocket({ endpoint, timeoutMs });
919
+ return openBridgeWebSocket({
920
+ endpoint,
921
+ timeoutMs,
922
+ onHello: supportsResponses => {
923
+ supportsUserMessageResponses = supportsResponses;
924
+ },
925
+ });
890
926
  };
891
927
 
892
928
  /*
@@ -899,9 +935,10 @@ export function createClaudeCode(
899
935
  */
900
936
  if (coords) {
901
937
  try {
902
- const endpoint = await sandboxSession.getPortEndpoint({
938
+ const endpoint = await resolveBridgeEndpoint({
939
+ sandboxSession,
940
+ override: settings.portEndpoint,
903
941
  port: coords.port,
904
- protocol: 'ws',
905
942
  });
906
943
  const attachEndpoint = withBridgeToken({
907
944
  endpoint,
@@ -938,6 +975,7 @@ export function createClaudeCode(
938
975
  builtinToolFiltering: startOpts.builtinToolFiltering,
939
976
  skills: startOpts.skills ?? [],
940
977
  mcpServers: settings.mcpServers,
978
+ supportsUserMessageResponses: () => supportsUserMessageResponses,
941
979
  });
942
980
  } catch {
943
981
  // Bridge no longer reachable — recover by respawning below.
@@ -958,7 +996,7 @@ export function createClaudeCode(
958
996
  : undefined;
959
997
  if (coords && isContinue) {
960
998
  const logRaw = await Promise.resolve(
961
- session.readTextFile({
999
+ toolSafeSandboxSession.readTextFile({
962
1000
  path: `${bridgeStateDir}/event-log.ndjson`,
963
1001
  abortSignal: startOpts.abortSignal,
964
1002
  }),
@@ -971,15 +1009,18 @@ export function createClaudeCode(
971
1009
  const sandboxHomeDir =
972
1010
  startOpts.skills && startOpts.skills.length > 0
973
1011
  ? await resolveSandboxHomeDir({
974
- sandbox: session,
1012
+ sandbox: toolSafeSandboxSession,
975
1013
  abortSignal: startOpts.abortSignal,
976
1014
  })
977
1015
  : undefined;
978
- const port = resolveBridgePort(sandboxSession, settings.port);
1016
+ const port = resolveBridgePort({
1017
+ sandboxSession,
1018
+ override: settings.port,
1019
+ });
979
1020
  const token =
980
1021
  settings.mintBridgeToken == null
981
1022
  ? randomBytes(32).toString('hex')
982
- : settings.mintBridgeToken(sandboxId);
1023
+ : settings.mintBridgeToken(sandboxId!);
983
1024
  const env = {
984
1025
  ...authEnv,
985
1026
  /*
@@ -1006,7 +1047,7 @@ export function createClaudeCode(
1006
1047
  * spawn — `BRIDGE_CHANNEL_TOKEN` rotates per start.
1007
1048
  */
1008
1049
  if (respawnStrategy === undefined) {
1009
- await session.run({
1050
+ await toolSafeSandboxSession.run({
1010
1051
  command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
1011
1052
  abortSignal: startOpts.abortSignal,
1012
1053
  });
@@ -1016,7 +1057,7 @@ export function createClaudeCode(
1016
1057
  throw new Error('Unable to resolve sandbox HOME directory.');
1017
1058
  }
1018
1059
  await writeClaudeCodeSkills({
1019
- sandbox: session,
1060
+ sandbox: toolSafeSandboxSession,
1020
1061
  homeDir: sandboxHomeDir,
1021
1062
  skills: startOpts.skills,
1022
1063
  abortSignal: startOpts.abortSignal,
@@ -1025,13 +1066,13 @@ export function createClaudeCode(
1025
1066
  }
1026
1067
 
1027
1068
  await markBridgeStarting({
1028
- sandbox: session,
1069
+ sandbox: toolSafeSandboxSession,
1029
1070
  bridgeStateDir,
1030
1071
  bridgeType: 'claude-code',
1031
1072
  abortSignal: startOpts.abortSignal,
1032
1073
  });
1033
1074
 
1034
- const proc = await session.spawn({
1075
+ const proc = await toolSafeSandboxSession.spawn({
1035
1076
  command: `node ${shellQuote(`${bootstrapDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)}`,
1036
1077
  env,
1037
1078
  abortSignal: startOpts.abortSignal,
@@ -1047,7 +1088,7 @@ export function createClaudeCode(
1047
1088
  void bridgeStartupStderrDone;
1048
1089
  const { port: boundPort } = await waitForBridgeReady({
1049
1090
  proc,
1050
- sandbox: session,
1091
+ sandbox: toolSafeSandboxSession,
1051
1092
  bridgeStateDir,
1052
1093
  bridgeType: 'claude-code',
1053
1094
  timeoutMs,
@@ -1071,9 +1112,10 @@ export function createClaudeCode(
1071
1112
  });
1072
1113
  void drainBridgeProcessStream(proc.stdout);
1073
1114
 
1074
- const endpoint = await sandboxSession.getPortEndpoint({
1115
+ const endpoint = await resolveBridgeEndpoint({
1116
+ sandboxSession,
1117
+ override: settings.portEndpoint,
1075
1118
  port: boundPort,
1076
- protocol: 'ws',
1077
1119
  });
1078
1120
  const bridgeEndpoint = withBridgeToken({ endpoint, token });
1079
1121
 
@@ -1113,17 +1155,23 @@ export function createClaudeCode(
1113
1155
  builtinToolFiltering: startOpts.builtinToolFiltering,
1114
1156
  skills: startOpts.skills ?? [],
1115
1157
  mcpServers: settings.mcpServers,
1158
+ supportsUserMessageResponses: () => supportsUserMessageResponses,
1116
1159
  });
1117
1160
  },
1118
1161
  };
1119
1162
  }
1120
1163
 
1121
- function resolveBridgePort(
1122
- sandboxSession: HarnessV1NetworkSandboxSession,
1123
- override: number | undefined,
1124
- ): number {
1164
+ function resolveBridgePort({
1165
+ sandboxSession,
1166
+ override,
1167
+ }: {
1168
+ sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
1169
+ override: number | undefined;
1170
+ }): number {
1125
1171
  if (override !== undefined) return override;
1126
- if (sandboxSession.ports.length > 0) return sandboxSession.ports[0];
1172
+ if ('ports' in sandboxSession && sandboxSession.ports.length > 0) {
1173
+ return sandboxSession.ports[0];
1174
+ }
1127
1175
  throw new HarnessCapabilityUnsupportedError({
1128
1176
  harnessId: 'claude-code',
1129
1177
  message:
@@ -1132,6 +1180,52 @@ function resolveBridgePort(
1132
1180
  });
1133
1181
  }
1134
1182
 
1183
+ function validateBasicSandboxSettings({
1184
+ sandboxSession,
1185
+ port,
1186
+ portEndpoint,
1187
+ }: {
1188
+ sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
1189
+ port: number | undefined;
1190
+ portEndpoint: HarnessV1PortEndpoint | undefined;
1191
+ }): void {
1192
+ if ('getPortEndpoint' in sandboxSession) return;
1193
+ if (port == null) {
1194
+ throw new HarnessCapabilityUnsupportedError({
1195
+ harnessId: 'claude-code',
1196
+ message:
1197
+ 'The Claude Code harness requires an explicit `port` when using a basic sandbox session.',
1198
+ });
1199
+ }
1200
+ if (portEndpoint == null) {
1201
+ throw new HarnessCapabilityUnsupportedError({
1202
+ harnessId: 'claude-code',
1203
+ message:
1204
+ 'The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session.',
1205
+ });
1206
+ }
1207
+ }
1208
+
1209
+ async function resolveBridgeEndpoint({
1210
+ sandboxSession,
1211
+ override,
1212
+ port,
1213
+ }: {
1214
+ sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
1215
+ override: HarnessV1PortEndpoint | undefined;
1216
+ port: number;
1217
+ }): Promise<HarnessV1PortEndpoint> {
1218
+ if (override != null) return override;
1219
+ if ('getPortEndpoint' in sandboxSession) {
1220
+ return sandboxSession.getPortEndpoint({ port, protocol: 'ws' });
1221
+ }
1222
+ throw new HarnessCapabilityUnsupportedError({
1223
+ harnessId: 'claude-code',
1224
+ message:
1225
+ 'The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session.',
1226
+ });
1227
+ }
1228
+
1135
1229
  /**
1136
1230
  * Materialise skill files into
1137
1231
  * `$HOME/.claude/skills/<name>/SKILL.md`. The `claude` CLI
@@ -1145,7 +1239,7 @@ async function writeClaudeCodeSkills({
1145
1239
  skills,
1146
1240
  abortSignal,
1147
1241
  }: {
1148
- sandbox: Experimental_SandboxSession;
1242
+ sandbox: SandboxSession;
1149
1243
  homeDir: string;
1150
1244
  skills: ReadonlyArray<HarnessV1Skill>;
1151
1245
  abortSignal?: AbortSignal;
@@ -1178,10 +1272,12 @@ function openWebSocketAndWaitForBridgeHello({
1178
1272
  endpoint,
1179
1273
  openTimeoutMs,
1180
1274
  getHelloTimeoutMs,
1275
+ onHello,
1181
1276
  }: {
1182
1277
  endpoint: HarnessV1PortEndpoint;
1183
1278
  openTimeoutMs: number;
1184
1279
  getHelloTimeoutMs: () => number;
1280
+ onHello: (supportsUserMessageResponses: boolean) => void;
1185
1281
  }): Promise<WebSocket> {
1186
1282
  return new Promise<WebSocket>((resolve, reject) => {
1187
1283
  const ws = new WebSocket(endpoint.url, {
@@ -1253,6 +1349,12 @@ function openWebSocketAndWaitForBridgeHello({
1253
1349
  !Array.isArray(value) &&
1254
1350
  (value as { type?: unknown }).type === 'bridge-hello'
1255
1351
  ) {
1352
+ const capabilities = (
1353
+ value as {
1354
+ capabilities?: { experimental_userMessageResponses?: unknown };
1355
+ }
1356
+ ).capabilities;
1357
+ onHello(capabilities?.experimental_userMessageResponses === true);
1256
1358
  sawBridgeHello = true;
1257
1359
  tryResolve();
1258
1360
  }
@@ -1280,9 +1382,11 @@ function openWebSocketAndWaitForBridgeHello({
1280
1382
  async function openBridgeWebSocket({
1281
1383
  endpoint,
1282
1384
  timeoutMs,
1385
+ onHello,
1283
1386
  }: {
1284
1387
  endpoint: HarnessV1PortEndpoint;
1285
1388
  timeoutMs: number;
1389
+ onHello: (supportsUserMessageResponses: boolean) => void;
1286
1390
  }): Promise<WebSocket> {
1287
1391
  const deadline = Date.now() + timeoutMs;
1288
1392
  let attempt = 0;
@@ -1297,6 +1401,7 @@ async function openBridgeWebSocket({
1297
1401
  openTimeoutMs: Math.min(10_000, remaining),
1298
1402
  getHelloTimeoutMs: () =>
1299
1403
  Math.min(5_000, Math.max(1, deadline - Date.now())),
1404
+ onHello,
1300
1405
  });
1301
1406
  } catch (err) {
1302
1407
  lastError = err;
@@ -1368,6 +1473,7 @@ function createSession({
1368
1473
  builtinToolFiltering,
1369
1474
  skills,
1370
1475
  mcpServers,
1476
+ supportsUserMessageResponses,
1371
1477
  }: {
1372
1478
  sessionId: string;
1373
1479
  channel: ClaudeCodeChannel;
@@ -1383,12 +1489,13 @@ function createSession({
1383
1489
  rerunContinue: boolean;
1384
1490
  bridgePort: number;
1385
1491
  bridgeToken: string;
1386
- sandboxId: string;
1492
+ sandboxId: string | undefined;
1387
1493
  debug: HarnessV1DebugConfig | undefined;
1388
1494
  permissionMode: HarnessV1PermissionMode | undefined;
1389
1495
  builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
1390
1496
  skills: ReadonlyArray<HarnessV1Skill>;
1391
1497
  mcpServers: Record<string, unknown> | undefined;
1498
+ supportsUserMessageResponses: () => boolean;
1392
1499
  }): HarnessV1Session {
1393
1500
  let stopped = false;
1394
1501
  let stopPromise: Promise<void> | undefined;
@@ -1417,7 +1524,13 @@ function createSession({
1417
1524
  pendingResolve = resolve;
1418
1525
  pendingReject = reject;
1419
1526
  });
1420
-
1527
+ const userMessageSubmitter = supportsUserMessageResponses()
1528
+ ? experimental_createBridgeUserMessageSubmitter({
1529
+ send: message => channel.send(message),
1530
+ onResponse: listener => channel.on('user-message-response', listener),
1531
+ onReconnect: listener => channel.onReconnect(listener),
1532
+ })
1533
+ : undefined;
1421
1534
  const unsubs: Array<() => void> = [];
1422
1535
  const forward = (event: HarnessV1StreamPart) => {
1423
1536
  try {
@@ -1429,12 +1542,14 @@ function createSession({
1429
1542
  const settleSuccess = () => {
1430
1543
  if (isSettled) return;
1431
1544
  isSettled = true;
1545
+ userMessageSubmitter?.close();
1432
1546
  for (const u of unsubs) u();
1433
1547
  pendingResolve!();
1434
1548
  };
1435
1549
  const settleError = (err: unknown) => {
1436
1550
  if (isSettled) return;
1437
1551
  isSettled = true;
1552
+ userMessageSubmitter?.close(err);
1438
1553
  for (const u of unsubs) u();
1439
1554
  pendingReject!(err);
1440
1555
  };
@@ -1528,9 +1643,13 @@ function createSession({
1528
1643
  reason: input.reason,
1529
1644
  });
1530
1645
  },
1531
- submitUserMessage: async text => {
1532
- channel.send({ type: 'user-message', text });
1533
- },
1646
+ ...(userMessageSubmitter == null
1647
+ ? {}
1648
+ : {
1649
+ submitUserMessage: async (text: string) => {
1650
+ await userMessageSubmitter.submit(text);
1651
+ },
1652
+ }),
1534
1653
  done,
1535
1654
  };
1536
1655
  };
@@ -1689,7 +1808,7 @@ function createSession({
1689
1808
  port: bridgePort,
1690
1809
  token: bridgeToken,
1691
1810
  lastSeenEventId,
1692
- sandboxId,
1811
+ ...(sandboxId == null ? {} : { sandboxId }),
1693
1812
  },
1694
1813
  },
1695
1814
  };
@@ -1827,7 +1946,7 @@ function createSession({
1827
1946
  port: bridgePort,
1828
1947
  token: bridgeToken,
1829
1948
  lastSeenEventId,
1830
- sandboxId,
1949
+ ...(sandboxId == null ? {} : { sandboxId }),
1831
1950
  },
1832
1951
  },
1833
1952
  };