@pouchy_ai/companion-sdk 0.34.0 → 0.36.0
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 +89 -1
- package/README.md +28 -8
- package/dist/call.d.ts +18 -0
- package/dist/call.d.ts.map +1 -1
- package/dist/call.js +54 -0
- package/dist/call.js.map +1 -1
- package/dist/client.d.ts +28 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +106 -18
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/package.json +2 -2
- package/src/call.ts +70 -0
- package/src/client.ts +120 -15
- package/src/errors.ts +2 -1
package/src/call.ts
CHANGED
|
@@ -187,6 +187,24 @@ export interface CompanionCall {
|
|
|
187
187
|
/** Inject an out-of-band context line. `speak: true` makes the companion react
|
|
188
188
|
* now; false stages it for the next turn. */
|
|
189
189
|
injectEvent: (text: string, speak?: boolean) => void;
|
|
190
|
+
/** SILENT CUT (0.36.0): stop the utterance currently being spoken WITHOUT
|
|
191
|
+
* generating any new speech — the "stale commentary at a beat boundary"
|
|
192
|
+
* case (field request: fast-paced game, last hand's commentary still
|
|
193
|
+
* playing when the next hand starts). Idempotent: a no-op when nothing is
|
|
194
|
+
* playing, after close(), and mid-connect.
|
|
195
|
+
*
|
|
196
|
+
* Provider tiers (check `provider`):
|
|
197
|
+
* - `openai-realtime` — GUARANTEED: cancels the active response and clears
|
|
198
|
+
* the WebRTC output buffer (`response.cancel` + `output_audio_buffer.clear`);
|
|
199
|
+
* playback stops within ~an audio frame. The session continues normally.
|
|
200
|
+
* - `elevenlabs-convai` — BEST-EFFORT: the EL client exposes no public
|
|
201
|
+
* "stop playback" API; we signal `user_activity` (halts the agent's
|
|
202
|
+
* stream when the agent's interruption handling allows) and attempt the
|
|
203
|
+
* client's internal output-buffer cut when the installed build exposes
|
|
204
|
+
* it. Verify on your rig; worst case it degrades to a no-op — for a
|
|
205
|
+
* guaranteed preemption on EL, `injectEvent(text, true)` (which DOES
|
|
206
|
+
* speak new content) remains the reliable lever. */
|
|
207
|
+
interrupt: () => void;
|
|
190
208
|
provider: CallCredentials['provider'];
|
|
191
209
|
}
|
|
192
210
|
|
|
@@ -296,6 +314,12 @@ async function openOpenAICall(
|
|
|
296
314
|
}, MIC_UNMUTE_TAIL_MS);
|
|
297
315
|
};
|
|
298
316
|
|
|
317
|
+
// Whether the model is audibly speaking right now (driven by the
|
|
318
|
+
// output_audio_buffer events below). Gates interrupt(): cancelling with no
|
|
319
|
+
// active response makes the server emit a benign-but-noisy error event, so
|
|
320
|
+
// an idle interrupt() must be a true no-op.
|
|
321
|
+
let oaiSpeaking = false;
|
|
322
|
+
|
|
299
323
|
const dc = pc.createDataChannel('oai-events');
|
|
300
324
|
// `response.function_call_arguments.done` carries the call_id + args but often
|
|
301
325
|
// not the name — the name arrives earlier on `response.output_item.added`; track it.
|
|
@@ -396,6 +420,7 @@ async function openOpenAICall(
|
|
|
396
420
|
) {
|
|
397
421
|
opts.onTranscript?.({ role: 'user', text: msg.transcript });
|
|
398
422
|
} else if (msg.type === 'output_audio_buffer.started') {
|
|
423
|
+
oaiSpeaking = true;
|
|
399
424
|
opts.onSpeakingChange?.(true);
|
|
400
425
|
// Model started speaking → mute the mic so its own audio can't
|
|
401
426
|
// echo-transcribe into a self-barge-in. bargeIn opts out (B wave):
|
|
@@ -411,6 +436,7 @@ async function openOpenAICall(
|
|
|
411
436
|
// and re-open the mic after a short tail so the last bit of the model's
|
|
412
437
|
// audio doesn't get captured the instant we unmute. Several event names
|
|
413
438
|
// can end a turn; any of them re-arms the mic.
|
|
439
|
+
oaiSpeaking = false;
|
|
414
440
|
opts.onSpeakingChange?.(false);
|
|
415
441
|
if (opts.bargeIn !== true) scheduleMicUnmute();
|
|
416
442
|
}
|
|
@@ -548,6 +574,21 @@ async function openOpenAICall(
|
|
|
548
574
|
})
|
|
549
575
|
);
|
|
550
576
|
if (speak) dc.send(JSON.stringify({ type: 'response.create' }));
|
|
577
|
+
},
|
|
578
|
+
interrupt: () => {
|
|
579
|
+
// Silent cut: stop generating (response.cancel) AND flush what's
|
|
580
|
+
// already buffered for playback (output_audio_buffer.clear — the
|
|
581
|
+
// WebRTC-mode control for exactly this). Order matters: cancel first
|
|
582
|
+
// so no new audio lands behind the clear. The `.cleared` event echoes
|
|
583
|
+
// back through the handler above, which resets the speaking state and
|
|
584
|
+
// re-arms the mic like any natural end of turn.
|
|
585
|
+
if (closed || !oaiSpeaking || dc.readyState !== 'open') return;
|
|
586
|
+
try {
|
|
587
|
+
dc.send(JSON.stringify({ type: 'response.cancel' }));
|
|
588
|
+
dc.send(JSON.stringify({ type: 'output_audio_buffer.clear' }));
|
|
589
|
+
} catch {
|
|
590
|
+
/* channel gone — the call is tearing down anyway */
|
|
591
|
+
}
|
|
551
592
|
}
|
|
552
593
|
};
|
|
553
594
|
}
|
|
@@ -583,6 +624,9 @@ async function openConvaiCall(
|
|
|
583
624
|
}
|
|
584
625
|
|
|
585
626
|
let closed = false;
|
|
627
|
+
// Whether the agent is audibly speaking (onModeChange) — gates interrupt()
|
|
628
|
+
// so an idle call stays a true no-op.
|
|
629
|
+
let elSpeaking = false;
|
|
586
630
|
// Half-duplex mic (mirrors the first-party path, #1524): mute while the agent
|
|
587
631
|
// speaks so its own voice can't echo-transcribe into a self-barge-in (the SDK
|
|
588
632
|
// voice "说一句就断了" report), re-open after a short tail. Convai exposes
|
|
@@ -612,6 +656,7 @@ async function openConvaiCall(
|
|
|
612
656
|
onModeChange: ({ mode }: { mode: string }) => {
|
|
613
657
|
if (closed) return;
|
|
614
658
|
const speaking = mode === 'speaking';
|
|
659
|
+
elSpeaking = speaking;
|
|
615
660
|
opts.onSpeakingChange?.(speaking);
|
|
616
661
|
// bargeIn opts out of half-duplex (B wave): the mic stays open and
|
|
617
662
|
// ElevenLabs' native interruption handling takes over.
|
|
@@ -694,6 +739,31 @@ async function openConvaiCall(
|
|
|
694
739
|
} catch {
|
|
695
740
|
/* channel not ready */
|
|
696
741
|
}
|
|
742
|
+
},
|
|
743
|
+
interrupt: () => {
|
|
744
|
+
// BEST-EFFORT silent cut — the EL client has no public "stop playback"
|
|
745
|
+
// API (interruption is normally server-driven: a server `interruption`
|
|
746
|
+
// event → the client's internal OutputController.interrupt()). Two
|
|
747
|
+
// guarded levers, both no-ops when unavailable:
|
|
748
|
+
// 1. user_activity: tells the agent the user is acting; with the
|
|
749
|
+
// agent's interruption handling enabled the server halts the
|
|
750
|
+
// current utterance WITHOUT generating a reply (unlike
|
|
751
|
+
// sendUserMessage, which speaks new content).
|
|
752
|
+
// 2. The internal output cut, when the installed client build exposes
|
|
753
|
+
// it — same guarded-internal doctrine as setMicMuted above.
|
|
754
|
+
if (closed || !elSpeaking) return;
|
|
755
|
+
try {
|
|
756
|
+
(conv as unknown as { sendUserActivity?: () => void }).sendUserActivity?.();
|
|
757
|
+
} catch {
|
|
758
|
+
/* not supported on this client build */
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
(
|
|
762
|
+
conv as unknown as { output?: { interrupt?: (resetDurationMs?: number) => void } }
|
|
763
|
+
).output?.interrupt?.();
|
|
764
|
+
} catch {
|
|
765
|
+
/* internal shape changed — the user_activity lever above still applies */
|
|
766
|
+
}
|
|
697
767
|
}
|
|
698
768
|
};
|
|
699
769
|
}
|
package/src/client.ts
CHANGED
|
@@ -122,6 +122,16 @@ export interface CompanionClientOptions {
|
|
|
122
122
|
* silently), else an in-memory page-lifetime store. The key is
|
|
123
123
|
* `pouchy-outbox:${surface}`, so each surface keeps its own queue. */
|
|
124
124
|
outboxStore?: OutboxStore;
|
|
125
|
+
/** Deadline for a request to produce RESPONSE HEADERS, in ms (0.35.0).
|
|
126
|
+
* Bounds every HTTP call the client makes (connect, sendText, recall, …) so
|
|
127
|
+
* a server that accepts the TCP connection but never answers can't hang a
|
|
128
|
+
* helper forever — previously only `sendText({ awaitReply })` had any
|
|
129
|
+
* deadline. Reading a response BODY (the SSE event stream, a streaming
|
|
130
|
+
* sendText reply) is never bounded by this — the timer is cleared the
|
|
131
|
+
* moment headers arrive. Composes with any per-call AbortSignal (whichever
|
|
132
|
+
* fires first wins); the timeout rejects as a status-0 CompanionError with
|
|
133
|
+
* code 'request_timeout'. Default 30_000; 0 disables. */
|
|
134
|
+
requestTimeoutMs?: number;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
export interface HelloAck {
|
|
@@ -414,6 +424,10 @@ export class CompanionClient {
|
|
|
414
424
|
private readonly outboxHandlers = new Set<(items: readonly QueuedSend[]) => void>();
|
|
415
425
|
// In-flight flush, shared so concurrent triggers run ONE flush at a time.
|
|
416
426
|
private outboxFlush: Promise<{ sent: number; remaining: number }> | null = null;
|
|
427
|
+
/** Epoch-ms before which flush attempts are skipped — set from a 429's
|
|
428
|
+
* Retry-After during replay so the auto-flush on every reconnect doesn't
|
|
429
|
+
* re-hammer a server that just told us to back off (0.35.0). */
|
|
430
|
+
private outboxBlockedUntil = 0;
|
|
417
431
|
|
|
418
432
|
constructor(opts: CompanionClientOptions) {
|
|
419
433
|
if (!opts.baseUrl)
|
|
@@ -530,6 +544,48 @@ export class CompanionClient {
|
|
|
530
544
|
return major > 1 || (major === 1 && minor >= 1);
|
|
531
545
|
}
|
|
532
546
|
|
|
547
|
+
/** doFetch raced against the `requestTimeoutMs` HEADERS deadline (0.35.0).
|
|
548
|
+
* The deadline only bounds time-to-response-headers: it does NOT swap the
|
|
549
|
+
* request's AbortSignal, so a streaming response body stays tied to the
|
|
550
|
+
* caller's own signal, and the timer is cleared the moment the race
|
|
551
|
+
* settles. On timeout the helper rejects `request_timeout` (status 0) —
|
|
552
|
+
* code-bearing, so the offline queue's network-failure predicate correctly
|
|
553
|
+
* ignores it (the server may have received the request). The abandoned
|
|
554
|
+
* fetch keeps its rejection silenced so a late network error can't surface
|
|
555
|
+
* as an unhandled rejection. */
|
|
556
|
+
private async doFetchBounded(
|
|
557
|
+
url: string,
|
|
558
|
+
init: RequestInit & { headers: Record<string, string> }
|
|
559
|
+
): Promise<Response> {
|
|
560
|
+
const budget = this.opts.requestTimeoutMs ?? 30_000;
|
|
561
|
+
if (!budget) return this.doFetch(url, init);
|
|
562
|
+
const req = this.doFetch(url, init);
|
|
563
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
564
|
+
try {
|
|
565
|
+
return await Promise.race([
|
|
566
|
+
req,
|
|
567
|
+
new Promise<never>((_, reject) => {
|
|
568
|
+
timer = setTimeout(
|
|
569
|
+
() =>
|
|
570
|
+
reject(
|
|
571
|
+
new CompanionError(
|
|
572
|
+
`request timed out after ${budget}ms waiting for response headers`,
|
|
573
|
+
0,
|
|
574
|
+
'request_timeout'
|
|
575
|
+
)
|
|
576
|
+
),
|
|
577
|
+
budget
|
|
578
|
+
);
|
|
579
|
+
})
|
|
580
|
+
]);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
req.catch(() => {});
|
|
583
|
+
throw e;
|
|
584
|
+
} finally {
|
|
585
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
533
589
|
private async fetchAuthed(
|
|
534
590
|
url: string,
|
|
535
591
|
init: RequestInit & { headers: Record<string, string> }
|
|
@@ -540,7 +596,7 @@ export class CompanionClient {
|
|
|
540
596
|
this.dbg({ kind: 'request', at: startedAt, method, path });
|
|
541
597
|
let res: Response;
|
|
542
598
|
try {
|
|
543
|
-
res = await this.
|
|
599
|
+
res = await this.doFetchBounded(url, init);
|
|
544
600
|
} catch (e) {
|
|
545
601
|
// A caller-supplied AbortSignal makes fetch throw a native AbortError —
|
|
546
602
|
// convert it at this single choke point so cancellation honors the
|
|
@@ -570,7 +626,7 @@ export class CompanionClient {
|
|
|
570
626
|
const retryAt = Date.now();
|
|
571
627
|
this.dbg({ kind: 'request', at: retryAt, method, path });
|
|
572
628
|
try {
|
|
573
|
-
const retryRes = await this.
|
|
629
|
+
const retryRes = await this.doFetchBounded(url, init);
|
|
574
630
|
this.dbg({
|
|
575
631
|
kind: 'response',
|
|
576
632
|
at: Date.now(),
|
|
@@ -856,6 +912,12 @@ export class CompanionClient {
|
|
|
856
912
|
turnId?: string
|
|
857
913
|
): Promise<SendTextReply> {
|
|
858
914
|
const timeoutMs = opts?.replyTimeoutMs ?? 60_000;
|
|
915
|
+
// Already-aborted fast-fail comes FIRST (0.35.0): it used to sit after the
|
|
916
|
+
// companion.message subscription below, whose cleanup lives in the finally
|
|
917
|
+
// of the try we never entered — every call with a spent signal leaked one
|
|
918
|
+
// permanent handler (a host reusing one AbortController across turns
|
|
919
|
+
// accumulated them without bound).
|
|
920
|
+
if (opts?.signal?.aborted) throw new CompanionError('request aborted', 0, 'aborted');
|
|
859
921
|
let resolveFallback!: (env: CompanionEnvelope) => void;
|
|
860
922
|
const fallback = new Promise<CompanionEnvelope>((res) => (resolveFallback = res));
|
|
861
923
|
// Correlate: resolve only on THIS turn's reply (`replyTo === turnId`).
|
|
@@ -874,10 +936,9 @@ export class CompanionClient {
|
|
|
874
936
|
const ac = new AbortController();
|
|
875
937
|
// Link the caller's signal: aborting it tears down the in-flight stream
|
|
876
938
|
// (via ac) AND rejects the whole operation with `aborted`, exactly like
|
|
877
|
-
// the deadline does.
|
|
939
|
+
// the deadline does.
|
|
878
940
|
let onCallerAbort: (() => void) | undefined;
|
|
879
941
|
if (opts?.signal) {
|
|
880
|
-
if (opts.signal.aborted) throw new CompanionError('request aborted', 0, 'aborted');
|
|
881
942
|
onCallerAbort = () => ac.abort();
|
|
882
943
|
opts.signal.addEventListener('abort', onCallerAbort, { once: true });
|
|
883
944
|
}
|
|
@@ -1066,7 +1127,14 @@ export class CompanionClient {
|
|
|
1066
1127
|
} catch {
|
|
1067
1128
|
/* stream already finished */
|
|
1068
1129
|
}
|
|
1069
|
-
|
|
1130
|
+
// Normalize (0.35.0): a mid-stream network drop rejects reader.read()
|
|
1131
|
+
// with a NATIVE TypeError. Rethrowing it raw broke the "every helper
|
|
1132
|
+
// rejects with CompanionError" contract AND slipped past the offline
|
|
1133
|
+
// queue's isNetworkSendFailure (which requires a code-less status-0
|
|
1134
|
+
// CompanionError) — the exact loss queueOffline exists to prevent.
|
|
1135
|
+
// Queueing the replay is safe: it reuses the ORIGINAL turnId and the
|
|
1136
|
+
// server dedupes completed turns, so a turn that did run isn't re-run.
|
|
1137
|
+
throw this.asCompanionError(e);
|
|
1070
1138
|
}
|
|
1071
1139
|
}
|
|
1072
1140
|
|
|
@@ -1152,6 +1220,10 @@ export class CompanionClient {
|
|
|
1152
1220
|
private async runOutboxFlush(): Promise<{ sent: number; remaining: number }> {
|
|
1153
1221
|
const id = this.requireSession();
|
|
1154
1222
|
const items = this.loadOutbox();
|
|
1223
|
+
// Respect the last replay 429's Retry-After (0.35.0): the flush re-arms
|
|
1224
|
+
// on EVERY stream reconnect, so without this gate a rate-limited server
|
|
1225
|
+
// got re-hammered on each reconnect despite naming its own window.
|
|
1226
|
+
if (Date.now() < this.outboxBlockedUntil) return { sent: 0, remaining: items.length };
|
|
1155
1227
|
let sent = 0;
|
|
1156
1228
|
while (items.length) {
|
|
1157
1229
|
const item = items[0];
|
|
@@ -1173,6 +1245,10 @@ export class CompanionClient {
|
|
|
1173
1245
|
this.commitOutbox();
|
|
1174
1246
|
continue;
|
|
1175
1247
|
}
|
|
1248
|
+
// A throttle names its own retry window — honor it across flushes.
|
|
1249
|
+
if (status === 429 && e instanceof CompanionError && typeof e.retryAfter === 'number') {
|
|
1250
|
+
this.outboxBlockedUntil = Date.now() + e.retryAfter * 1000;
|
|
1251
|
+
}
|
|
1176
1252
|
// Unreachable again (or throttled / a server-side failure): keep
|
|
1177
1253
|
// this item and everything behind it for the next flush.
|
|
1178
1254
|
break;
|
|
@@ -1513,7 +1589,10 @@ export class CompanionClient {
|
|
|
1513
1589
|
* arrive as `control.error`. Since 0.30.0 `code` is typed as
|
|
1514
1590
|
* `ControlErrorCodeValue` (the CONTROL_ERROR_CODES vocabulary + the
|
|
1515
1591
|
* SDK-synthesized stream codes, open for newer servers) so a `switch` on it
|
|
1516
|
-
* autocompletes.
|
|
1592
|
+
* autocompletes. A malformed frame with no string `code` is delivered with
|
|
1593
|
+
* the literal fallback `'error'` (not part of the exported vocabulary) —
|
|
1594
|
+
* give your `switch` a `default:` arm rather than enumerating every code.
|
|
1595
|
+
* Returns an unsubscribe fn. */
|
|
1517
1596
|
onError(
|
|
1518
1597
|
handler: (
|
|
1519
1598
|
err: { code: ControlErrorCodeValue; message: string },
|
|
@@ -1970,13 +2049,19 @@ export class CompanionClient {
|
|
|
1970
2049
|
let backoff = 500;
|
|
1971
2050
|
while (this.streaming && gen === this.streamGen) {
|
|
1972
2051
|
let errored = false;
|
|
1973
|
-
|
|
2052
|
+
// Stamped in onopen (0.35.0) — measuring from before the constructor
|
|
2053
|
+
// counted DNS/TCP/handshake time as "connection lifetime", so a gateway
|
|
2054
|
+
// that took ~2s to accept and then instantly closed read as long-lived
|
|
2055
|
+
// and RESET the backoff (a near-hot reconnect loop). 0 = never opened,
|
|
2056
|
+
// which always backs off.
|
|
2057
|
+
let openedAt = 0;
|
|
1974
2058
|
try {
|
|
1975
2059
|
await new Promise<void>((resolve, reject) => {
|
|
1976
2060
|
const ws = new WS(deriveWsStreamUrl(this.opts.baseUrl, id, this.opts.token, this.cursor));
|
|
1977
2061
|
let poll: ReturnType<typeof setInterval> | undefined;
|
|
1978
2062
|
let settled = false;
|
|
1979
2063
|
ws.onopen = () => {
|
|
2064
|
+
openedAt = Date.now();
|
|
1980
2065
|
if (this.streaming && gen === this.streamGen) this.setStreamState('connected');
|
|
1981
2066
|
};
|
|
1982
2067
|
const settle = (err?: unknown) => {
|
|
@@ -2029,7 +2114,7 @@ export class CompanionClient {
|
|
|
2029
2114
|
// back off like the SSE loop, or this reconnects in a zero-delay hot
|
|
2030
2115
|
// loop. A connection that lived a while resets the backoff.
|
|
2031
2116
|
if (this.streaming && gen === this.streamGen) this.setStreamState('reconnecting');
|
|
2032
|
-
if (Date.now() -
|
|
2117
|
+
if (openedAt === 0 || Date.now() - openedAt < 2000) {
|
|
2033
2118
|
await this.backoffSleep(backoff);
|
|
2034
2119
|
backoff = Math.min(backoff * 2, 8000);
|
|
2035
2120
|
} else {
|
|
@@ -2194,13 +2279,33 @@ export class CompanionClient {
|
|
|
2194
2279
|
const reader = body.getReader();
|
|
2195
2280
|
const decoder = new TextDecoder();
|
|
2196
2281
|
let buf = '';
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2282
|
+
// Liveness watchdog (0.35.0). The server writes a `: ping` comment every
|
|
2283
|
+
// ~1.5s and closes each stream window at 45s — a healthy connection is
|
|
2284
|
+
// NEVER byte-silent for long. On a silent half-open TCP drop (NAT/LB
|
|
2285
|
+
// idle-kill, laptop sleep) no FIN ever arrives: reader.read() blocks for
|
|
2286
|
+
// minutes while streamState still reads 'connected' and every reply is
|
|
2287
|
+
// silently lost. 10× the ping interval without a single byte can only be
|
|
2288
|
+
// a dead connection — cancel the reader so read() returns and the loop
|
|
2289
|
+
// reconnects through its normal backoff + cursor resume (replays dedupe
|
|
2290
|
+
// by envelope id). Only the SSE plane gets this: the WS plane has no
|
|
2291
|
+
// heartbeat contract, so an idle-but-healthy socket can't be told apart.
|
|
2292
|
+
const SSE_STALL_MS = 15_000;
|
|
2293
|
+
let lastByteAt = Date.now();
|
|
2294
|
+
const watchdog = setInterval(() => {
|
|
2295
|
+
if (Date.now() - lastByteAt > SSE_STALL_MS) void reader.cancel().catch(() => {});
|
|
2296
|
+
}, 5_000);
|
|
2297
|
+
try {
|
|
2298
|
+
while (this.streaming && gen === this.streamGen) {
|
|
2299
|
+
const { done, value } = await reader.read();
|
|
2300
|
+
if (done) break;
|
|
2301
|
+
lastByteAt = Date.now();
|
|
2302
|
+
buf += decoder.decode(value, { stream: true });
|
|
2303
|
+
const { events, rest } = parseSse(buf);
|
|
2304
|
+
buf = rest;
|
|
2305
|
+
for (const ev of events) this.handleFrame(ev.event, ev.data);
|
|
2306
|
+
}
|
|
2307
|
+
} finally {
|
|
2308
|
+
clearInterval(watchdog);
|
|
2204
2309
|
}
|
|
2205
2310
|
}
|
|
2206
2311
|
|
package/src/errors.ts
CHANGED
|
@@ -21,7 +21,8 @@ export const SDK_SYNTHESIZED_ERROR_CODES = [
|
|
|
21
21
|
'call_connect_failed', // WebRTC/provider handshake failed
|
|
22
22
|
'call_dependency_missing', // optional voice dependency not installed
|
|
23
23
|
'aborted', // a caller-supplied AbortSignal cancelled the request (0.29.0)
|
|
24
|
-
'queued_offline' // queueOffline: the text was queued while offline and will send on reconnect (0.34.0)
|
|
24
|
+
'queued_offline', // queueOffline: the text was queued while offline and will send on reconnect (0.34.0)
|
|
25
|
+
'request_timeout' // requestTimeoutMs elapsed before response headers arrived (0.35.0)
|
|
25
26
|
] as const;
|
|
26
27
|
export type SdkSynthesizedErrorCode = (typeof SDK_SYNTHESIZED_ERROR_CODES)[number];
|
|
27
28
|
|