@openclaw/voice-call 2026.10.10 → 2026.11.5-beta.1

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.
Files changed (120) hide show
  1. package/dist/.setup/{carrier-api-BgJdQC-e.mjs → carrier-api-CZQtPnYQ.mjs} +15 -28
  2. package/dist/.setup/{cli-CI9QzIaT.mjs → cli-BUVmNuk8.mjs} +113 -61
  3. package/dist/.setup/{config-BxESUUSm.mjs → config-D_w9HzxP.mjs} +55 -111
  4. package/dist/.setup/{mock-BO3pptb-.mjs → mock-fux1a_i_.mjs} +3 -4
  5. package/dist/.setup/{plivo-C5cLYAup.mjs → plivo-Cpxxx8BT.mjs} +6 -21
  6. package/dist/.setup/{realtime-handler-B2JJ-8wv.mjs → realtime-handler-BkoZ-ZYd.mjs} +190 -265
  7. package/dist/.setup/{response-generator-B2nVYBf_.mjs → response-generator-COluXT2F.mjs} +11 -15
  8. package/dist/.setup/{runtime-entry-CKPPin7r.mjs → runtime-entry-Br7ttAwq.mjs} +382 -522
  9. package/dist/.setup/{state-retention-api-y5WYfZsQ.mjs → state-retention-api-BDJR8dnG.mjs} +1 -1
  10. package/dist/.setup/{store-path-Bn3bLr8y.mjs → store-path-C0yJCDE8.mjs} +2 -2
  11. package/dist/.setup/{telnyx-ZopMHuZl.mjs → telnyx-DdFzdUEf.mjs} +4 -5
  12. package/dist/.setup/{twilio-BGLnhzpD.mjs → twilio-BqUbz9jp.mjs} +7 -15
  13. package/dist/.setup/webhook-replay-DnlMxw4z.mjs +30 -0
  14. package/dist/.source-module-urls/8b7cb5a3dc0f034090cbbda318ba348c0d48853d04f0e3b500529cf7666abe03.json +1 -1
  15. package/dist/doctor-contract-api.js +21 -2
  16. package/dist/index.js +106 -195
  17. package/dist/runtime-entry.js +1 -1
  18. package/dist/setup-api.js +1 -2
  19. package/dist/state-retention-api.js +1 -1
  20. package/node_modules/typebox/build/format/duration.d.mts +2 -2
  21. package/node_modules/typebox/build/format/duration.mjs +2 -2
  22. package/node_modules/typebox/build/format/idn_email.d.mts +1 -0
  23. package/node_modules/typebox/build/format/idn_email.mjs +1 -0
  24. package/node_modules/typebox/build/format/idn_hostname.d.mts +1 -1
  25. package/node_modules/typebox/build/format/idn_hostname.mjs +1 -1
  26. package/node_modules/typebox/build/format/ipv4.d.mts +2 -2
  27. package/node_modules/typebox/build/format/ipv4.mjs +2 -2
  28. package/node_modules/typebox/build/format/ipv6.d.mts +1 -1
  29. package/node_modules/typebox/build/format/ipv6.mjs +1 -1
  30. package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
  31. package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
  32. package/node_modules/typebox/build/schema/build.mjs +1 -0
  33. package/node_modules/typebox/build/schema/engine/_unique.d.mts +4 -2
  34. package/node_modules/typebox/build/schema/engine/_unique.mjs +7 -3
  35. package/node_modules/typebox/build/schema/engine/additionalItems.mjs +3 -3
  36. package/node_modules/typebox/build/schema/engine/additionalProperties.d.mts +0 -3
  37. package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +15 -17
  38. package/node_modules/typebox/build/schema/engine/contains.mjs +3 -3
  39. package/node_modules/typebox/build/schema/engine/index.d.mts +1 -0
  40. package/node_modules/typebox/build/schema/engine/index.mjs +1 -0
  41. package/node_modules/typebox/build/schema/engine/items.mjs +7 -4
  42. package/node_modules/typebox/build/schema/engine/maxContains.mjs +2 -2
  43. package/node_modules/typebox/build/schema/engine/minContains.mjs +3 -3
  44. package/node_modules/typebox/build/schema/engine/oneOf.mjs +2 -2
  45. package/node_modules/typebox/build/schema/engine/patternProperties.mjs +2 -2
  46. package/node_modules/typebox/build/schema/engine/propertyNames.mjs +2 -2
  47. package/node_modules/typebox/build/schema/engine/unevaluatedItems.mjs +2 -2
  48. package/node_modules/typebox/build/schema/engine/unevaluatedProperties.mjs +2 -2
  49. package/node_modules/typebox/build/system/hashing/hash.mjs +63 -34
  50. package/node_modules/typebox/build/system/locale/en_US.mjs +2 -1
  51. package/node_modules/typebox/build/system/memory/assign.d.mts +2 -8
  52. package/node_modules/typebox/build/system/memory/assign.mjs +13 -6
  53. package/node_modules/typebox/build/system/memory/clone.d.mts +4 -5
  54. package/node_modules/typebox/build/system/memory/clone.mjs +8 -96
  55. package/node_modules/typebox/build/system/memory/create.d.mts +1 -5
  56. package/node_modules/typebox/build/system/memory/create.mjs +18 -13
  57. package/node_modules/typebox/build/system/memory/deep.d.mts +2 -0
  58. package/node_modules/typebox/build/system/memory/deep.mjs +108 -0
  59. package/node_modules/typebox/build/system/memory/discard.d.mts +1 -1
  60. package/node_modules/typebox/build/system/memory/discard.mjs +9 -8
  61. package/node_modules/typebox/build/system/memory/finalize.d.mts +4 -0
  62. package/node_modules/typebox/build/system/memory/finalize.mjs +30 -0
  63. package/node_modules/typebox/build/system/memory/keys.d.mts +2 -0
  64. package/node_modules/typebox/build/system/memory/keys.mjs +8 -0
  65. package/node_modules/typebox/build/system/memory/update.d.mts +1 -4
  66. package/node_modules/typebox/build/system/memory/update.mjs +43 -16
  67. package/node_modules/typebox/build/type/engine/call/index.d.mts +1 -0
  68. package/node_modules/typebox/build/type/engine/call/index.mjs +1 -0
  69. package/node_modules/typebox/build/type/engine/evaluate/composite.d.mts +2 -24
  70. package/node_modules/typebox/build/type/engine/evaluate/composite.mjs +2 -29
  71. package/node_modules/typebox/build/type/engine/evaluate/index.d.mts +1 -0
  72. package/node_modules/typebox/build/type/engine/evaluate/index.mjs +1 -0
  73. package/node_modules/typebox/build/type/engine/evaluate/narrow.d.mts +5 -1
  74. package/node_modules/typebox/build/type/engine/evaluate/narrow.mjs +6 -1
  75. package/node_modules/typebox/build/type/engine/evaluate/property.d.mts +30 -0
  76. package/node_modules/typebox/build/type/engine/evaluate/property.mjs +29 -0
  77. package/node_modules/typebox/build/type/engine/immutable/instantiate_add.d.mts +1 -3
  78. package/node_modules/typebox/build/type/engine/immutable/instantiate_add.mjs +1 -4
  79. package/node_modules/typebox/build/type/engine/object/from_intersect.d.mts +2 -2
  80. package/node_modules/typebox/build/type/engine/object/from_intersect.mjs +2 -2
  81. package/node_modules/typebox/build/type/engine/object/from_union.d.mts +2 -2
  82. package/node_modules/typebox/build/type/engine/object/from_union.mjs +2 -2
  83. package/node_modules/typebox/build/type/engine/optional/instantiate_add.d.mts +1 -3
  84. package/node_modules/typebox/build/type/engine/optional/instantiate_add.mjs +1 -4
  85. package/node_modules/typebox/build/type/engine/readonly/instantiate_add.d.mts +1 -3
  86. package/node_modules/typebox/build/type/engine/readonly/instantiate_add.mjs +1 -4
  87. package/node_modules/typebox/build/type/engine/this/index.d.mts +1 -0
  88. package/node_modules/typebox/build/type/engine/this/index.mjs +1 -0
  89. package/node_modules/typebox/build/value/clone/clone.d.mts +4 -3
  90. package/node_modules/typebox/build/value/clone/clone.mjs +5 -7
  91. package/node_modules/typebox/build/value/repair/from_cyclic.d.mts +2 -0
  92. package/node_modules/typebox/build/value/repair/from_cyclic.mjs +6 -0
  93. package/node_modules/typebox/build/value/shared/union_priority_sort.mjs +25 -14
  94. package/node_modules/typebox/package.json +1 -1
  95. package/node_modules/typebox/readme.md +1 -1
  96. package/node_modules/zod/package.json +1 -1
  97. package/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
  98. package/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
  99. package/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
  100. package/node_modules/zod/src/v4/core/compile.ts +9 -0
  101. package/node_modules/zod/src/v4/core/schemas.ts +35 -1
  102. package/node_modules/zod/src/v4/core/util.ts +13 -0
  103. package/node_modules/zod/src/v4/core/versions.ts +1 -1
  104. package/node_modules/zod/v4/core/compile.cjs +8 -0
  105. package/node_modules/zod/v4/core/compile.js +8 -0
  106. package/node_modules/zod/v4/core/schemas.cjs +30 -0
  107. package/node_modules/zod/v4/core/schemas.d.cts +3 -1
  108. package/node_modules/zod/v4/core/schemas.d.ts +3 -1
  109. package/node_modules/zod/v4/core/schemas.js +30 -0
  110. package/node_modules/zod/v4/core/util.cjs +15 -0
  111. package/node_modules/zod/v4/core/util.d.cts +2 -0
  112. package/node_modules/zod/v4/core/util.d.ts +2 -0
  113. package/node_modules/zod/v4/core/util.js +14 -0
  114. package/node_modules/zod/v4/core/versions.cjs +1 -1
  115. package/node_modules/zod/v4/core/versions.js +1 -1
  116. package/openclaw.plugin.json +9 -2
  117. package/package.json +6 -6
  118. package/dist/.setup/webhook-replay-B0Eo3Ll5.mjs +0 -42
  119. package/node_modules/typebox/build/system/memory/freeze.d.mts +0 -4
  120. package/node_modules/typebox/build/system/memory/freeze.mjs +0 -6
