realtime-avatar 0.27.0 → 0.27.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.
package/README.md CHANGED
@@ -353,6 +353,28 @@ a last line first.
353
353
  On the lower-level hooks, `reconnect()` redials (a double tap mints once), and so does setting
354
354
  `active` to false and back. `reset()` does not undo an end.
355
355
 
356
+ ### Waiting for the character
357
+
358
+ Joining the room is not enough to start a conversation: the character's agent must also
359
+ arrive. The shared React and React Native lifecycle allows at most 30 seconds from the first
360
+ held grant to that readiness, including automatic retries and signaling reconnects. The
361
+ existing connect watchdog (12 seconds by default) also recovers a connected room whose agent
362
+ never arrives. A transport connection alone does not reset the retry count or buy more time.
363
+
364
+ When the budget or retry count runs out, the SDK releases the session, leaves the room and
365
+ ends with `phase.reason: "error"` / `onEnded({ reason: "failed" })`. The optional `phase.code`
366
+ distinguishes `agent_timeout` from `connection_timeout`; these are client observations, not
367
+ HTTP status codes. `agent_timeout` means at least one room connected but the call never became
368
+ ready; retries may also have transport delays. `connection_timeout` means no room connected.
369
+ While connecting, `phase.waitingFor` distinguishes `agent` from `transport`.
370
+ No automatic mint follows that failure; `reconnect()` starts a fresh budget. A call that
371
+ already reached live keeps LiveKit's normal recovery of its existing room.
372
+
373
+ On `useRealtimeSession` or `useSessionLifecycle`, `readyTimeoutSeconds` changes the total
374
+ budget (positive, default 30). `connectWatchdogSeconds` changes the per-attempt wait; disabling
375
+ that watchdog with zero still leaves the total budget in force. Neither setting fabricates
376
+ media readiness or changes the application's first-video-frame gate.
377
+
356
378
  ### Optional connection details
357
379
 
358
380
  `AvatarCall` provides call status, actions and end reasons for your default UI. To show
