@burdenoff/fe-libs 2026.1013.13 → 2026.1014.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 (48) hide show
  1. package/dist/chrome/AppShellLayout.js +17 -8
  2. package/dist/shared/assistant/avatar/live/LiveAvatarStage.d.ts +15 -0
  3. package/dist/shared/assistant/avatar/live/LiveAvatarStage.js +139 -0
  4. package/dist/shared/assistant/avatar/live/avatarAudioDecode.d.ts +40 -0
  5. package/dist/shared/assistant/avatar/live/avatarAudioDecode.js +72 -0
  6. package/dist/shared/assistant/avatar/live/avatarAudioErrors.d.ts +25 -0
  7. package/dist/shared/assistant/avatar/live/avatarAudioErrors.js +27 -0
  8. package/dist/shared/assistant/avatar/live/avatarAudioMeter.d.ts +37 -0
  9. package/dist/shared/assistant/avatar/live/avatarAudioMeter.js +62 -0
  10. package/dist/shared/assistant/avatar/live/avatarMp3Inspect.d.ts +30 -0
  11. package/dist/shared/assistant/avatar/live/avatarMp3Inspect.js +168 -0
  12. package/dist/shared/assistant/avatar/live/avatarPcm16.d.ts +21 -0
  13. package/dist/shared/assistant/avatar/live/avatarPcm16.js +54 -0
  14. package/dist/shared/assistant/avatar/live/avatarPlaybackTracker.d.ts +181 -0
  15. package/dist/shared/assistant/avatar/live/avatarPlaybackTracker.js +221 -0
  16. package/dist/shared/assistant/avatar/live/avatarPorts.d.ts +66 -0
  17. package/dist/shared/assistant/avatar/live/avatarPorts.js +44 -0
  18. package/dist/shared/assistant/avatar/live/avatarProtocol.d.ts +22 -0
  19. package/dist/shared/assistant/avatar/live/avatarProtocol.js +27 -0
  20. package/dist/shared/assistant/avatar/live/avatarReplyOutputs.d.ts +170 -0
  21. package/dist/shared/assistant/avatar/live/avatarReplyOutputs.js +525 -0
  22. package/dist/shared/assistant/avatar/live/avatarSessionClient.d.ts +96 -0
  23. package/dist/shared/assistant/avatar/live/avatarSessionClient.js +261 -0
  24. package/dist/shared/assistant/avatar/live/avatarTransport.d.ts +270 -0
  25. package/dist/shared/assistant/avatar/live/avatarTransport.js +333 -0
  26. package/dist/shared/assistant/avatar/live/avatarTransportPort.d.ts +17 -0
  27. package/dist/shared/assistant/avatar/live/avatarTransportPort.js +27 -0
  28. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +44 -0
  29. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +124 -0
  30. package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +133 -0
  31. package/dist/shared/assistant/avatar/live/liveAvatarController.js +488 -0
  32. package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +4 -0
  33. package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +14 -0
  34. package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.d.ts +6 -0
  35. package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.js +86 -0
  36. package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +2 -0
  37. package/dist/shared/assistant/avatar/useAvatarCopy.js +1 -0
  38. package/dist/shared/assistant/i18nManifest.d.ts +1 -0
  39. package/dist/shared/assistant/i18nManifest.js +1 -0
  40. package/dist/shared/assistant/index.d.ts +17 -0
  41. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.d.ts +37 -0
  42. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.js +118 -63
  43. package/dist/shared/assistant/speech/types.d.ts +14 -2
  44. package/dist/shared/assistant/ui/AssistantOverlay.js +98 -95
  45. package/dist/shared/assistant/ui/assistantOverlayState.d.ts +12 -0
  46. package/dist/shared/assistant/ui/assistantOverlayState.js +14 -14
  47. package/dist/shared-assistant.js +250 -233
  48. package/package.json +1 -1
