@ai-sdk/harness-claude-code 1.0.167 → 1.0.169
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.
- package/CHANGELOG.md +23 -0
- package/README.md +4 -23
- package/dist/bridge/index.mjs +1 -1
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -203
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/bridge/create-emit-stream-event.ts +8 -1
- package/src/claude-code-harness.ts +43 -325
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-claude-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.169",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
30
|
-
"@ai-sdk/provider": "4.0.
|
|
31
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.165",
|
|
30
|
+
"@ai-sdk/provider": "4.0.34",
|
|
31
|
+
"@ai-sdk/provider-utils": "5.0.69",
|
|
32
32
|
"ws": "^8.21.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
@@ -194,7 +194,14 @@ export function createEmitStreamEvent({
|
|
|
194
194
|
// Emit `stream-start` once, on the first message, carrying the model the
|
|
195
195
|
// CLI resolved to (the `system`/`init` message reports it — this is the
|
|
196
196
|
// default model when none was configured).
|
|
197
|
-
|
|
197
|
+
/*
|
|
198
|
+
* `command_lifecycle` frames are skipped here. The CLI's stdin reader
|
|
199
|
+
* forwards them (e.g. `queued` and `started` for the prompt) straight to
|
|
200
|
+
* the output stream, independently of the engine that yields
|
|
201
|
+
* `system`/`init` at the start of each turn, so they can arrive before
|
|
202
|
+
* `init`. They produce no stream parts.
|
|
203
|
+
*/
|
|
204
|
+
if (!streamStarted && type !== 'command_lifecycle') {
|
|
198
205
|
const initModel =
|
|
199
206
|
type === 'system' &&
|
|
200
207
|
msg.subtype === 'init' &&
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
type HarnessV1ResponseFormat,
|
|
17
17
|
type HarnessV1PortEndpoint,
|
|
18
18
|
type HarnessV1ResumeSessionState,
|
|
19
|
-
type HarnessV1NetworkSandboxSession,
|
|
20
19
|
type HarnessV1Session,
|
|
21
20
|
type HarnessV1Skill,
|
|
22
21
|
type HarnessV1StreamPart,
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
classifyDiskLog,
|
|
29
28
|
resolveSandboxCredentialEnvironment,
|
|
30
29
|
createBridgeToken,
|
|
30
|
+
createBridgeWebSocketConnect,
|
|
31
31
|
experimental_createBridgeUserMessageSubmitter,
|
|
32
32
|
createBridgeErrorHandler,
|
|
33
33
|
createBridgeStartupError,
|
|
@@ -36,18 +36,20 @@ import {
|
|
|
36
36
|
forwardBridgeProcessStream,
|
|
37
37
|
getRestrictedSandboxSession,
|
|
38
38
|
markBridgeStarting,
|
|
39
|
+
resolveBridgeEndpoint,
|
|
40
|
+
resolveBridgePort,
|
|
39
41
|
resolveSandboxHomeDir,
|
|
40
42
|
SandboxChannel,
|
|
41
43
|
shellQuote,
|
|
42
|
-
|
|
44
|
+
validateBridgeSandboxSettings,
|
|
43
45
|
warnCredentialBrokeringUnavailable,
|
|
44
46
|
waitForBridgeReady,
|
|
45
|
-
withBridgeToken,
|
|
46
47
|
writeSkills,
|
|
48
|
+
type BridgeHello,
|
|
49
|
+
type SandboxChannelDebugEvent,
|
|
47
50
|
type SandboxChannelReconnectOptions,
|
|
48
51
|
} from '@ai-sdk/harness/utils';
|
|
49
52
|
import {
|
|
50
|
-
safeParseJSON,
|
|
51
53
|
tool,
|
|
52
54
|
type Experimental_SandboxSession as SandboxSession,
|
|
53
55
|
type Experimental_SandboxProcess,
|
|
@@ -148,6 +150,14 @@ export type ClaudeCodeHarnessSettings = {
|
|
|
148
150
|
* milliseconds up to 2 seconds.
|
|
149
151
|
*/
|
|
150
152
|
readonly reconnect?: SandboxChannelReconnectOptions;
|
|
153
|
+
/**
|
|
154
|
+
* Called with bridge connection lifecycle events: reconnect attempts,
|
|
155
|
+
* successful reconnects, and `reconnect-failed` once the connection is
|
|
156
|
+
* closed for good. Hosts that cache idle sessions can use it to reattach
|
|
157
|
+
* before sending the next prompt. The `cause` of a failed reconnect can
|
|
158
|
+
* contain connection details, so avoid logging it as is.
|
|
159
|
+
*/
|
|
160
|
+
readonly onDebug?: (event: SandboxChannelDebugEvent) => void;
|
|
151
161
|
/**
|
|
152
162
|
* Creates the authentication token used by the sandbox bridge. Defaults to
|
|
153
163
|
* a random 32-byte hexadecimal token.
|
|
@@ -862,18 +872,13 @@ export function createClaudeCode(
|
|
|
862
872
|
const toolSafeSandboxSession =
|
|
863
873
|
getRestrictedSandboxSession(sandboxSession);
|
|
864
874
|
const sandboxId = 'id' in sandboxSession ? sandboxSession.id : undefined;
|
|
865
|
-
|
|
875
|
+
validateBridgeSandboxSettings({
|
|
866
876
|
sandboxSession,
|
|
867
|
-
|
|
868
|
-
|
|
877
|
+
sandboxId,
|
|
878
|
+
portOverride: settings.port,
|
|
879
|
+
portEndpointOverride: settings.portEndpoint,
|
|
880
|
+
mintBridgeToken: settings.mintBridgeToken,
|
|
869
881
|
});
|
|
870
|
-
if (settings.mintBridgeToken != null && sandboxId == null) {
|
|
871
|
-
throw new HarnessCapabilityUnsupportedError({
|
|
872
|
-
harnessId: 'claude-code',
|
|
873
|
-
message:
|
|
874
|
-
'The Claude Code harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id.',
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
882
|
const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
|
|
878
883
|
const isResume = lifecycleState != null;
|
|
879
884
|
const isContinue = startOpts.continueFrom != null;
|
|
@@ -1000,26 +1005,10 @@ export function createClaudeCode(
|
|
|
1000
1005
|
sessionId: startOpts.sessionId,
|
|
1001
1006
|
});
|
|
1002
1007
|
let supportsUserMessageResponses = false;
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const buildConnect =
|
|
1008
|
-
(endpoint: HarnessV1PortEndpoint) =>
|
|
1009
|
-
async ({
|
|
1010
|
-
abortSignal,
|
|
1011
|
-
}: {
|
|
1012
|
-
abortSignal: AbortSignal;
|
|
1013
|
-
}): Promise<WebSocket> => {
|
|
1014
|
-
return openBridgeWebSocket({
|
|
1015
|
-
endpoint,
|
|
1016
|
-
timeoutMs,
|
|
1017
|
-
abortSignal,
|
|
1018
|
-
onHello: supportsResponses => {
|
|
1019
|
-
supportsUserMessageResponses = supportsResponses;
|
|
1020
|
-
},
|
|
1021
|
-
});
|
|
1022
|
-
};
|
|
1008
|
+
const onHello = (hello: BridgeHello) => {
|
|
1009
|
+
supportsUserMessageResponses =
|
|
1010
|
+
hello.capabilities?.experimental_userMessageResponses === true;
|
|
1011
|
+
};
|
|
1023
1012
|
|
|
1024
1013
|
/*
|
|
1025
1014
|
* Rung 1 — ATTACH. When lifecycle state carries live bridge coordinates,
|
|
@@ -1031,22 +1020,24 @@ export function createClaudeCode(
|
|
|
1031
1020
|
*/
|
|
1032
1021
|
if (coords) {
|
|
1033
1022
|
try {
|
|
1034
|
-
const
|
|
1023
|
+
const attachEndpoint = await resolveBridgeEndpoint({
|
|
1035
1024
|
sandboxSession,
|
|
1036
|
-
override: settings.portEndpoint,
|
|
1037
1025
|
port: coords.port,
|
|
1038
|
-
|
|
1039
|
-
const attachEndpoint = withBridgeToken({
|
|
1040
|
-
endpoint,
|
|
1026
|
+
portEndpointOverride: settings.portEndpoint,
|
|
1041
1027
|
token: coords.token,
|
|
1042
1028
|
});
|
|
1043
1029
|
const attachChannel: ClaudeCodeChannel = new SandboxChannel({
|
|
1044
|
-
connect:
|
|
1030
|
+
connect: createBridgeWebSocketConnect({
|
|
1031
|
+
endpoint: attachEndpoint,
|
|
1032
|
+
webSocketConstructor: WebSocket,
|
|
1033
|
+
onHello,
|
|
1034
|
+
}),
|
|
1045
1035
|
outboundSchema: outboundMessageSchema,
|
|
1046
1036
|
initialLastSeenEventId: coords.lastSeenEventId,
|
|
1047
1037
|
onDiagnostic,
|
|
1048
1038
|
onBridgeError,
|
|
1049
1039
|
reconnect: settings.reconnect,
|
|
1040
|
+
onDebug: settings.onDebug,
|
|
1050
1041
|
});
|
|
1051
1042
|
const finishListenerAttachment = isContinue
|
|
1052
1043
|
? attachChannel.beginListenerAttachment()
|
|
@@ -1118,7 +1109,7 @@ export function createClaudeCode(
|
|
|
1118
1109
|
|
|
1119
1110
|
const port = resolveBridgePort({
|
|
1120
1111
|
sandboxSession,
|
|
1121
|
-
|
|
1112
|
+
portOverride: settings.port,
|
|
1122
1113
|
});
|
|
1123
1114
|
const token =
|
|
1124
1115
|
settings.mintBridgeToken == null
|
|
@@ -1191,19 +1182,26 @@ export function createClaudeCode(
|
|
|
1191
1182
|
});
|
|
1192
1183
|
void drainBridgeProcessStream(proc.stdout);
|
|
1193
1184
|
|
|
1194
|
-
const
|
|
1185
|
+
const bridgeEndpoint = await resolveBridgeEndpoint({
|
|
1195
1186
|
sandboxSession,
|
|
1196
|
-
override: settings.portEndpoint,
|
|
1197
1187
|
port: boundPort,
|
|
1188
|
+
portEndpointOverride: settings.portEndpoint,
|
|
1189
|
+
token,
|
|
1198
1190
|
});
|
|
1199
|
-
const bridgeEndpoint = withBridgeToken({ endpoint, token });
|
|
1200
1191
|
|
|
1201
1192
|
const channel: ClaudeCodeChannel = new SandboxChannel({
|
|
1202
|
-
connect:
|
|
1193
|
+
connect: createBridgeWebSocketConnect({
|
|
1194
|
+
endpoint: bridgeEndpoint,
|
|
1195
|
+
webSocketConstructor: WebSocket,
|
|
1196
|
+
openRetryTimeoutMs: timeoutMs,
|
|
1197
|
+
startAbortSignal: startOpts.abortSignal,
|
|
1198
|
+
onHello,
|
|
1199
|
+
}),
|
|
1203
1200
|
outboundSchema: outboundMessageSchema,
|
|
1204
1201
|
onDiagnostic,
|
|
1205
1202
|
onBridgeError,
|
|
1206
1203
|
reconnect: settings.reconnect,
|
|
1204
|
+
onDebug: settings.onDebug,
|
|
1207
1205
|
// In replay mode the respawned bridge reloaded the finished turn from
|
|
1208
1206
|
// disk; seed the cursor and resume so it streams the tail (incl.
|
|
1209
1207
|
// `finish`) rather than starting empty.
|
|
@@ -1255,71 +1253,6 @@ export function createClaudeCode(
|
|
|
1255
1253
|
};
|
|
1256
1254
|
}
|
|
1257
1255
|
|
|
1258
|
-
function resolveBridgePort({
|
|
1259
|
-
sandboxSession,
|
|
1260
|
-
override,
|
|
1261
|
-
}: {
|
|
1262
|
-
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
1263
|
-
override: number | undefined;
|
|
1264
|
-
}): number {
|
|
1265
|
-
if (override !== undefined) return override;
|
|
1266
|
-
if ('ports' in sandboxSession && sandboxSession.ports.length > 0) {
|
|
1267
|
-
return sandboxSession.ports[0];
|
|
1268
|
-
}
|
|
1269
|
-
throw new HarnessCapabilityUnsupportedError({
|
|
1270
|
-
harnessId: 'claude-code',
|
|
1271
|
-
message:
|
|
1272
|
-
'The claude-code harness needs a TCP port exposed by the sandbox. ' +
|
|
1273
|
-
'Create the sandbox with `ports: [<port>]` or pass `createClaudeCode({ port })`.',
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
function validateBasicSandboxSettings({
|
|
1278
|
-
sandboxSession,
|
|
1279
|
-
port,
|
|
1280
|
-
portEndpoint,
|
|
1281
|
-
}: {
|
|
1282
|
-
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
1283
|
-
port: number | undefined;
|
|
1284
|
-
portEndpoint: HarnessV1PortEndpoint | undefined;
|
|
1285
|
-
}): void {
|
|
1286
|
-
if ('getPortEndpoint' in sandboxSession) return;
|
|
1287
|
-
if (port == null) {
|
|
1288
|
-
throw new HarnessCapabilityUnsupportedError({
|
|
1289
|
-
harnessId: 'claude-code',
|
|
1290
|
-
message:
|
|
1291
|
-
'The Claude Code harness requires an explicit `port` when using a basic sandbox session.',
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
if (portEndpoint == null) {
|
|
1295
|
-
throw new HarnessCapabilityUnsupportedError({
|
|
1296
|
-
harnessId: 'claude-code',
|
|
1297
|
-
message:
|
|
1298
|
-
'The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session.',
|
|
1299
|
-
});
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
async function resolveBridgeEndpoint({
|
|
1304
|
-
sandboxSession,
|
|
1305
|
-
override,
|
|
1306
|
-
port,
|
|
1307
|
-
}: {
|
|
1308
|
-
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
1309
|
-
override: HarnessV1PortEndpoint | undefined;
|
|
1310
|
-
port: number;
|
|
1311
|
-
}): Promise<HarnessV1PortEndpoint> {
|
|
1312
|
-
if (override != null) return override;
|
|
1313
|
-
if ('getPortEndpoint' in sandboxSession) {
|
|
1314
|
-
return sandboxSession.getPortEndpoint({ port, protocol: 'ws' });
|
|
1315
|
-
}
|
|
1316
|
-
throw new HarnessCapabilityUnsupportedError({
|
|
1317
|
-
harnessId: 'claude-code',
|
|
1318
|
-
message:
|
|
1319
|
-
'The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session.',
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
1256
|
/**
|
|
1324
1257
|
* Materialise skill files into
|
|
1325
1258
|
* `$HOME/.claude/skills/<name>/SKILL.md`. The `claude` CLI
|
|
@@ -1327,221 +1260,6 @@ async function resolveBridgeEndpoint({
|
|
|
1327
1260
|
* be in place before the bridge is spawned without mutating the session
|
|
1328
1261
|
* workdir. Each file uses the YAML-frontmatter shape the CLI expects.
|
|
1329
1262
|
*/
|
|
1330
|
-
/**
|
|
1331
|
-
* Wait for the bridge's `bridge-hello` message to arrive on the freshly
|
|
1332
|
-
* opened WebSocket before any other host-side code touches it.
|
|
1333
|
-
*
|
|
1334
|
-
* Some sandbox runtimes (Vercel in particular) complete the WS upgrade
|
|
1335
|
-
* with the host long before the connection is actually forwarded to the
|
|
1336
|
-
* sandbox-side bridge. Anything sent in that gap is silently dropped —
|
|
1337
|
-
* including the `start` message we send next. The bridge emits
|
|
1338
|
-
* `bridge-hello` the instant it accepts the connection, so receiving it
|
|
1339
|
-
* is the only reliable evidence that the end-to-end link is live.
|
|
1340
|
-
*/
|
|
1341
|
-
function openWebSocketAndWaitForBridgeHello({
|
|
1342
|
-
endpoint,
|
|
1343
|
-
openTimeoutMs,
|
|
1344
|
-
getHelloTimeoutMs,
|
|
1345
|
-
abortSignal,
|
|
1346
|
-
onHello,
|
|
1347
|
-
}: {
|
|
1348
|
-
endpoint: HarnessV1PortEndpoint;
|
|
1349
|
-
openTimeoutMs: number;
|
|
1350
|
-
getHelloTimeoutMs: () => number;
|
|
1351
|
-
abortSignal: AbortSignal;
|
|
1352
|
-
onHello: (supportsUserMessageResponses: boolean) => void;
|
|
1353
|
-
}): Promise<WebSocket> {
|
|
1354
|
-
return new Promise<WebSocket>((resolve, reject) => {
|
|
1355
|
-
const abortReason = () =>
|
|
1356
|
-
abortSignal.reason ?? new Error('WebSocket connection aborted');
|
|
1357
|
-
if (abortSignal.aborted) {
|
|
1358
|
-
reject(abortReason());
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
const ws = new WebSocket(endpoint.url, {
|
|
1363
|
-
headers: endpoint.headers == null ? undefined : { ...endpoint.headers },
|
|
1364
|
-
});
|
|
1365
|
-
let opened = false;
|
|
1366
|
-
let sawBridgeHello = false;
|
|
1367
|
-
let settled = false;
|
|
1368
|
-
let openTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1369
|
-
let helloTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1370
|
-
let onAbort: (() => void) | undefined;
|
|
1371
|
-
|
|
1372
|
-
const cleanup = ({
|
|
1373
|
-
keepTerminationListeners = false,
|
|
1374
|
-
}: {
|
|
1375
|
-
keepTerminationListeners?: boolean;
|
|
1376
|
-
} = {}) => {
|
|
1377
|
-
if (openTimer) clearTimeout(openTimer);
|
|
1378
|
-
if (helloTimer) clearTimeout(helloTimer);
|
|
1379
|
-
ws.off('open', onOpen);
|
|
1380
|
-
ws.off('message', onMessage);
|
|
1381
|
-
if (onAbort != null) {
|
|
1382
|
-
abortSignal.removeEventListener('abort', onAbort);
|
|
1383
|
-
}
|
|
1384
|
-
if (!keepTerminationListeners) {
|
|
1385
|
-
ws.off('close', onClose);
|
|
1386
|
-
ws.off('error', onError);
|
|
1387
|
-
}
|
|
1388
|
-
};
|
|
1389
|
-
const settle = (err?: unknown) => {
|
|
1390
|
-
if (settled) return;
|
|
1391
|
-
settled = true;
|
|
1392
|
-
if (err) {
|
|
1393
|
-
cleanup({ keepTerminationListeners: true });
|
|
1394
|
-
try {
|
|
1395
|
-
ws.terminate();
|
|
1396
|
-
} catch {
|
|
1397
|
-
cleanup();
|
|
1398
|
-
}
|
|
1399
|
-
reject(err);
|
|
1400
|
-
} else {
|
|
1401
|
-
cleanup();
|
|
1402
|
-
resolve(ws);
|
|
1403
|
-
}
|
|
1404
|
-
};
|
|
1405
|
-
const tryResolve = () => {
|
|
1406
|
-
if (opened && sawBridgeHello) settle();
|
|
1407
|
-
};
|
|
1408
|
-
const startHelloTimer = () => {
|
|
1409
|
-
if (helloTimer) return;
|
|
1410
|
-
const helloTimeoutMs = getHelloTimeoutMs();
|
|
1411
|
-
helloTimer = setTimeout(
|
|
1412
|
-
() =>
|
|
1413
|
-
settle(
|
|
1414
|
-
new Error(
|
|
1415
|
-
`claude-code bridge did not send bridge-hello within ${helloTimeoutMs}ms`,
|
|
1416
|
-
),
|
|
1417
|
-
),
|
|
1418
|
-
helloTimeoutMs,
|
|
1419
|
-
);
|
|
1420
|
-
helloTimer.unref?.();
|
|
1421
|
-
};
|
|
1422
|
-
const onOpen = () => {
|
|
1423
|
-
opened = true;
|
|
1424
|
-
if (openTimer) {
|
|
1425
|
-
clearTimeout(openTimer);
|
|
1426
|
-
openTimer = undefined;
|
|
1427
|
-
}
|
|
1428
|
-
startHelloTimer();
|
|
1429
|
-
tryResolve();
|
|
1430
|
-
};
|
|
1431
|
-
const onMessage = (raw: unknown) => {
|
|
1432
|
-
void (async () => {
|
|
1433
|
-
const parsed = await safeParseJSON({
|
|
1434
|
-
text: webSocketMessageToString(raw),
|
|
1435
|
-
});
|
|
1436
|
-
if (!parsed.success || settled) return;
|
|
1437
|
-
const value = parsed.value;
|
|
1438
|
-
if (
|
|
1439
|
-
typeof value === 'object' &&
|
|
1440
|
-
value !== null &&
|
|
1441
|
-
!Array.isArray(value) &&
|
|
1442
|
-
(value as { type?: unknown }).type === 'bridge-hello'
|
|
1443
|
-
) {
|
|
1444
|
-
const capabilities = (
|
|
1445
|
-
value as {
|
|
1446
|
-
capabilities?: { experimental_userMessageResponses?: unknown };
|
|
1447
|
-
}
|
|
1448
|
-
).capabilities;
|
|
1449
|
-
onHello(capabilities?.experimental_userMessageResponses === true);
|
|
1450
|
-
sawBridgeHello = true;
|
|
1451
|
-
tryResolve();
|
|
1452
|
-
}
|
|
1453
|
-
})();
|
|
1454
|
-
};
|
|
1455
|
-
const onClose = () => {
|
|
1456
|
-
settle(
|
|
1457
|
-
new Error('claude-code bridge closed before sending bridge-hello'),
|
|
1458
|
-
);
|
|
1459
|
-
cleanup();
|
|
1460
|
-
};
|
|
1461
|
-
const onError = (err: Error) => settle(err);
|
|
1462
|
-
onAbort = () => settle(abortReason());
|
|
1463
|
-
openTimer = setTimeout(
|
|
1464
|
-
() =>
|
|
1465
|
-
settle(new Error(`WebSocket open timed out after ${openTimeoutMs}ms`)),
|
|
1466
|
-
openTimeoutMs,
|
|
1467
|
-
);
|
|
1468
|
-
openTimer.unref?.();
|
|
1469
|
-
ws.on('open', onOpen);
|
|
1470
|
-
ws.on('message', onMessage);
|
|
1471
|
-
ws.on('close', onClose);
|
|
1472
|
-
ws.on('error', onError);
|
|
1473
|
-
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
1474
|
-
});
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
async function openBridgeWebSocket({
|
|
1478
|
-
endpoint,
|
|
1479
|
-
timeoutMs,
|
|
1480
|
-
abortSignal,
|
|
1481
|
-
onHello,
|
|
1482
|
-
}: {
|
|
1483
|
-
endpoint: HarnessV1PortEndpoint;
|
|
1484
|
-
timeoutMs: number;
|
|
1485
|
-
abortSignal: AbortSignal;
|
|
1486
|
-
onHello: (supportsUserMessageResponses: boolean) => void;
|
|
1487
|
-
}): Promise<WebSocket> {
|
|
1488
|
-
const deadline = Date.now() + timeoutMs;
|
|
1489
|
-
let attempt = 0;
|
|
1490
|
-
let lastError: unknown;
|
|
1491
|
-
|
|
1492
|
-
while (!abortSignal.aborted && Date.now() < deadline) {
|
|
1493
|
-
attempt++;
|
|
1494
|
-
try {
|
|
1495
|
-
const remaining = Math.max(1, deadline - Date.now());
|
|
1496
|
-
return await openWebSocketAndWaitForBridgeHello({
|
|
1497
|
-
endpoint,
|
|
1498
|
-
openTimeoutMs: Math.min(10_000, remaining),
|
|
1499
|
-
getHelloTimeoutMs: () =>
|
|
1500
|
-
Math.min(5_000, Math.max(1, deadline - Date.now())),
|
|
1501
|
-
abortSignal,
|
|
1502
|
-
onHello,
|
|
1503
|
-
});
|
|
1504
|
-
} catch (err) {
|
|
1505
|
-
if (abortSignal.aborted) {
|
|
1506
|
-
throw abortSignal.reason ?? new Error('WebSocket connection aborted');
|
|
1507
|
-
}
|
|
1508
|
-
lastError = err;
|
|
1509
|
-
const remaining = deadline - Date.now();
|
|
1510
|
-
if (remaining <= 0) break;
|
|
1511
|
-
await sleep({
|
|
1512
|
-
ms: Math.min(250 * attempt, 1_000, remaining),
|
|
1513
|
-
abortSignal,
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
if (abortSignal.aborted) {
|
|
1519
|
-
throw abortSignal.reason ?? new Error('WebSocket connection aborted');
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
throw new Error(
|
|
1523
|
-
`claude-code bridge did not complete WebSocket handshake within ${timeoutMs}ms after ${attempt} attempt(s). Last error: ${formatUnknownError(lastError)}`,
|
|
1524
|
-
);
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
function webSocketMessageToString(raw: unknown): string {
|
|
1528
|
-
if (typeof raw === 'string') return raw;
|
|
1529
|
-
if (Buffer.isBuffer(raw)) return raw.toString('utf8');
|
|
1530
|
-
if (Array.isArray(raw)) return Buffer.concat(raw).toString('utf8');
|
|
1531
|
-
if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
|
|
1532
|
-
if (ArrayBuffer.isView(raw)) {
|
|
1533
|
-
return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString(
|
|
1534
|
-
'utf8',
|
|
1535
|
-
);
|
|
1536
|
-
}
|
|
1537
|
-
return String(raw);
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
function formatUnknownError(error: unknown): string {
|
|
1541
|
-
if (error instanceof Error) return error.message;
|
|
1542
|
-
return String(error);
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
1263
|
function createSession({
|
|
1546
1264
|
sessionId,
|
|
1547
1265
|
channel,
|