@@ -665,7 +665,7 @@ var liveKitSessionReleaseReasonSchema = z.enum([
665
665
 
666
666
  // ../http-client/src/client.ts
667
667
  var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
668
- var SDK_VERSION = "0.27.0";
668
+ var SDK_VERSION = "0.27.1";
669
669
  var RealtimeAvatar = class {
670
670
  #apiKey;
671
671
  #baseUrl;
@@ -1,4 +1,4 @@
1
- import { sessionModeSchema, liveKitSessionReleaseReasonSchema, RealtimeAvatar, ROUTE_TIMEOUT_MS, isQueued, RealtimeAvatarHttpError, RealtimeAvatarError } from './chunk-NMJQEGYW.js';
1
+ import { sessionModeSchema, liveKitSessionReleaseReasonSchema, RealtimeAvatar, ROUTE_TIMEOUT_MS, isQueued, RealtimeAvatarHttpError, RealtimeAvatarError } from './chunk-4IEGR75H.js';
2
2
  import { z } from 'zod';
3
3
 
4
4
  // ../http-client/src/generated/error-semantics.ts
package/dist/express.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-SUKZ77GL.js';
2
- import './chunk-NMJQEGYW.js';
1
+ import { createProxyHandler } from './chunk-DOHZ365B.js';
2
+ import './chunk-4IEGR75H.js';
3
3
  import './chunk-MCIXU6XP.js';
4
4
 
5
5
  // ../proxy/src/express.ts
package/dist/hono.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-SUKZ77GL.js';
2
- import './chunk-NMJQEGYW.js';
1
+ import { createProxyHandler } from './chunk-DOHZ365B.js';
2
+ import './chunk-4IEGR75H.js';
3
3
  import './chunk-MCIXU6XP.js';
4
4
 
5
5
  // ../proxy/src/hono.ts
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript } from './chunk-NMJQEGYW.js';
1
+ export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript } from './chunk-4IEGR75H.js';
2
2
  import './chunk-MCIXU6XP.js';
package/dist/nextjs.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-SUKZ77GL.js';
2
- import './chunk-NMJQEGYW.js';
1
+ import { createProxyHandler } from './chunk-DOHZ365B.js';
2
+ import './chunk-4IEGR75H.js';
3
3
  import './chunk-MCIXU6XP.js';
4
4
 
5
5
  // ../proxy/src/nextjs.ts
@@ -1370,6 +1370,9 @@ type ReconnectPolicy = {
1370
1370
  * {@link SessionLifecycleApi.reconnect}.
1371
1371
  */
1372
1372
  type SessionEndReason = "idle" | "disconnected" | "error" | "user";
1373
+ /** Client-observed readiness failure, not an HTTP/provider error code. `agent_timeout` means
1374
+ * at least one room connected without reaching agent readiness; retries may also have transport delays. */
1375
+ type SessionReadinessFailure = "agent_timeout" | "connection_timeout";
1373
1376
  type SessionLifecyclePhase = {
1374
1377
  kind: "idle";
1375
1378
  } | {
@@ -1380,6 +1383,7 @@ type SessionLifecyclePhase = {
1380
1383
  } | {
1381
1384
  kind: "connecting";
1382
1385
  grant: LiveKitSessionGrant;
1386
+ waitingFor?: "transport" | "agent";
1383
1387
  } | {
1384
1388
  kind: "live";
1385
1389
  } | {
@@ -1392,9 +1396,11 @@ type SessionLifecyclePhase = {
1392
1396
  attempt: number;
1393
1397
  /** Native LiveKit recovery keeps the same grant; fresh-grant recovery re-mints. */
1394
1398
  strategy: "in-place" | "fresh-grant";
1399
+ reason?: SessionReadinessFailure;
1395
1400
  } | {
1396
1401
  kind: "ended";
1397
1402
  reason?: SessionEndReason;
1403
+ code?: SessionReadinessFailure;
1398
1404
  };
1399
1405
  type SessionLifecyclePhaseKind = SessionLifecyclePhase["kind"];
1400
1406
  /**
@@ -1409,14 +1415,18 @@ type RecoveryState = {
1409
1415
  } | {
1410
1416
  kind: "refreshing";
1411
1417
  attempt: number;
1418
+ reason?: SessionReadinessFailure;
1412
1419
  } | {
1413
1420
  kind: "reconnecting";
1414
1421
  attempt: number;
1422
+ reason?: SessionReadinessFailure;
1415
1423
  } | {
1416
1424
  kind: "failed";
1425
+ reason?: SessionReadinessFailure;
1417
1426
  } | {
1418
1427
  kind: "ended";
1419
1428
  reason?: SessionEndReason;
1429
+ code?: SessionReadinessFailure;
1420
1430
  };
1421
1431
  /**
1422
1432
  * The CLIENT-OWNED idle budget (seconds): how long "quiet" before the client-
@@ -1466,8 +1476,8 @@ type UseSessionLifecycleInput<T extends LLMProvider = LLMProvider> = {
1466
1476
  /** Auto-reconnect give-up bound. Default {@link MAX_RECONNECT_ATTEMPTS}. */
1467
1477
  maxReconnectAttempts?: number;
1468
1478
  /**
1469
- * How long a held grant may sit UNCONNECTED before we give the slot back
1470
- * (seconds). Default {@link DEFAULT_CONNECT_WATCHDOG_SECONDS}.
1479
+ * Per-attempt wait for the room connection, then for its first agent, before giving the
1480
+ * slot back (seconds). Default {@link DEFAULT_CONNECT_WATCHDOG_SECONDS}.
1471
1481
  *
1472
1482
  * A grant that never reaches a live room is invisible to every other timer here:
1473
1483
  * the idle clock only runs once connected, and LiveKit reports no disconnect for
@@ -1476,9 +1486,16 @@ type UseSessionLifecycleInput<T extends LLMProvider = LLMProvider> = {
1476
1486
  * the caller's only slot, so their retry is refused by a call they never saw.
1477
1487
  * Measured in prod: 8 of 12 never-connected sessions held for 76-81s.
1478
1488
  *
1479
- * Set 0 to disable (the platform timeout remains the backstop).
1489
+ * Set 0 to disable per-attempt recovery; the total readiness budget still applies.
1480
1490
  */
1481
1491
  connectWatchdogSeconds?: number;
1492
+ /**
1493
+ * Total pre-live readiness budget from the first held grant, including automatic retries.
1494
+ * A transport connection alone never resets it. Default {@link DEFAULT_READY_TIMEOUT_SECONDS}.
1495
+ * Non-positive/non-finite overrides use the default. Explicit reconnect starts a new budget.
1496
+ * The existing connect watchdog also bounds each connected room's wait for its first agent.
1497
+ */
1498
+ readyTimeoutSeconds?: number;
1482
1499
  /** Forwarded to {@link useLiveKitAvatarGrant}. Defaults true (queue auto-retry). */
1483
1500
  autoRetryBusy?: boolean;
1484
1501
  requestOptions?: UseLiveKitAvatarGrantInput<T>["requestOptions"];
@@ -1499,7 +1516,7 @@ type SessionLifecycleApi = {
1499
1516
  grant: LiveKitSessionGrant | null;
1500
1517
  /** The underlying capacity signal (queue position/size, error) — unchanged. */
1501
1518
  capacity: LiveKitCapacityState;
1502
- /** The DOM recovery contract: auto-reconnect attempts since the last connect. */
1519
+ /** The DOM recovery contract: auto-reconnect attempts since the last room + agent readiness. */
1503
1520
  attempt: number;
1504
1521
  /**
1505
1522
  * ms until the CLIENT-ENFORCED idle end, or null when not connected/live. Drives
@@ -1558,6 +1575,8 @@ type SessionLifecycleApi = {
1558
1575
  /** Reset to idle (mode/avatar change). Does not undo {@link end}: an ended call stays ended. */
1559
1576
  reset: () => void;
1560
1577
  };
1578
+ /** Maximum automatic startup wait across grants; successful room + agent readiness ends it. */
1579
+ declare const DEFAULT_READY_TIMEOUT_SECONDS = 30;
1561
1580
  declare function useSessionLifecycle<T extends LLMProvider = LLMProvider>(input: UseSessionLifecycleInput<T>): SessionLifecycleApi;
1562
1581
  /**
1563
1582
  * The optional realtime-session sinks the bridge ALSO fills when given the richer
@@ -2026,4 +2045,4 @@ interface ProxyClientOptions {
2026
2045
  }
2027
2046
  declare function createProxyClient(options: ProxyClientOptions): AvatarSessionClient;
2028
2047
 
2029
- export { type ProxyClientOptions as $, type AvatarVideoFit as A, type BehaviorSnapshot as B, type CallTranscript as C, DEFAULT_APPROACHING_END_LEAD_SECONDS as D, type EndReason as E, type FishTtsModel as F, type Governor as G, type GovernorSignal as H, type GovernorState as I, type GraceWindowClosedEvent as J, type GraceWindowOpenEvent as K, type LiveKitSessionGrant as L, type GraceWindowState as M, type IdleWarningEvent as N, type InboundRtpCursor as O, type InboundRtpReading as P, type KnownBehaviorState as Q, type LLMProvider as R, type LLMSelection as S, type LiveKitAvatarGrantState as T, type LiveKitAvatarGrantStatus as U, type LiveKitCapacityState as V, type LiveKitConnectionStatus as W, type LiveKitSessionRequest as X, type LiveKitSessionStartResult as Y, MAX_RECONNECT_ATTEMPTS as Z, MAX_SESSION_INSTRUCTIONS_CHARS as _, AdaptivePlayoutController as a, RealtimeAvatarCapacityError as a$, type QualityCap as a0, RECONNECT_BACKOFF_MS as a1, type RealtimeAvatarRequestOptions as a2, type RealtimeSessionApi as a3, type RealtimeSessionMedia as a4, type RealtimeSessionRoomSinks as a5, type ReconnectPolicy as a6, type ReconnectingEvent as a7, type RecoveryState as a8, type RetryStep as a9, mapTurnState as aA, readInboundRtp as aB, sessionBehaviorSchema as aC, sessionClipSchema as aD, splitCallTranscript as aE, useAvatarAdaptivePlayoutDelay as aF, useAvatarCamera as aG, useAvatarPlayoutDelay as aH, useAvatarQualityGovernor as aI, useCallTranscript as aJ, useLiveKitAvatarGrant as aK, useLiveTrackProducing as aL, useMicLease as aM, useRealtimeSession as aN, useReleaseMicLeaseOnTrackEnded as aO, useSessionLifecycle as aP, type CallMicrophone as aQ, type CallAudioPlayback as aR, type CallMicrophoneProblem as aS, AvatarVideoSurface as aT, type AvatarVideoSurfaceProps as aU, type LivePlaybackKeeper as aV, MIC_LEASE_ENDED_TIMEOUT_MS as aW, type MicLease as aX, NETWORK_QUALITY_POLICY as aY, type NetworkQualityStatus as aZ, type PlayableVideoElement as a_, type SendTurnOptions as aa, type SessionBehavior as ab, type SessionClip as ac, type SessionClocks as ad, type SessionEndReason as ae, type SessionLifecycleApi as af, type SessionLifecyclePhase as ag, type SessionLifecyclePhaseKind as ah, SessionLifecycleRoomBridge as ai, type SessionLifecycleRoomBridgeProps as aj, type SurfaceLayers as ak, type TranscriptSender as al, type TranscriptSenderOptions as am, type TurnState as an, type TurnTimeoutEvent as ao, type UseAvatarQualityGovernorInput as ap, type UseLiveKitAvatarGrantInput as aq, type UseRealtimeSessionInput as ar, type UseSessionLifecycleInput as as, type VoiceSpec as at, type VoiceSpecInput as au, capacityErrorFromBusy as av, capacityStateFromGrant as aw, createProxyClient as ax, isNativeLiveTrackSubscribed as ay, knownBehaviorStates as az, type AdaptivePlayoutDecision as b, RealtimeAvatarLiveKitRoom as b0, type RealtimeAvatarLiveKitRoomProps as b1, shouldReplayPendingTurn as b2, type AdaptivePlayoutOptions as c, type AdaptivePlayoutSample as d, type ApproachingEndEvent as e, type ApproachingEndReason as f, type AvatarConnectionDetails as g, type AvatarSessionClient as h, type CallTranscriptSegment as i, type CartesiaTtsModel as j, type ClipResult as k, type ClosingTurnResult as l, type CreditsLowEvent as m, DEFAULT_CREDITS_LOW_LEAD_SECONDS as n, DEFAULT_GOVERNOR_CONFIG as o, DEFAULT_GRACE_CEILING_SECONDS as p, DEFAULT_GRACE_WINDOW_LEAD_SECONDS as q, DEFAULT_IDLE_SECONDS as r, DEFAULT_IDLE_WARN_LEAD_SECONDS as s, DEFAULT_TURN_TIMEOUT_SECONDS as t, type DeclaredInputSource as u, type EndedEvent as v, type ExtendResult as w, type FreezeReadingFn as x, type GovernorAction as y, type GovernorConfig as z };
2048
+ export { MAX_SESSION_INSTRUCTIONS_CHARS as $, type AvatarVideoFit as A, type BehaviorSnapshot as B, type CallTranscript as C, DEFAULT_APPROACHING_END_LEAD_SECONDS as D, type EndReason as E, type FishTtsModel as F, type Governor as G, type GovernorConfig as H, type GovernorSignal as I, type GovernorState as J, type GraceWindowClosedEvent as K, type LiveKitSessionGrant as L, type GraceWindowOpenEvent as M, type GraceWindowState as N, type IdleWarningEvent as O, type InboundRtpCursor as P, type InboundRtpReading as Q, type KnownBehaviorState as R, type LLMProvider as S, type LLMSelection as T, type LiveKitAvatarGrantState as U, type LiveKitAvatarGrantStatus as V, type LiveKitCapacityState as W, type LiveKitConnectionStatus as X, type LiveKitSessionRequest as Y, type LiveKitSessionStartResult as Z, MAX_RECONNECT_ATTEMPTS as _, AdaptivePlayoutController as a, type NetworkQualityStatus as a$, type ProxyClientOptions as a0, type QualityCap as a1, RECONNECT_BACKOFF_MS as a2, type RealtimeAvatarRequestOptions as a3, type RealtimeSessionApi as a4, type RealtimeSessionMedia as a5, type RealtimeSessionRoomSinks as a6, type ReconnectPolicy as a7, type ReconnectingEvent as a8, type RecoveryState as a9, isNativeLiveTrackSubscribed as aA, knownBehaviorStates as aB, mapTurnState as aC, readInboundRtp as aD, sessionBehaviorSchema as aE, sessionClipSchema as aF, splitCallTranscript as aG, useAvatarAdaptivePlayoutDelay as aH, useAvatarCamera as aI, useAvatarPlayoutDelay as aJ, useAvatarQualityGovernor as aK, useCallTranscript as aL, useLiveKitAvatarGrant as aM, useLiveTrackProducing as aN, useMicLease as aO, useRealtimeSession as aP, useReleaseMicLeaseOnTrackEnded as aQ, useSessionLifecycle as aR, type CallMicrophone as aS, type CallAudioPlayback as aT, type CallMicrophoneProblem as aU, AvatarVideoSurface as aV, type AvatarVideoSurfaceProps as aW, type LivePlaybackKeeper as aX, MIC_LEASE_ENDED_TIMEOUT_MS as aY, type MicLease as aZ, NETWORK_QUALITY_POLICY as a_, type RetryStep as aa, type SendTurnOptions as ab, type SessionBehavior as ac, type SessionClip as ad, type SessionClocks as ae, type SessionEndReason as af, type SessionLifecycleApi as ag, type SessionLifecyclePhase as ah, type SessionLifecyclePhaseKind as ai, SessionLifecycleRoomBridge as aj, type SessionLifecycleRoomBridgeProps as ak, type SessionReadinessFailure as al, type SurfaceLayers as am, type TranscriptSender as an, type TranscriptSenderOptions as ao, type TurnState as ap, type TurnTimeoutEvent as aq, type UseAvatarQualityGovernorInput as ar, type UseLiveKitAvatarGrantInput as as, type UseRealtimeSessionInput as at, type UseSessionLifecycleInput as au, type VoiceSpec as av, type VoiceSpecInput as aw, capacityErrorFromBusy as ax, capacityStateFromGrant as ay, createProxyClient as az, type AdaptivePlayoutDecision as b, type PlayableVideoElement as b0, RealtimeAvatarCapacityError as b1, RealtimeAvatarLiveKitRoom as b2, type RealtimeAvatarLiveKitRoomProps as b3, shouldReplayPendingTurn as b4, type AdaptivePlayoutOptions as c, type AdaptivePlayoutSample as d, type ApproachingEndEvent as e, type ApproachingEndReason as f, type AvatarConnectionDetails as g, type AvatarSessionClient as h, type CallTranscriptSegment as i, type CartesiaTtsModel as j, type ClipResult as k, type ClosingTurnResult as l, type CreditsLowEvent as m, DEFAULT_CREDITS_LOW_LEAD_SECONDS as n, DEFAULT_GOVERNOR_CONFIG as o, DEFAULT_GRACE_CEILING_SECONDS as p, DEFAULT_GRACE_WINDOW_LEAD_SECONDS as q, DEFAULT_IDLE_SECONDS as r, DEFAULT_IDLE_WARN_LEAD_SECONDS as s, DEFAULT_READY_TIMEOUT_SECONDS as t, DEFAULT_TURN_TIMEOUT_SECONDS as u, type DeclaredInputSource as v, type EndedEvent as w, type ExtendResult as x, type FreezeReadingFn as y, type GovernorAction as z };
@@ -1,7 +1,7 @@
1
1
  import { LiveKitRoomProps } from '@livekit/react-native';
2
2
  export { AudioSession, VideoTrack, VideoTrackProps, registerGlobals } from '@livekit/react-native';
3
- import { L as LiveKitSessionGrant, A as AvatarVideoFit } from './proxy-client-D75TLU5X.js';
4
- export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarConnectionDetails, h as AvatarSessionClient, B as BehaviorSnapshot, C as CallTranscript, i as CallTranscriptSegment, j as CartesiaTtsModel, k as ClipResult, l as ClosingTurnResult, m as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, n as DEFAULT_CREDITS_LOW_LEAD_SECONDS, o as DEFAULT_GOVERNOR_CONFIG, p as DEFAULT_GRACE_CEILING_SECONDS, q as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, r as DEFAULT_IDLE_SECONDS, s as DEFAULT_IDLE_WARN_LEAD_SECONDS, t as DEFAULT_TURN_TIMEOUT_SECONDS, u as DeclaredInputSource, E as EndReason, v as EndedEvent, w as ExtendResult, F as FishTtsModel, x as FreezeReadingFn, G as Governor, y as GovernorAction, z as GovernorConfig, H as GovernorSignal, I as GovernorState, J as GraceWindowClosedEvent, K as GraceWindowOpenEvent, M as GraceWindowState, N as IdleWarningEvent, O as InboundRtpCursor, P as InboundRtpReading, Q as KnownBehaviorState, R as LLMProvider, S as LLMSelection, T as LiveKitAvatarGrantState, U as LiveKitAvatarGrantStatus, V as LiveKitCapacityState, W as LiveKitConnectionStatus, X as LiveKitSessionRequest, Y as LiveKitSessionStartResult, Z as MAX_RECONNECT_ATTEMPTS, _ as MAX_SESSION_INSTRUCTIONS_CHARS, $ as ProxyClientOptions, a0 as QualityCap, a1 as RECONNECT_BACKOFF_MS, a2 as RealtimeAvatarRequestOptions, a3 as RealtimeSessionApi, a4 as RealtimeSessionMedia, a5 as RealtimeSessionRoomSinks, a6 as ReconnectPolicy, a7 as ReconnectingEvent, a8 as RecoveryState, a9 as RetryStep, aa as SendTurnOptions, ab as SessionBehavior, ac as SessionClip, ad as SessionClocks, ae as SessionEndReason, af as SessionLifecycleApi, ag as SessionLifecyclePhase, ah as SessionLifecyclePhaseKind, ai as SessionLifecycleRoomBridge, aj as SessionLifecycleRoomBridgeProps, ak as SurfaceLayers, al as TranscriptSender, am as TranscriptSenderOptions, an as TurnState, ao as TurnTimeoutEvent, ap as UseAvatarQualityGovernorInput, aq as UseLiveKitAvatarGrantInput, ar as UseRealtimeSessionInput, as as UseSessionLifecycleInput, at as VoiceSpec, au as VoiceSpecInput, av as capacityErrorFromBusy, aw as capacityStateFromGrant, ax as createProxyClient, ay as isNativeLiveTrackSubscribed, az as knownBehaviorStates, aA as mapTurnState, aB as readInboundRtp, aC as sessionBehaviorSchema, aD as sessionClipSchema, aE as splitCallTranscript, aF as useAvatarAdaptivePlayoutDelay, aG as useAvatarCamera, aH as useAvatarPlayoutDelay, aI as useAvatarQualityGovernor, aJ as useCallTranscript, aK as useLiveKitAvatarGrant, aL as useLiveTrackProducing, aM as useMicLease, aN as useRealtimeSession, aO as useReleaseMicLeaseOnTrackEnded, aP as useSessionLifecycle } from './proxy-client-D75TLU5X.js';
3
+ import { L as LiveKitSessionGrant, A as AvatarVideoFit } from './proxy-client-Cs2azqD1.js';
4
+ export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarConnectionDetails, h as AvatarSessionClient, B as BehaviorSnapshot, C as CallTranscript, i as CallTranscriptSegment, j as CartesiaTtsModel, k as ClipResult, l as ClosingTurnResult, m as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, n as DEFAULT_CREDITS_LOW_LEAD_SECONDS, o as DEFAULT_GOVERNOR_CONFIG, p as DEFAULT_GRACE_CEILING_SECONDS, q as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, r as DEFAULT_IDLE_SECONDS, s as DEFAULT_IDLE_WARN_LEAD_SECONDS, t as DEFAULT_READY_TIMEOUT_SECONDS, u as DEFAULT_TURN_TIMEOUT_SECONDS, v as DeclaredInputSource, E as EndReason, w as EndedEvent, x as ExtendResult, F as FishTtsModel, y as FreezeReadingFn, G as Governor, z as GovernorAction, H as GovernorConfig, I as GovernorSignal, J as GovernorState, K as GraceWindowClosedEvent, M as GraceWindowOpenEvent, N as GraceWindowState, O as IdleWarningEvent, P as InboundRtpCursor, Q as InboundRtpReading, R as KnownBehaviorState, S as LLMProvider, T as LLMSelection, U as LiveKitAvatarGrantState, V as LiveKitAvatarGrantStatus, W as LiveKitCapacityState, X as LiveKitConnectionStatus, Y as LiveKitSessionRequest, Z as LiveKitSessionStartResult, _ as MAX_RECONNECT_ATTEMPTS, $ as MAX_SESSION_INSTRUCTIONS_CHARS, a0 as ProxyClientOptions, a1 as QualityCap, a2 as RECONNECT_BACKOFF_MS, a3 as RealtimeAvatarRequestOptions, a4 as RealtimeSessionApi, a5 as RealtimeSessionMedia, a6 as RealtimeSessionRoomSinks, a7 as ReconnectPolicy, a8 as ReconnectingEvent, a9 as RecoveryState, aa as RetryStep, ab as SendTurnOptions, ac as SessionBehavior, ad as SessionClip, ae as SessionClocks, af as SessionEndReason, ag as SessionLifecycleApi, ah as SessionLifecyclePhase, ai as SessionLifecyclePhaseKind, aj as SessionLifecycleRoomBridge, ak as SessionLifecycleRoomBridgeProps, al as SessionReadinessFailure, am as SurfaceLayers, an as TranscriptSender, ao as TranscriptSenderOptions, ap as TurnState, aq as TurnTimeoutEvent, ar as UseAvatarQualityGovernorInput, as as UseLiveKitAvatarGrantInput, at as UseRealtimeSessionInput, au as UseSessionLifecycleInput, av as VoiceSpec, aw as VoiceSpecInput, ax as capacityErrorFromBusy, ay as capacityStateFromGrant, az as createProxyClient, aA as isNativeLiveTrackSubscribed, aB as knownBehaviorStates, aC as mapTurnState, aD as readInboundRtp, aE as sessionBehaviorSchema, aF as sessionClipSchema, aG as splitCallTranscript, aH as useAvatarAdaptivePlayoutDelay, aI as useAvatarCamera, aJ as useAvatarPlayoutDelay, aK as useAvatarQualityGovernor, aL as useCallTranscript, aM as useLiveKitAvatarGrant, aN as useLiveTrackProducing, aO as useMicLease, aP as useRealtimeSession, aQ as useReleaseMicLeaseOnTrackEnded, aR as useSessionLifecycle } from './proxy-client-Cs2azqD1.js';
5
5
  import { ReactNode, ReactElement } from 'react';
6
6
  import { StyleProp, ViewStyle } from 'react-native';
7
7
  export { useChat, useConnectionState, useLocalParticipant, useRoomContext, useTrackToggle, useTranscriptions, useVoiceAssistant } from '@livekit/components-react';
@@ -177,6 +177,8 @@ function RealtimeAvatarLiveKitRoom(props) {
177
177
  LiveKitRoom,
178
178
  {
179
179
  ...roomProps,
180
+ // Keep the room and its in-room observers scoped to the grant, as on web.
181
+ key: grant?.session_id ?? "no-session",
180
182
  serverUrl: grant?.livekit_url,
181
183
  token: grant?.participant_token,
182
184
  connect: shouldConnect,
@@ -2395,7 +2397,8 @@ function lifecyclePhaseFrom(args) {
2395
2397
  kind: "reconnectable",
2396
2398
  reconnecting: true,
2397
2399
  attempt: recovery.attempt,
2398
- strategy: "fresh-grant"
2400
+ strategy: "fresh-grant",
2401
+ ...recovery.reason ? { reason: recovery.reason } : {}
2399
2402
  };
2400
2403
  }
2401
2404
  if (recovery.kind === "failed") {
@@ -2403,11 +2406,16 @@ function lifecyclePhaseFrom(args) {
2403
2406
  kind: "reconnectable",
2404
2407
  reconnecting: false,
2405
2408
  attempt: MAX_RECONNECT_ATTEMPTS,
2406
- strategy: "fresh-grant"
2409
+ strategy: "fresh-grant",
2410
+ ...recovery.reason ? { reason: recovery.reason } : {}
2407
2411
  };
2408
2412
  }
2409
2413
  if (recovery.kind === "ended") {
2410
- return recovery.reason ? { kind: "ended", reason: recovery.reason } : { kind: "ended" };
2414
+ return {
2415
+ kind: "ended",
2416
+ ...recovery.reason ? { reason: recovery.reason } : {},
2417
+ ...recovery.code ? { code: recovery.code } : {}
2418
+ };
2411
2419
  }
2412
2420
  switch (capacity.kind) {
2413
2421
  case "idle":
@@ -2420,7 +2428,7 @@ function lifecyclePhaseFrom(args) {
2420
2428
  return { kind: "ended", reason: "error" };
2421
2429
  case "active": {
2422
2430
  if (!connected || !agentPresent) {
2423
- return { kind: "connecting", grant: capacity.grant };
2431
+ return { kind: "connecting", grant: capacity.grant, waitingFor: connected ? "agent" : "transport" };
2424
2432
  }
2425
2433
  if (idle.kind === "idle-warning") {
2426
2434
  return { kind: "idle-warning", secondsRemaining: idle.secondsRemaining, deadlineAt: idle.deadlineAt };
@@ -2448,6 +2456,7 @@ function resolveIdleTimeoutMs(args) {
2448
2456
  return DEFAULT_IDLE_SECONDS * 1e3;
2449
2457
  }
2450
2458
  var DEFAULT_CONNECT_WATCHDOG_SECONDS = 12;
2459
+ var DEFAULT_READY_TIMEOUT_SECONDS = 30;
2451
2460
  function resolveWarnBeforeMs(idleTimeoutMs, override, idleWarnLeadSeconds) {
2452
2461
  if (idleTimeoutMs <= 0) return 0;
2453
2462
  const maxLeadMs = Math.max(0, idleTimeoutMs - 1e3);
@@ -2473,6 +2482,7 @@ function useSessionLifecycle(input) {
2473
2482
  maxReconnectAttempts,
2474
2483
  autoRetryBusy = true,
2475
2484
  connectWatchdogSeconds,
2485
+ readyTimeoutSeconds,
2476
2486
  requestOptions,
2477
2487
  onBehaviorChange
2478
2488
  } = input;
@@ -2483,17 +2493,27 @@ function useSessionLifecycle(input) {
2483
2493
  const reconnectPolicyRef = useRef(reconnectPolicy);
2484
2494
  reconnectPolicyRef.current = reconnectPolicy;
2485
2495
  const connectWatchdogMs = typeof connectWatchdogSeconds === "number" && Number.isFinite(connectWatchdogSeconds) ? Math.max(0, Math.floor(connectWatchdogSeconds)) * 1e3 : DEFAULT_CONNECT_WATCHDOG_SECONDS * 1e3;
2496
+ const readyTimeoutMs = typeof readyTimeoutSeconds === "number" && Number.isFinite(readyTimeoutSeconds) && readyTimeoutSeconds > 0 ? Math.max(1, Math.floor(readyTimeoutSeconds)) * 1e3 : DEFAULT_READY_TIMEOUT_SECONDS * 1e3;
2486
2497
  const [userEnded, setUserEnded] = useState(false);
2487
2498
  const userEndedRef = useRef(false);
2499
+ const [readinessFailed, setReadinessFailed] = useState(false);
2500
+ const readinessFailureRef = useRef(null);
2501
+ const readinessRef = useRef(null);
2502
+ const readyGrantRef = useRef(null);
2503
+ const agentWaitRef = useRef(null);
2488
2504
  const phaseRef = useRef({ kind: "idle" });
2489
2505
  const grantState = useLiveKitAvatarGrant({
2490
2506
  client,
2491
2507
  session,
2492
- active: active && !userEnded,
2508
+ active: active && !userEnded && !readinessFailed,
2493
2509
  autoRetryBusy,
2494
2510
  requestOptions
2495
2511
  });
2496
2512
  const capacity = grantState.capacity;
2513
+ const grantId = grantState.grant?.session_id ?? null;
2514
+ const grantIdRef = useRef(null);
2515
+ grantIdRef.current = grantId;
2516
+ const retiredGrantRef = useRef(null);
2497
2517
  const [recovery, setRecovery] = useState({ kind: "connected" });
2498
2518
  const recoveryRef = useRef(recovery);
2499
2519
  recoveryRef.current = recovery;
@@ -2516,8 +2536,19 @@ function useSessionLifecycle(input) {
2516
2536
  grantIdleSeconds: grantState.grant?.idle_timeout_seconds ?? 0
2517
2537
  });
2518
2538
  const warnAtMs = resolveWarnBeforeMs(idleTimeoutMs, warnBeforeMs, idleWarnLeadSeconds);
2519
- const [agentPresent, setAgentPresentState] = useState(false);
2520
- const [connected, setConnected] = useState(false);
2539
+ const [agentObservation, setAgentObservation] = useState({ grantId: null, present: false });
2540
+ const [connectionObservation, setConnectionObservation] = useState({ grantId: null, connected: false });
2541
+ const agentPresent = agentObservation.grantId === grantId && agentObservation.present;
2542
+ const connected = connectionObservation.grantId === grantId && connectionObservation.connected;
2543
+ connectedRef.current = connected;
2544
+ const setAgentPresentState = useCallback((present) => {
2545
+ const owner = grantIdRef.current;
2546
+ setAgentObservation((prev) => prev.grantId === owner && prev.present === present ? prev : { grantId: owner, present });
2547
+ }, []);
2548
+ const setConnected = useCallback((value) => {
2549
+ const owner = grantIdRef.current;
2550
+ setConnectionObservation((prev) => prev.grantId === owner && prev.connected === value ? prev : { grantId: owner, connected: value });
2551
+ }, []);
2521
2552
  const lastActivityRef = useRef(Date.now());
2522
2553
  const leaveRoomRef = useRef(null);
2523
2554
  const [, setClockTick] = useState(0);
@@ -2534,7 +2565,11 @@ function useSessionLifecycle(input) {
2534
2565
  refreshBaselineCapacityRef.current = null;
2535
2566
  attemptRef.current = 0;
2536
2567
  setAttempt(0);
2537
- setRecovery(userEndedRef.current ? { kind: "ended", reason: "user" } : { kind: "connected" });
2568
+ readinessRef.current = null;
2569
+ readyGrantRef.current = null;
2570
+ agentWaitRef.current = null;
2571
+ retiredGrantRef.current = null;
2572
+ setRecovery(userEndedRef.current ? { kind: "ended", reason: "user" } : readinessFailureRef.current ? { kind: "ended", reason: "error", code: readinessFailureRef.current } : { kind: "connected" });
2538
2573
  setConnected(false);
2539
2574
  setAgentPresentState(false);
2540
2575
  lastActivityRef.current = Date.now();
@@ -2543,26 +2578,33 @@ function useSessionLifecycle(input) {
2543
2578
  if (active) return;
2544
2579
  userEndedRef.current = false;
2545
2580
  setUserEnded(false);
2581
+ readinessFailureRef.current = null;
2582
+ setReadinessFailed(false);
2546
2583
  reset();
2547
2584
  }, [active, reset]);
2548
2585
  const markActivity = useCallback(() => {
2586
+ if (grantId === null || grantIdRef.current !== grantId) return;
2549
2587
  lastActivityRef.current = Date.now();
2550
2588
  setClockTick((value) => value + 1);
2551
- }, []);
2589
+ }, [grantId]);
2552
2590
  const stayConnected = useCallback(() => {
2591
+ if (grantId === null || grantIdRef.current !== grantId) return;
2553
2592
  if (!connectedRef.current) return;
2554
2593
  lastActivityRef.current = Date.now();
2555
2594
  setClockTick((value) => value + 1);
2556
- }, []);
2595
+ }, [grantId]);
2557
2596
  const setAgentPresent = useCallback((present) => {
2558
- setAgentPresentState((prev) => {
2559
- if (present && !prev) lastActivityRef.current = Date.now();
2560
- return present;
2597
+ if (grantId === null || grantIdRef.current !== grantId) return;
2598
+ if (userEndedRef.current || readinessFailureRef.current || retiredGrantRef.current !== null && retiredGrantRef.current === grantIdRef.current) return;
2599
+ setAgentObservation((prev) => {
2600
+ if (present && (prev.grantId !== grantId || !prev.present)) lastActivityRef.current = Date.now();
2601
+ return prev.grantId === grantId && prev.present === present ? prev : { grantId, present };
2561
2602
  });
2562
- }, []);
2603
+ }, [grantId]);
2563
2604
  const registerLeaveRoom = useCallback((leave) => {
2605
+ if (grantId === null || grantIdRef.current !== grantId) return;
2564
2606
  leaveRoomRef.current = leave;
2565
- }, []);
2607
+ }, [grantId]);
2566
2608
  const endIdle = useCallback(() => {
2567
2609
  if (!connectedRef.current) return;
2568
2610
  clearTimer();
@@ -2574,24 +2616,25 @@ function useSessionLifecycle(input) {
2574
2616
  leaveRoomRef.current?.();
2575
2617
  }, [clearTimer]);
2576
2618
  const onConnected = useCallback(() => {
2577
- if (userEndedRef.current) return;
2619
+ if (grantId === null || grantIdRef.current !== grantId) return;
2620
+ if (userEndedRef.current || readinessFailureRef.current || retiredGrantRef.current !== null && retiredGrantRef.current === grantIdRef.current) return;
2578
2621
  clearTimer();
2579
2622
  connectedRef.current = true;
2580
2623
  manualReconnectPendingRef.current = false;
2581
2624
  refreshBaselineCapacityRef.current = null;
2582
2625
  setConnected(true);
2583
- attemptRef.current = 0;
2584
- setAttempt(0);
2626
+ if (readinessRef.current) readinessRef.current.sawTransport = true;
2585
2627
  setRecovery({ kind: "connected" });
2586
2628
  lastActivityRef.current = Date.now();
2587
- }, [clearTimer]);
2629
+ }, [clearTimer, grantId]);
2588
2630
  const onDisconnected = useCallback(
2589
2631
  (reason) => {
2590
- if (!active || userEndedRef.current) return;
2632
+ if (grantId === null || grantIdRef.current !== grantId) return;
2633
+ if (!active || userEndedRef.current || readinessFailureRef.current) return;
2591
2634
  const action = disconnectAction(reason, connectedRef.current);
2592
2635
  if (action === "noop") return;
2593
2636
  if (action === "reset") {
2594
- if (recoveryRef.current.kind === "refreshing") return;
2637
+ if (recoveryRef.current.kind === "refreshing" || recoveryRef.current.kind === "reconnecting") return;
2595
2638
  reset();
2596
2639
  return;
2597
2640
  }
@@ -2608,11 +2651,12 @@ function useSessionLifecycle(input) {
2608
2651
  refreshBaselineCapacityRef.current = null;
2609
2652
  setRecovery({ kind: "reconnecting", attempt: attemptRef.current });
2610
2653
  },
2611
- [active, reset]
2654
+ [active, reset, grantId]
2612
2655
  );
2613
2656
  const onConnectionError = useCallback(
2614
2657
  (error) => {
2615
- if (!active || userEndedRef.current || !isRecoverableConnectionError(error)) return;
2658
+ if (grantId === null || grantIdRef.current !== grantId) return;
2659
+ if (!active || userEndedRef.current || readinessFailureRef.current || !isRecoverableConnectionError(error)) return;
2616
2660
  connectedRef.current = false;
2617
2661
  setConnected(false);
2618
2662
  setAgentPresentState(false);
@@ -2623,7 +2667,7 @@ function useSessionLifecycle(input) {
2623
2667
  return { kind: "reconnecting", attempt: attemptRef.current };
2624
2668
  });
2625
2669
  },
2626
- [active]
2670
+ [active, grantId]
2627
2671
  );
2628
2672
  useEffect(() => {
2629
2673
  if (capacity.kind === "error" && recovery.kind === "connected") manualReconnectPendingRef.current = false;
@@ -2633,11 +2677,12 @@ function useSessionLifecycle(input) {
2633
2677
  const baseline = refreshBaselineCapacityRef.current;
2634
2678
  if (baseline?.kind === "error" && baseline.error === capacity.error) return;
2635
2679
  refreshBaselineCapacityRef.current = null;
2636
- setRecovery({ kind: "reconnecting", attempt: recovery.attempt });
2680
+ setRecovery({ kind: "reconnecting", attempt: recovery.attempt, ...recovery.reason ? { reason: recovery.reason } : {} });
2637
2681
  }, [capacity, recovery]);
2638
2682
  const onConnectionStateChange = useCallback(
2639
2683
  (state) => {
2640
- if (!active || userEndedRef.current) return;
2684
+ if (grantId === null || grantIdRef.current !== grantId) return;
2685
+ if (!active || userEndedRef.current || readinessFailureRef.current) return;
2641
2686
  if (state === "connected") {
2642
2687
  onConnected();
2643
2688
  return;
@@ -2651,14 +2696,78 @@ function useSessionLifecycle(input) {
2651
2696
  }
2652
2697
  }
2653
2698
  },
2654
- [active, onConnected]
2699
+ [active, onConnected, grantId]
2655
2700
  );
2701
+ useEffect(() => {
2702
+ if (!active || userEnded || readinessFailed || !connected || !agentPresent || !grantState.grant) return;
2703
+ readyGrantRef.current = grantState.grant.session_id;
2704
+ readinessRef.current = null;
2705
+ agentWaitRef.current = null;
2706
+ attemptRef.current = 0;
2707
+ setAttempt(0);
2708
+ }, [active, userEnded, readinessFailed, connected, agentPresent, grantState.grant]);
2709
+ const failReadiness = useCallback((code) => {
2710
+ if (userEndedRef.current || readinessFailureRef.current) return;
2711
+ readinessFailureRef.current = code;
2712
+ clearTimer();
2713
+ connectedRef.current = false;
2714
+ manualReconnectPendingRef.current = false;
2715
+ refreshBaselineCapacityRef.current = null;
2716
+ const ended = { kind: "ended", reason: "error", code };
2717
+ recoveryRef.current = ended;
2718
+ setRecovery(ended);
2719
+ setConnected(false);
2720
+ setAgentPresentState(false);
2721
+ releaseRef.current("disconnected");
2722
+ setReadinessFailed(true);
2723
+ leaveRoomRef.current?.();
2724
+ }, [clearTimer]);
2725
+ useEffect(() => {
2726
+ if (!active || userEnded || readinessFailed) return;
2727
+ if (recovery.kind === "ended" || recovery.kind === "failed") return;
2728
+ if (grantState.grant && readyGrantRef.current === grantState.grant.session_id) return;
2729
+ if (!readinessRef.current && grantState.grant) {
2730
+ readinessRef.current = { startedAt: Date.now(), sawTransport: connected };
2731
+ }
2732
+ const budget = readinessRef.current;
2733
+ if (!budget) return;
2734
+ const timer = window.setTimeout(() => {
2735
+ if (readinessRef.current !== budget) return;
2736
+ failReadiness(budget.sawTransport ? "agent_timeout" : "connection_timeout");
2737
+ }, Math.max(0, budget.startedAt + readyTimeoutMs - Date.now()));
2738
+ return () => window.clearTimeout(timer);
2739
+ }, [active, userEnded, readinessFailed, connected, agentPresent, grantState.grant, recovery.kind, readyTimeoutMs, failReadiness]);
2740
+ useEffect(() => {
2741
+ const sessionId = grantState.grant?.session_id;
2742
+ if (!active || userEnded || readinessFailed || !sessionId || readyGrantRef.current === sessionId || agentPresent) return;
2743
+ if (recovery.kind === "reconnecting" || recovery.kind === "failed" || recovery.kind === "ended") return;
2744
+ if (agentWaitRef.current?.sessionId !== sessionId) {
2745
+ if (!connected) return;
2746
+ agentWaitRef.current = { sessionId, startedAt: Date.now() };
2747
+ }
2748
+ const wait = agentWaitRef.current;
2749
+ if (connectWatchdogMs <= 0) return;
2750
+ const timer = window.setTimeout(() => {
2751
+ if (agentWaitRef.current !== wait || readinessFailureRef.current || userEndedRef.current) return;
2752
+ retiredGrantRef.current = sessionId;
2753
+ connectedRef.current = false;
2754
+ setConnected(false);
2755
+ setAgentPresentState(false);
2756
+ const next = { kind: "reconnecting", attempt: attemptRef.current, reason: "agent_timeout" };
2757
+ recoveryRef.current = next;
2758
+ setRecovery(next);
2759
+ releaseRef.current("disconnected");
2760
+ leaveRoomRef.current?.();
2761
+ }, Math.max(0, wait.startedAt + connectWatchdogMs - Date.now()));
2762
+ return () => window.clearTimeout(timer);
2763
+ }, [active, userEnded, readinessFailed, connected, agentPresent, grantState.grant, recovery.kind, connectWatchdogMs]);
2656
2764
  useEffect(() => {
2657
2765
  if (!active || connected || connectWatchdogMs <= 0) return;
2658
2766
  if (grantState.status !== "ready" || !grantState.grant) return;
2659
2767
  if (recovery.kind === "failed" || recovery.kind === "ended") return;
2660
2768
  if (recovery.kind === "in-place-reconnecting") return;
2661
2769
  const timer = window.setTimeout(() => {
2770
+ if (readinessFailureRef.current || userEndedRef.current) return;
2662
2771
  releaseRef.current("disconnected");
2663
2772
  setRecovery({ kind: "reconnecting", attempt: attemptRef.current });
2664
2773
  }, connectWatchdogMs);
@@ -2668,6 +2777,10 @@ function useSessionLifecycle(input) {
2668
2777
  if (!active || !needsFreshGrant(recovery)) return;
2669
2778
  const step2 = retryStep(attemptRef.current, reconnectPolicyRef.current);
2670
2779
  if (step2.kind === "give-up") {
2780
+ if (readinessRef.current) {
2781
+ failReadiness(readinessRef.current.sawTransport ? "agent_timeout" : "connection_timeout");
2782
+ return;
2783
+ }
2671
2784
  manualReconnectPendingRef.current = false;
2672
2785
  refreshBaselineCapacityRef.current = null;
2673
2786
  releaseRef.current("disconnected");
@@ -2676,15 +2789,16 @@ function useSessionLifecycle(input) {
2676
2789
  }
2677
2790
  timerRef.current = window.setTimeout(() => {
2678
2791
  timerRef.current = null;
2792
+ if (readinessFailureRef.current || userEndedRef.current) return;
2679
2793
  attemptRef.current = step2.attempt;
2680
2794
  setAttempt(step2.attempt);
2681
2795
  refreshBaselineCapacityRef.current = capacityRef.current;
2682
2796
  releaseRef.current("superseded");
2683
- setRecovery({ kind: "refreshing", attempt: step2.attempt });
2797
+ setRecovery({ kind: "refreshing", attempt: step2.attempt, ...recovery.reason ? { reason: recovery.reason } : {} });
2684
2798
  refreshRef.current();
2685
2799
  }, step2.delayMs);
2686
2800
  return clearTimer;
2687
- }, [recovery.kind, active, attempt, clearTimer]);
2801
+ }, [recovery.kind, active, attempt, clearTimer, failReadiness]);
2688
2802
  const end = useCallback(() => {
2689
2803
  if (!active || userEndedRef.current || phaseRef.current.kind === "ended") return;
2690
2804
  userEndedRef.current = true;
@@ -2701,6 +2815,13 @@ function useSessionLifecycle(input) {
2701
2815
  }, [active, clearTimer]);
2702
2816
  const reconnect = useCallback(() => {
2703
2817
  if (!active) return;
2818
+ if (readinessFailureRef.current) {
2819
+ readinessFailureRef.current = null;
2820
+ setReadinessFailed(false);
2821
+ reset();
2822
+ manualReconnectPendingRef.current = true;
2823
+ return;
2824
+ }
2704
2825
  if (userEndedRef.current) {
2705
2826
  userEndedRef.current = false;
2706
2827
  setUserEnded(false);
@@ -2711,6 +2832,12 @@ function useSessionLifecycle(input) {
2711
2832
  if (manualReconnectPendingRef.current || recovery.kind === "in-place-reconnecting") return;
2712
2833
  manualReconnectPendingRef.current = true;
2713
2834
  clearTimer();
2835
+ readinessRef.current = null;
2836
+ agentWaitRef.current = null;
2837
+ readyGrantRef.current = null;
2838
+ connectedRef.current = false;
2839
+ setConnected(false);
2840
+ setAgentPresentState(false);
2714
2841
  attemptRef.current = 0;
2715
2842
  setAttempt(0);
2716
2843
  refreshBaselineCapacityRef.current = capacityRef.current;
@@ -2830,6 +2957,7 @@ function SessionLifecycleRoomBridge({
2830
2957
  const assistant = useVoiceAssistant();
2831
2958
  const transcriptions = useTranscriptions();
2832
2959
  const room = useRoomContext();
2960
+ const roomMatchesGrant = lifecycle.grant === void 0 || lifecycle.grant !== null && room.name === lifecycle.grant.room_name;
2833
2961
  useCallMedia(room, {
2834
2962
  setMicrophoneFacts,
2835
2963
  setAudioPlayback,
@@ -2983,11 +3111,11 @@ function SessionLifecycleRoomBridge({
2983
3111
  detailsEnabled ? video?.publication : void 0
2984
3112
  ]);
2985
3113
  useEffect(() => {
2986
- onConnectionStateChange(connectionState);
2987
- }, [connectionState, onConnectionStateChange]);
3114
+ if (roomMatchesGrant) onConnectionStateChange(connectionState);
3115
+ }, [connectionState, onConnectionStateChange, roomMatchesGrant]);
2988
3116
  useEffect(() => {
2989
- setAgentPresent(agentPresent);
2990
- }, [agentPresent, setAgentPresent]);
3117
+ if (roomMatchesGrant) setAgentPresent(agentPresent);
3118
+ }, [agentPresent, setAgentPresent, roomMatchesGrant]);
2991
3119
  useEffect(() => {
2992
3120
  const active = isCallActivity({
2993
3121
  assistantState,
@@ -3695,4 +3823,4 @@ function createProxyClient(options) {
3695
3823
  };
3696
3824
  }
3697
3825
 
3698
- export { AdaptivePlayoutController, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, DEFAULT_CREDITS_LOW_LEAD_SECONDS, DEFAULT_GOVERNOR_CONFIG, DEFAULT_GRACE_CEILING_SECONDS, DEFAULT_GRACE_WINDOW_LEAD_SECONDS, DEFAULT_IDLE_SECONDS, DEFAULT_IDLE_WARN_LEAD_SECONDS, DEFAULT_TURN_TIMEOUT_SECONDS, MAX_RECONNECT_ATTEMPTS, MAX_SESSION_INSTRUCTIONS_CHARS, RECONNECT_BACKOFF_MS, RealtimeAvatarLiveKitRoom, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, isNativeLiveTrackSubscribed, knownBehaviorStates, mapTurnState, readInboundRtp, sessionBehaviorSchema, sessionClipSchema, splitCallTranscript, useAvatarAdaptivePlayoutDelay, useAvatarCamera, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useLiveKitAvatarGrant, useLiveTrackProducing, useMicLease, useRealtimeAvatarAudioSession, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
3826
+ export { AdaptivePlayoutController, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, DEFAULT_CREDITS_LOW_LEAD_SECONDS, DEFAULT_GOVERNOR_CONFIG, DEFAULT_GRACE_CEILING_SECONDS, DEFAULT_GRACE_WINDOW_LEAD_SECONDS, DEFAULT_IDLE_SECONDS, DEFAULT_IDLE_WARN_LEAD_SECONDS, DEFAULT_READY_TIMEOUT_SECONDS, DEFAULT_TURN_TIMEOUT_SECONDS, MAX_RECONNECT_ATTEMPTS, MAX_SESSION_INSTRUCTIONS_CHARS, RECONNECT_BACKOFF_MS, RealtimeAvatarLiveKitRoom, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, isNativeLiveTrackSubscribed, knownBehaviorStates, mapTurnState, readInboundRtp, sessionBehaviorSchema, sessionClipSchema, splitCallTranscript, useAvatarAdaptivePlayoutDelay, useAvatarCamera, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useLiveKitAvatarGrant, useLiveTrackProducing, useMicLease, useRealtimeAvatarAudioSession, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
package/dist/react.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ToolRegistration, RegisteredTool } from './tools.js';
2
- import { aj as SessionLifecycleRoomBridgeProps, h as AvatarSessionClient, A as AvatarVideoFit, aQ as CallMicrophone, aR as CallAudioPlayback, aS as CallMicrophoneProblem } from './proxy-client-D75TLU5X.js';
3
- export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarConnectionDetails, aT as AvatarVideoSurface, aU as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, i as CallTranscriptSegment, j as CartesiaTtsModel, k as ClipResult, l as ClosingTurnResult, m as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, n as DEFAULT_CREDITS_LOW_LEAD_SECONDS, o as DEFAULT_GOVERNOR_CONFIG, p as DEFAULT_GRACE_CEILING_SECONDS, q as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, r as DEFAULT_IDLE_SECONDS, s as DEFAULT_IDLE_WARN_LEAD_SECONDS, t as DEFAULT_TURN_TIMEOUT_SECONDS, u as DeclaredInputSource, E as EndReason, v as EndedEvent, w as ExtendResult, F as FishTtsModel, x as FreezeReadingFn, G as Governor, y as GovernorAction, z as GovernorConfig, H as GovernorSignal, I as GovernorState, J as GraceWindowClosedEvent, K as GraceWindowOpenEvent, M as GraceWindowState, N as IdleWarningEvent, O as InboundRtpCursor, P as InboundRtpReading, Q as KnownBehaviorState, R as LLMProvider, S as LLMSelection, T as LiveKitAvatarGrantState, U as LiveKitAvatarGrantStatus, V as LiveKitCapacityState, W as LiveKitConnectionStatus, X as LiveKitSessionRequest, Y as LiveKitSessionStartResult, aV as LivePlaybackKeeper, Z as MAX_RECONNECT_ATTEMPTS, _ as MAX_SESSION_INSTRUCTIONS_CHARS, aW as MIC_LEASE_ENDED_TIMEOUT_MS, aX as MicLease, aY as NETWORK_QUALITY_POLICY, aZ as NetworkQualityStatus, a_ as PlayableVideoElement, $ as ProxyClientOptions, a0 as QualityCap, a1 as RECONNECT_BACKOFF_MS, a$ as RealtimeAvatarCapacityError, b0 as RealtimeAvatarLiveKitRoom, b1 as RealtimeAvatarLiveKitRoomProps, a2 as RealtimeAvatarRequestOptions, a3 as RealtimeSessionApi, a4 as RealtimeSessionMedia, a5 as RealtimeSessionRoomSinks, a6 as ReconnectPolicy, a7 as ReconnectingEvent, a8 as RecoveryState, a9 as RetryStep, aa as SendTurnOptions, ab as SessionBehavior, ac as SessionClip, ad as SessionClocks, ae as SessionEndReason, af as SessionLifecycleApi, ag as SessionLifecyclePhase, ah as SessionLifecyclePhaseKind, ai as SessionLifecycleRoomBridge, ak as SurfaceLayers, al as TranscriptSender, am as TranscriptSenderOptions, an as TurnState, ao as TurnTimeoutEvent, ap as UseAvatarQualityGovernorInput, aq as UseLiveKitAvatarGrantInput, ar as UseRealtimeSessionInput, as as UseSessionLifecycleInput, at as VoiceSpec, au as VoiceSpecInput, av as capacityErrorFromBusy, aw as capacityStateFromGrant, ax as createProxyClient, az as knownBehaviorStates, aA as mapTurnState, aB as readInboundRtp, aC as sessionBehaviorSchema, aD as sessionClipSchema, b2 as shouldReplayPendingTurn, aE as splitCallTranscript, aF as useAvatarAdaptivePlayoutDelay, aG as useAvatarCamera, aH as useAvatarPlayoutDelay, aI as useAvatarQualityGovernor, aJ as useCallTranscript, aK as useLiveKitAvatarGrant, aM as useMicLease, aN as useRealtimeSession, aO as useReleaseMicLeaseOnTrackEnded, aP as useSessionLifecycle } from './proxy-client-D75TLU5X.js';
2
+ import { ak as SessionLifecycleRoomBridgeProps, h as AvatarSessionClient, A as AvatarVideoFit, aS as CallMicrophone, aT as CallAudioPlayback, aU as CallMicrophoneProblem } from './proxy-client-Cs2azqD1.js';
3
+ export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarConnectionDetails, aV as AvatarVideoSurface, aW as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, i as CallTranscriptSegment, j as CartesiaTtsModel, k as ClipResult, l as ClosingTurnResult, m as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, n as DEFAULT_CREDITS_LOW_LEAD_SECONDS, o as DEFAULT_GOVERNOR_CONFIG, p as DEFAULT_GRACE_CEILING_SECONDS, q as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, r as DEFAULT_IDLE_SECONDS, s as DEFAULT_IDLE_WARN_LEAD_SECONDS, t as DEFAULT_READY_TIMEOUT_SECONDS, u as DEFAULT_TURN_TIMEOUT_SECONDS, v as DeclaredInputSource, E as EndReason, w as EndedEvent, x as ExtendResult, F as FishTtsModel, y as FreezeReadingFn, G as Governor, z as GovernorAction, H as GovernorConfig, I as GovernorSignal, J as GovernorState, K as GraceWindowClosedEvent, M as GraceWindowOpenEvent, N as GraceWindowState, O as IdleWarningEvent, P as InboundRtpCursor, Q as InboundRtpReading, R as KnownBehaviorState, S as LLMProvider, T as LLMSelection, U as LiveKitAvatarGrantState, V as LiveKitAvatarGrantStatus, W as LiveKitCapacityState, X as LiveKitConnectionStatus, Y as LiveKitSessionRequest, Z as LiveKitSessionStartResult, aX as LivePlaybackKeeper, _ as MAX_RECONNECT_ATTEMPTS, $ as MAX_SESSION_INSTRUCTIONS_CHARS, aY as MIC_LEASE_ENDED_TIMEOUT_MS, aZ as MicLease, a_ as NETWORK_QUALITY_POLICY, a$ as NetworkQualityStatus, b0 as PlayableVideoElement, a0 as ProxyClientOptions, a1 as QualityCap, a2 as RECONNECT_BACKOFF_MS, b1 as RealtimeAvatarCapacityError, b2 as RealtimeAvatarLiveKitRoom, b3 as RealtimeAvatarLiveKitRoomProps, a3 as RealtimeAvatarRequestOptions, a4 as RealtimeSessionApi, a5 as RealtimeSessionMedia, a6 as RealtimeSessionRoomSinks, a7 as ReconnectPolicy, a8 as ReconnectingEvent, a9 as RecoveryState, aa as RetryStep, ab as SendTurnOptions, ac as SessionBehavior, ad as SessionClip, ae as SessionClocks, af as SessionEndReason, ag as SessionLifecycleApi, ah as SessionLifecyclePhase, ai as SessionLifecyclePhaseKind, aj as SessionLifecycleRoomBridge, al as SessionReadinessFailure, am as SurfaceLayers, an as TranscriptSender, ao as TranscriptSenderOptions, ap as TurnState, aq as TurnTimeoutEvent, ar as UseAvatarQualityGovernorInput, as as UseLiveKitAvatarGrantInput, at as UseRealtimeSessionInput, au as UseSessionLifecycleInput, av as VoiceSpec, aw as VoiceSpecInput, ax as capacityErrorFromBusy, ay as capacityStateFromGrant, az as createProxyClient, aB as knownBehaviorStates, aC as mapTurnState, aD as readInboundRtp, aE as sessionBehaviorSchema, aF as sessionClipSchema, b4 as shouldReplayPendingTurn, aG as splitCallTranscript, aH as useAvatarAdaptivePlayoutDelay, aI as useAvatarCamera, aJ as useAvatarPlayoutDelay, aK as useAvatarQualityGovernor, aL as useCallTranscript, aM as useLiveKitAvatarGrant, aO as useMicLease, aP as useRealtimeSession, aQ as useReleaseMicLeaseOnTrackEnded, aR as useSessionLifecycle } from './proxy-client-Cs2azqD1.js';
4
4
  import { CSSProperties, ReactNode } from 'react';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
6
  import { R as RecordingArtifact } from './recording-DjswTx5B.js';
package/dist/react.js CHANGED
@@ -1074,6 +1074,8 @@ function RealtimeAvatarLiveKitRoom(props) {
1074
1074
  LiveKitRoom,
1075
1075
  {
1076
1076
  ...roomProps,
1077
+ // A new grant owns a new room and bridge; an old room must not report its agent into it.
1078
+ key: grant?.session_id ?? "no-session",
1077
1079
  serverUrl: grant?.livekit_url,
1078
1080
  token: grant?.participant_token,
1079
1081
  connect: shouldConnect,
@@ -3019,7 +3021,8 @@ function lifecyclePhaseFrom(args) {
3019
3021
  kind: "reconnectable",
3020
3022
  reconnecting: true,
3021
3023
  attempt: recovery.attempt,
3022
- strategy: "fresh-grant"
3024
+ strategy: "fresh-grant",
3025
+ ...recovery.reason ? { reason: recovery.reason } : {}
3023
3026
  };
3024
3027
  }
3025
3028
  if (recovery.kind === "failed") {
@@ -3027,11 +3030,16 @@ function lifecyclePhaseFrom(args) {
3027
3030
  kind: "reconnectable",
3028
3031
  reconnecting: false,
3029
3032
  attempt: MAX_RECONNECT_ATTEMPTS,
3030
- strategy: "fresh-grant"
3033
+ strategy: "fresh-grant",
3034
+ ...recovery.reason ? { reason: recovery.reason } : {}
3031
3035
  };
3032
3036
  }
3033
3037
  if (recovery.kind === "ended") {
3034
- return recovery.reason ? { kind: "ended", reason: recovery.reason } : { kind: "ended" };
3038
+ return {
3039
+ kind: "ended",
3040
+ ...recovery.reason ? { reason: recovery.reason } : {},
3041
+ ...recovery.code ? { code: recovery.code } : {}
3042
+ };
3035
3043
  }
3036
3044
  switch (capacity.kind) {
3037
3045
  case "idle":
@@ -3044,7 +3052,7 @@ function lifecyclePhaseFrom(args) {
3044
3052
  return { kind: "ended", reason: "error" };
3045
3053
  case "active": {
3046
3054
  if (!connected || !agentPresent) {
3047
- return { kind: "connecting", grant: capacity.grant };
3055
+ return { kind: "connecting", grant: capacity.grant, waitingFor: connected ? "agent" : "transport" };
3048
3056
  }
3049
3057
  if (idle.kind === "idle-warning") {
3050
3058
  return { kind: "idle-warning", secondsRemaining: idle.secondsRemaining, deadlineAt: idle.deadlineAt };
@@ -3072,6 +3080,7 @@ function resolveIdleTimeoutMs(args) {
3072
3080
  return DEFAULT_IDLE_SECONDS * 1e3;
3073
3081
  }
3074
3082
  var DEFAULT_CONNECT_WATCHDOG_SECONDS = 12;
3083
+ var DEFAULT_READY_TIMEOUT_SECONDS = 30;
3075
3084
  function resolveWarnBeforeMs(idleTimeoutMs, override, idleWarnLeadSeconds) {
3076
3085
  if (idleTimeoutMs <= 0) return 0;
3077
3086
  const maxLeadMs = Math.max(0, idleTimeoutMs - 1e3);
@@ -3097,6 +3106,7 @@ function useSessionLifecycle(input) {
3097
3106
  maxReconnectAttempts,
3098
3107
  autoRetryBusy = true,
3099
3108
  connectWatchdogSeconds,
3109
+ readyTimeoutSeconds,
3100
3110
  requestOptions,
3101
3111
  onBehaviorChange
3102
3112
  } = input;
@@ -3107,17 +3117,27 @@ function useSessionLifecycle(input) {
3107
3117
  const reconnectPolicyRef = useRef(reconnectPolicy);
3108
3118
  reconnectPolicyRef.current = reconnectPolicy;
3109
3119
  const connectWatchdogMs = typeof connectWatchdogSeconds === "number" && Number.isFinite(connectWatchdogSeconds) ? Math.max(0, Math.floor(connectWatchdogSeconds)) * 1e3 : DEFAULT_CONNECT_WATCHDOG_SECONDS * 1e3;
3120
+ const readyTimeoutMs = typeof readyTimeoutSeconds === "number" && Number.isFinite(readyTimeoutSeconds) && readyTimeoutSeconds > 0 ? Math.max(1, Math.floor(readyTimeoutSeconds)) * 1e3 : DEFAULT_READY_TIMEOUT_SECONDS * 1e3;
3110
3121
  const [userEnded, setUserEnded] = useState(false);
3111
3122
  const userEndedRef = useRef(false);
3123
+ const [readinessFailed, setReadinessFailed] = useState(false);
3124
+ const readinessFailureRef = useRef(null);
3125
+ const readinessRef = useRef(null);
3126
+ const readyGrantRef = useRef(null);
3127
+ const agentWaitRef = useRef(null);
3112
3128
  const phaseRef = useRef({ kind: "idle" });
3113
3129
  const grantState = useLiveKitAvatarGrant({
3114
3130
  client,
3115
3131
  session,
3116
- active: active && !userEnded,
3132
+ active: active && !userEnded && !readinessFailed,
3117
3133
  autoRetryBusy,
3118
3134
  requestOptions
3119
3135
  });
3120
3136
  const capacity = grantState.capacity;
3137
+ const grantId = grantState.grant?.session_id ?? null;
3138
+ const grantIdRef = useRef(null);
3139
+ grantIdRef.current = grantId;
3140
+ const retiredGrantRef = useRef(null);
3121
3141
  const [recovery, setRecovery] = useState({ kind: "connected" });
3122
3142
  const recoveryRef = useRef(recovery);
3123
3143
  recoveryRef.current = recovery;
@@ -3140,8 +3160,19 @@ function useSessionLifecycle(input) {
3140
3160
  grantIdleSeconds: grantState.grant?.idle_timeout_seconds ?? 0
3141
3161
  });
3142
3162
  const warnAtMs = resolveWarnBeforeMs(idleTimeoutMs, warnBeforeMs, idleWarnLeadSeconds);
3143
- const [agentPresent, setAgentPresentState] = useState(false);
3144
- const [connected, setConnected] = useState(false);
3163
+ const [agentObservation, setAgentObservation] = useState({ grantId: null, present: false });
3164
+ const [connectionObservation, setConnectionObservation] = useState({ grantId: null, connected: false });
3165
+ const agentPresent = agentObservation.grantId === grantId && agentObservation.present;
3166
+ const connected = connectionObservation.grantId === grantId && connectionObservation.connected;
3167
+ connectedRef.current = connected;
3168
+ const setAgentPresentState = useCallback((present) => {
3169
+ const owner = grantIdRef.current;
3170
+ setAgentObservation((prev) => prev.grantId === owner && prev.present === present ? prev : { grantId: owner, present });
3171
+ }, []);
3172
+ const setConnected = useCallback((value) => {
3173
+ const owner = grantIdRef.current;
3174
+ setConnectionObservation((prev) => prev.grantId === owner && prev.connected === value ? prev : { grantId: owner, connected: value });
3175
+ }, []);
3145
3176
  const lastActivityRef = useRef(Date.now());
3146
3177
  const leaveRoomRef = useRef(null);
3147
3178
  const [, setClockTick] = useState(0);
@@ -3158,7 +3189,11 @@ function useSessionLifecycle(input) {
3158
3189
  refreshBaselineCapacityRef.current = null;
3159
3190
  attemptRef.current = 0;
3160
3191
  setAttempt(0);
3161
- setRecovery(userEndedRef.current ? { kind: "ended", reason: "user" } : { kind: "connected" });
3192
+ readinessRef.current = null;
3193
+ readyGrantRef.current = null;
3194
+ agentWaitRef.current = null;
3195
+ retiredGrantRef.current = null;
3196
+ setRecovery(userEndedRef.current ? { kind: "ended", reason: "user" } : readinessFailureRef.current ? { kind: "ended", reason: "error", code: readinessFailureRef.current } : { kind: "connected" });
3162
3197
  setConnected(false);
3163
3198
  setAgentPresentState(false);
3164
3199
  lastActivityRef.current = Date.now();
@@ -3167,26 +3202,33 @@ function useSessionLifecycle(input) {
3167
3202
  if (active) return;
3168
3203
  userEndedRef.current = false;
3169
3204
  setUserEnded(false);
3205
+ readinessFailureRef.current = null;
3206
+ setReadinessFailed(false);
3170
3207
  reset();
3171
3208
  }, [active, reset]);
3172
3209
  const markActivity = useCallback(() => {
3210
+ if (grantId === null || grantIdRef.current !== grantId) return;
3173
3211
  lastActivityRef.current = Date.now();
3174
3212
  setClockTick((value) => value + 1);
3175
- }, []);
3213
+ }, [grantId]);
3176
3214
  const stayConnected = useCallback(() => {
3215
+ if (grantId === null || grantIdRef.current !== grantId) return;
3177
3216
  if (!connectedRef.current) return;
3178
3217
  lastActivityRef.current = Date.now();
3179
3218
  setClockTick((value) => value + 1);
3180
- }, []);
3219
+ }, [grantId]);
3181
3220
  const setAgentPresent = useCallback((present) => {
3182
- setAgentPresentState((prev) => {
3183
- if (present && !prev) lastActivityRef.current = Date.now();
3184
- return present;
3221
+ if (grantId === null || grantIdRef.current !== grantId) return;
3222
+ if (userEndedRef.current || readinessFailureRef.current || retiredGrantRef.current !== null && retiredGrantRef.current === grantIdRef.current) return;
3223
+ setAgentObservation((prev) => {
3224
+ if (present && (prev.grantId !== grantId || !prev.present)) lastActivityRef.current = Date.now();
3225
+ return prev.grantId === grantId && prev.present === present ? prev : { grantId, present };
3185
3226
  });
3186
- }, []);
3227
+ }, [grantId]);
3187
3228
  const registerLeaveRoom = useCallback((leave) => {
3229
+ if (grantId === null || grantIdRef.current !== grantId) return;
3188
3230
  leaveRoomRef.current = leave;
3189
- }, []);
3231
+ }, [grantId]);
3190
3232
  const endIdle = useCallback(() => {
3191
3233
  if (!connectedRef.current) return;
3192
3234
  clearTimer();
@@ -3198,24 +3240,25 @@ function useSessionLifecycle(input) {
3198
3240
  leaveRoomRef.current?.();
3199
3241
  }, [clearTimer]);
3200
3242
  const onConnected = useCallback(() => {
3201
- if (userEndedRef.current) return;
3243
+ if (grantId === null || grantIdRef.current !== grantId) return;
3244
+ if (userEndedRef.current || readinessFailureRef.current || retiredGrantRef.current !== null && retiredGrantRef.current === grantIdRef.current) return;
3202
3245
  clearTimer();
3203
3246
  connectedRef.current = true;
3204
3247
  manualReconnectPendingRef.current = false;
3205
3248
  refreshBaselineCapacityRef.current = null;
3206
3249
  setConnected(true);
3207
- attemptRef.current = 0;
3208
- setAttempt(0);
3250
+ if (readinessRef.current) readinessRef.current.sawTransport = true;
3209
3251
  setRecovery({ kind: "connected" });
3210
3252
  lastActivityRef.current = Date.now();
3211
- }, [clearTimer]);
3253
+ }, [clearTimer, grantId]);
3212
3254
  const onDisconnected = useCallback(
3213
3255
  (reason) => {
3214
- if (!active || userEndedRef.current) return;
3256
+ if (grantId === null || grantIdRef.current !== grantId) return;
3257
+ if (!active || userEndedRef.current || readinessFailureRef.current) return;
3215
3258
  const action = disconnectAction(reason, connectedRef.current);
3216
3259
  if (action === "noop") return;
3217
3260
  if (action === "reset") {
3218
- if (recoveryRef.current.kind === "refreshing") return;
3261
+ if (recoveryRef.current.kind === "refreshing" || recoveryRef.current.kind === "reconnecting") return;
3219
3262
  reset();
3220
3263
  return;
3221
3264
  }
@@ -3232,11 +3275,12 @@ function useSessionLifecycle(input) {
3232
3275
  refreshBaselineCapacityRef.current = null;
3233
3276
  setRecovery({ kind: "reconnecting", attempt: attemptRef.current });
3234
3277
  },
3235
- [active, reset]
3278
+ [active, reset, grantId]
3236
3279
  );
3237
3280
  const onConnectionError = useCallback(
3238
3281
  (error) => {
3239
- if (!active || userEndedRef.current || !isRecoverableConnectionError(error)) return;
3282
+ if (grantId === null || grantIdRef.current !== grantId) return;
3283
+ if (!active || userEndedRef.current || readinessFailureRef.current || !isRecoverableConnectionError(error)) return;
3240
3284
  connectedRef.current = false;
3241
3285
  setConnected(false);
3242
3286
  setAgentPresentState(false);
@@ -3247,7 +3291,7 @@ function useSessionLifecycle(input) {
3247
3291
  return { kind: "reconnecting", attempt: attemptRef.current };
3248
3292
  });
3249
3293
  },
3250
- [active]
3294
+ [active, grantId]
3251
3295
  );
3252
3296
  useEffect(() => {
3253
3297
  if (capacity.kind === "error" && recovery.kind === "connected") manualReconnectPendingRef.current = false;
@@ -3257,11 +3301,12 @@ function useSessionLifecycle(input) {
3257
3301
  const baseline = refreshBaselineCapacityRef.current;
3258
3302
  if (baseline?.kind === "error" && baseline.error === capacity.error) return;
3259
3303
  refreshBaselineCapacityRef.current = null;
3260
- setRecovery({ kind: "reconnecting", attempt: recovery.attempt });
3304
+ setRecovery({ kind: "reconnecting", attempt: recovery.attempt, ...recovery.reason ? { reason: recovery.reason } : {} });
3261
3305
  }, [capacity, recovery]);
3262
3306
  const onConnectionStateChange = useCallback(
3263
3307
  (state) => {
3264
- if (!active || userEndedRef.current) return;
3308
+ if (grantId === null || grantIdRef.current !== grantId) return;
3309
+ if (!active || userEndedRef.current || readinessFailureRef.current) return;
3265
3310
  if (state === "connected") {
3266
3311
  onConnected();
3267
3312
  return;
@@ -3275,14 +3320,78 @@ function useSessionLifecycle(input) {
3275
3320
  }
3276
3321
  }
3277
3322
  },
3278
- [active, onConnected]
3323
+ [active, onConnected, grantId]
3279
3324
  );
3325
+ useEffect(() => {
3326
+ if (!active || userEnded || readinessFailed || !connected || !agentPresent || !grantState.grant) return;
3327
+ readyGrantRef.current = grantState.grant.session_id;
3328
+ readinessRef.current = null;
3329
+ agentWaitRef.current = null;
3330
+ attemptRef.current = 0;
3331
+ setAttempt(0);
3332
+ }, [active, userEnded, readinessFailed, connected, agentPresent, grantState.grant]);
3333
+ const failReadiness = useCallback((code) => {
3334
+ if (userEndedRef.current || readinessFailureRef.current) return;
3335
+ readinessFailureRef.current = code;
3336
+ clearTimer();
3337
+ connectedRef.current = false;
3338
+ manualReconnectPendingRef.current = false;
3339
+ refreshBaselineCapacityRef.current = null;
3340
+ const ended = { kind: "ended", reason: "error", code };
3341
+ recoveryRef.current = ended;
3342
+ setRecovery(ended);
3343
+ setConnected(false);
3344
+ setAgentPresentState(false);
3345
+ releaseRef.current("disconnected");
3346
+ setReadinessFailed(true);
3347
+ leaveRoomRef.current?.();
3348
+ }, [clearTimer]);
3349
+ useEffect(() => {
3350
+ if (!active || userEnded || readinessFailed) return;
3351
+ if (recovery.kind === "ended" || recovery.kind === "failed") return;
3352
+ if (grantState.grant && readyGrantRef.current === grantState.grant.session_id) return;
3353
+ if (!readinessRef.current && grantState.grant) {
3354
+ readinessRef.current = { startedAt: Date.now(), sawTransport: connected };
3355
+ }
3356
+ const budget = readinessRef.current;
3357
+ if (!budget) return;
3358
+ const timer = window.setTimeout(() => {
3359
+ if (readinessRef.current !== budget) return;
3360
+ failReadiness(budget.sawTransport ? "agent_timeout" : "connection_timeout");
3361
+ }, Math.max(0, budget.startedAt + readyTimeoutMs - Date.now()));
3362
+ return () => window.clearTimeout(timer);
3363
+ }, [active, userEnded, readinessFailed, connected, agentPresent, grantState.grant, recovery.kind, readyTimeoutMs, failReadiness]);
3364
+ useEffect(() => {
3365
+ const sessionId = grantState.grant?.session_id;
3366
+ if (!active || userEnded || readinessFailed || !sessionId || readyGrantRef.current === sessionId || agentPresent) return;
3367
+ if (recovery.kind === "reconnecting" || recovery.kind === "failed" || recovery.kind === "ended") return;
3368
+ if (agentWaitRef.current?.sessionId !== sessionId) {
3369
+ if (!connected) return;
3370
+ agentWaitRef.current = { sessionId, startedAt: Date.now() };
3371
+ }
3372
+ const wait = agentWaitRef.current;
3373
+ if (connectWatchdogMs <= 0) return;
3374
+ const timer = window.setTimeout(() => {
3375
+ if (agentWaitRef.current !== wait || readinessFailureRef.current || userEndedRef.current) return;
3376
+ retiredGrantRef.current = sessionId;
3377
+ connectedRef.current = false;
3378
+ setConnected(false);
3379
+ setAgentPresentState(false);
3380
+ const next = { kind: "reconnecting", attempt: attemptRef.current, reason: "agent_timeout" };
3381
+ recoveryRef.current = next;
3382
+ setRecovery(next);
3383
+ releaseRef.current("disconnected");
3384
+ leaveRoomRef.current?.();
3385
+ }, Math.max(0, wait.startedAt + connectWatchdogMs - Date.now()));
3386
+ return () => window.clearTimeout(timer);
3387
+ }, [active, userEnded, readinessFailed, connected, agentPresent, grantState.grant, recovery.kind, connectWatchdogMs]);
3280
3388
  useEffect(() => {
3281
3389
  if (!active || connected || connectWatchdogMs <= 0) return;
3282
3390
  if (grantState.status !== "ready" || !grantState.grant) return;
3283
3391
  if (recovery.kind === "failed" || recovery.kind === "ended") return;
3284
3392
  if (recovery.kind === "in-place-reconnecting") return;
3285
3393
  const timer = window.setTimeout(() => {
3394
+ if (readinessFailureRef.current || userEndedRef.current) return;
3286
3395
  releaseRef.current("disconnected");
3287
3396
  setRecovery({ kind: "reconnecting", attempt: attemptRef.current });
3288
3397
  }, connectWatchdogMs);
@@ -3292,6 +3401,10 @@ function useSessionLifecycle(input) {
3292
3401
  if (!active || !needsFreshGrant(recovery)) return;
3293
3402
  const step2 = retryStep(attemptRef.current, reconnectPolicyRef.current);
3294
3403
  if (step2.kind === "give-up") {
3404
+ if (readinessRef.current) {
3405
+ failReadiness(readinessRef.current.sawTransport ? "agent_timeout" : "connection_timeout");
3406
+ return;
3407
+ }
3295
3408
  manualReconnectPendingRef.current = false;
3296
3409
  refreshBaselineCapacityRef.current = null;
3297
3410
  releaseRef.current("disconnected");
@@ -3300,15 +3413,16 @@ function useSessionLifecycle(input) {
3300
3413
  }
3301
3414
  timerRef.current = window.setTimeout(() => {
3302
3415
  timerRef.current = null;
3416
+ if (readinessFailureRef.current || userEndedRef.current) return;
3303
3417
  attemptRef.current = step2.attempt;
3304
3418
  setAttempt(step2.attempt);
3305
3419
  refreshBaselineCapacityRef.current = capacityRef.current;
3306
3420
  releaseRef.current("superseded");
3307
- setRecovery({ kind: "refreshing", attempt: step2.attempt });
3421
+ setRecovery({ kind: "refreshing", attempt: step2.attempt, ...recovery.reason ? { reason: recovery.reason } : {} });
3308
3422
  refreshRef.current();
3309
3423
  }, step2.delayMs);
3310
3424
  return clearTimer;
3311
- }, [recovery.kind, active, attempt, clearTimer]);
3425
+ }, [recovery.kind, active, attempt, clearTimer, failReadiness]);
3312
3426
  const end = useCallback(() => {
3313
3427
  if (!active || userEndedRef.current || phaseRef.current.kind === "ended") return;
3314
3428
  userEndedRef.current = true;
@@ -3325,6 +3439,13 @@ function useSessionLifecycle(input) {
3325
3439
  }, [active, clearTimer]);
3326
3440
  const reconnect = useCallback(() => {
3327
3441
  if (!active) return;
3442
+ if (readinessFailureRef.current) {
3443
+ readinessFailureRef.current = null;
3444
+ setReadinessFailed(false);
3445
+ reset();
3446
+ manualReconnectPendingRef.current = true;
3447
+ return;
3448
+ }
3328
3449
  if (userEndedRef.current) {
3329
3450
  userEndedRef.current = false;
3330
3451
  setUserEnded(false);
@@ -3335,6 +3456,12 @@ function useSessionLifecycle(input) {
3335
3456
  if (manualReconnectPendingRef.current || recovery.kind === "in-place-reconnecting") return;
3336
3457
  manualReconnectPendingRef.current = true;
3337
3458
  clearTimer();
3459
+ readinessRef.current = null;
3460
+ agentWaitRef.current = null;
3461
+ readyGrantRef.current = null;
3462
+ connectedRef.current = false;
3463
+ setConnected(false);
3464
+ setAgentPresentState(false);
3338
3465
  attemptRef.current = 0;
3339
3466
  setAttempt(0);
3340
3467
  refreshBaselineCapacityRef.current = capacityRef.current;
@@ -3454,6 +3581,7 @@ function SessionLifecycleRoomBridge({
3454
3581
  const assistant = useVoiceAssistant();
3455
3582
  const transcriptions = useTranscriptions();
3456
3583
  const room = useRoomContext();
3584
+ const roomMatchesGrant = lifecycle.grant === void 0 || lifecycle.grant !== null && room.name === lifecycle.grant.room_name;
3457
3585
  useCallMedia(room, {
3458
3586
  setMicrophoneFacts,
3459
3587
  setAudioPlayback,
@@ -3607,11 +3735,11 @@ function SessionLifecycleRoomBridge({
3607
3735
  detailsEnabled ? video?.publication : void 0
3608
3736
  ]);
3609
3737
  useEffect(() => {
3610
- onConnectionStateChange(connectionState);
3611
- }, [connectionState, onConnectionStateChange]);
3738
+ if (roomMatchesGrant) onConnectionStateChange(connectionState);
3739
+ }, [connectionState, onConnectionStateChange, roomMatchesGrant]);
3612
3740
  useEffect(() => {
3613
- setAgentPresent(agentPresent);
3614
- }, [agentPresent, setAgentPresent]);
3741
+ if (roomMatchesGrant) setAgentPresent(agentPresent);
3742
+ }, [agentPresent, setAgentPresent, roomMatchesGrant]);
3615
3743
  useEffect(() => {
3616
3744
  const active = isCallActivity({
3617
3745
  assistantState,
@@ -4675,4 +4803,4 @@ function RecordingPlayer({ assets, className }) {
4675
4803
  ] });
4676
4804
  }
4677
4805
 
4678
- export { AdaptivePlayoutController, AvatarCall, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, DEFAULT_AVATAR_UNSTABLE_STALL_MS, DEFAULT_CREDITS_LOW_LEAD_SECONDS, DEFAULT_GOVERNOR_CONFIG, DEFAULT_GRACE_CEILING_SECONDS, DEFAULT_GRACE_WINDOW_LEAD_SECONDS, DEFAULT_IDLE_SECONDS, DEFAULT_IDLE_WARN_LEAD_SECONDS, DEFAULT_TURN_TIMEOUT_SECONDS, MAX_RECONNECT_ATTEMPTS, MAX_SESSION_INSTRUCTIONS_CHARS, MIC_LEASE_ENDED_TIMEOUT_MS, NETWORK_QUALITY_POLICY, RECONNECT_BACKOFF_MS, RealtimeAvatarCapacityError, RealtimeAvatarLiveKitRoom, RecordingPlayer, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, knownBehaviorStates, mapTurnState, readInboundRtp, sessionBehaviorSchema, sessionClipSchema, shouldReplayPendingTurn, splitCallTranscript, useAvatarAdaptivePlayoutDelay, useAvatarCall, useAvatarCamera, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useCharacterTools, useLiveKitAvatarGrant, useMicLease, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
4806
+ export { AdaptivePlayoutController, AvatarCall, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, DEFAULT_AVATAR_UNSTABLE_STALL_MS, DEFAULT_CREDITS_LOW_LEAD_SECONDS, DEFAULT_GOVERNOR_CONFIG, DEFAULT_GRACE_CEILING_SECONDS, DEFAULT_GRACE_WINDOW_LEAD_SECONDS, DEFAULT_IDLE_SECONDS, DEFAULT_IDLE_WARN_LEAD_SECONDS, DEFAULT_READY_TIMEOUT_SECONDS, DEFAULT_TURN_TIMEOUT_SECONDS, MAX_RECONNECT_ATTEMPTS, MAX_SESSION_INSTRUCTIONS_CHARS, MIC_LEASE_ENDED_TIMEOUT_MS, NETWORK_QUALITY_POLICY, RECONNECT_BACKOFF_MS, RealtimeAvatarCapacityError, RealtimeAvatarLiveKitRoom, RecordingPlayer, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, knownBehaviorStates, mapTurnState, readInboundRtp, sessionBehaviorSchema, sessionClipSchema, shouldReplayPendingTurn, splitCallTranscript, useAvatarAdaptivePlayoutDelay, useAvatarCall, useAvatarCamera, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useCharacterTools, useLiveKitAvatarGrant, useMicLease, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
package/dist/server.js CHANGED
@@ -1,2 +1,2 @@
1
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript } from './chunk-NMJQEGYW.js';
1
+ export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript } from './chunk-4IEGR75H.js';
2
2
  import './chunk-MCIXU6XP.js';
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-SUKZ77GL.js';
2
- import './chunk-NMJQEGYW.js';
1
+ import { createProxyHandler } from './chunk-DOHZ365B.js';
2
+ import './chunk-4IEGR75H.js';
3
3
  import './chunk-MCIXU6XP.js';
4
4
 
5
5
  // src/tanstack-start.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "realtime-avatar",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "description": "Realtime Avatar SDK. `realtime-avatar` and the route adapters hold your API key and are server-only; `realtime-avatar/react`, `/browser` and `/tools` never can — importing a server entry into a browser build throws at bundle time.",
5
5
  "license": "MIT",
6
6
  "author": "The Influence Company",