@@ -1,20 +1,18 @@
1
1
  import { fetchWithSsrFGuard } from "../api.js";
2
- import { c as getHeader, r as normalizeProxyIp } from "./runtime-entry-CKPPin7r.mjs";
3
- import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-B0Eo3Ll5.mjs";
2
+ import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-Br7ttAwq.mjs";
3
+ import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-DnlMxw4z.mjs";
4
+ import crypto from "node:crypto";
4
5
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
6
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
6
7
  import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
7
- import crypto from "node:crypto";
8
8
  import { isIP } from "node:net";
9
+ import { sha256Hex } from "openclaw/plugin-sdk/node-crypto-runtime";
9
10
  import { redactSensitiveText, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
10
11
  import { readResponseTextPrefix, readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
11
12
  // extensions/voice-call/src/webhook-security.ts
12
13
  const twilioReplayCache = createWebhookReplayCache();
13
14
  const plivoReplayCache = createWebhookReplayCache();
14
15
  const telnyxReplayCache = createWebhookReplayCache();
15
- function sha256Hex(input) {
16
- return crypto.createHash("sha256").update(input).digest("hex");
17
- }
18
16
  function skipWebhookVerification(provider, ctx, cache) {
19
17
  const replayKey = `${provider}:skip:${sha256Hex(`${ctx.method}\n${ctx.url}\n${ctx.rawBody}`)}`;
20
18
  return {
@@ -254,9 +252,8 @@ function verifyTelnyxWebhook(ctx, publicKey, options) {
254
252
  ok: false,
255
253
  reason: "Invalid signature"
256
254
  };
257
- const maxSkewMs = options?.maxSkewMs ?? 3e5;
258
255
  const eventTimeMs = eventTimeSec * 1e3;
259
- if (Math.abs(Date.now() - eventTimeMs) > maxSkewMs) return {
256
+ if (Math.abs(Date.now() - eventTimeMs) > 3e5) return {
260
257
  ok: false,
261
258
  reason: "Timestamp too old"
262
259
  };
@@ -297,7 +294,6 @@ function verifyTwilioWebhook(ctx, authToken, options) {
297
294
  });
298
295
  return {
299
296
  ok: true,
300
- verificationUrl: candidateUrl,
301
297
  ...reserveWebhookReplay(twilioReplayCache, replayKey)
302
298
  };
303
299
  };
@@ -317,11 +313,9 @@ function verifyTwilioWebhook(ctx, authToken, options) {
317
313
  const result = verifyUrl(candidateUrl);
318
314
  if (result) return result;
319
315
  }
320
- const isNgrokFreeTier = verificationUrl.includes(".ngrok-free.app") || verificationUrl.includes(".ngrok.io");
321
316
  return {
322
317
  ok: false,
323
- reason: `Invalid signature for URL: ${redactTwilioVerificationUrlForDiagnostics(verificationUrl)}`,
324
- isNgrokFreeTier
318
+ reason: `Invalid signature for URL: ${redactTwilioVerificationUrlForDiagnostics(verificationUrl)}`
325
319
  };
326
320
  }
327
321
  function normalizeSignatureBase64(input) {
@@ -352,19 +346,13 @@ function constructPlivoV3BaseUrl(params) {
352
346
  function verifyPlivoSignature(params) {
353
347
  const hmacBase = `${params.baseUrl}${params.version === "v3" ? "." : ""}${params.nonce}`;
354
348
  const expected = normalizeSignatureBase64(crypto.createHmac("sha256", params.authToken).update(hmacBase).digest("base64"));
355
- const ok = params.signatures.map(normalizeSignatureBase64).some((signature) => safeEqualSecret(expected, signature));
356
- const result = {
357
- ok,
358
- version: params.version,
359
- verificationUrl: params.verificationUrl
360
- };
361
- if (!ok) return {
362
- ...result,
349
+ if (!params.signatures.map(normalizeSignatureBase64).some((signature) => safeEqualSecret(expected, signature))) return {
350
+ ok: false,
363
351
  reason: `Invalid Plivo ${params.version.toUpperCase()} signature`
364
352
  };
365
353
  const replayKey = `plivo:${params.version}:${sha256Hex(`${params.baseUrl}\n${params.nonce}`)}`;
366
354
  return {
367
- ...result,
355
+ ok: true,
368
356
  ...reserveWebhookReplay(plivoReplayCache, replayKey)
369
357
  };
370
358
  }
@@ -400,8 +388,6 @@ function verifyPlivoWebhook(ctx, authToken, options) {
400
388
  const method = ctx.method === "GET" || ctx.method === "POST" ? ctx.method : null;
401
389
  if (!method) return {
402
390
  ok: false,
403
- version: "v3",
404
- verificationUrl,
405
391
  reason: `Unsupported HTTP method for Plivo V3 signature: ${ctx.method}`
406
392
  };
407
393
  const postParams = new URLSearchParams(ctx.rawBody);
@@ -413,7 +399,6 @@ function verifyPlivoWebhook(ctx, authToken, options) {
413
399
  return verifyPlivoSignature({
414
400
  authToken,
415
401
  version: "v3",
416
- verificationUrl,
417
402
  signatures: normalizeStringEntries(signatureV3.split(",")),
418
403
  nonce: nonceV3,
419
404
  baseUrl
@@ -422,17 +407,19 @@ function verifyPlivoWebhook(ctx, authToken, options) {
422
407
  if (signatureV2 && nonceV2) return verifyPlivoSignature({
423
408
  authToken,
424
409
  version: "v2",
425
- verificationUrl,
426
410
  signatures: [signatureV2],
427
411
  nonce: nonceV2,
428
412
  baseUrl: getBaseUrlNoQuery(verificationUrl)
429
413
  });
430
414
  return {
431
415
  ok: false,
432
- reason: "Missing Plivo signature headers (V3 or V2)",
433
- verificationUrl
416
+ reason: "Missing Plivo signature headers (V3 or V2)"
434
417
  };
435
418
  }
419
+ // extensions/voice-call/src/providers/base.ts
420
+ function assertVoiceCallProviderSetting(value, message) {
421
+ if (!value) throw new Error(message);
422
+ }
436
423
  // extensions/voice-call/src/providers/shared/guarded-json-api.ts
437
424
  const VOICE_CALL_PROVIDER_API_TIMEOUT_MS = 3e4;
438
425
  const PROVIDER_JSON_RESPONSE_MAX_BYTES = 1048576;
@@ -524,4 +511,4 @@ function createCarrierApi(name, baseUrl, authorization, options = {}) {
524
511
  }
525
512
  };
526
513
  }
527
- export { verifyTwilioWebhook as a, verifyTelnyxWebhook as i, reconstructWebhookUrl as n, verifyPlivoWebhook as r, createCarrierApi as t };
514
+ export { verifyTelnyxWebhook as a, verifyPlivoWebhook as i, assertVoiceCallProviderSetting as n, verifyTwilioWebhook as o, reconstructWebhookUrl as r, createCarrierApi as t };
@@ -1,18 +1,19 @@
1
- import { sleep } from "../api.js";
2
- import { g as CallBriefSchema, l as resolveVoiceCallStreamExposurePaths, u as validateProviderConfig } from "./config-BxESUUSm.mjs";
3
- import { S as resolveVoiceCallAgentId, _ as findCallInStore, b as setVoiceCallStateRuntime, d as getTailscaleSelfInfo, f as setupTailscaleExposureRoutes, g as MAX_CALL_RECORD_EVENTS, l as resolveWebhookExposureStatus, u as cleanupTailscaleExposureRoute, v as getCallHistoryFromStore, y as loadActiveCallsFromStore } from "./runtime-entry-CKPPin7r.mjs";
4
- import { r as resolveUserPath, t as resolveDefaultVoiceCallStoreDir } from "./store-path-Bn3bLr8y.mjs";
1
+ import { c as resolveVoiceCallStreamExposurePaths, h as CallBriefSchema, l as validateProviderConfig } from "./config-D_w9HzxP.mjs";
2
+ import { _ as getCallHistoryFromStore, c as resolveWebhookExposureStatus, d as setupTailscaleExposureRoutes, g as findCallInStore, h as MAX_CALL_RECORD_EVENTS, l as cleanupTailscaleExposureRoute, u as getTailscaleSelfInfo, v as loadActiveCallsFromStore, x as resolveVoiceCallAgentId, y as setVoiceCallStateRuntime } from "./runtime-entry-Br7ttAwq.mjs";
3
+ import { r as resolveUserPath, t as resolveDefaultVoiceCallStoreDir } from "./store-path-C0yJCDE8.mjs";
5
4
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6
5
  import { callGatewayFromCli, isGatewayClientRequestError, isGatewayTransportError, redactSensitiveUrlLikeString } from "openclaw/plugin-sdk/gateway-runtime";
7
- import { isRecord, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
8
6
  import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
7
+ import { isRecord, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
8
+ import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
- import { realpath } from "node:fs/promises";
11
+ import { format } from "node:util";
12
12
  import { createDeferred } from "openclaw/plugin-sdk/concurrency-runtime";
13
+ import { realpath } from "node:fs/promises";
13
14
  import { readRegularFile } from "openclaw/plugin-sdk/file-access-runtime";
14
15
  import { once } from "node:events";
15
- import { format } from "node:util";
16
+ import { setTimeout } from "node:timers/promises";
16
17
  // extensions/voice-call/src/cli-command-io.ts
17
18
  function writeCliLine(...values) {
18
19
  process.stdout.write(`${format(...values)}\n`);
@@ -28,8 +29,9 @@ function parseCliInteger(raw, optionName, opts) {
28
29
  }
29
30
  // extensions/voice-call/src/cli-call-log.ts
30
31
  const READ_BYTES = 65536;
31
- async function writeLogChunk(chunk) {
32
- if (!process.stdout.write(chunk)) await once(process.stdout, "drain");
32
+ async function writeLogChunk(chunk, signal) {
33
+ signal.throwIfAborted();
34
+ if (!process.stdout.write(chunk)) await once(process.stdout, "drain", { signal });
33
35
  }
34
36
  function* readLogLine({ fd, start, end }) {
35
37
  for (let offset = start; offset < end;) {
@@ -40,20 +42,21 @@ function* readLogLine({ fd, start, end }) {
40
42
  yield chunk.subarray(0, bytesRead);
41
43
  }
42
44
  }
43
- async function* readLogLines(file, last, pollMs) {
45
+ async function* readLogLines(file, last, pollMs, signal) {
44
46
  let initial = true;
45
47
  let offset = 0;
46
48
  let lineStart = 0;
47
49
  let previous;
48
50
  const buffer = Buffer.alloc(READ_BYTES);
49
51
  for (;;) {
52
+ signal?.throwIfAborted();
50
53
  let fd;
51
54
  try {
52
55
  const flags = process.platform === "win32" ? "r" : fs.constants.O_RDONLY | fs.constants.O_NONBLOCK;
53
56
  fd = fs.openSync(file, flags);
54
57
  } catch (error) {
55
58
  if (initial || pollMs === void 0 || !isRecord(error) || error.code !== "ENOENT") throw error;
56
- await sleep(pollMs);
59
+ await setTimeout(pollMs, void 0, { signal });
57
60
  continue;
58
61
  }
59
62
  try {
@@ -95,7 +98,7 @@ async function* readLogLines(file, last, pollMs) {
95
98
  }
96
99
  initial = false;
97
100
  if (pollMs === void 0) return;
98
- await sleep(pollMs);
101
+ await setTimeout(pollMs, void 0, { signal });
99
102
  }
100
103
  }
101
104
  function summarizeSeries(values) {
@@ -142,24 +145,43 @@ function registerVoiceCallLogs(params) {
142
145
  const file = options.file;
143
146
  const since = parseCliInteger(options.since, "--since", { min: 0 });
144
147
  const pollMs = parseCliInteger(options.poll, "--poll", { min: 50 });
145
- if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") for await (const line of readLogLines(file, since, pollMs)) {
146
- for (const chunk of readLogLine(line)) await writeLogChunk(chunk);
147
- await writeLogChunk("\n");
148
- }
149
- else {
150
- params.ensureHistoryStateRuntime();
151
- let initial = true;
152
- let seen = /* @__PURE__ */ new Set();
153
- for (;;) {
154
- const lines = (await getCallHistoryFromStore(path.dirname(file), MAX_CALL_RECORD_EVENTS)).map((call) => JSON.stringify(call));
155
- for (const line of initial ? lines.slice(Math.max(0, lines.length - since)) : lines) if (!seen.has(line)) {
156
- await writeLogChunk(`${line}\n`);
157
- seen.add(line);
148
+ const cancellation = new AbortController();
149
+ const { signal } = cancellation;
150
+ const interrupt = (exitCode) => {
151
+ if (!signal.aborted) {
152
+ process.exitCode = exitCode;
153
+ cancellation.abort();
154
+ }
155
+ };
156
+ const onSigint = () => interrupt(130);
157
+ const onSigterm = () => interrupt(143);
158
+ process.on("SIGINT", onSigint);
159
+ process.on("SIGTERM", onSigterm);
160
+ try {
161
+ if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") for await (const line of readLogLines(file, since, pollMs, signal)) {
162
+ for (const chunk of readLogLine(line)) await writeLogChunk(chunk, signal);
163
+ await writeLogChunk("\n", signal);
164
+ }
165
+ else {
166
+ params.ensureHistoryStateRuntime();
167
+ let initial = true;
168
+ let seen = /* @__PURE__ */ new Set();
169
+ while (!signal.aborted) {
170
+ const lines = (await getCallHistoryFromStore(path.dirname(file), MAX_CALL_RECORD_EVENTS)).map((call) => JSON.stringify(call));
171
+ for (const line of initial ? lines.slice(Math.max(0, lines.length - since)) : lines) if (!seen.has(line)) {
172
+ await writeLogChunk(`${line}\n`, signal);
173
+ seen.add(line);
174
+ }
175
+ seen = new Set(lines);
176
+ initial = false;
177
+ await setTimeout(pollMs, void 0, { signal });
158
178
  }
159
- seen = new Set(lines);
160
- initial = false;
161
- await sleep(pollMs);
162
179
  }
180
+ } catch (error) {
181
+ if (!signal.aborted || !(error instanceof Error) || error.name !== "AbortError") throw error;
182
+ } finally {
183
+ process.off("SIGINT", onSigint);
184
+ process.off("SIGTERM", onSigterm);
163
185
  }
164
186
  });
165
187
  params.root.command("latency").description("Summarize turn latency metrics from voice-call history or a custom JSONL log").option("--file <path>", "Path to calls.jsonl", params.defaultFile).option("--last <n>", "Analyze last N records", "200").action(async (options) => {
@@ -187,9 +209,6 @@ const VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS = 5e3;
187
209
  const VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS = 3e4;
188
210
  const VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS = 1e4;
189
211
  const VOICE_CALL_GATEWAY_POLL_INTERVAL_MS = 1e3;
190
- function isGatewayUnavailableForLocalFallback(err) {
191
- return isGatewayTransportError(err) && err.kind === "closed" && (err.code === void 0 || err.code === 1006);
192
- }
193
212
  function isGatewayCredentialFailure(err) {
194
213
  return err instanceof Error && (err.name === "GatewayCredentialsRequiredError" || err.name === "GatewayExplicitAuthRequiredError" || err.name === "GatewaySecretRefUnavailableError");
195
214
  }
@@ -207,21 +226,23 @@ function gatewayOperationalError(err) {
207
226
  })();
208
227
  return new Error(redactSensitiveUrlLikeString(detail));
209
228
  }
210
- function isUnknownMethod(err, method) {
211
- return formatErrorMessage(err).includes(`unknown method: ${method}`);
212
- }
213
229
  async function callVoiceCallGateway(method, params, opts) {
214
230
  try {
231
+ opts?.signal?.throwIfAborted();
215
232
  const timeoutMs = typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? Math.max(1, Math.ceil(opts.timeoutMs)) : VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS;
216
233
  return {
217
234
  ok: true,
218
235
  payload: await callGatewayFromCli(method, {
219
236
  json: true,
220
237
  timeout: String(timeoutMs)
221
- }, params, { progress: false })
238
+ }, params, {
239
+ progress: false,
240
+ ...opts?.signal ? { signal: opts.signal } : {}
241
+ })
222
242
  };
223
243
  } catch (err) {
224
- if (isGatewayUnavailableForLocalFallback(err)) return {
244
+ if (opts?.signal?.aborted && err instanceof Error && err.name === "AbortError") throw err;
245
+ if (isGatewayTransportError(err) && err.kind === "closed" && (err.code === void 0 || err.code === 1006)) return {
225
246
  ok: false,
226
247
  error: err
227
248
  };
@@ -234,25 +255,31 @@ function resolveOperationTimeout(config) {
234
255
  function resolveContinueTimeout(config) {
235
256
  return clampTimerTimeoutMs(config.transcriptTimeoutMs + VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS + VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS) ?? 1;
236
257
  }
237
- async function pollContinueGateway(payload, fallbackTimeoutMs) {
258
+ async function pollContinueGateway(payload, fallbackTimeoutMs, signal) {
259
+ signal?.throwIfAborted();
238
260
  if (!isRecord(payload) || typeof payload.operationId !== "string") return payload;
239
261
  if (!payload.operationId) throw new Error("voicecall gateway response missing operationId");
240
262
  const operationId = payload.operationId;
241
263
  const timeoutMs = typeof payload.pollTimeoutMs === "number" ? clampTimerTimeoutMs(payload.pollTimeoutMs) ?? fallbackTimeoutMs : fallbackTimeoutMs;
242
264
  const deadlineMs = performance.now() + (clampTimerTimeoutMs(timeoutMs) ?? MAX_TIMER_TIMEOUT_MS);
243
265
  for (;;) {
266
+ signal?.throwIfAborted();
244
267
  const remainingMs = deadlineMs - performance.now();
245
268
  if (remainingMs <= 0) break;
246
- const gateway = await callVoiceCallGateway("voicecall.continue.result", { operationId }, { timeoutMs: Math.min(VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS, remainingMs) });
269
+ const gateway = await callVoiceCallGateway("voicecall.continue.result", { operationId }, {
270
+ timeoutMs: Math.min(VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS, remainingMs),
271
+ signal
272
+ });
247
273
  if (!gateway.ok) throw new Error(`gateway unavailable while waiting for voicecall continue result: ${formatErrorMessage(gateway.error)}`);
248
274
  const result = gateway.payload;
249
275
  if (!isRecord(result)) throw new Error("voicecall gateway response missing operation status");
250
- if (result.status === "completed") return result.result;
251
276
  if (result.status === "failed") throw new Error(typeof result.error === "string" ? result.error : "continue failed");
252
- if (result.status !== "pending") throw new Error("voicecall gateway response has unknown operation status");
277
+ if (result.status !== "pending" && result.status !== "completed") throw new Error("voicecall gateway response has unknown operation status");
278
+ signal?.throwIfAborted();
279
+ if (result.status === "completed") return result.result;
253
280
  const sleepMs = Math.min(VOICE_CALL_GATEWAY_POLL_INTERVAL_MS, deadlineMs - performance.now());
254
281
  if (sleepMs <= 0) break;
255
- await sleep(sleepMs);
282
+ await sleepWithAbort(sleepMs, signal);
256
283
  }
257
284
  throw new Error("voicecall continue timed out waiting for gateway operation");
258
285
  }
@@ -265,14 +292,20 @@ async function ensureStandaloneRuntime(params) {
265
292
  }
266
293
  }
267
294
  async function runGatewayManagerCommand(params) {
295
+ params.signal?.throwIfAborted();
268
296
  const gateway = await params.gatewayCall();
297
+ params.signal?.throwIfAborted();
269
298
  if (gateway.ok) {
270
- writeCliJson(params.resolveGatewayPayload ? await params.resolveGatewayPayload(gateway.payload) : gateway.payload);
299
+ const payload = params.resolveGatewayPayload ? await params.resolveGatewayPayload(gateway.payload) : gateway.payload;
300
+ params.signal?.throwIfAborted();
301
+ writeCliJson(payload);
271
302
  return;
272
303
  }
273
304
  const runtime = await ensureStandaloneRuntime(params);
305
+ params.signal?.throwIfAborted();
274
306
  const result = await params.managerFallback(runtime.manager);
275
307
  if (!result.success) throw new Error(result.error || `${params.failureLabel} failed`);
308
+ params.signal?.throwIfAborted();
276
309
  writeCliJson(result);
277
310
  }
278
311
  function readGatewayCallId(payload, invalidCallIdMessage) {
@@ -305,9 +338,21 @@ async function initiateVoiceCall(params) {
305
338
  return result.callId;
306
339
  }
307
340
  // extensions/voice-call/src/cli.ts
341
+ const briefFileDecoder = new TextDecoder("utf-8", {
342
+ fatal: true,
343
+ ignoreBOM: true
344
+ });
308
345
  async function readCliBrief(options) {
309
346
  if (options.brief && options.briefFile) throw new Error("Use either --brief or --brief-file");
310
- const raw = options.briefFile ? (await readRegularFile({ filePath: await realpath(resolveUserPath(options.briefFile)) })).buffer.toString("utf8") : options.brief;
347
+ let raw = options.brief;
348
+ if (options.briefFile) {
349
+ const { buffer } = await readRegularFile({ filePath: await realpath(resolveUserPath(options.briefFile)) });
350
+ try {
351
+ raw = briefFileDecoder.decode(buffer);
352
+ } catch (error) {
353
+ throw new Error(`Brief file must be valid UTF-8: ${options.briefFile}. Re-save it as UTF-8 and retry.`, { cause: error });
354
+ }
355
+ }
311
356
  if (raw === void 0) return;
312
357
  if (raw.length > 16e3) throw new Error("Brief input is too large");
313
358
  const value = options.briefFile || raw.trimStart().startsWith("{") || raw.trimStart().startsWith("[") ? JSON.parse(raw) : { task: raw };
@@ -375,29 +420,40 @@ function writeSetupStatus(status) {
375
420
  writeCliLine("Voice Call setup: %s", status.ok ? "OK" : "needs attention");
376
421
  for (const check of status.checks) writeCliLine("%s %s: %s", check.ok ? "OK" : "FAIL", check.id, check.message);
377
422
  }
378
- async function runWithStandaloneRuntime(createRuntime, run) {
423
+ async function runWithStandaloneRuntime(createRuntime, run, interruptGateway = false) {
379
424
  let runtime;
380
425
  const interrupted = createDeferred();
426
+ const cancellation = new AbortController();
381
427
  let exitSignal;
382
428
  const interrupt = (signal) => {
383
429
  exitSignal ??= signal;
384
430
  process.exitCode = exitSignal === "SIGINT" ? 130 : 143;
431
+ cancellation.abort();
385
432
  interrupted.resolve();
386
433
  };
387
434
  const onSigint = () => interrupt("SIGINT");
388
435
  const onSigterm = () => interrupt("SIGTERM");
436
+ const listen = () => {
437
+ process.on("SIGINT", onSigint);
438
+ process.on("SIGTERM", onSigterm);
439
+ };
389
440
  try {
441
+ if (interruptGateway) listen();
390
442
  await run(async () => {
391
443
  if (!runtime) {
392
- process.on("SIGINT", onSigint);
393
- process.on("SIGTERM", onSigterm);
444
+ if (!interruptGateway) listen();
394
445
  runtime = createRuntime();
395
446
  }
396
447
  const active = await runtime;
397
- if (exitSignal) throw new Error(`Voice call command interrupted by ${exitSignal}`);
448
+ if (exitSignal) {
449
+ if (interruptGateway) cancellation.signal.throwIfAborted();
450
+ throw new Error(`Voice call command interrupted by ${exitSignal}`);
451
+ }
398
452
  return active;
399
- });
453
+ }, interruptGateway ? cancellation.signal : void 0);
400
454
  if (runtime) await interrupted.promise;
455
+ } catch (error) {
456
+ if (!interruptGateway || !cancellation.signal.aborted || !(error instanceof Error) || error.name !== "AbortError") throw error;
401
457
  } finally {
402
458
  try {
403
459
  await (await runtime?.catch(() => void 0))?.stop();
@@ -496,19 +552,15 @@ function registerVoiceCallCli(params) {
496
552
  message: options.message
497
553
  };
498
554
  const continueTimeoutMs = resolveContinueTimeout(config);
499
- return managerAction({
500
- gatewayCall: async () => {
501
- try {
502
- return await callVoiceCallGateway("voicecall.continue.start", gatewayParams, { timeoutMs: resolveOperationTimeout(config) });
503
- } catch (err) {
504
- if (!isUnknownMethod(err, "voicecall.continue.start")) throw err;
505
- return callVoiceCallGateway("voicecall.continue", gatewayParams, { timeoutMs: continueTimeoutMs });
506
- }
507
- },
508
- resolveGatewayPayload: (payload) => pollContinueGateway(payload, continueTimeoutMs),
555
+ return runWithStandaloneRuntime(createRuntime, (ensureRuntime, signal) => runGatewayManagerCommand({
556
+ config,
557
+ ensureRuntime,
558
+ signal,
559
+ gatewayCall: () => callVoiceCallGateway("voicecall.continue.start", gatewayParams, { timeoutMs: resolveOperationTimeout(config) }),
560
+ resolveGatewayPayload: (payload) => pollContinueGateway(payload, continueTimeoutMs, signal),
509
561
  managerFallback: (manager) => manager.continueCall(options.callId, options.message),
510
562
  failureLabel: "continue"
511
- });
563
+ }), true);
512
564
  });
513
565
  root.command("speak").description("Speak a message without waiting for response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action((options) => managerAction({
514
566
  gatewayCall: () => callVoiceCallGateway("voicecall.speak", {
@@ -575,11 +627,11 @@ function registerVoiceCallCli(params) {
575
627
  });
576
628
  root.command("expose").description("Enable/disable Tailscale serve/funnel for the webhook").option("--mode <mode>", "off | serve (tailnet) | funnel (public)", "funnel").option("--path <path>", "Tailscale path to expose (recommend matching serve.path)").option("--port <port>", "Local webhook port").option("--serve-path <path>", "Local webhook path").action(async (options) => {
577
629
  const mode = resolveMode(options.mode ?? "funnel");
578
- const servePort = parseCliInteger(options.port ?? String(config.serve.port ?? 3334), "--port", {
630
+ const servePort = parseCliInteger(options.port ?? String(config.serve.port), "--port", {
579
631
  min: 1,
580
632
  max: MAX_TCP_PORT
581
633
  });
582
- const servePath = options.servePath ?? config.serve.path ?? "/voice/webhook";
634
+ const servePath = options.servePath ?? config.serve.path;
583
635
  const tsPath = options.path ?? config.tailscale?.path ?? servePath;
584
636
  const streamExposurePaths = resolveVoiceCallStreamExposurePaths(config, {
585
637
  publicWebhookPath: tsPath,