@absolutejs/voice 0.0.22-beta.677 → 0.0.22-beta.678

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 CHANGED
@@ -6,6 +6,12 @@ This file is generated by `absolute-changelog` from the entries in
6
6
  `changelog/`. Edit an entry, not this file — and add new ones under
7
7
  `changelog/unreleased/`.
8
8
 
9
+ ## 0.0.22-beta.678 — 2026-09-27
10
+
11
+ ### Fixed
12
+
13
+ - **Classify exhausted fetch header deadlines as timeouts with per-attempt metadata, preserve caller cancellation, and expose a configurable header deadline without changing the six-second default.** (`hardenFetch`, `VoiceFetchHeadersTimeoutError`, `HardenFetchOptions`, `createVoiceProviderRouter`, `VoiceProviderRouterEvent`)
14
+
9
15
  ## 0.0.22-beta.677 — 2026-09-16
10
16
 
11
17
  ### Fixed
package/changelog.json CHANGED
@@ -2,6 +2,23 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/voice",
4
4
  "releases": [
5
+ {
6
+ "changes": [
7
+ {
8
+ "kind": "fixed",
9
+ "summary": "Classify exhausted fetch header deadlines as timeouts with per-attempt metadata, preserve caller cancellation, and expose a configurable header deadline without changing the six-second default.",
10
+ "symbols": [
11
+ "hardenFetch",
12
+ "VoiceFetchHeadersTimeoutError",
13
+ "HardenFetchOptions",
14
+ "createVoiceProviderRouter",
15
+ "VoiceProviderRouterEvent"
16
+ ]
17
+ }
18
+ ],
19
+ "date": "2026-09-27",
20
+ "version": "0.0.22-beta.678"
21
+ },
5
22
  {
6
23
  "changes": [
7
24
  {
@@ -316,7 +316,7 @@ var serverMessageToAction = (message) => {
316
316
  }
317
317
  };
318
318
 
319
- // ../../home/alexkahn/abs/voice/node_modules/@absolutejs/media/dist/index.js
319
+ // node_modules/@absolutejs/media/dist/index.js
320
320
  var TAU = Math.PI * 2;
321
321
  var pushIssue = (issues, severity, code, message) => {
322
322
  issues.push({ code, message, severity });
@@ -1 +1,9 @@
1
- export declare const hardenFetch: (baseFetch?: typeof fetch) => (input: Parameters<typeof fetch>[0], init: Parameters<typeof fetch>[1]) => Promise<Response>;
1
+ export declare class VoiceFetchHeadersTimeoutError extends Error {
2
+ readonly timeoutMs: number;
3
+ readonly attempt: number;
4
+ constructor(timeoutMs: number, attempt: number);
5
+ }
6
+ export type HardenFetchOptions = {
7
+ attemptTimeoutMs?: number;
8
+ };
9
+ export declare const hardenFetch: (baseFetch?: typeof fetch, options?: HardenFetchOptions) => (input: Parameters<typeof fetch>[0], init: Parameters<typeof fetch>[1]) => Promise<Response>;
@@ -64,7 +64,9 @@ export type VoiceProviderRouterEvent<TProvider extends string = string> = {
64
64
  suppressedUntil?: number;
65
65
  status: "error" | "fallback" | "success";
66
66
  timedOut?: boolean;
67
- timeoutKind?: "latency-budget" | "stream-inactivity";
67
+ timeoutKind?: "latency-budget" | "stream-inactivity" | "response-headers";
68
+ /** Fetch attempt number within this provider attempt. */
69
+ fetchAttempt?: number;
68
70
  timeoutMs?: number;
69
71
  totalElapsedMs?: number;
70
72
  };
@@ -313,7 +313,7 @@ var serverMessageToAction = (message) => {
313
313
  }
314
314
  };
315
315
 
316
- // ../../home/alexkahn/abs/voice/node_modules/@absolutejs/media/dist/index.js
316
+ // node_modules/@absolutejs/media/dist/index.js
317
317
  var TAU = Math.PI * 2;
318
318
  var pushIssue = (issues, severity, code, message) => {
319
319
  issues.push({ code, message, severity });
package/dist/index.d.ts CHANGED
@@ -72,7 +72,7 @@ export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool, } from "
72
72
  export { createPersonaVoiceCaller, createScriptedVoiceCaller, renderVoiceSimulationTranscript, runVoiceConversationSimulation, } from "./core/conversationSimulator";
73
73
  export type { RunVoiceConversationSimulationInput, VoiceConversationSimulationEndedReason, VoiceConversationSimulationResult, VoicePersonaCallerCompletion, VoiceScriptedCallerStep, VoiceSimulatedSpeaker, VoiceSimulatedTurn, VoiceSimulatorCaller, VoiceSimulatorCallerModel, VoiceSimulatorCallerReply, } from "./core/conversationSimulator";
74
74
  export { logVoiceTiming, startVoiceTimer, voiceTimingEnabled, } from "./core/debugTiming";
75
- export { hardenFetch } from "./core/hardenedFetch";
75
+ export { hardenFetch, VoiceFetchHeadersTimeoutError, type HardenFetchOptions, } from "./core/hardenedFetch";
76
76
  export { createVoiceMCPToolset } from "./core/mcpToolset";
77
77
  export type { CreateVoiceMCPToolsetOptions, MCPClientLike, MCPToolCallResult, MCPToolContentBlock, MCPToolDefinition, VoiceMCPToolResult, } from "./core/mcpToolset";
78
78
  export { createAIVoiceModel } from "./core/aiVoiceModel";
package/dist/index.js CHANGED
@@ -42225,8 +42225,19 @@ Respond with only your spoken line. When your goal is met or you want to hang up
42225
42225
  };
42226
42226
  // src/core/hardenedFetch.ts
42227
42227
  var ATTEMPT_TIMEOUT_MS = 6000;
42228
+
42229
+ class VoiceFetchHeadersTimeoutError extends Error {
42230
+ timeoutMs;
42231
+ attempt;
42232
+ constructor(timeoutMs, attempt) {
42233
+ super(`fetch attempt ${attempt} exceeded ${timeoutMs}ms before response headers`);
42234
+ this.name = "VoiceFetchHeadersTimeoutError";
42235
+ this.timeoutMs = timeoutMs;
42236
+ this.attempt = attempt;
42237
+ }
42238
+ }
42228
42239
  var isBun = "Bun" in globalThis;
42229
- var oneAttempt = async (baseFetch, input, init) => {
42240
+ var oneAttempt = async (baseFetch, input, init, timeoutMs, attempt) => {
42230
42241
  const controller = new AbortController;
42231
42242
  const callerSignal = init?.signal ?? undefined;
42232
42243
  const onCallerAbort = () => controller.abort(callerSignal?.reason);
@@ -42235,8 +42246,8 @@ var oneAttempt = async (baseFetch, input, init) => {
42235
42246
  else
42236
42247
  callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
42237
42248
  const timer = setTimeout(() => {
42238
- controller.abort(new Error(`fetch exceeded ${ATTEMPT_TIMEOUT_MS}ms before response headers (stale Bun keep-alive socket?)`));
42239
- }, ATTEMPT_TIMEOUT_MS);
42249
+ controller.abort(new VoiceFetchHeadersTimeoutError(timeoutMs, attempt));
42250
+ }, timeoutMs);
42240
42251
  const headers = new Headers(init?.headers);
42241
42252
  if (isBun)
42242
42253
  headers.set("Connection", "close");
@@ -42246,20 +42257,29 @@ var oneAttempt = async (baseFetch, input, init) => {
42246
42257
  headers,
42247
42258
  signal: controller.signal
42248
42259
  });
42260
+ } catch (error) {
42261
+ if (callerSignal?.aborted)
42262
+ throw callerSignal.reason;
42263
+ if (controller.signal.reason instanceof VoiceFetchHeadersTimeoutError)
42264
+ throw controller.signal.reason;
42265
+ throw error;
42249
42266
  } finally {
42250
42267
  clearTimeout(timer);
42251
42268
  callerSignal?.removeEventListener("abort", onCallerAbort);
42252
42269
  }
42253
42270
  };
