@ai-sdk/harness-opencode 1.0.119 → 1.0.121

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-opencode",
3
- "version": "1.0.119",
3
+ "version": "1.0.121",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.117",
29
+ "@ai-sdk/harness": "1.0.119",
30
30
  "@ai-sdk/provider-utils": "5.0.45",
31
31
  "ws": "8.21.0"
32
32
  },
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@modelcontextprotocol/sdk": "1.29.0",
38
- "@opencode-ai/sdk": "1.18.23",
38
+ "@opencode-ai/sdk": "1.18.31",
39
39
  "@types/node": "22.19.19",
40
40
  "@types/ws": "^8.5.13",
41
41
  "@vercel/ai-tsconfig": "0.0.0",
@@ -4,6 +4,7 @@ import {
4
4
  type BridgeTurn,
5
5
  } from '@ai-sdk/harness/bridge';
6
6
  import { randomUUID } from 'node:crypto';
7
+ import { realpathSync } from 'node:fs';
7
8
  import path from 'node:path';
8
9
  import { argv, env as procEnv } from 'node:process';
9
10
  import { isDeepStrictEqual } from 'node:util';
@@ -1304,9 +1305,6 @@ async function selectPermissionReply({
1304
1305
  emit: Emit;
1305
1306
  }): Promise<{ reply: 'once' | 'always' | 'reject'; message?: string }> {
1306
1307
  const toolName = toPermissionToolName(action);
1307
- if (resources.some(resource => isExternalPath(resource))) {
1308
- return { reply: 'reject', message: 'External directory access rejected.' };
1309
- }
1310
1308
  if (
1311
1309
  isBuiltinToolInactive({ toolName, toolFiltering: builtinToolFiltering })
1312
1310
  ) {
@@ -1326,6 +1324,9 @@ async function selectPermissionReply({
1326
1324
  if (!permissionMode || permissionMode === 'allow-all') {
1327
1325
  return { reply: 'always' };
1328
1326
  }
1327
+ if (resources.some(resource => isExternalPath(resource))) {
1328
+ return { reply: 'reject', message: 'External directory access rejected.' };
1329
+ }
1329
1330
  const kind = TOOL_KIND[toolName] ?? 'bash';
1330
1331
  const allowed =
1331
1332
  permissionMode === 'allow-edits'
@@ -1387,16 +1388,38 @@ function isBuiltinToolInactive(input: {
1387
1388
 
1388
1389
  function isExternalPath(resource: string): boolean {
1389
1390
  if (!path.isAbsolute(resource)) return false;
1390
- const normalized = path.resolve(resource);
1391
1391
  return (
1392
- !isPathInsideOrEqual(normalized, workdir) &&
1393
- (!skillsDir || !isPathInsideOrEqual(normalized, skillsDir))
1392
+ !isPathInsideOrEqual(resource, workdir) &&
1393
+ (!skillsDir || !isPathInsideOrEqual(resource, skillsDir))
1394
1394
  );
1395
1395
  }
1396
1396
 
1397
1397
  function isPathInsideOrEqual(file: string, root: string): boolean {
1398
- const normalizedRoot = path.resolve(root);
1399
- return file === normalizedRoot || file.startsWith(`${normalizedRoot}/`);
1398
+ const relative = path.relative(
1399
+ canonicalizeForContainment(root),
1400
+ canonicalizeForContainment(file),
1401
+ );
1402
+ return (
1403
+ relative === '' ||
1404
+ (relative !== '..' &&
1405
+ !relative.startsWith(`..${path.sep}`) &&
1406
+ !path.isAbsolute(relative))
1407
+ );
1408
+ }
1409
+
1410
+ function canonicalizeForContainment(inputPath: string): string {
1411
+ const normalized = path.resolve(inputPath);
1412
+ try {
1413
+ return realpathSync.native(normalized);
1414
+ } catch {
1415
+ const parent = path.dirname(normalized);
1416
+ return parent === normalized
1417
+ ? normalized
1418
+ : path.join(
1419
+ canonicalizeForContainment(parent),
1420
+ path.basename(normalized),
1421
+ );
1422
+ }
1400
1423
  }
1401
1424
 
1402
1425
  function toWireToolName(nativeName: string): string {
@@ -5,8 +5,8 @@
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@modelcontextprotocol/sdk": "1.29.0",
8
- "@opencode-ai/sdk": "1.18.23",
9
- "opencode-ai": "1.18.23",
8
+ "@opencode-ai/sdk": "1.18.31",
9
+ "opencode-ai": "1.18.31",
10
10
  "ws": "8.21.0",
11
11
  "zod": "3.25.76"
12
12
  }
@@ -12,11 +12,11 @@ importers:
12
12
  specifier: 1.29.0
13
13
  version: 1.29.0(zod@3.25.76)
14
14
  '@opencode-ai/sdk':
15
- specifier: 1.18.23
16
- version: 1.18.23
15
+ specifier: 1.18.31
16
+ version: 1.18.31
17
17
  opencode-ai:
18
- specifier: 1.18.23
19
- version: 1.18.23
18
+ specifier: 1.18.31
19
+ version: 1.18.31
20
20
  ws:
21
21
  specifier: 8.21.0
22
22
  version: 8.21.0
@@ -42,8 +42,8 @@ packages:
42
42
  '@cfworker/json-schema':
43
43
  optional: true
44
44
 
45
- '@opencode-ai/sdk@1.18.23':
46
- resolution: {integrity: sha512-VouYbL8O2ynLq0atr5fjzCv8YLtFi/zKLQ/uYkCvTJ4CmUdA01XwPn8om+u3TvxnGEfQsBfmThGU141vt3sg5w==}
45
+ '@opencode-ai/sdk@1.18.31':
46
+ resolution: {integrity: sha512-Raouthf8Lhe9edjvYeeSK7SgvdoU6bBjH9qV3f70dHoa6h+z0X2TMz/e22/wKp/StlFUZ4kIRpYYxFnY8/k01w==}
47
47
 
48
48
  accepts@2.0.0:
49
49
  resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
@@ -286,72 +286,72 @@ packages:
286
286
  once@1.4.0:
287
287
  resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
288
288
 
289
- opencode-ai@1.18.23:
290
- resolution: {integrity: sha512-3NkT0XINL7d0HYkTyGV1SPChHXhvRgKqNaTgKRTGb0TXUWszXA7MW/y3zMZw29y1AQuUDAzRvVYmQ9KGRQhroA==}
289
+ opencode-ai@1.18.31:
290
+ resolution: {integrity: sha512-J95feefeWwtIaw3irx76WjzWcgQXxmuHmDVphvs5ep9X30fBJ6T6bFhw50i9Kx50MG/xPn5w2pafXIfNtdry9w==}
291
291
  cpu: [arm64, x64]
292
292
  os: [darwin, linux, win32]
293
293
  hasBin: true
294
294
 
295
- opencode-darwin-arm64@1.18.23:
296
- resolution: {integrity: sha512-QP9PjwpHtZoLVXw2WvUmPZecz7mWbQkT4t3K36B//fCaDG+zWa+SsztIeaW5azujNwwtUemLA5icE/zINng48Q==}
295
+ opencode-darwin-arm64@1.18.31:
296
+ resolution: {integrity: sha512-IN642oFENleWVDNqMnXi8NQLsZnA8hMswJ6X+GWz+CfBFuCpotjDb5OHgE2HBdC6r5wjG6v0mATb+Mn8xXCp1A==}
297
297
  cpu: [arm64]
298
298
  os: [darwin]
299
299
 
300
- opencode-darwin-x64-baseline@1.18.23:
301
- resolution: {integrity: sha512-QGx6I/nFYur7qJ/Nx2L3fC4XYQt44cyDsm7p8twNA+cdjGX3ndnPbMdAl5ikdZyAfSMUGYK8VWY2JMxv0rmfjw==}
300
+ opencode-darwin-x64-baseline@1.18.31:
301
+ resolution: {integrity: sha512-4/4UMyPhsIeHln3AT3bxBvrWC+cmbOcdGOeBfoeOQIWcNZ3ycSvFnoJdLX969p9CtR6oNtroaQoNKZHgzSqg6A==}
302
302
  cpu: [x64]
303
303
  os: [darwin]
304
304
 
305
- opencode-darwin-x64@1.18.23:
306
- resolution: {integrity: sha512-R9nWP3edz/0FnEfwmuxtiWBB7bS4NtZCyCffJyiMlrbwdDC+bIXYrWxWXVrzaP1mJujs6g2MAwTCUSx/qpBhDw==}
305
+ opencode-darwin-x64@1.18.31:
306
+ resolution: {integrity: sha512-xrw512dou9Drxs/1yH4CHEt/kvrSl8j1Ulez0wTOsW0lWZaATJMnlfHBjagWzv+QWIC4nz/cBgYzjn3JvxS7zQ==}
307
307
  cpu: [x64]
308
308
  os: [darwin]
309
309
 
310
- opencode-linux-arm64-musl@1.18.23:
311
- resolution: {integrity: sha512-VyDkzUJfJgkx9h9RhazTW9xeTgSXBmVFPblsbPGBW9tR612f6gjQxfOfu4cpHnQHK1qjuW9ClzLKHWqv3EcJTA==}
310
+ opencode-linux-arm64-musl@1.18.31:
311
+ resolution: {integrity: sha512-qeEg7HsYupDeLkFqr+QNIgcE4kVRPycb/7ymyYUffSg+z2A6fU065pvU6uaZ+LjIzdJaZHUe1wDQvrSP/EJAqg==}
312
312
  cpu: [arm64]
313
313
  os: [linux]
314
314
  libc: [musl]
315
315
 
316
- opencode-linux-arm64@1.18.23:
317
- resolution: {integrity: sha512-g1zDFhuE9FOYwjSGderlu69wfd4GQzS0xsDiIY11QUciuBmM6DrHqLvmhlLFsUVHYVnCPY1YeN1pq5ewE3x72Q==}
316
+ opencode-linux-arm64@1.18.31:
317
+ resolution: {integrity: sha512-tLTZlOeWz4u48WQWg7PhKTn4gIXPHf7xaiWSHcmci1O3qKXEDLv/TaMzmqIuMChdwtflu2PSNIC00W+Hij/IkA==}
318
318
  cpu: [arm64]
319
319
  os: [linux]
320
320
 
321
- opencode-linux-x64-baseline-musl@1.18.23:
322
- resolution: {integrity: sha512-c1DPxauhzAurlIBhJBr/rokDpc65l084T4qTl36gDDT9Xzc/Nk5Q5yMDaPm1DDI3WeHKDt11MDlxT5AjQW5gtw==}
321
+ opencode-linux-x64-baseline-musl@1.18.31:
322
+ resolution: {integrity: sha512-gZ9TkeoIkckNZjDRreHj0sQk91uRQTgeuMCIZgSKewHN1fq/jjQHMzSAI0HevPEov9O2cDRRzPBF4cppzc+AYg==}
323
323
  cpu: [x64]
324
324
  os: [linux]
325
325
  libc: [musl]
326
326
 
327
- opencode-linux-x64-baseline@1.18.23:
328
- resolution: {integrity: sha512-yUhBOXfTQour2JCdAkwD3DDqSnyxB0grefwdPqEhYmJHIkYxfJIIzyy6V//pyouvkE0XMouFtiuZXw8S6Wo0iQ==}
327
+ opencode-linux-x64-baseline@1.18.31:
328
+ resolution: {integrity: sha512-T/hyCHb7gSCuJzLDXs8YfEhjelUes2zkWFPTGEdBPqV/eKrKbEo9U1YdA/WvntjXv0I6k+blZEID54QIcF7Djw==}
329
329
  cpu: [x64]
330
330
  os: [linux]
331
331
 
332
- opencode-linux-x64-musl@1.18.23:
333
- resolution: {integrity: sha512-t/5mlnTBZKdZpqKHwdwxlWqGakntauvMSmXtyJc17M7XJRmZaaGHtNSaSefbbYFIL4agoQCXTvIkhhyxOvr7zQ==}
332
+ opencode-linux-x64-musl@1.18.31:
333
+ resolution: {integrity: sha512-TxKfcJII53MZ17NSrJ0p51wx0dJtZrX8By60N5O5/M+eP0oO4jCXKIkdBpP/Bku44gm8QZkTNLBWpPc2OTJweg==}
334
334
  cpu: [x64]
335
335
  os: [linux]
336
336
  libc: [musl]
337
337
 
338
- opencode-linux-x64@1.18.23:
339
- resolution: {integrity: sha512-5x9d1Cm/YtqzR6lAlNbgVprTQ3R3hx7qGTWCzm5l5u6lBNkhYTrhy2s8k25dxKKuxqZ9Kngqz9JYWvSVHy2Lmw==}
338
+ opencode-linux-x64@1.18.31:
339
+ resolution: {integrity: sha512-gTJ5nGNq+KJblwmeusEdz8YFwvkV7a3xTOV9EKKEmsv0Lkbajq/oQO9jEC0sMiS66mjHDOrhLgIUxSVqfLBM8g==}
340
340
  cpu: [x64]
341
341
  os: [linux]
342
342
 
343
- opencode-windows-arm64@1.18.23:
344
- resolution: {integrity: sha512-QtJQcLU0yPz6on3jjks3f/EHgZuIDFw7FvAKu3wsHhL09NYDh7GczfRXDPRHa3NgqnU8dkB8p9mhqgaPRPogoQ==}
343
+ opencode-windows-arm64@1.18.31:
344
+ resolution: {integrity: sha512-K+Dh5r5yLQJ6Maeo1+JhQr27Un6lzUIWEp3LGoeXSfLkcSqi7ueTecGbg7HAbtEIY5a9j3yNCS3//AjgTcZa+w==}
345
345
  cpu: [arm64]
346
346
  os: [win32]
347
347
 
348
- opencode-windows-x64-baseline@1.18.23:
349
- resolution: {integrity: sha512-AqXsTKaPcDx3rrid5bLUwJbQ/3vr9rJ6fvOStIznTzwrbOgP8wy5G4jCoIzu6KB/WxGx/d1MrV4cGaJ73qnjBA==}
348
+ opencode-windows-x64-baseline@1.18.31:
349
+ resolution: {integrity: sha512-wz5K8SpL+GTKez4BpYX1AbhRwBY7AVt+95ijWZrow84ibLTDoZ1skEsN5UUcQJ8LYeC+o1Vq0xi4A5kLGfvQ0w==}
350
350
  cpu: [x64]
351
351
  os: [win32]
352
352
 
353
- opencode-windows-x64@1.18.23:
354
- resolution: {integrity: sha512-mMaIITuXzkNfjdcYL8uZaZuMDjulFyH/UCq9bxblam2mUZf9uWisoi5J6CXFsS/mkN7CZfTAt6PttSp4n3PH4g==}
353
+ opencode-windows-x64@1.18.31:
354
+ resolution: {integrity: sha512-1Za7uKeGLonNUkUIGHD7xfV5omi7ju/7z2WYfS5YvDLTr1iU/W1PPVHzHXiwiyV4gMxsWGwHHLubWdH99D7pSA==}
355
355
  cpu: [x64]
356
356
  os: [win32]
357
357
 
@@ -508,7 +508,7 @@ snapshots:
508
508
  transitivePeerDependencies:
509
509
  - supports-color
510
510
 
511
- '@opencode-ai/sdk@1.18.23':
511
+ '@opencode-ai/sdk@1.18.31':
512
512
  dependencies:
513
513
  cross-spawn: 7.0.6
514
514
 
@@ -752,55 +752,55 @@ snapshots:
752
752
  dependencies:
753
753
  wrappy: 1.0.2
754
754
 
755
- opencode-ai@1.18.23:
755
+ opencode-ai@1.18.31:
756
756
  optionalDependencies:
757
- opencode-darwin-arm64: 1.18.23
758
- opencode-darwin-x64: 1.18.23
759
- opencode-darwin-x64-baseline: 1.18.23
760
- opencode-linux-arm64: 1.18.23
761
- opencode-linux-arm64-musl: 1.18.23
762
- opencode-linux-x64: 1.18.23
763
- opencode-linux-x64-baseline: 1.18.23
764
- opencode-linux-x64-baseline-musl: 1.18.23
765
- opencode-linux-x64-musl: 1.18.23
766
- opencode-windows-arm64: 1.18.23
767
- opencode-windows-x64: 1.18.23
768
- opencode-windows-x64-baseline: 1.18.23
769
-
770
- opencode-darwin-arm64@1.18.23:
757
+ opencode-darwin-arm64: 1.18.31
758
+ opencode-darwin-x64: 1.18.31
759
+ opencode-darwin-x64-baseline: 1.18.31
760
+ opencode-linux-arm64: 1.18.31
761
+ opencode-linux-arm64-musl: 1.18.31
762
+ opencode-linux-x64: 1.18.31
763
+ opencode-linux-x64-baseline: 1.18.31
764
+ opencode-linux-x64-baseline-musl: 1.18.31
765
+ opencode-linux-x64-musl: 1.18.31
766
+ opencode-windows-arm64: 1.18.31
767
+ opencode-windows-x64: 1.18.31
768
+ opencode-windows-x64-baseline: 1.18.31
769
+
770
+ opencode-darwin-arm64@1.18.31:
771
771
  optional: true
772
772
 
773
- opencode-darwin-x64-baseline@1.18.23:
773
+ opencode-darwin-x64-baseline@1.18.31:
774
774
  optional: true
775
775
 
776
- opencode-darwin-x64@1.18.23:
776
+ opencode-darwin-x64@1.18.31:
777
777
  optional: true
778
778
 
779
- opencode-linux-arm64-musl@1.18.23:
779
+ opencode-linux-arm64-musl@1.18.31:
780
780
  optional: true
781
781
 
782
- opencode-linux-arm64@1.18.23:
782
+ opencode-linux-arm64@1.18.31:
783
783
  optional: true
784
784
 
785
- opencode-linux-x64-baseline-musl@1.18.23:
785
+ opencode-linux-x64-baseline-musl@1.18.31:
786
786
  optional: true
787
787
 
788
- opencode-linux-x64-baseline@1.18.23:
788
+ opencode-linux-x64-baseline@1.18.31:
789
789
  optional: true
790
790
 
791
- opencode-linux-x64-musl@1.18.23:
791
+ opencode-linux-x64-musl@1.18.31:
792
792
  optional: true
793
793
 
794
- opencode-linux-x64@1.18.23:
794
+ opencode-linux-x64@1.18.31:
795
795
  optional: true
796
796
 
797
- opencode-windows-arm64@1.18.23:
797
+ opencode-windows-arm64@1.18.31:
798
798
  optional: true
799
799
 
800
- opencode-windows-x64-baseline@1.18.23:
800
+ opencode-windows-x64-baseline@1.18.31:
801
801
  optional: true
802
802
 
803
- opencode-windows-x64@1.18.23:
803
+ opencode-windows-x64@1.18.31:
804
804
  optional: true
805
805
 
806
806
  parseurl@1.3.3: {}
@@ -1,2 +1,2 @@
1
1
  allowBuilds:
2
- 'opencode-ai@1.18.23': true
2
+ 'opencode-ai@1.18.31': true
@@ -43,6 +43,7 @@ import {
43
43
  waitForBridgeReady,
44
44
  withBridgeToken,
45
45
  writeSkills,
46
+ type SandboxChannelReconnectOptions,
46
47
  type WriteSkillsResult,
47
48
  } from '@ai-sdk/harness/utils';
48
49
  import {
@@ -120,6 +121,13 @@ export type OpenCodeHarnessSettings = {
120
121
  */
121
122
  readonly portEndpoint?: HarnessV1PortEndpoint;
122
123
  readonly startupTimeoutMs?: number;
124
+ /**
125
+ * Configures reconnection attempts after an established bridge connection
126
+ * drops. The reconnect window includes connection establishment and
127
+ * backoff delays. Defaults to 30 seconds with exponential backoff from 50
128
+ * milliseconds up to 2 seconds.
129
+ */
130
+ readonly reconnect?: SandboxChannelReconnectOptions;
123
131
  /**
124
132
  * Creates the authentication token used by the sandbox bridge. Defaults to
125
133
  * a random 32-byte hexadecimal token.
@@ -437,10 +445,11 @@ export function createOpenCode(
437
445
  });
438
446
  let supportsUserMessageResponses = false;
439
447
  const attachChannel: OpenCodeChannel = new SandboxChannel({
440
- connect: () =>
448
+ connect: ({ abortSignal }) =>
441
449
  openWebSocket({
442
450
  endpoint: attachEndpoint,
443
451
  helloTimeoutMs: Math.min(timeoutMs, 5_000),
452
+ abortSignal,
444
453
  onHello: supported => {
445
454
  supportsUserMessageResponses = supported;
446
455
  },
@@ -449,6 +458,7 @@ export function createOpenCode(
449
458
  initialLastSeenEventId: coords.lastSeenEventId,
450
459
  onDiagnostic,
451
460
  onBridgeError,
461
+ reconnect: settings.reconnect,
452
462
  });
453
463
  await attachChannel.open(isContinue ? { resume: true } : undefined);
454
464
  return createSession({
@@ -610,10 +620,11 @@ export function createOpenCode(
610
620
  let supportsUserMessageResponses = false;
611
621
 
612
622
  const channel: OpenCodeChannel = new SandboxChannel({
613
- connect: () =>
623
+ connect: ({ abortSignal }) =>
614
624
  openWebSocket({
615
625
  endpoint: bridgeEndpoint,
616
626
  helloTimeoutMs: Math.min(timeoutMs, 5_000),
627
+ abortSignal,
617
628
  onHello: supported => {
618
629
  supportsUserMessageResponses = supported;
619
630
  },
@@ -621,6 +632,7 @@ export function createOpenCode(
621
632
  outboundSchema: outboundMessageSchema,
622
633
  onDiagnostic,
623
634
  onBridgeError,
635
+ reconnect: settings.reconnect,
624
636
  ...(respawnStrategy === 'replay'
625
637
  ? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 }
626
638
  : {}),
@@ -726,25 +738,39 @@ async function resolveBridgeEndpoint({
726
738
  function openWebSocket({
727
739
  endpoint,
728
740
  helloTimeoutMs,
741
+ abortSignal,
729
742
  onHello,
730
743
  }: {
731
744
  endpoint: HarnessV1PortEndpoint;
732
745
  helloTimeoutMs: number;
746
+ abortSignal: AbortSignal;
733
747
  onHello(supportsUserMessageResponses: boolean): void;
734
748
  }): Promise<WebSocket> {
735
749
  return new Promise((resolve, reject) => {
750
+ const abortReason = () =>
751
+ abortSignal.reason ?? new Error('WebSocket connection aborted');
752
+ if (abortSignal.aborted) {
753
+ reject(abortReason());
754
+ return;
755
+ }
756
+
736
757
  const ws = new WebSocket(endpoint.url, {
737
758
  headers: endpoint.headers == null ? undefined : { ...endpoint.headers },
738
759
  });
739
760
  let opened = false;
740
761
  let receivedHello = false;
741
762
  let settled = false;
763
+ let helloTimer: ReturnType<typeof setTimeout> | undefined;
764
+ let onAbort: (() => void) | undefined;
742
765
  const cleanup = () => {
743
766
  clearTimeout(helloTimer);
744
767
  ws.off('open', onOpen);
745
768
  ws.off('message', onMessage);
746
769
  ws.off('close', onClose);
747
770
  ws.off('error', onError);
771
+ if (onAbort != null) {
772
+ abortSignal.removeEventListener('abort', onAbort);
773
+ }
748
774
  };
749
775
  const settle = (error?: unknown) => {
750
776
  if (settled) return;
@@ -753,6 +779,20 @@ function openWebSocket({
753
779
  if (error == null) {
754
780
  resolve(ws);
755
781
  } else {
782
+ const suppressError = () => {};
783
+ const socketWithOnce = ws as WebSocket & {
784
+ once?: (event: string, listener: () => void) => void;
785
+ };
786
+ socketWithOnce.once?.('error', suppressError);
787
+ try {
788
+ ws.terminate();
789
+ } catch {
790
+ try {
791
+ ws.close();
792
+ } catch {
793
+ // best-effort
794
+ }
795
+ }
756
796
  reject(error);
757
797
  }
758
798
  };
@@ -793,11 +833,17 @@ function openWebSocket({
793
833
  const onError = (err: Error) => {
794
834
  settle(err);
795
835
  };
836
+ onAbort = () => settle(abortReason());
796
837
  ws.on('open', onOpen);
797
838
  ws.on('message', onMessage);
798
839
  ws.on('close', onClose);
799
840
  ws.on('error', onError);
800
- const helloTimer = setTimeout(
841
+ abortSignal.addEventListener('abort', onAbort, { once: true });
842
+ if (abortSignal.aborted) {
843
+ onAbort();
844
+ return;
845
+ }
846
+ helloTimer = setTimeout(
801
847
  () =>
802
848
  settle(
803
849
  new Error(