@@ -0,0 +1,96 @@
1
+ import { AvatarProblem } from '../useAvatarCopy';
2
+ /** The gateway route; the base URL is the TTS endpoint's origin plus this. */
3
+ export declare const AVATAR_API_PATH = "/api/avatar/v1";
4
+ /**
5
+ * The avatar API's base URL from the hosted TTS endpoint the host configured, or null when it cannot be used (none configured, not a URL,
6
+ * credentials in it, or plain `http:` off a loopback host). A relative endpoint is resolved against `locationHref`.
7
+ */
8
+ export declare function avatarApiBaseFromTtsEndpoint(ttsEndpoint: string | null | undefined, locationHref?: string): string | null;
9
+ export interface AvatarAvailability {
10
+ readonly available: boolean;
11
+ /** Coarse on purpose (§3.3): no verified workspace reached the server, or the avatar is not enabled for this caller. */
12
+ readonly reason: 'workspace_context_required' | 'not_enabled' | null;
13
+ /** Approved and provider-ready ids; names and pictures are the client's. */
14
+ readonly avatars: readonly string[];
15
+ /** The disclosure version the client must show and echo back on create. */
16
+ readonly consentVersion: string;
17
+ /** The server's session cap, D. */
18
+ readonly maxSessionSeconds: number;
19
+ }
20
+ export interface AvatarSessionStart {
21
+ readonly sessionId: string;
22
+ readonly livekitUrl: string;
23
+ /** Joins one room, for connecting only: its expiry (`connectBy`) bounds the connect, not the session. */
24
+ readonly participantToken: string;
25
+ /** The avatar worker's identity in that room. */
26
+ readonly avatarIdentity: string;
27
+ /** Wall-clock milliseconds: the token cannot connect after this. */
28
+ readonly connectBy: number;
29
+ /** Wall-clock milliseconds: the server's hard end (create + D - 5 s). */
30
+ readonly endsAt: number;
31
+ }
32
+ export type AvatarSessionState = 'starting' | 'ready' | 'ending' | 'ended' | 'failed' | 'canceled';
33
+ export interface AvatarSessionStatus {
34
+ readonly sessionId: string;
35
+ readonly state: AvatarSessionState;
36
+ /** Who or what ended it; never a claim that a reply was heard (P1c). */
37
+ readonly endedReason: string | null;
38
+ }
39
+ /** The reasons a client may give when it ends a session; analytics only (§3.3). */
40
+ export type AvatarClientEndReason = 'user' | 'voice_end' | 'workspace_switch' | 'idle' | 'page_hide' | 'client_error';
41
+ /** A non-2xx answer, a refused response shape, or no answer at all (`status` 0). */
42
+ export declare class AvatarApiError extends Error {
43
+ readonly status: number;
44
+ /** The server's or the gateway's code, `invalid_response` for a body we refused, `network` for no answer. */
45
+ readonly code: string;
46
+ readonly retryAfterSeconds: number | null;
47
+ constructor(status: number,
48
+ /** The server's or the gateway's code, `invalid_response` for a body we refused, `network` for no answer. */
49
+ code: string, retryAfterSeconds: number | null);
50
+ }
51
+ export interface AvatarSessionClientOptions {
52
+ /** `https://<gateway>/api/avatar/v1` (avatarApiBaseFromTtsEndpoint). */
53
+ readonly baseUrl: string;
54
+ /** The person's `Authorization` value (`Bearer …`), read at request time; null when signed out. */
55
+ readonly getAuthorization: () => string | null;
56
+ /**
57
+ * The workspace token as an `X-Workspace-Authorization` value (`Bearer …`), or null when none can be had. `fresh`: the gateway refused
58
+ * the last one, mint it again instead of reusing a stored one.
59
+ */
60
+ readonly getWorkspaceAuthorization: (options: {
61
+ readonly fresh: boolean;
62
+ }) => Promise<string | null>;
63
+ readonly fetchImpl?: typeof fetch;
64
+ /** Waits between the contract's retries; injectable for tests. */
65
+ readonly sleep?: (ms: number) => Promise<void>;
66
+ /** How long 409 `start_in_progress` may be retried with the same key before giving up. */
67
+ readonly startInProgressBudgetMs?: number;
68
+ }
69
+ export interface AvatarSessionClient {
70
+ availability(signal?: AbortSignal): Promise<AvatarAvailability>;
71
+ create(input: {
72
+ readonly avatarId: string;
73
+ readonly consentVersion: string;
74
+ readonly idempotencyKey: string;
75
+ }, signal?: AbortSignal): Promise<AvatarSessionStart>;
76
+ status(sessionId: string, signal?: AbortSignal): Promise<AvatarSessionStatus>;
77
+ /** Ends one of the person's own sessions. `keepalive`: sent on page hide, best effort (it may not survive a CORS preflight). */
78
+ end(sessionId: string, options: {
79
+ readonly reason: AvatarClientEndReason;
80
+ readonly keepalive?: boolean;
81
+ }): Promise<AvatarSessionStatus>;
82
+ }
83
+ /** A UUID v4, from the platform when it has one. */
84
+ export declare function avatarIdempotencyKey(random?: Crypto | undefined): string;
85
+ export declare function createAvatarSessionClient(options: AvatarSessionClientOptions): AvatarSessionClient;
86
+ /** What the avatar UI does with a refusal: what to tell the person, and whether to stop offering the toggle for this page. */
87
+ export interface AvatarRefusal {
88
+ readonly problem: AvatarProblem;
89
+ /** The avatar is not offered here at all (404, 403 not allowed, no workspace): hide the toggle until the next page load. */
90
+ readonly hide: boolean;
91
+ }
92
+ /**
93
+ * The person's side of every failure (§3.4): the normal voice always continues; this says what to show. 404 means "not here" (the module
94
+ * or the route is off), 403 "not for you", 429/503 "not now".
95
+ */
96
+ export declare function avatarRefusalFor(error: unknown): AvatarRefusal;
@@ -0,0 +1,261 @@
1
+ //#region src/shared/assistant/avatar/live/avatarSessionClient.ts
2
+ var e = "/api/avatar/v1", t = new Set([
3
+ "localhost",
4
+ "127.0.0.1",
5
+ "[::1]"
6
+ ]), n = /^avs_[0-9a-f]{32}$/, r = /^av_[a-z0-9_]{1,64}$/i, i = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
+ function a(n, r) {
8
+ if (typeof n != "string" || n.trim() === "") return null;
9
+ let i;
10
+ try {
11
+ i = new URL(n.trim(), r);
12
+ } catch {
13
+ return null;
14
+ }
15
+ return i.username !== "" || i.password !== "" || i.protocol === "http:" && !t.has(i.hostname.toLowerCase()) || i.protocol !== "https:" && i.protocol !== "http:" ? null : `${i.protocol}//${i.host}${e}`;
16
+ }
17
+ var o = class extends Error {
18
+ status;
19
+ code;
20
+ retryAfterSeconds;
21
+ constructor(e, t, n) {
22
+ super(`avatar api ${e} ${t}`), this.status = e, this.code = t, this.retryAfterSeconds = n, this.name = "AvatarApiError";
23
+ }
24
+ };
25
+ function s(e = globalThis.crypto) {
26
+ if (e && typeof e.randomUUID == "function") return e.randomUUID();
27
+ let t = new Uint8Array(16);
28
+ if (e && typeof e.getRandomValues == "function") e.getRandomValues(t);
29
+ else throw Error("no secure random source for an Idempotency-Key");
30
+ t[6] = (t[6] ?? 0) & 15 | 64, t[8] = (t[8] ?? 0) & 63 | 128;
31
+ let n = [...t].map((e) => e.toString(16).padStart(2, "0")).join("");
32
+ return `${n.slice(0, 8)}-${n.slice(8, 12)}-${n.slice(12, 16)}-${n.slice(16, 20)}-${n.slice(20)}`;
33
+ }
34
+ function c(e) {
35
+ return typeof e == "object" && !!e && !Array.isArray(e);
36
+ }
37
+ function l(e, t) {
38
+ let n = e.headers.get("retry-after"), r = n === null || n.trim() === "" ? NaN : Number(n);
39
+ if (Number.isFinite(r) && r >= 0) return r;
40
+ let i = c(t) && c(t.error) ? Number(t.error.retryAfterSeconds) : NaN;
41
+ return Number.isFinite(i) && i >= 0 ? i : null;
42
+ }
43
+ async function u(e) {
44
+ try {
45
+ return await e.json();
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ async function d(e) {
51
+ let t = await u(e), n = c(t) && c(t.error) && typeof t.error.code == "string" ? t.error.code : "http_error";
52
+ return new o(e.status, n, l(e, t));
53
+ }
54
+ function f(e) {
55
+ if (!c(e) || typeof e.available != "boolean") throw new o(200, "invalid_response", null);
56
+ let t = Array.isArray(e.avatars) ? e.avatars.flatMap((e) => c(e) && typeof e.id == "string" && r.test(e.id) ? [e.id] : []) : [], n = e.reason === "workspace_context_required" || e.reason === "not_enabled" ? e.reason : null, i = typeof e.consentVersion == "string" ? e.consentVersion : "", a = typeof e.maxSessionSeconds == "number" && Number.isFinite(e.maxSessionSeconds) ? e.maxSessionSeconds : 0, s = e.available && t.length > 0 && i !== "" && a > 0;
57
+ return {
58
+ available: s,
59
+ reason: s ? null : n ?? "not_enabled",
60
+ avatars: t,
61
+ consentVersion: i,
62
+ maxSessionSeconds: a
63
+ };
64
+ }
65
+ function p(e) {
66
+ try {
67
+ let n = new URL(e);
68
+ return n.protocol === "wss:" ? !0 : n.protocol === "ws:" && t.has(n.hostname.toLowerCase());
69
+ } catch {
70
+ return !1;
71
+ }
72
+ }
73
+ function m(e) {
74
+ if (!c(e)) throw new o(201, "invalid_response", null);
75
+ let { sessionId: t, livekitUrl: r, participantToken: i, avatarIdentity: a, connectBy: s, endsAt: l } = e, u = typeof s == "string" ? Date.parse(s) : NaN, d = typeof l == "string" ? Date.parse(l) : NaN;
76
+ if (typeof t != "string" || !n.test(t) || typeof r != "string" || !p(r) || typeof i != "string" || i === "" || typeof a != "string" || a === "" || !Number.isFinite(u) || !Number.isFinite(d)) throw new o(201, "invalid_response", null);
77
+ return {
78
+ sessionId: t,
79
+ livekitUrl: r,
80
+ participantToken: i,
81
+ avatarIdentity: a,
82
+ connectBy: u,
83
+ endsAt: d
84
+ };
85
+ }
86
+ var h = [
87
+ "starting",
88
+ "ready",
89
+ "ending",
90
+ "ended",
91
+ "failed",
92
+ "canceled"
93
+ ];
94
+ function g(e, t) {
95
+ if (!c(e) || !h.includes(e.state)) throw new o(200, "invalid_response", null);
96
+ return {
97
+ sessionId: t,
98
+ state: e.state,
99
+ endedReason: typeof e.endedReason == "string" ? e.endedReason : null
100
+ };
101
+ }
102
+ var _ = (e) => new Promise((t) => {
103
+ setTimeout(t, e);
104
+ });
105
+ function v(e) {
106
+ let t = e.fetchImpl ?? ((e, t) => fetch(e, t)), a = e.sleep ?? _, s = e.startInProgressBudgetMs ?? 15e3, c = e.baseUrl.replace(/\/+$/, "");
107
+ async function l(t, n) {
108
+ let r = e.getAuthorization();
109
+ if (!r) throw new o(401, "unauthenticated", null);
110
+ let i = { authorization: r };
111
+ if (t) {
112
+ let t = await e.getWorkspaceAuthorization({ fresh: n });
113
+ t && (i["x-workspace-authorization"] = t);
114
+ }
115
+ return i;
116
+ }
117
+ async function p(e, n) {
118
+ try {
119
+ return await t(e, {
120
+ ...n,
121
+ credentials: "omit",
122
+ cache: "no-store"
123
+ });
124
+ } catch (e) {
125
+ throw n.signal?.aborted ? e : new o(0, "network", null);
126
+ }
127
+ }
128
+ return {
129
+ async availability(e) {
130
+ for (let t = 1;; t += 1) {
131
+ let n = await l(!0, t > 1), r = await p(`${c}/availability`, {
132
+ method: "GET",
133
+ headers: n,
134
+ signal: e
135
+ });
136
+ if (r.ok) return f(await u(r));
137
+ if (!(r.status === 401 && t === 1)) throw await d(r);
138
+ }
139
+ },
140
+ async create(e, t) {
141
+ if (!r.test(e.avatarId) || !i.test(e.idempotencyKey)) throw new o(400, "invalid_request", null);
142
+ let n = JSON.stringify({
143
+ avatarId: e.avatarId,
144
+ consentVersion: e.consentVersion
145
+ }), f = Date.now(), h = !1, g = !1;
146
+ for (;;) {
147
+ let r = await l(!0, h);
148
+ if (r["x-workspace-authorization"] === void 0) throw new o(403, "workspace_context_required", null);
149
+ let i = await p(`${c}/sessions`, {
150
+ method: "POST",
151
+ headers: {
152
+ ...r,
153
+ "content-type": "application/json",
154
+ "idempotency-key": e.idempotencyKey
155
+ },
156
+ body: n,
157
+ signal: t
158
+ });
159
+ if (i.status === 201) return m(await u(i));
160
+ let _ = await d(i);
161
+ if (i.status === 401 && !h) {
162
+ h = !0;
163
+ continue;
164
+ }
165
+ if (i.status === 409 && _.code === "start_in_progress" && Date.now() - f < s) {
166
+ if (await a(Math.max(250, (_.retryAfterSeconds ?? 1) * 1e3)), t?.aborted) throw _;
167
+ continue;
168
+ }
169
+ if (i.status === 502 && !g) {
170
+ g = !0;
171
+ continue;
172
+ }
173
+ throw _;
174
+ }
175
+ },
176
+ async status(e, t) {
177
+ if (!n.test(e)) throw new o(400, "invalid_request", null);
178
+ let r = await l(!1, !1), i = await p(`${c}/sessions/${e}`, {
179
+ method: "GET",
180
+ headers: r,
181
+ signal: t
182
+ });
183
+ if (!i.ok) throw await d(i);
184
+ return g(await u(i), e);
185
+ },
186
+ async end(e, { reason: t, keepalive: r = !1 }) {
187
+ if (!n.test(e)) throw new o(400, "invalid_request", null);
188
+ let i = await l(!1, !1), a = await p(`${c}/sessions/${e}/end`, {
189
+ method: "POST",
190
+ headers: {
191
+ ...i,
192
+ "content-type": "application/json"
193
+ },
194
+ body: JSON.stringify({ reason: t }),
195
+ keepalive: r
196
+ });
197
+ if (!a.ok) throw await d(a);
198
+ return g(await u(a), e);
199
+ }
200
+ };
201
+ }
202
+ function y(e) {
203
+ if (!(e instanceof o) || e.status === 0) return {
204
+ problem: "connection",
205
+ hide: !1
206
+ };
207
+ if (e.status === 404) return {
208
+ problem: "unavailable",
209
+ hide: !0
210
+ };
211
+ switch (e.code) {
212
+ case "avatar_not_allowed":
213
+ case "workspace_context_required": return {
214
+ problem: "unavailable",
215
+ hide: !0
216
+ };
217
+ case "avatar_not_approved":
218
+ case "avatar_not_ready": return {
219
+ problem: "not-ready",
220
+ hide: !1
221
+ };
222
+ case "actor_already_active": return {
223
+ problem: "already-running",
224
+ hide: !1
225
+ };
226
+ case "workspace_busy":
227
+ case "env_busy":
228
+ case "provider_concurrency_limited":
229
+ case "provider_no_capacity":
230
+ case "counters_unavailable":
231
+ case "avatar_busy": return {
232
+ problem: "capacity",
233
+ hide: !1
234
+ };
235
+ case "start_rate_limited":
236
+ case "provider_rate_limited": return {
237
+ problem: "rate-limited",
238
+ hide: !1
239
+ };
240
+ case "avatar_paused":
241
+ case "budget_exhausted": return {
242
+ problem: "paused",
243
+ hide: !1
244
+ };
245
+ case "avatar_unavailable": return {
246
+ problem: "unavailable",
247
+ hide: !1
248
+ };
249
+ case "provider_unavailable":
250
+ case "provider_out_of_credits": return {
251
+ problem: "provider",
252
+ hide: !1
253
+ };
254
+ default: return {
255
+ problem: e.status === 401 ? "unavailable" : "provider",
256
+ hide: !1
257
+ };
258
+ }
259
+ }
260
+ //#endregion
261
+ export { e as AVATAR_API_PATH, o as AvatarApiError, a as avatarApiBaseFromTtsEndpoint, s as avatarIdempotencyKey, y as avatarRefusalFor, v as createAvatarSessionClient };
@@ -0,0 +1,270 @@
1
+ import { AvatarRoom, AvatarByteWriter, AvatarRemoteTrack, AvatarScheduler, AvatarUnsubscribe } from './avatarPorts';
2
+ export declare class AvatarNotReadyError extends Error {
3
+ readonly missing: 'participant' | 'video' | 'connection';
4
+ constructor(missing: 'participant' | 'video' | 'connection');
5
+ }
6
+ export declare class AvatarSegmentBusyError extends Error {
7
+ constructor();
8
+ }
9
+ /** The segment was ended by the provider, a clear, a disconnect or disposal: stop writing. */
10
+ export declare class AvatarSegmentEndedError extends Error {
11
+ constructor();
12
+ }
13
+ export declare class AvatarTransportClosedError extends Error {
14
+ constructor();
15
+ }
16
+ export type AvatarTransportEvent = {
17
+ readonly type: 'ready';
18
+ } | {
19
+ readonly type: 'playbackStarted';
20
+ }
21
+ /** positionSec / interrupted are null when the provider payload was not the documented shape. */
22
+ | {
23
+ readonly type: 'playbackFinished';
24
+ readonly positionSec: number | null;
25
+ readonly interrupted: boolean | null;
26
+ readonly synthetic: boolean;
27
+ } | {
28
+ readonly type: 'unmatchedFinished';
29
+ }
30
+ /**
31
+ * A lk.playback_finished taken as the late report owed by a segment that a clear of ours ended without the avatar's confirmation (see
32
+ * owedFinishes): it is about that segment, never a later one. positionSec / interrupted as reported (null when not the documented shape).
33
+ */
34
+ | {
35
+ readonly type: 'owedFinished';
36
+ readonly positionSec: number | null;
37
+ readonly interrupted: boolean | null;
38
+ }
39
+ /** A lk.playback_started while no segment of ours was open or waiting (a stray start for audio we already cleared). */
40
+ | {
41
+ readonly type: 'unmatchedStarted';
42
+ } | {
43
+ readonly type: 'avatarLeft';
44
+ } | {
45
+ readonly type: 'disconnected';
46
+ } | {
47
+ readonly type: 'reconnecting';
48
+ } | {
49
+ readonly type: 'reconnected';
50
+ } | {
51
+ readonly type: 'audioPlayback';
52
+ readonly canPlay: boolean;
53
+ }
54
+ /** Result of the readiness probe at connect (see AvatarReadinessProbeOptions). */
55
+ | {
56
+ readonly type: 'readiness';
57
+ readonly status: AvatarReadinessStatus;
58
+ readonly attempts: number;
59
+ readonly ms: number;
60
+ /** Why the last unsuccessful attempt failed: "rejected", "unsupported", "timeout", "recipient-disconnected", "error". */
61
+ readonly lastReason: string | null;
62
+ } | {
63
+ readonly type: 'error';
64
+ readonly message: string;
65
+ };
66
+ /**
67
+ * confirmed: the avatar answered our probe as the sender it will play (its stream handler is registered for us).
68
+ * unconfirmed: it never did within the time limit. The transport is still usable: the engine's first-reply retry and voice fallback
69
+ * remain as the safety net. skipped: the probe is disabled.
70
+ */
71
+ export type AvatarReadinessStatus = 'confirmed' | 'unconfirmed' | 'skipped';
72
+ /** What one lk.clear_buffer achieved. The engine uses it before it ever writes the same reply again. */
73
+ export interface AvatarClearOutcome {
74
+ /** The playback position the avatar reported for what it dropped; null when it did not say. */
75
+ readonly positionSec: number | null;
76
+ /** True when no lk.playback_finished answered (timeout, failed call, nothing to clear): positionSec is then our own estimate or null. */
77
+ readonly synthetic: boolean;
78
+ /** How the call itself was answered: "ok", "rejected", "unsupported", "timeout", "recipient-disconnected", "error", or "none" if not sent. */
79
+ readonly rpc: string;
80
+ }
81
+ /** What the caller of a clear knows about the segment it ends (see LiveKitAvatarTransport.clearBuffer). */
82
+ export interface AvatarClearOptions {
83
+ /**
84
+ * The avatar's report of that segment is already overdue: its own tracker stopped waiting for it (no start, or no finish, within its
85
+ * deadline). When the avatar does not confirm the clear, such a segment then owes no lk.playback_finished (see owedFinishes): a report that
86
+ * late is presumed lost, as the tracker presumes, and waiting for it would take a later segment's own report for it. Default false.
87
+ */
88
+ readonly finishOverdue?: boolean;
89
+ }
90
+ /**
91
+ * The first byte stream of a connection can reach the worker before the worker has seen us join (signalling and the data channel are
92
+ * different paths) and be dropped silently: LiveKit's reference receiver registers its stream handler, and its lk.clear_buffer handler,
93
+ * only after it has seen its sender. So before the first reply, the transport calls lk.clear_buffer (a no-op on an idle avatar) until the
94
+ * avatar answers it with anything but "reject": the reference answers "reject" to a caller it does not know and fails with
95
+ * "method not supported" before it has registered. Bounded: never more than timeoutMs, never a fixed sleep.
96
+ */
97
+ export interface AvatarReadinessProbeOptions {
98
+ /** Give up after this long (default 6000 ms); the connection is then "unconfirmed" but usable. */
99
+ readonly timeoutMs?: number;
100
+ /** Response timeout of one probe call (default 1500 ms). */
101
+ readonly rpcTimeoutMs?: number;
102
+ /** Wait before the second attempt (default 150 ms); doubles up to maxBackoffMs (default 1000 ms). */
103
+ readonly initialBackoffMs?: number;
104
+ readonly maxBackoffMs?: number;
105
+ }
106
+ export type AvatarTransportState = 'idle' | 'connecting' | 'ready' | 'closed';
107
+ export interface AvatarSegmentWriter {
108
+ readonly id: number;
109
+ readonly state: 'open' | 'closed' | 'ended';
110
+ /** Seconds of audio handed to the stream, a write still in flight included (its bytes may already be on their way to the avatar). */
111
+ readonly pushedSec: number;
112
+ write(pcm: Uint8Array): Promise<void>;
113
+ /** End of segment (flush): the avatar plays what it has and then reports lk.playback_finished. */
114
+ close(): Promise<void>;
115
+ /** Abandon the segment without expecting a lk.playback_finished for it (best-effort trailer). Idempotent. */
116
+ discard(): void;
117
+ }
118
+ export interface AvatarTransportOptions {
119
+ readonly room: AvatarRoom;
120
+ readonly avatarIdentity: string;
121
+ readonly scheduler: AvatarScheduler;
122
+ readonly readyTimeoutMs?: number;
123
+ readonly clearTimeoutMs?: number;
124
+ /** The first-stream readiness probe. Default: enabled with the defaults above. `false` disables it (tests, comparisons). */
125
+ readonly readinessProbe?: AvatarReadinessProbeOptions | false;
126
+ }
127
+ export interface AvatarConnectParams {
128
+ readonly url: string;
129
+ readonly token: string;
130
+ readonly videoElement?: HTMLVideoElement | null;
131
+ readonly audioElement?: HTMLAudioElement | null;
132
+ /** Called when the avatar's audio track arrives, e.g. to start a level meter on its MediaStreamTrack. */
133
+ readonly onAvatarAudio?: (track: AvatarRemoteTrack) => void;
134
+ /**
135
+ * Called when the avatar's video track arrives (BOFF-8822 P8, additive): the stage attaches it to a video element it owns and may
136
+ * remount, so the element is not fixed at connect time. Readiness still counts the track whether or not anyone takes it.
137
+ */
138
+ readonly onAvatarVideo?: (track: AvatarRemoteTrack) => void;
139
+ }
140
+ declare class Segment implements AvatarSegmentWriter {
141
+ readonly id: number;
142
+ private readonly writer;
143
+ private readonly owner;
144
+ state: 'open' | 'closed' | 'ended';
145
+ private pushedBytes;
146
+ constructor(id: number, writer: AvatarByteWriter, owner: LiveKitAvatarTransport);
147
+ get pushedSec(): number;
148
+ write(pcm: Uint8Array): Promise<void>;
149
+ close(): Promise<void>;
150
+ discard(): void;
151
+ /** Called by the transport when the segment can no longer be written to. Best-effort trailer. */
152
+ end(): void;
153
+ }
154
+ export declare class LiveKitAvatarTransport {
155
+ private readonly options;
156
+ private static nextInstanceId;
157
+ /** Distinguishes connections: each transport instance is one connection to one avatar session. */
158
+ readonly instanceId: number;
159
+ private state;
160
+ private readonly listeners;
161
+ private readonly unsubscribes;
162
+ private nextSegmentId;
163
+ private current;
164
+ private readonly awaiting;
165
+ private clearInFlight;
166
+ private clearSettle;
167
+ private avatarPresent;
168
+ private videoPresent;
169
+ private degraded;
170
+ private audioElement;
171
+ private disposed;
172
+ private rejectReady;
173
+ private mediaReady;
174
+ private resolveMediaReady;
175
+ private readinessStatus;
176
+ private wakeProbe;
177
+ /**
178
+ * A clear of ours that no longer waits (a probe call, a clear answered "ok" whose lk.playback_finished did not come in time, or one for a
179
+ * segment whose report was already overdue) may still be answered with lk.playback_finished(interrupted), even when nothing was playing
180
+ * (the reference does not do that, a provider may). Such an answer must never end the segment we write next. At most ONE answer is absorbed
181
+ * per such clear, and only within the clear timeout: anything beyond is a real interruption. Trade-off: if the old clear is never answered,
182
+ * a real interruption inside the window is taken for its answer; the reply then ends through the tracker's quiet/no-finish nets
183
+ * (uncertain, never replayed) instead of at once. A clear the avatar confirmed nothing of is handled by owedFinishes instead.
184
+ */
185
+ private strayFinishDeadline;
186
+ private strayAnswers;
187
+ /**
188
+ * lk.playback_finished reports still owed by segments that a clear of ours ended while the avatar confirmed nothing (no finished answered
189
+ * the clear, and the call itself got no "ok": it timed out or failed). The avatar may have ignored that clear and play the segment on, and
190
+ * report its end at any later time. It plays streams in the order they were opened, so such a report comes before any later segment's:
191
+ * every lk.playback_finished goes to these first, oldest first and whatever it says, before a waiting clear, the stray-answer window and any
192
+ * later segment. So a stopped segment's late end can never complete or end a LATER reply, nor confirm a later clear (BOFF-8822 G1: after a
193
+ * time-based release that came too early, the stopped segment's natural end was taken for the next reply's). Counted on this connection
194
+ * (one transport is one connection), and only for the avatar the clear was sent to: forgotten when it leaves, and not counted for a clear
195
+ * it left during or was absent for (see avatarDepartures), since a worker that comes back knows nothing of those segments.
196
+ * The cost when such a report never comes (the avatar cleared the segment after all, or dropped it): the next lk.playback_finished is taken
197
+ * for it, so that reply is never completed by its own report; it ends through the tracker's no-finish or quiet nets as uncertain, bounded by
198
+ * its own deadline and never claimed. A reply left to reach that deadline ends the cost: its tracker's clear is for an overdue report,
199
+ * which owes nothing (AvatarClearOptions.finishOverdue), so the reply after it completes on its own report. A Stop or a new question that ends
200
+ * such a reply before its deadline does not: that clear is for a report not yet overdue, so when the avatar confirms nothing of it either,
201
+ * that reply owes its report in turn and the reply after it is taken the same way. The cost repeats, one reply at a time, for as long as
202
+ * each such reply is interrupted before its deadline by a clear the avatar does not confirm, and stops at the first reply left to its
203
+ * deadline, a clear the avatar confirms, or the avatar leaving; no reply it reaches is ever claimed. Counting that interrupted reply's report
204
+ * as already paid would stop the repetition, but whenever the report taken was the stopped segment's after all, that reply's own late end
205
+ * could then complete the reply after it: the false completion this rule exists to prevent (kept for safety, review AD-R1 of 2026-10-11).
206
+ * Starts are not counted the same way: a segment stopped before it started, that the avatar then plays on, may send its lk.playback_started
207
+ * while a later segment of ours is open or waiting, and that starts the later reply (its captions) early. It never completes it.
208
+ */
209
+ private owedFinishes;
210
+ /**
211
+ * How often the avatar left on this connection. A clear notes it when it is sent to an avatar that is present: if the avatar leaves before
212
+ * the clear resolves (the LiveKit client SDK reports the participant gone first, and only then rejects the calls pending to it, as
213
+ * RECIPIENT_DISCONNECTED), or was absent when it was sent, the clear leaves nothing to expect, neither an owed report nor a late answer:
214
+ * the worker that left reports nothing more, and one that comes back knows nothing of that clear (review AD-R2 of 2026-10-11).
215
+ */
216
+ private avatarDepartures;
217
+ constructor(options: AvatarTransportOptions);
218
+ get transportState(): AvatarTransportState;
219
+ /** The readiness probe's result once connect() has resolved; null before. */
220
+ get readiness(): AvatarReadinessStatus | null;
221
+ /** True while the browser blocks the avatar's audio (autoplay policy): resumeAudio() from a user gesture is needed. */
222
+ get audioBlocked(): boolean;
223
+ subscribe(handler: (event: AvatarTransportEvent) => void): AvatarUnsubscribe;
224
+ private emit;
225
+ /**
226
+ * Resume audio playback. Call it synchronously from the user's click on the Avatar toggle (a user gesture is required by
227
+ * the browser's autoplay policy), and again from a "tap to enable sound" control when audioPlayback reports canPlay=false.
228
+ */
229
+ resumeAudio(): Promise<void>;
230
+ /** Mute or unmute the avatar's audio element instantly (used to silence output on cancel before the provider confirms). */
231
+ setOutputMuted(muted: boolean): void;
232
+ connect(params: AvatarConnectParams): Promise<void>;
233
+ /** Expect at most one more late answer to a clear of ours that no longer waits (see strayFinishDeadline). */
234
+ private armStrayWindow;
235
+ /**
236
+ * Once a clear's outcome is known: what the avatar may still report of the segment it ended. Nothing when a real lk.playback_finished
237
+ * answered the clear, or when the avatar refused it (it plays nothing of ours and reports nothing). A brief window for one late
238
+ * interrupted answer when the call was answered "ok", or when that segment's report was already overdue (see strayFinishDeadline).
239
+ * Otherwise the avatar confirmed nothing: it may have ignored the clear and play the segment on, so that segment owes its report, however
240
+ * late (see owedFinishes). Called only while the avatar the clear was sent to is still there (see avatarDepartures).
241
+ */
242
+ private expectLateAnswer;
243
+ private checkReady;
244
+ private waitMediaReady;
245
+ /**
246
+ * Bounded readiness probe (see AvatarReadinessProbeOptions). Resolves with the readiness event to emit; rejects when the transport was closed, the
247
+ * connection dropped, or the avatar left meanwhile. Every call is capped by the remaining budget, so the whole probe never exceeds timeoutMs.
248
+ */
249
+ private probeReadiness;
250
+ private assertReady;
251
+ openSegment(): Promise<AvatarSegmentWriter>;
252
+ /** Called by Segment.close(). */
253
+ segmentClosed(segment: Segment): void;
254
+ /** Called by Segment.discard(): forget it so the next reply is not blocked waiting for a finished that will not be reported. */
255
+ segmentDiscarded(segment: Segment): void;
256
+ private endAllSegments;
257
+ private handleStarted;
258
+ private handleFinished;
259
+ /**
260
+ * Interrupt: stop writing at once, ask the avatar to drop its buffer, and wait up to the timeout for its
261
+ * lk.playback_finished(interrupted). Like the reference sender, a missing answer is treated as interrupted.
262
+ * Resolves when settled; callers must not open a new segment before it resolves. When the avatar confirms nothing of it, the segment it
263
+ * ended still owes its lk.playback_finished, unless `options` says that report is already overdue (see owedFinishes), or the avatar left
264
+ * before the clear resolved or was absent when it was sent (see avatarDepartures).
265
+ */
266
+ clearBuffer(options?: AvatarClearOptions): Promise<AvatarClearOutcome>;
267
+ dispose(): Promise<void>;
268
+ private teardown;
269
+ }
270
+ export {};