42254
- var hardenFetch = (baseFetch = globalThis.fetch) => {
42271
+ var hardenFetch = (baseFetch = globalThis.fetch, options = {}) => {
42272
+ const timeoutMs = options.attemptTimeoutMs ?? ATTEMPT_TIMEOUT_MS;
42273
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
42274
+ throw new RangeError("attemptTimeoutMs must be finite and positive");
42255
42275
  const hardenedFetch = async (input, init) => {
42256
42276
  try {
42257
- return await oneAttempt(baseFetch, input, init);
42277
+ return await oneAttempt(baseFetch, input, init, timeoutMs, 1);
42258
42278
  } catch (error) {
42259
42279
  if (init?.signal?.aborted)
42260
42280
  throw error;
42261
42281
  console.warn(`[voice] hardened fetch retrying on a fresh connection: ${error instanceof Error ? error.message : String(error)}`);
42262
- return oneAttempt(baseFetch, input, init);
42282
+ return oneAttempt(baseFetch, input, init, timeoutMs, 2);
42263
42283
  }
42264
42284
  };
42265
42285
  return typeof baseFetch.preconnect === "function" ? Object.assign(hardenedFetch, {
@@ -46725,7 +46745,7 @@ var createVoiceProviderRouter = (options) => {
46725
46745
  lastError = error;
46726
46746
  const hasNextProvider = index < order.length - 1;
46727
46747
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
46728
- const timedOut = options.isTimeoutError?.(error, provider) ?? (error instanceof VoiceProviderTimeoutError || error instanceof VoiceProviderStreamInactivityError);
46748
+ const timedOut = options.isTimeoutError?.(error, provider) ?? (error instanceof VoiceProviderTimeoutError || error instanceof VoiceProviderStreamInactivityError || error instanceof VoiceFetchHeadersTimeoutError);
46729
46749
  const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
46730
46750
  const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
46731
46751
  const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
@@ -46745,8 +46765,9 @@ var createVoiceProviderRouter = (options) => {
46745
46765
  suppressedUntil: providerHealth?.suppressedUntil,
46746
46766
  status: "error",
46747
46767
  timedOut,
46748
- timeoutKind: error instanceof VoiceProviderStreamInactivityError ? "stream-inactivity" : error instanceof VoiceProviderTimeoutError ? "latency-budget" : undefined,
46749
- timeoutMs: error instanceof VoiceProviderStreamInactivityError ? error.inactivityMs : error instanceof VoiceProviderTimeoutError ? error.timeoutMs : undefined,
46768
+ fetchAttempt: error instanceof VoiceFetchHeadersTimeoutError ? error.attempt : undefined,
46769
+ timeoutKind: error instanceof VoiceFetchHeadersTimeoutError ? "response-headers" : error instanceof VoiceProviderStreamInactivityError ? "stream-inactivity" : error instanceof VoiceProviderTimeoutError ? "latency-budget" : undefined,
46770
+ timeoutMs: error instanceof VoiceFetchHeadersTimeoutError ? error.timeoutMs : error instanceof VoiceProviderStreamInactivityError ? error.inactivityMs : error instanceof VoiceProviderTimeoutError ? error.timeoutMs : undefined,
46750
46771
  totalElapsedMs: Date.now() - routingStartedAt
46751
46772
  }, input);
46752
46773
  if (!hasNextProvider || !shouldFallback) {
@@ -54471,6 +54492,7 @@ export {
54471
54492
  VOICE_WEBHOOK_SIGNATURE_HEADER,
54472
54493
  VOICE_WEBHOOK_TIMESTAMP_HEADER,
54473
54494
  VOICE_ZERO_DATA_RETENTION_REDACTION_MARKER,
54495
+ VoiceFetchHeadersTimeoutError,
54474
54496
  acknowledgeVoiceMonitorIssue,
54475
54497
  aggregateVoiceTurnLatencySpans,
54476
54498
  appendVoiceIOProviderRouterTraceEvent,
@@ -5121,8 +5121,19 @@ var voiceTimingEnabled = () => timingEnabled();
5121
5121
 
5122
5122
  // src/core/hardenedFetch.ts
5123
5123
  var ATTEMPT_TIMEOUT_MS = 6000;
5124
+
5125
+ class VoiceFetchHeadersTimeoutError extends Error {
5126
+ timeoutMs;
5127
+ attempt;
5128
+ constructor(timeoutMs, attempt) {
5129
+ super(`fetch attempt ${attempt} exceeded ${timeoutMs}ms before response headers`);
5130
+ this.name = "VoiceFetchHeadersTimeoutError";
5131
+ this.timeoutMs = timeoutMs;
5132
+ this.attempt = attempt;
5133
+ }
5134
+ }
5124
5135
  var isBun = "Bun" in globalThis;
5125
- var oneAttempt = async (baseFetch, input, init) => {
5136
+ var oneAttempt = async (baseFetch, input, init, timeoutMs, attempt) => {
5126
5137
  const controller = new AbortController;
5127
5138
  const callerSignal = init?.signal ?? undefined;
5128
5139
  const onCallerAbort = () => controller.abort(callerSignal?.reason);
@@ -5131,8 +5142,8 @@ var oneAttempt = async (baseFetch, input, init) => {
5131
5142
  else
5132
5143
  callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
5133
5144
  const timer = setTimeout(() => {
5134
- controller.abort(new Error(`fetch exceeded ${ATTEMPT_TIMEOUT_MS}ms before response headers (stale Bun keep-alive socket?)`));
5135
- }, ATTEMPT_TIMEOUT_MS);
5145
+ controller.abort(new VoiceFetchHeadersTimeoutError(timeoutMs, attempt));
5146
+ }, timeoutMs);
5136
5147
  const headers = new Headers(init?.headers);
5137
5148
  if (isBun)
5138
5149
  headers.set("Connection", "close");
@@ -5142,20 +5153,29 @@ var oneAttempt = async (baseFetch, input, init) => {
5142
5153
  headers,
5143
5154
  signal: controller.signal
5144
5155
  });
5156
+ } catch (error) {
5157
+ if (callerSignal?.aborted)
5158
+ throw callerSignal.reason;
5159
+ if (controller.signal.reason instanceof VoiceFetchHeadersTimeoutError)
5160
+ throw controller.signal.reason;
5161
+ throw error;
5145
5162
  } finally {
5146
5163
  clearTimeout(timer);
5147
5164
  callerSignal?.removeEventListener("abort", onCallerAbort);
5148
5165
  }
5149
5166
  };
5150
- var hardenFetch = (baseFetch = globalThis.fetch) => {
5167
+ var hardenFetch = (baseFetch = globalThis.fetch, options = {}) => {
5168
+ const timeoutMs = options.attemptTimeoutMs ?? ATTEMPT_TIMEOUT_MS;
5169
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
5170
+ throw new RangeError("attemptTimeoutMs must be finite and positive");
5151
5171
  const hardenedFetch = async (input, init) => {
5152
5172
  try {
5153
- return await oneAttempt(baseFetch, input, init);
5173
+ return await oneAttempt(baseFetch, input, init, timeoutMs, 1);
5154
5174
  } catch (error) {
5155
5175
  if (init?.signal?.aborted)
5156
5176
  throw error;
5157
5177
  console.warn(`[voice] hardened fetch retrying on a fresh connection: ${error instanceof Error ? error.message : String(error)}`);
5158
- return oneAttempt(baseFetch, input, init);
5178
+ return oneAttempt(baseFetch, input, init, timeoutMs, 2);
5159
5179
  }
5160
5180
  };
5161
5181
  return typeof baseFetch.preconnect === "function" ? Object.assign(hardenedFetch, {
@@ -5612,7 +5632,7 @@ var createVoiceProviderRouter = (options) => {
5612
5632
  lastError = error;
5613
5633
  const hasNextProvider = index < order.length - 1;
5614
5634
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
5615
- const timedOut = options.isTimeoutError?.(error, provider) ?? (error instanceof VoiceProviderTimeoutError || error instanceof VoiceProviderStreamInactivityError);
5635
+ const timedOut = options.isTimeoutError?.(error, provider) ?? (error instanceof VoiceProviderTimeoutError || error instanceof VoiceProviderStreamInactivityError || error instanceof VoiceFetchHeadersTimeoutError);
5616
5636
  const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
5617
5637
  const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
5618
5638
  const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
@@ -5632,8 +5652,9 @@ var createVoiceProviderRouter = (options) => {
5632
5652
  suppressedUntil: providerHealth?.suppressedUntil,
5633
5653
  status: "error",
5634
5654
  timedOut,
5635
- timeoutKind: error instanceof VoiceProviderStreamInactivityError ? "stream-inactivity" : error instanceof VoiceProviderTimeoutError ? "latency-budget" : undefined,
5636
- timeoutMs: error instanceof VoiceProviderStreamInactivityError ? error.inactivityMs : error instanceof VoiceProviderTimeoutError ? error.timeoutMs : undefined,
5655
+ fetchAttempt: error instanceof VoiceFetchHeadersTimeoutError ? error.attempt : undefined,
5656
+ timeoutKind: error instanceof VoiceFetchHeadersTimeoutError ? "response-headers" : error instanceof VoiceProviderStreamInactivityError ? "stream-inactivity" : error instanceof VoiceProviderTimeoutError ? "latency-budget" : undefined,
5657
+ timeoutMs: error instanceof VoiceFetchHeadersTimeoutError ? error.timeoutMs : error instanceof VoiceProviderStreamInactivityError ? error.inactivityMs : error instanceof VoiceProviderTimeoutError ? error.timeoutMs : undefined,
5637
5658
  totalElapsedMs: Date.now() - routingStartedAt
5638
5659
  }, input);
5639
5660
  if (!hasNextProvider || !shouldFallback) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.677",
3
+ "version": "0.0.22-beta.678",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",