@memberjunction/realtime-runtime 6.2.0-edge.3 → 6.2.0-edge.4

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 (98) hide show
  1. package/README.md +75 -0
  2. package/dist/channels/base-realtime-channel-client.d.ts +535 -28
  3. package/dist/channels/base-realtime-channel-client.d.ts.map +1 -1
  4. package/dist/channels/base-realtime-channel-client.js +864 -10
  5. package/dist/channels/base-realtime-channel-client.js.map +1 -1
  6. package/dist/channels/channel-action-dispatcher.d.ts +70 -0
  7. package/dist/channels/channel-action-dispatcher.d.ts.map +1 -0
  8. package/dist/channels/channel-action-dispatcher.js +136 -0
  9. package/dist/channels/channel-action-dispatcher.js.map +1 -0
  10. package/dist/channels/channel-catalog-note.d.ts +48 -0
  11. package/dist/channels/channel-catalog-note.d.ts.map +1 -0
  12. package/dist/channels/channel-catalog-note.js +119 -0
  13. package/dist/channels/channel-catalog-note.js.map +1 -0
  14. package/dist/channels/channel-contract-types.d.ts +107 -0
  15. package/dist/channels/channel-contract-types.d.ts.map +1 -0
  16. package/dist/channels/channel-contract-types.js +12 -0
  17. package/dist/channels/channel-contract-types.js.map +1 -0
  18. package/dist/channels/channel-descriptor-synthesis.d.ts +56 -0
  19. package/dist/channels/channel-descriptor-synthesis.d.ts.map +1 -0
  20. package/dist/channels/channel-descriptor-synthesis.js +90 -0
  21. package/dist/channels/channel-descriptor-synthesis.js.map +1 -0
  22. package/dist/channels/channel-perception.d.ts +82 -0
  23. package/dist/channels/channel-perception.d.ts.map +1 -0
  24. package/dist/channels/channel-perception.js +120 -0
  25. package/dist/channels/channel-perception.js.map +1 -0
  26. package/dist/channels/channel-schema-format.d.ts +17 -0
  27. package/dist/channels/channel-schema-format.d.ts.map +1 -0
  28. package/dist/channels/channel-schema-format.js +45 -0
  29. package/dist/channels/channel-schema-format.js.map +1 -0
  30. package/dist/channels/channel-state-delta.d.ts +59 -0
  31. package/dist/channels/channel-state-delta.d.ts.map +1 -0
  32. package/dist/channels/channel-state-delta.js +116 -0
  33. package/dist/channels/channel-state-delta.js.map +1 -0
  34. package/dist/channels/channel-surface-placement.d.ts +25 -0
  35. package/dist/channels/channel-surface-placement.d.ts.map +1 -0
  36. package/dist/channels/channel-surface-placement.js +29 -0
  37. package/dist/channels/channel-surface-placement.js.map +1 -0
  38. package/dist/channels/channel-visual-pump.d.ts +126 -0
  39. package/dist/channels/channel-visual-pump.d.ts.map +1 -0
  40. package/dist/channels/channel-visual-pump.js +174 -0
  41. package/dist/channels/channel-visual-pump.js.map +1 -0
  42. package/dist/hosts/IRealtimeMediaHost.d.ts +61 -4
  43. package/dist/hosts/IRealtimeMediaHost.d.ts.map +1 -1
  44. package/dist/hosts/IRealtimeMediaHost.js +2 -1
  45. package/dist/hosts/IRealtimeMediaHost.js.map +1 -1
  46. package/dist/index.d.ts +21 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +24 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/session/RealtimeSessionRuntime.d.ts +727 -74
  51. package/dist/session/RealtimeSessionRuntime.d.ts.map +1 -1
  52. package/dist/session/RealtimeSessionRuntime.js +1732 -242
  53. package/dist/session/RealtimeSessionRuntime.js.map +1 -1
  54. package/dist/session/RecordingShardQueue.d.ts +77 -0
  55. package/dist/session/RecordingShardQueue.d.ts.map +1 -0
  56. package/dist/session/RecordingShardQueue.js +172 -0
  57. package/dist/session/RecordingShardQueue.js.map +1 -0
  58. package/dist/session/app-client-tool-registry.d.ts +76 -0
  59. package/dist/session/app-client-tool-registry.d.ts.map +1 -0
  60. package/dist/session/app-client-tool-registry.js +100 -0
  61. package/dist/session/app-client-tool-registry.js.map +1 -0
  62. package/dist/session/avatar-notice.d.ts +43 -0
  63. package/dist/session/avatar-notice.d.ts.map +1 -0
  64. package/dist/session/avatar-notice.js +60 -0
  65. package/dist/session/avatar-notice.js.map +1 -0
  66. package/dist/session/call-end.d.ts +21 -0
  67. package/dist/session/call-end.d.ts.map +1 -0
  68. package/dist/session/call-end.js +19 -0
  69. package/dist/session/call-end.js.map +1 -0
  70. package/dist/session/channel-exposure-preferences.d.ts +70 -0
  71. package/dist/session/channel-exposure-preferences.d.ts.map +1 -0
  72. package/dist/session/channel-exposure-preferences.js +138 -0
  73. package/dist/session/channel-exposure-preferences.js.map +1 -0
  74. package/dist/session/channel-session-scope.d.ts +142 -0
  75. package/dist/session/channel-session-scope.d.ts.map +1 -0
  76. package/dist/session/channel-session-scope.js +128 -0
  77. package/dist/session/channel-session-scope.js.map +1 -0
  78. package/dist/session/client-session-deadline.d.ts +28 -0
  79. package/dist/session/client-session-deadline.d.ts.map +1 -0
  80. package/dist/session/client-session-deadline.js +50 -0
  81. package/dist/session/client-session-deadline.js.map +1 -0
  82. package/dist/session/realtime-captures.d.ts +236 -0
  83. package/dist/session/realtime-captures.d.ts.map +1 -0
  84. package/dist/session/realtime-captures.js +383 -0
  85. package/dist/session/realtime-captures.js.map +1 -0
  86. package/dist/session/realtime-microphone.d.ts +50 -0
  87. package/dist/session/realtime-microphone.d.ts.map +1 -0
  88. package/dist/session/realtime-microphone.js +51 -0
  89. package/dist/session/realtime-microphone.js.map +1 -0
  90. package/dist/session/session-event-hub.d.ts +72 -0
  91. package/dist/session/session-event-hub.d.ts.map +1 -0
  92. package/dist/session/session-event-hub.js +151 -0
  93. package/dist/session/session-event-hub.js.map +1 -0
  94. package/dist/session/session-launcher.d.ts +128 -0
  95. package/dist/session/session-launcher.d.ts.map +1 -0
  96. package/dist/session/session-launcher.js +160 -0
  97. package/dist/session/session-launcher.js.map +1 -0
  98. package/package.json +9 -9
@@ -1,11 +1,20 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import { IMetadataProvider } from '@memberjunction/core';
3
- import { ClientRealtimeSessionConfig, RealtimeToolDefinition, RealtimeTrackDirection } from '@memberjunction/ai';
4
- import { AppContextSnapshot } from '@memberjunction/ai-core-plus';
5
- import { BaseRealtimeClient, RealtimeAudioActivity } from '@memberjunction/ai-realtime-client';
3
+ import { ClientRealtimeSessionConfig, JSONObject, RealtimeToolDefinition, RealtimeTrackDirection } from '@memberjunction/ai';
4
+ import { AppContextSnapshot, type IdentityVerifiedEventPayload, type RealtimeChannelExposure, type ResolvedRealtimeChannel } from '@memberjunction/ai-core-plus';
5
+ import { BaseRealtimeClient, RealtimeAudioActivity, RealtimeClientError, type DisplayCaptureOptions, type MediaVideoSource, type VideoSourceState } from '@memberjunction/ai-realtime-client';
6
+ import { type IRealtimeSessionLauncher } from './session-launcher.js';
7
+ import { type RealtimeAvatarNotice } from './avatar-notice.js';
8
+ import { type RealtimeCaptureOffers, type RealtimeCaptureState, type RealtimeCaptureStates } from './realtime-captures.js';
9
+ import { type RealtimeMicrophoneState } from './realtime-microphone.js';
10
+ import { type IRealtimeSessionEventSource, type RealtimeSessionStreamEvent } from './session-event-hub.js';
6
11
  import { ParsedDelegationArtifact } from './delegation-result-parser.js';
7
12
  import { BaseRealtimeChannelClient } from '../channels/base-realtime-channel-client.js';
8
13
  import { IRealtimeMediaHost } from '../hosts/IRealtimeMediaHost.js';
14
+ import type { RealtimeContextActionResult } from '../channels/channel-contract-types.js';
15
+ import { type AppClientToolRegistration } from './app-client-tool-registry.js';
16
+ import { type IChannelExposurePreferences } from './channel-exposure-preferences.js';
17
+ import { type RealtimeSessionStartOptions } from './channel-session-scope.js';
9
18
  /**
10
19
  * `MJ: User Settings` key for the per-user "record this voice call" consent toggle. Stored as
11
20
  * the literal string `'true'`/`'false'` (read with `=== 'true'`), cross-device via
@@ -130,6 +139,7 @@ export interface StartRealtimeClientSessionResult {
130
139
  ConversationId: string | null;
131
140
  Provider: string;
132
141
  Model: string;
142
+ /** The provider credential the client driver presents. Empty on a relay session, which has {@link RelayUrl} instead. */
133
143
  EphemeralToken: string;
134
144
  ExpiresAt: string;
135
145
  /** JSON.stringify of the provider session config (instructions + tools) to apply at connect. */
@@ -150,6 +160,31 @@ export interface StartRealtimeClientSessionResult {
150
160
  * last session left off.
151
161
  */
152
162
  PriorChannelStatesJson: string | null;
163
+ /**
164
+ * OPTIONAL — the server's resolved `RealtimeSessionClientPolicy` (from `@memberjunction/ai-core-plus`) as JSON: which of the channel
165
+ * candidates the client reported at mint are in this session (agent/app scoping and the registry's
166
+ * kill switch already applied), their resolved display and config, and the app/static client-tool
167
+ * tiers. Absent from a server that predates channel scoping and from a host that mints through its
168
+ * own proxy; the runtime then resolves the scope locally from code defaults and host declarations.
169
+ */
170
+ ClientPolicyJson?: string | null;
171
+ /**
172
+ * OPTIONAL — the session's live-avatar status (`RealtimeAvatarStatus` from `@memberjunction/ai`) as JSON: whether the
173
+ * voiced agent asked for an avatar, whether the model renders it, and why not. The runtime reads it after connecting
174
+ * to publish {@link RealtimeSessionRuntime.AvatarNotice$}. Absent from a server that predates it, when the agent asked
175
+ * for no avatar, and from a host that mints for itself and does not pass it on; the call then shows no notice.
176
+ */
177
+ AvatarStatusJson?: string | null;
178
+ /**
179
+ * OPTIONAL — how the browser reaches the provider: `'direct'` or `'relay'` (`RealtimeClientTransport` from
180
+ * `@memberjunction/ai`). Absent, null or unknown means direct: a direct session, or a server that predates it.
181
+ */
182
+ Transport?: string | null;
183
+ /**
184
+ * OPTIONAL — where a relay session's client driver connects (MJAPI's realtime relay). It carries the session's ticket,
185
+ * so it is a credential: never log it. A host that mints for itself must pass it on for a relay session.
186
+ */
187
+ RelayUrl?: string | null;
153
188
  }
154
189
  /**
155
190
  * Host-supplied inputs that accompany an already-minted session on
@@ -221,6 +256,7 @@ export declare class RealtimeSessionRuntime {
221
256
  */
222
257
  constructor(mediaHost: IRealtimeMediaHost);
223
258
  private _connectionState$;
259
+ private readonly _resuming$;
224
260
  private _captions$;
225
261
  private _active$;
226
262
  private _delegationProgress$;
@@ -230,6 +266,7 @@ export declare class RealtimeSessionRuntime {
230
266
  private _agentName$;
231
267
  private _modelName$;
232
268
  private _minimized$;
269
+ private _savingRecording$;
233
270
  private _activeChannels$;
234
271
  private _channelFocus$;
235
272
  private _sessionStarted$;
@@ -237,6 +274,16 @@ export declare class RealtimeSessionRuntime {
237
274
  private _channelActivity$;
238
275
  /** Current connection / turn state. */
239
276
  readonly ConnectionState$: Observable<RealtimeConnectionState>;
277
+ /**
278
+ * Whether the call is resuming: moving to a new connection with its provider mid-call. `true` from a `'connecting'` the
279
+ * driver reports while the call is live, until the call is live again (`'listening'`, `'speaking'` or `'thinking'`),
280
+ * fails (`'error'`) or ends (`'closed'`). A call's first connect is not a resume, so it is `false` until the call has
281
+ * been live, and outside a call. Gemini Live resumes when Google ends a connection (`goAway`) and after a dropped
282
+ * socket, and the agent's video can stop for a few seconds meanwhile: a host can show that as a pause rather than as
283
+ * the agent leaving. It changes before {@link ConnectionState$} does, so a resume's `'connecting'` arrives with it
284
+ * already `true`.
285
+ */
286
+ readonly Resuming$: Observable<boolean>;
240
287
  /** Live captions for both sides of the conversation. */
241
288
  readonly Captions$: Observable<RealtimeCaption[]>;
242
289
  /** True while a session is open (mic button active, overlay shown). */
@@ -273,6 +320,16 @@ export declare class RealtimeSessionRuntime {
273
320
  * pure presentation state, reset to `false` at session start and teardown.
274
321
  */
275
322
  readonly Minimized$: Observable<boolean>;
323
+ /**
324
+ * True while the end-of-call recording is being saved: from the first statement of a teardown
325
+ * that holds a recorder until that teardown settles. False at construction and for unrecorded
326
+ * sessions. Exists for #5195: the page must stay open until the consolidated upload lands, or
327
+ * the recording is lost. The runtime is platform-free, so hosts own the reaction (a browser
328
+ * `beforeunload` guard, a "saving" indicator, etc.).
329
+ */
330
+ readonly SavingRecording$: Observable<boolean>;
331
+ /** Synchronous read of {@link SavingRecording$}. */
332
+ get IsSavingRecording(): boolean;
276
333
  /**
277
334
  * The session's ACTIVE interactive-channel plugins, resolved from the `MJ: AI Agent
278
335
  * Channels` registry at session start (one instance per session, per channel). Emits
@@ -280,6 +337,59 @@ export declare class RealtimeSessionRuntime {
280
337
  * one surface tab per plugin — it never knows any concrete channel type.
281
338
  */
282
339
  readonly ActiveChannels$: Observable<BaseRealtimeChannelClient[]>;
340
+ private readonly _videoSources$;
341
+ /**
342
+ * The video sources the agent is perceiving or could perceive right now (a whiteboard, a remote browser, a
343
+ * shared screen), with whether each is on and whether its frames are reaching the model. Empty when no
344
+ * session is live or nothing has offered a frame source. This is what the "agent can see" control renders;
345
+ * toggle one with {@link SetVideoSourceEnabled}.
346
+ */
347
+ readonly VideoSources$: Observable<readonly VideoSourceState[]>;
348
+ private readonly _captures$;
349
+ /**
350
+ * The user's camera and screen share: off, starting, on (with the stream to show the user) or failed (with the
351
+ * reason). Both off outside a session. Start and stop them with {@link StartCamera}, {@link StartScreenShare},
352
+ * {@link StopCamera} and {@link StopScreenShare}; while one is on, it is also a source on {@link VideoSources$}.
353
+ */
354
+ readonly Captures$: Observable<RealtimeCaptureStates>;
355
+ private readonly _captureOffers$;
356
+ /**
357
+ * Which captures the call offers, now and on every change. A capture is offered while the call is connected, the model
358
+ * takes inbound video, the host can open it (a camera controller; screen sharing) and the call's policy admits it (its
359
+ * channel is in the call and may show the agent pixels). A host shows its Camera and Share controls from this. Neither
360
+ * is offered outside a call.
361
+ */
362
+ readonly CaptureOffers$: Observable<RealtimeCaptureOffers>;
363
+ private readonly _microphone$;
364
+ /**
365
+ * The call's microphone, now and on every change: the one in use and the ones the user can move it to with
366
+ * {@link SwitchMicrophone}, as the host's camera-and-microphone controller lists them. Offered from the moment the call
367
+ * connects until it ends, and kept current as devices come and go. When the call loses its microphone (the one in use
368
+ * fails and so does the fallback to the default, or the last one goes away), its `Failure` says why: the agent hears
369
+ * nothing until the user picks a microphone, which opens it again. {@link REALTIME_MICROPHONE_NONE} outside a call and
370
+ * on a host without a controller, which opens the microphone itself (`IRealtimeMediaHost.AcquireMicrophone`).
371
+ */
372
+ readonly Microphone$: Observable<RealtimeMicrophoneState>;
373
+ private readonly _agentVideo$;
374
+ /**
375
+ * The agent's video (an avatar) while the model sends it: a live stream, or a player that owns the `<video>` element.
376
+ * `null` until the video arrives, for a model that sends none, and outside a call. The driver hands it over once it is
377
+ * live, and a newer one replaces it. A host shows it with `AttachVideoSource`; a channel that sinks outbound video
378
+ * follows it through its context, and counts as used once it arrives, so the host shows that channel's surface.
379
+ */
380
+ readonly AgentVideo$: Observable<MediaVideoSource | null>;
381
+ private readonly _avatarNotice$;
382
+ /**
383
+ * Why the call shows no avatar the agent asked for: set once per call, after it connects, when the model renders none,
384
+ * the persona has no face for it, the app shows no agent video (`host`) or the browser can't play it (`browser`); with
385
+ * no `Reason` when the mint gave one this version doesn't know, such as a newer server's. `null` when there is nothing
386
+ * to say (no avatar asked for, the avatar shows, or the mint reported no status) and after the call ends. A provider
387
+ * resume inside the call does not set it again. A host turns the reason into words once, for example "Audio only: this
388
+ * voice model can't show an avatar".
389
+ */
390
+ readonly AvatarNotice$: Observable<RealtimeAvatarNotice | null>;
391
+ /** Synchronous access to {@link AvatarNotice$}'s current value. */
392
+ get CurrentAvatarNotice(): RealtimeAvatarNotice | null;
283
393
  /**
284
394
  * Channel requests to enter / leave the FOCUS layout (see
285
395
  * {@link RealtimeChannelFocusEvent}). Fired when a plugin calls its host context's
@@ -341,7 +451,11 @@ export declare class RealtimeSessionRuntime {
341
451
  */
342
452
  /** Conversation id the SERVER created for this session (null when the host supplied one). */
343
453
  private createdConversationId;
344
- /** The session's conversation id (supplied or server-created). */
454
+ /**
455
+ * The session's conversation id (supplied or server-created). While a start's mint runs, the
456
+ * conversation the start asked for; null when it asked the server to create one. Null after
457
+ * teardown.
458
+ */
345
459
  private sessionConversationId;
346
460
  /** First final user utterance of the live session (the naming seed). */
347
461
  private firstUserTranscript;
@@ -367,8 +481,28 @@ export declare class RealtimeSessionRuntime {
367
481
  SetMinimized(minimized: boolean): void;
368
482
  /** The provider-direct realtime client driving the live session (ClassFactory-resolved). */
369
483
  private client;
370
- /** The mic capture stream — acquired here (permission UX) and handed to the client. */
484
+ /**
485
+ * The mic capture stream — acquired here (permission UX) and handed to the client. A microphone the controller opens
486
+ * again after the call lost one replaces it.
487
+ */
371
488
  private localStream;
489
+ /**
490
+ * Whether the user muted the call's microphone, as {@link ToggleMute} last reported it. While the call has no microphone
491
+ * track there is nothing to mute, and a microphone opened again after the call lost one starts this way.
492
+ */
493
+ private microphoneMuted;
494
+ /** The host's camera-and-microphone controller for this session, when the host offers one. */
495
+ private localMedia;
496
+ /** Follows the controller's microphone, so a swapped-in track or a reopened microphone reaches the driver and the recorder. */
497
+ private localMediaSubscription;
498
+ /**
499
+ * Whether the call offers its microphone for switching ({@link Microphone$}, {@link SwitchMicrophone}): from the moment
500
+ * the client is connected until the call ends. Not before: a driver still connecting may have bound the old track.
501
+ */
502
+ private microphoneOffered;
503
+ /** The live session's camera and screen share; created once the client is connected. */
504
+ private captures;
505
+ private capturesSubscription;
372
506
  private agentSessionId;
373
507
  /**
374
508
  * The application the active session runs in (sources the server-side app config cascade +
@@ -410,8 +544,15 @@ export declare class RealtimeSessionRuntime {
410
544
  private recordingStartedAtIso;
411
545
  /** Interval that flushes ~15s crash-recovery shards to the server during a recording. */
412
546
  private segmentTimer;
413
- /** 0-based index of the next recording shard to upload. */
414
- private segmentIndex;
547
+ /**
548
+ * Owns shard indexing and retry for the current recording: it assigns each shard its index and
549
+ * keeps failed shards to resend under that same index. A fresh queue per recording keeps a flush
550
+ * still in flight from a previous session off this session's indexes. Null when not recording.
551
+ * A recording that ends discards its queue, and any retained shard with it: the consolidated
552
+ * end-of-call upload supersedes them, and if that upload is lost too, server-side recovery
553
+ * (#5196) fills the gap with silence.
554
+ */
555
+ private shardQueue;
415
556
  /** How often crash-recovery shards are flushed during a recording. */
416
557
  private static readonly segmentFlushMs;
417
558
  /**
@@ -486,6 +627,11 @@ export declare class RealtimeSessionRuntime {
486
627
  private pendingUsageInput;
487
628
  /** Accumulated output-token delta since the last flush. */
488
629
  private pendingUsageOutput;
630
+ /**
631
+ * Accumulated per-modality usage since the last flush (input and output detail blocks, avatar
632
+ * video seconds included), relayed in full; `null` when no update carried any.
633
+ */
634
+ private pendingUsageDetails;
489
635
  /** Pending debounced usage flush; also force-flushed at teardown. */
490
636
  private usageFlushTimer;
491
637
  /** Active push-status subscription that feeds delegation progress; cleared on teardown. */
@@ -528,7 +674,20 @@ export declare class RealtimeSessionRuntime {
528
674
  */
529
675
  private teardownInFlight;
530
676
  /**
531
- * Why the last session start failed, or `null` when none has.
677
+ * The latest start's opening of the host's microphone, until {@link IRealtimeMediaHost.ReleaseMicrophone} hands it
678
+ * back; `null` when no opening is owed a release, so a session that ends before asking for the microphone releases
679
+ * nothing.
680
+ *
681
+ * The host gets one release per opening, after the opening returns. A release while the microphone is still
682
+ * opening undoes nothing: the React Native host puts iOS into the call audio category inside `AcquireMicrophone`,
683
+ * after the permission prompt. So {@link runTeardown} releases an opening that has returned and leaves one still
684
+ * opening to its start, which releases it once it returns ({@link unwindAbandonedStart}), unless a newer start has
685
+ * opened the microphone since; that start's own end releases it. Whoever releases clears this first, so no
686
+ * opening is released twice.
687
+ */
688
+ private microphoneLease;
689
+ /**
690
+ * Why the latest session start failed, or `null` when it has not. Each start clears it ({@link beginSessionStart}).
532
691
  *
533
692
  * The runtime reports failure as `'error'` on {@link ConnectionState$}, which is enough to show
534
693
  * *that* something went wrong but not *what* — and the difference matters at exactly one point:
@@ -537,6 +696,11 @@ export declare class RealtimeSessionRuntime {
537
696
  * runtime's own try/catch, so the host never sees that rejection itself.
538
697
  */
539
698
  private lastStartError;
699
+ /**
700
+ * The fatal error the call's client reported, or `null` when it has reported none. Each start clears it
701
+ * ({@link beginSessionStart}). See {@link LastSessionError}.
702
+ */
703
+ private lastSessionError;
540
704
  /** Debounce window for persisting a channel's state of record after a change burst. */
541
705
  private static readonly channelSaveDebounceMs;
542
706
  /**
@@ -561,8 +725,82 @@ export declare class RealtimeSessionRuntime {
561
725
  */
562
726
  get Provider(): IMetadataProvider;
563
727
  set Provider(value: IMetadataProvider | null);
728
+ private _launcher;
729
+ /**
730
+ * How this runtime mints a session — the stock `StartRealtimeClientSession` mutation unless a host
731
+ * installs its own {@link IRealtimeSessionLauncher} (a guest-session exchange, a deployment-specific
732
+ * mutation). Everything after the mint — channel scoping, the driver, transcripts, teardown — is
733
+ * unchanged. Assign `null` to restore the default.
734
+ */
735
+ get Launcher(): IRealtimeSessionLauncher;
736
+ set Launcher(value: IRealtimeSessionLauncher | null);
737
+ /**
738
+ * Loads channel code on demand. Called, with the `ClientPluginClass` keys of every channel about to be
739
+ * resolved (the registry's rows plus the host's declarations), after the registry is read and before any
740
+ * plugin is built — so a host can register a heavy channel's class only when the session could use it.
741
+ * It must resolve once the classes it knows are registered; a rejection is logged, never fatal.
742
+ */
743
+ ChannelClassLoader: ((clientPluginClasses: readonly string[]) => Promise<void>) | null;
744
+ private readonly _sessionEvents$;
745
+ /**
746
+ * Every event the server publishes to THIS session, for the session's life (identity verification
747
+ * today; apps add their own types). Hot, with no replay — subscribe before the session starts, once,
748
+ * and it keeps working across sessions. The runtime has already acted on the events it understands
749
+ * (see {@link handleIdentityVerified}) by the time one reaches here; this stream is for the host UI.
750
+ *
751
+ * Silent when the session's provider has no subscription transport (see {@link createSessionEventSource}).
752
+ */
753
+ readonly SessionEvents$: Observable<RealtimeSessionStreamEvent>;
754
+ /** Keeps the live session's event stream open; null between sessions. */
755
+ private sessionEventHub;
756
+ /** Per-type handlers the runtime runs (before publishing on {@link SessionEvents$}) for events it understands. */
757
+ private readonly sessionEventHandlers;
758
+ /**
759
+ * What the agent should SAY the moment the user's identity is verified, or `null` (the default) to
760
+ * say nothing — the agent is still told, silently, through a context note. A string is used as the
761
+ * instruction verbatim; a function receives the verified payload (e.g. to greet by name) and returns
762
+ * the instruction, or `null` to stay silent for this verification.
763
+ */
764
+ IdentityVerifiedSpokenResponse: string | ((payload: IdentityVerifiedEventPayload) => string | null) | null;
765
+ private readonly clientDeadline;
766
+ /**
767
+ * The client's copy of the session's absolute deadline (`null` when none is known). A host that
768
+ * knows the session's cap at start sets it with {@link SetSessionDeadline}; verification extends it
769
+ * from the server's `identity.verified` payload. The SERVER enforces the real deadline — this exists
770
+ * so the host can end the call gracefully first and show a countdown. It only ever moves later by
771
+ * the server's word, so nothing a client does can extend the real one.
772
+ */
773
+ get SessionDeadline$(): Observable<Date | null>;
774
+ /** Synchronous access to {@link SessionDeadline$}. */
775
+ get SessionDeadline(): Date | null;
776
+ /** Sets the baseline deadline a host knows at session start (`null` clears). See {@link SessionDeadline$}. */
777
+ SetSessionDeadline(deadline: Date | null): void;
778
+ /**
779
+ * Builds the transport that reads the session's events. The default rides the session's GraphQL
780
+ * provider; it returns `null` — "no session events here" — for a provider with no subscription
781
+ * support (a test double, a host on another transport), which the runtime tolerates. Override to
782
+ * supply another transport.
783
+ */
784
+ protected createSessionEventSource(): IRealtimeSessionEventSource | null;
785
+ /** Opens the live session's event stream. Best-effort: never disturbs the call. */
786
+ private startSessionEvents;
787
+ private stopSessionEvents;
788
+ /** Routes one event to its per-type handler, then publishes it. An event for another session is dropped. */
789
+ private routeSessionEvent;
790
+ /**
791
+ * `identity.verified`: keep the client's deadline in step with the server's, tell the model — as a
792
+ * silent, structured note — who it is now talking to, and, when the host configured it, have it say
793
+ * something about that. The payload is validated again here (it crossed a wire); the name rides as
794
+ * JSON so user-entered text can never break out of the note's frame.
795
+ */
796
+ private handleIdentityVerified;
564
797
  /** True when a session is currently open. */
565
798
  get IsActive(): boolean;
799
+ /**
800
+ * True when a session is open or starting for `conversationId`. Ids are compared without
801
+ * regard to case.
802
+ */
803
+ IsActiveFor(conversationId: string | null | undefined): boolean;
566
804
  /**
567
805
  * Start a client-direct voice session fronting `targetAgentId`.
568
806
  *
@@ -600,8 +838,15 @@ export declare class RealtimeSessionRuntime {
600
838
  * the server-side Media channel resolves THIS collection as the agent's media kit for the session,
601
839
  * taking precedence over the agent's `DefaultMediaCollectionID`. The server UUID-validates it
602
840
  * (malformed ⇒ ignored, the agent default applies). Omit/`null` to use the agent default kit.
841
+ * @param applicationId Optional application the session runs in (sources the app config cascade,
842
+ * including `Application.AgentSettings.Realtime.Channels`).
843
+ * @param appContext Optional live app-context snapshot injected into the companion prompt at mint.
844
+ * @param options Optional per-start extras — chiefly the channels the HOST brings
845
+ * ({@link RealtimeSessionStartOptions.HostChannels}), the way a connect-only embed with no
846
+ * registry gets channels at all, and the fork the session starts on
847
+ * ({@link RealtimeSessionStartOptions.ConversationBranchID}; omitted is Main, where its transcript is written).
603
848
  */
604
- StartRealtimeSession(targetAgentId: string, conversationId?: string | null, lastSessionId?: string | null, agentName?: string | null, preferredModelId?: string | null, clientTools?: RealtimeToolDefinition[] | null, coAgentId?: string | null, configOverridesJson?: string | null, recordingConsent?: boolean | null, mediaCollectionId?: string | null, applicationId?: string | null, appContext?: AppContextSnapshot | null): Promise<void>;
849
+ StartRealtimeSession(targetAgentId: string, conversationId?: string | null, lastSessionId?: string | null, agentName?: string | null, preferredModelId?: string | null, clientTools?: RealtimeToolDefinition[] | null, coAgentId?: string | null, configOverridesJson?: string | null, recordingConsent?: boolean | null, mediaCollectionId?: string | null, applicationId?: string | null, appContext?: AppContextSnapshot | null, options?: RealtimeSessionStartOptions | null): Promise<void>;
605
850
  /**
606
851
  * Declares which provider keys this host can actually carry audio for.
607
852
  *
@@ -641,42 +886,173 @@ export declare class RealtimeSessionRuntime {
641
886
  * owns that half too.
642
887
  *
643
888
  * @param result The minted session — the same ten fields the `StartRealtimeClientSession`
644
- * mutation returns. `EphemeralToken` and `Provider` are what actually open the call.
889
+ * mutation returns. `EphemeralToken` and `Provider` are what actually open the call; a relay
890
+ * session opens with its `Transport` and `RelayUrl` instead, so a host must pass those on.
645
891
  * @param options Host-side inputs the result cannot carry; see {@link RealtimeSessionRunOptions}.
646
892
  * Every field defaults exactly as its {@link StartRealtimeSession} counterpart does.
647
893
  */
648
894
  StartRealtimeSessionFromResult(result: StartRealtimeClientSessionResult, options?: RealtimeSessionRunOptions): Promise<void>;
649
895
  /**
650
- * Start prologue shared by both entry points: bind the app layer, publish the agent name, reset
651
- * per-session state, and flip the session live (which is ALSO what makes the `IsActive` guard
652
- * suppress duplicate starts while the mint is still in flight — hence it runs before minting, not
653
- * after). Returns the resolved recording consent, which the mint half reports to the server and
654
- * the run half uses to decide whether to record.
896
+ * Start prologue shared by both entry points: clear the last start's failure, bind the app layer,
897
+ * publish the agent name, reset per-session state, and flip the session live (which is ALSO what
898
+ * makes the `IsActive` guard suppress duplicate starts while the mint is still in flight — hence it
899
+ * runs before minting, not after). Returns the resolved recording consent, which the mint half
900
+ * reports to the server and the run half uses to decide whether to record.
655
901
  */
656
902
  private beginSessionStart;
657
903
  /**
658
904
  * The RUN half of a session start, shared by both entry points: consume the minted result, open
659
905
  * the provider connection, and go live. `inputConversationId` is the conversation the START asked
660
- * for (null ⇒ "server, make me one") — the result alone can't distinguish the two.
906
+ * for (null ⇒ "server, make me one") — the result alone can't distinguish the two. `cameraCheck` is
907
+ * whether the host shows a camera check ({@link RealtimeSessionStartOptions.CameraCheck}).
661
908
  */
662
909
  private runMintedSession;
910
+ /**
911
+ * Tells the host, once per call, why it shows no avatar ({@link AvatarNotice$}): from the mint's status, whether this
912
+ * session asked for the agent's video, and whether that track is live now that the call is connected.
913
+ */
914
+ private publishAvatarNotice;
663
915
  /**
664
916
  * Releases everything a start acquired after the host had already ended the session.
665
917
  *
666
918
  * Reached only when {@link teardown} ran while this start was awaiting the microphone or the
667
- * provider connection. Teardown found nothing to release because nothing existed yet, so this
668
- * start owns the cleanup: stop the microphone, close the provider connection, and close the
669
- * server-side session row that the mint created.
919
+ * provider connection, whether that wait then succeeded or failed: the host has ended the call,
920
+ * so a failure there is not reported. Teardown could not release what did not exist yet, so this
921
+ * start owns the cleanup: stop the microphone, hand it back to the host when teardown left that
922
+ * to it ({@link microphoneLease}), close the provider connection, and close the server-side
923
+ * session row that the mint created when no teardown has closed it or is closing it.
924
+ *
925
+ * @param lease This start's opening of the microphone.
670
926
  */
671
927
  private unwindAbandonedStart;
672
928
  /**
673
- * Why the last session start failed, or `null` when the last start succeeded or none has run.
929
+ * Opens the user's microphone: through the host's controller when it offers one, otherwise through
930
+ * {@link IRealtimeMediaHost.AcquireMicrophone}. A failure is returned rather than thrown, so the caller can
931
+ * first check whether the start was abandoned meanwhile (a teardown disposes the controller, which fails a
932
+ * start still in flight) and unwind quietly instead of reporting an error nobody is waiting for.
933
+ */
934
+ private openMicrophone;
935
+ /**
936
+ * Opens the provider connection. As with {@link openMicrophone}, a failure is returned rather than thrown, so the
937
+ * caller can first check whether the start was abandoned meanwhile and unwind quietly instead of reporting an error
938
+ * nobody is waiting for: a teardown disconnects the client, which with most drivers fails a connection still opening.
939
+ */
940
+ private connectClient;
941
+ /**
942
+ * Moves the driver and the recorder onto the call's microphone whenever the controller gives it a new track. A device
943
+ * switch, or a lost device replaced by the default, swaps a track into the same stream, and the controller carries the
944
+ * old track's mute over. A microphone opened again after the call lost one comes in a new stream: its track takes the
945
+ * call's mute, and the stream becomes the one {@link ToggleMute} and the teardown act on. Mid-swap the stream holds no
946
+ * track, so only a finished swap is followed. Every report also keeps {@link Microphone$} current once the call offers
947
+ * it.
948
+ */
949
+ private followMicrophone;
950
+ /** The client is connected: the call offers its microphone on {@link Microphone$}, and {@link SwitchMicrophone} moves it. */
951
+ private offerMicrophone;
952
+ /** Publishes the call's microphone from the controller's state while the call offers it, when it names a change. */
953
+ private publishMicrophone;
954
+ /** Stops following the controller and disposes it, which releases its devices; the call no longer offers a microphone. */
955
+ private closeLocalMedia;
956
+ /**
957
+ * Creates the session's camera and screen share and mirrors their state on {@link Captures$}. With `cameraCheck`, the
958
+ * call's first camera start waits for {@link ConfirmCamera}.
959
+ */
960
+ private openCaptures;
961
+ /**
962
+ * The host's frame sampler for the captures ({@link IRealtimeMediaHost.CreateFrameSampler}), or `undefined` when it offers
963
+ * none and the captures use the DOM default.
964
+ */
965
+ private hostFrameSampler;
966
+ /** Stops the camera and screen share, if any, and reports both off. */
967
+ private closeCaptures;
968
+ /** Works out which captures the call offers ({@link CaptureOffers$}) and publishes a change. */
969
+ private refreshCaptureOffers;
970
+ /** Whether the host can open a capture and the call's policy admits it. */
971
+ private canOffer;
972
+ /** What a capture reports when there is no live session to show it to. */
973
+ private noSessionCapture;
974
+ /**
975
+ * The session's policy for a capture. The channel that fronts it (its `CaptureKind`) must be in the session, open or
976
+ * advertised, and the server's policy must let the agent see pixels through it (the agent's configuration and any
977
+ * zero-data-retention rule). The user's own "agent can see" choice for the channel does not refuse a start: it decides
978
+ * whether the frames reach the model.
979
+ */
980
+ private admitCapture;
981
+ /** The channel in the session, open or advertised, that fronts a capture. */
982
+ private findCaptureChannel;
983
+ /**
984
+ * Brings up the channel that fronts a capture once the capture is starting, so the host shows its surface: an advertised
985
+ * channel is opened (and announces itself to the agent), an open one counts as used.
986
+ */
987
+ private revealCaptureChannel;
988
+ /**
989
+ * Starts the user's camera and shows it to the agent. Call it from the user's click: it may ask for camera
990
+ * permission. Resolves with the camera's state; a failure (no session, a model that takes no video, a refused
991
+ * permission) is a state with a message, never a throw. While the camera is on, it is a source on
992
+ * {@link VideoSources$}, and it stops by itself if the device goes away.
993
+ *
994
+ * When the host shows a camera check ({@link RealtimeSessionStartOptions.CameraCheck}), the call's first start
995
+ * resolves still starting, with `Checking` set and the stream for the host to preview. The agent sees the camera
996
+ * once the host calls {@link ConfirmCamera}; {@link StopCamera} is the user's "not now".
997
+ *
998
+ * @param deviceId The camera to open; the system default when absent.
999
+ */
1000
+ StartCamera(deviceId?: string): Promise<RealtimeCaptureState>;
1001
+ /** Stops the user's camera. Safe to call when it is off. */
1002
+ StopCamera(): void;
1003
+ /**
1004
+ * Moves the user's camera to another device while it is open (being checked or on): one of the camera's `Devices` on
1005
+ * {@link Captures$}. The stream stays the same. Resolves with the camera's state; nothing changes while the camera is
1006
+ * off.
1007
+ *
1008
+ * @param deviceId The camera to move to.
1009
+ */
1010
+ SwitchCamera(deviceId: string): Promise<RealtimeCaptureState>;
1011
+ /**
1012
+ * Moves the call's microphone to another device: one of the `Devices` on {@link Microphone$}. The host's controller
1013
+ * swaps the new track into the same stream and carries the mute over, and the driver and the call's recording move to
1014
+ * it, so the agent keeps hearing the user. When the new microphone cannot open, the controller goes back to the one in
1015
+ * use. While the call has no microphone open (it lost the one in use and the default, or the last one went away), the
1016
+ * pick opens one again: the driver and the recording move to it, and it keeps the call's mute. Resolves with the
1017
+ * microphone's state once the switch is over. Nothing changes outside a call or on a host without a controller.
1018
+ *
1019
+ * @param deviceId The microphone to move to.
1020
+ */
1021
+ SwitchMicrophone(deviceId: string): Promise<RealtimeMicrophoneState>;
1022
+ /**
1023
+ * The user checked the camera and turned it on: the agent sees it from now on, and later camera starts in this call
1024
+ * skip the check. Returns the camera's state; unless the camera is waiting for its check (see {@link StartCamera}),
1025
+ * nothing changes.
1026
+ */
1027
+ ConfirmCamera(): RealtimeCaptureState;
1028
+ /**
1029
+ * Asks the user for a screen, window or browser tab (or one panel of the page) and shows it to the agent. Call it
1030
+ * from the user's click: the browser's picker needs one. Resolves with the share's state; a failure (no session,
1031
+ * a model that takes no video, a closed picker, a host that cannot share) is a state with a message. The share
1032
+ * also stops when the user ends it from the browser's own bar.
1033
+ *
1034
+ * @param options What the picker offers first, or the panel to share and its name (`PanelLabel`), which the share's
1035
+ * state carries and the agent sees as "<name> (shared panel)".
1036
+ */
1037
+ StartScreenShare(options?: DisplayCaptureOptions): Promise<RealtimeCaptureState>;
1038
+ /** Stops the user's screen share. Safe to call when nothing is shared. */
1039
+ StopScreenShare(): void;
1040
+ /**
1041
+ * Why the latest session start failed, or `null` when it has not failed or none has run.
674
1042
  *
675
1043
  * Read it when {@link ConnectionState$} reports `'error'`, to tell a denied microphone apart from
676
- * a provider or backend failure and show copy the user can act on. Cleared at the start of every
677
- * session.
1044
+ * a provider or backend failure and show copy the user can act on. Each start clears it before it
1045
+ * reports `'connecting'`, so it never describes an earlier start.
678
1046
  */
679
1047
  get LastStartError(): Error | null;
1048
+ /**
1049
+ * The fatal error the call's client reported, or `null` when it has reported none.
1050
+ *
1051
+ * Read it when {@link ConnectionState$} reports `'error'` on a call that was live, to tell a call that reached its time
1052
+ * limit (`Code` `'time-limit'`; {@link DescribeCallEnd} words it) from a dropped connection. Each start clears it before
1053
+ * it reports `'connecting'`.
1054
+ */
1055
+ get LastSessionError(): RealtimeClientError | null;
680
1056
  /**
681
1057
  * The single failure path for a session start (mint half or run half): report it, latch the
682
1058
  * overlay into 'error', and unwind whatever the half-built session already opened.
@@ -701,7 +1077,11 @@ export declare class RealtimeSessionRuntime {
701
1077
  * No-op when no session is open / the control channel isn't ready, or when the text is empty.
702
1078
  */
703
1079
  SendText(text: string): void;
704
- /** Mute / unmute the local microphone track. Returns the new muted state. */
1080
+ /**
1081
+ * Mute / unmute the local microphone track. Returns the new muted state. While the call has no microphone track (a
1082
+ * switch is putting a new one in, or the call lost its microphone) there is nothing to mute: nothing changes, and it
1083
+ * returns the call's mute, which the next microphone keeps. Outside a call it returns `false`.
1084
+ */
705
1085
  ToggleMute(): boolean;
706
1086
  /**
707
1087
  * Registers a handler for CLIENT-EXECUTED UI tools whose names start with `toolNamePrefix`
@@ -748,9 +1128,17 @@ export declare class RealtimeSessionRuntime {
748
1128
  */
749
1129
  get Client(): BaseRealtimeClient | null;
750
1130
  /**
751
- * Relays a video frame to the underlying realtime client if active.
1131
+ * Relays a video frame straight to the underlying realtime client, if a session is live.
1132
+ *
1133
+ * It bypasses the session's {@link VideoSourceArbiter}: the frame overrides the source the arbiter picked, and the
1134
+ * model is not told what it is looking at. Channels no longer send through it (their context's `SendVideoFrame` goes
1135
+ * through the arbiter); a host that shows the model something registers a source there instead, as
1136
+ * `ChannelInboundVideoBridge` does.
1137
+ *
1138
+ * @returns `true` when the client accepted and sent the frame; `false` when no session is live, the
1139
+ * client cannot take video frames, or the client dropped the frame.
752
1140
  */
753
- SendVideoFrame(base64Image: string, mimeType?: string): void;
1141
+ SendVideoFrame(base64Image: string, mimeType?: string): boolean;
754
1142
  /**
755
1143
  * Checks whether a media track is established on the active realtime client.
756
1144
  */
@@ -803,50 +1191,212 @@ export declare class RealtimeSessionRuntime {
803
1191
  private pulseLiveness;
804
1192
  /**
805
1193
  * Uploads the chunks captured since the last flush as one crash-recovery shard (durability only;
806
- * the canonical file is still the full upload at teardown). Best-effort — never disturbs the call.
1194
+ * the canonical file is still the full upload at teardown). Best-effort — never disturbs the call:
1195
+ * a failed shard is retained by the queue and resent under its own index on later flushes, up to a
1196
+ * bounded number of attempts (#5197).
807
1197
  */
808
1198
  private flushRecordingSegment;
809
1199
  /**
810
- * Stops the active recorder and uploads the captured audio via `UploadRealtimeRecording`.
811
- * Fully best-effort and wrapped in try/catch — recording upload must NEVER block teardown.
812
- * No-op when nothing was recorded or there's no session id to attach the file to.
1200
+ * Sends one shard and reports whether the server stored it. The resolver reports a failed
1201
+ * ownership load or storage write as `false`, not as an error — that is how #5197 lost a shard
1202
+ * without a single client-side log line — so the typed result must be checked, not just the absence
1203
+ * of a throw.
1204
+ */
1205
+ private uploadRecordingShard;
1206
+ /**
1207
+ * Stops the active recorder and encodes the captured audio. Split from the upload so teardown can
1208
+ * close the server session in between (#5195). Best-effort: returns `null` when nothing was
1209
+ * recorded or the stop failed (warned) — it must NEVER block teardown.
1210
+ */
1211
+ private stopRecording;
1212
+ /**
1213
+ * Uploads a recording returned by {@link stopRecording}. Never throws. No-op when there is
1214
+ * nothing to upload; warns when there is no session id to attach the file to.
813
1215
  */
814
- private stopAndUploadRecording;
1216
+ private uploadStoppedRecording;
815
1217
  /**
816
- * Runs the `UploadRealtimeRecording` mutation; failures are logged, never thrown. Sends the
1218
+ * Runs the `UploadRealtimeRecording` mutation. A `Success:false` payload is logged; a transport
1219
+ * rejection propagates to `uploadStoppedRecording`, which catches and logs it. Sends the
817
1220
  * capture-time waveform `peaks` (max-abs per bucket, normalized 0..1) so the server can persist a
818
1221
  * `peaks.json` sidecar for fast waveform rendering without re-decoding the audio.
819
1222
  */
820
1223
  private uploadRecording;
1224
+ /** Whether the channel catalog note still has to be sent (see {@link flushChannelCatalogNote}). */
1225
+ private catalogNotePending;
1226
+ /** Plugins resolved for the current start but not yet initialized. Consumed by {@link activateChannels}. */
1227
+ private preparedChannels;
1228
+ /** The scope the browser resolves on its own — the fallback when no server policy comes back. */
1229
+ private localChannelScope;
1230
+ /** `on-demand` channels that are in the session but not opened yet (initialized only when opened). */
1231
+ private advertisedChannels;
1232
+ /** What the policy resolved for each in-session channel, keyed by normalized channel key. */
1233
+ private readonly resolvedChannels;
1234
+ /** The prior session's saved channel states, kept so an `on-demand` channel opened later is restored too. */
1235
+ private priorChannelStates;
1236
+ /** The app/static client-tool tiers the server resolved for this session. */
1237
+ private sessionClientTools;
1238
+ /** Validates and routes channel-addressed `ContextTool` calls. */
1239
+ private readonly channelDispatcher;
1240
+ /**
1241
+ * The resolved behavior of an in-session channel (display, exposure ceiling, config, what put it
1242
+ * in the session), or `null` when the channel is not in this session. Hosts and overlays read the
1243
+ * resolved display from here rather than from the channel's code default.
1244
+ *
1245
+ * @param channelName The channel's key (case-insensitive).
1246
+ */
1247
+ GetResolvedChannel(channelName: string): ResolvedRealtimeChannel | null;
1248
+ /** Where the user's per-channel "how much can the agent see" choices are kept. Replace with {@link SetExposurePreferences}. */
1249
+ private exposurePreferences;
1250
+ /**
1251
+ * Replaces the store the user's per-channel exposure choices are kept in. The default persists them in
1252
+ * the signed-in user's settings (`mj.realtime.visualPerception.v1`) and keeps them in memory for an
1253
+ * anonymous principal or a connect-only embed; a host that remembers them somewhere else supplies its own.
1254
+ */
1255
+ SetExposurePreferences(preferences: IChannelExposurePreferences): void;
1256
+ /** The store the user's per-channel exposure choices are currently kept in (so a host can wrap it). */
1257
+ get ExposurePreferences(): IChannelExposurePreferences;
1258
+ /** Whether user settings can be written: a signed-in user on a provider that has entity metadata. */
1259
+ private canPersistUserSettings;
1260
+ /**
1261
+ * How much of a channel the model may perceive right now: the channel's ceiling, lowered by the server's
1262
+ * policy and the user's choice. `null` when the channel is not in this session.
1263
+ *
1264
+ * @param channelName The channel's key (case-insensitive).
1265
+ */
1266
+ GetChannelExposure(channelName: string): RealtimeChannelExposure | null;
1267
+ /**
1268
+ * The user's own choice of how much of a channel the agent may perceive. Takes effect immediately
1269
+ * (frames and notes the new level forbids stop, and the model is told), and is remembered per channel key
1270
+ * for next time. The level can only LOWER what the server's policy allows; choosing `'pixels'` for a channel
1271
+ * the agent's policy capped at `'state'` changes nothing.
1272
+ *
1273
+ * @param channelName The channel's key (case-insensitive).
1274
+ * @param level The user's choice; `undefined` clears it (back to what policy allows).
1275
+ * @returns `false` when the channel is not in this session (nothing is applied, but the choice is still remembered).
1276
+ */
1277
+ SetUserChannelExposure(channelName: string, level: RealtimeChannelExposure | undefined): boolean;
1278
+ /** Pushes a channel's current exposure inputs (server policy, user choice) into the channel. */
1279
+ private applyChannelExposure;
1280
+ /**
1281
+ * Sends a frame from a channel's context ({@link RealtimeChannelContext.SendVideoFrame}, deprecated) through the
1282
+ * session's {@link VideoSourceArbiter}, as the channel's own source: it reaches the model only while the arbiter picks
1283
+ * that source, and the arbiter tells the model when it switches. The source registers with the first frame, under the
1284
+ * id the channel's own frame bridge uses, switched on only while the channel's exposure allows pixels.
1285
+ */
1286
+ private sendChannelVideoFrame;
1287
+ /**
1288
+ * Keeps a channel's video source at the arbiter, when it has one, in step with the channel's exposure: on while the
1289
+ * agent may see its pixels, off otherwise. Quiet, because the channel tells the model about its own exposure change.
1290
+ */
1291
+ private syncChannelVideoSource;
1292
+ /** Follows the session client's video-source arbiter so {@link VideoSources$} reflects it. */
1293
+ private watchVideoSources;
1294
+ /** The channel whose surface the user is looking at, kept so it applies to an arbiter created after it was set. */
1295
+ private focusedChannelKey;
1296
+ /**
1297
+ * Tells the session which channel's surface the user is looking at (`null` for none, e.g. the activity tab).
1298
+ * When the model can see only one video source and several are live, the one the user is looking at is the one
1299
+ * it sees (after an explicit pick, and after a camera or screen share the user started). Safe to call before the
1300
+ * session is live: it applies when the connection comes up.
1301
+ *
1302
+ * @param channelKey The focused channel's key, as it appears on {@link VideoSources$} entries' `ChannelKey`.
1303
+ */
1304
+ SetFocusedChannel(channelKey: string | null): void;
1305
+ /** Stops following the arbiter and clears {@link VideoSources$}. Safe to call when nothing is watched. */
1306
+ private unwatchVideoSources;
1307
+ private stopWatchingVideoSources;
1308
+ /**
1309
+ * Turns the agent's view of one video source on or off — what the "agent can see" control calls.
1310
+ *
1311
+ * A source that belongs to a channel goes through {@link SetUserChannelExposure}, so the choice is
1312
+ * remembered per channel and the channel itself tells the model. Any other source (a camera or screen
1313
+ * share that is not a channel) is switched at the arbiter, which tells the model.
1314
+ *
1315
+ * @param sourceId The source's id (from {@link VideoSources$}).
1316
+ * @param enabled Whether the agent may see it.
1317
+ * @returns `false` when there is no such source.
1318
+ */
1319
+ SetVideoSourceEnabled(sourceId: string, enabled: boolean): boolean;
821
1320
  /**
822
- * Resolves, instantiates and initializes the session's interactive-channel plugins from
823
- * the `MJ: AI Agent Channels` registry, publishes them on {@link ActiveChannels$}, and
824
- * returns their aggregated client-executed tool declarations for the session mint.
825
- * Tolerant by design: registry/resolution failures degrade to "no channels" — the voice
826
- * session itself always proceeds.
1321
+ * Picks the video source the agent sees: what the "agent can see" control calls when more sources are on than the
1322
+ * model takes. The pick beats every other rule (a camera or screen share the user started, the surface the user is
1323
+ * looking at) until it is cleared, or its source is turned off or leaves. The pick shows on {@link VideoSources$}
1324
+ * (`Picked`), and the model is told when what it sees changes.
1325
+ *
1326
+ * @param sourceId The source's id (from {@link VideoSources$}), or `null` to let the call decide again.
1327
+ * @returns `false` with no live session, or for a source that is not there or is turned off.
1328
+ */
1329
+ SelectVideoSource(sourceId: string | null): boolean;
1330
+ /**
1331
+ * The `on-demand` channels that are in the session but not open yet — what the agent can open
1332
+ * through `ContextTool`. A fresh array; empty before a session starts and after teardown.
1333
+ */
1334
+ get AdvertisedChannels(): readonly BaseRealtimeChannelClient[];
1335
+ /**
1336
+ * Resolves, initializes and mounts the session's channels in one step using the scope the browser
1337
+ * can decide alone (code defaults + host declarations), and returns the native tools to declare at
1338
+ * mint. This is the single-step composition of {@link prepareChannelScope} + {@link activateChannels}
1339
+ * that a session start spreads across the mint; it is kept as a unit because it is the cleanest seam
1340
+ * for exercising the plugin plumbing without a mint.
827
1341
  */
828
1342
  private startChannels;
829
1343
  /**
830
- * Loads the ACTIVE channel definitions from the registry and resolves each row's
831
- * `ClientPluginClass` through the MJ ClassFactory into a per-session plugin instance —
832
- * the client-side mirror of how realtime-model drivers resolve from `BaseRealtimeModel`
833
- * / `BaseRealtimeClient`. Rows whose plugin class isn't registered are skipped (logged),
834
- * never fatal.
1344
+ * Stage 1: constructs the channel plugins (registry rows and host declarations), reads their
1345
+ * descriptors, resolves the scope the browser can decide alone, and builds what the mint needs —
1346
+ * the native tools to declare and the candidates to report. Starts nothing.
1347
+ *
1348
+ * @param hostChannels Channels the host brings to this session.
1349
+ * @returns The native tools to declare at mint, the candidates as JSON (`null` when there are none,
1350
+ * so a channel-less session sends exactly the mint it always did), and whether the host could show the agent's
1351
+ * video ({@link PreparedChannelsShowAgentVideo}).
1352
+ */
1353
+ private prepareChannelScope;
1354
+ /**
1355
+ * Constructs a plugin for every active-or-inactive registry row and every host declaration. An
1356
+ * INACTIVE row is constructed too — only to learn its key so the kill switch can name it — and is
1357
+ * never initialized. A plugin whose descriptor cannot be read is skipped (logged), never fatal.
835
1358
  */
836
- private loadActiveChannels;
1359
+ private prepareChannels;
837
1360
  /**
838
- * Reads the ACTIVE `MJ: AI Agent Channels` rows. With entity metadata: from {@link AIEngineBase}'s
839
- * cached `AgentChannels` (provider-scoped engine instance, lazy `Config` — no RunView round-trip;
840
- * the engine's BaseEntity-event reactivity keeps the registry fresh). On a connect-only provider:
841
- * one `RunDynamicView` query ({@link fetchChannelDefinitionsOverGraphQL}). Failures are logged and
842
- * degrade to an empty list — channel availability must never block the voice session.
1361
+ * Gives a host that loads channel code on demand (a widget that keeps a heavy channel in its own download)
1362
+ * the chance to load it before the plugins are built. Never fatal: a loader that throws is logged and the
1363
+ * channel simply is not available, exactly as if its class had never been registered.
1364
+ *
1365
+ * @param keys The `ClientPluginClass` keys of every channel about to be resolved (blank/absent ones included).
1366
+ */
1367
+ private loadChannelClasses;
1368
+ /** Whether a prepared plugin's descriptor can be read; logs and rejects one that throws. */
1369
+ private canDescribe;
1370
+ /** Builds a plugin from a host declaration; `null` (logged) when it names nothing buildable. */
1371
+ private createHostChannelPlugin;
1372
+ /**
1373
+ * Stage 3: initializes the channels the resolved policy puts in the session. `open-on-start` and
1374
+ * `headless` channels are mounted and published on {@link ActiveChannels$}; `on-demand` channels
1375
+ * are held until opened. Prepared plugins the policy left out are dropped without ever having run.
1376
+ */
1377
+ private activateChannels;
1378
+ /**
1379
+ * Applies the mint's outcome to the channel set: the server's policy when it sent a usable one,
1380
+ * else the scope the browser resolved on its own. A policy can only SELECT among the plugins this
1381
+ * host prepared — it cannot add one.
1382
+ */
1383
+ private applySessionClientPolicy;
1384
+ /** Drops plugins that were prepared for a start that never mounted them. They were never initialized, so there is nothing to release. */
1385
+ private discardUnmountedChannels;
1386
+ /**
1387
+ * Loads the `MJ: AI Agent Channels` registry rows (ACTIVE AND INACTIVE — an inactive row is the master
1388
+ * kill switch, so the scope decision has to see it). With entity metadata: from {@link AIEngineBase}'s
1389
+ * cached `AgentChannels` (provider-scoped engine instance, lazy `Config` — no RunView round-trip; the
1390
+ * engine's BaseEntity-event reactivity keeps the registry fresh). On a connect-only provider: one
1391
+ * `RunDynamicView` query ({@link fetchChannelDefinitionsOverGraphQL}). Failures are logged and degrade to an
1392
+ * empty list — channel availability must never block the voice session.
843
1393
  */
844
1394
  private fetchChannelDefinitions;
845
1395
  /**
846
- * The connect-only path of {@link fetchChannelDefinitions}: the same ACTIVE `MJ: AI Agent Channels`
847
- * rows, read with a dynamic view because a connect-only client has no entity metadata to build a
848
- * typed RunView from. Same tolerance as the engine path — a failure is logged and means "no
849
- * channels", never a blocked session.
1396
+ * The connect-only path of {@link fetchChannelDefinitions}: the same `MJ: AI Agent Channels` rows, active
1397
+ * and inactive (an inactive row is the kill switch), read with a dynamic view because a connect-only client
1398
+ * has no entity metadata to build a typed RunView from. Same tolerance as the engine path — a failure is
1399
+ * logged and means "no channels", never a blocked session.
850
1400
  */
851
1401
  private fetchChannelDefinitionsOverGraphQL;
852
1402
  /**
@@ -854,45 +1404,108 @@ export declare class RealtimeSessionRuntime {
854
1404
  * checked first, exactly like the realtime-client drivers) and instantiates a fresh
855
1405
  * per-session plugin. Returns `null` (logged) when no plugin is registered for the key
856
1406
  * — e.g. its Load function was never called or the package isn't included client-side.
1407
+ * An inactive row is resolved quietly: it only needs to be named, and a missing plugin for a
1408
+ * channel nobody wants is not worth a warning.
857
1409
  */
858
1410
  private resolveChannelPlugin;
859
1411
  /**
860
- * Wires one plugin into the session: hands it its host context and registers its
861
- * prefix-routed local tool executor (so `<ToolNamePrefix>*` calls run in the browser
862
- * through {@link BaseRealtimeChannelClient.ApplyAgentTool}, never the server relay).
1412
+ * Wires one plugin into the session: hands it its host context (carrying its resolved config) and,
1413
+ * when it has native tools, registers its prefix-routed local tool executor (so `<ToolNamePrefix>*`
1414
+ * calls run in the browser through {@link BaseRealtimeChannelClient.ApplyAgentTool}, never the
1415
+ * server relay). A channel with no tool prefix has no native route — it is reached through
1416
+ * `ContextTool` — and registering an empty prefix would match EVERY tool name.
1417
+ */
1418
+ private mountChannel;
1419
+ /** Records that the agent acted on a channel (the overlay's first-use reveal signal). */
1420
+ private noteChannelActivity;
1421
+ /**
1422
+ * Mounts an advertised `on-demand` channel when the agent opens it: initializes it, restores its
1423
+ * prior-session state, publishes it on {@link ActiveChannels$} (so the overlay can give it a tab)
1424
+ * and, if the call is already connected, tells it so. If initialization throws the channel goes
1425
+ * back to being advertised, so the agent can retry or move on.
1426
+ */
1427
+ private mountAdvertisedChannel;
1428
+ /** Tells a channel the session is connected, containing anything it throws. */
1429
+ private notifyChannelSessionStarted;
1430
+ /** Finds a session channel (open or merely advertised) for the dispatcher. */
1431
+ private findDispatchableChannel;
1432
+ /** The keys the agent can address, for "unknown channel" messages — channels with nothing to address are left out. */
1433
+ private listAddressableChannelKeys;
1434
+ /**
1435
+ * Runs a channel-addressed `ContextTool` call (the {@link RealtimeChannelContext.DispatchContextAction}
1436
+ * implementation): validates it, opens an `on-demand` channel when asked, runs the verb, and on
1437
+ * success records the channel as used so the overlay reveals its tab. Never throws.
1438
+ */
1439
+ private dispatchContextAction;
1440
+ /**
1441
+ * Opens (and seeds) a channel from the HOST — the same path the agent's `open` action takes: the channel
1442
+ * is mounted if it was only advertised, its `Inputs` schema validates `inputs`, and it announces itself to
1443
+ * the model. A channel that is already open is re-seeded. Never throws; a refusal is a structured result.
1444
+ *
1445
+ * @param channelKey The channel to open (case-insensitive).
1446
+ * @param inputs Seed inputs, validated against the channel's descriptor.
863
1447
  */
864
- private initializeChannel;
1448
+ OpenChannel(channelKey: string, inputs?: JSONObject): Promise<RealtimeContextActionResult>;
1449
+ /**
1450
+ * Tells the model which channels exist and how to use them — rendered from the channels' own
1451
+ * descriptors, so a new channel needs no prompt change. Sent ONCE, the first moment the control
1452
+ * channel is usable — a context note sent before then is dropped, and `Connect` can resolve before the
1453
+ * provider reports `listening`, so this runs at start and again on each state change until it has gone.
1454
+ * A session whose only channels describe themselves through native tools sends nothing.
1455
+ */
1456
+ private flushChannelCatalogNote;
1457
+ /** One catalog entry: a channel's descriptor, whether it is open, and whether its tools were declared natively. */
1458
+ private catalogEntry;
865
1459
  /** Builds the host-services context one channel plugin sees (its only line to the session). */
866
1460
  private buildChannelContext;
867
1461
  /**
868
1462
  * Host registry of surface CLIENT TOOLS (Name → handler), fed by the host (Explorer) from the
869
- * active surface's `SetAgentClientTools`. The headless ClientContextChannel's `ContextTool` proxy
870
- * executes against this via {@link executeAppClientTool}. Keys are lower-cased for case-insensitive
871
- * model-supplied action names.
1463
+ * active surface's `SetAgentClientTools` and from its always-on globals. The headless
1464
+ * ClientContextChannel's `ContextTool` proxy executes against this via {@link executeAppClientTool}.
1465
+ * Owner-keyed — see {@link AppClientToolRegistry}.
1466
+ */
1467
+ private readonly appToolRegistry;
1468
+ /**
1469
+ * Registers the tools ONE source of the host can run for the realtime `ContextTool`, replacing only
1470
+ * that source's previous set — other owners' tools are untouched. A host with two sources (Explorer's
1471
+ * always-available globals and the active surface's tools) registers each under its own owner key, so
1472
+ * refreshing the surface's tools can neither drop the globals nor leave the previous surface's tools
1473
+ * behind. Passing `[]` clears just that owner. When two owners register the same tool name, the
1474
+ * later-registered owner wins.
1475
+ *
1476
+ * Calling it with one argument (the original form) registers under {@link DEFAULT_APP_TOOL_OWNER}, so
1477
+ * a host with a single source behaves exactly as before.
1478
+ *
1479
+ * @param tools The owner's complete current set (name + handler; description/schema optional).
1480
+ * @param owner A stable key naming the source (e.g. `'explorer.surface'`). Defaults to a shared owner.
872
1481
  */
873
- private readonly appClientToolHandlers;
1482
+ RegisterAppClientTools(tools: ReadonlyArray<AppClientToolRegistration>, owner?: string): void;
1483
+ /** Removes one owner's tools (and its place in the collision order). No-op when it has none. */
1484
+ UnregisterAppClientTools(owner: string): void;
1485
+ /** Removes every owner's tools. */
1486
+ ClearAppClientTools(): void;
874
1487
  /**
875
- * Replaces the set of host-registered surface client tools the realtime ContextTool can execute.
876
- * The host calls this at session start and whenever the active surface's tool set changes (the
877
- * continuous-capability half of client-context delivery). Passing `[]` clears them.
878
- *
879
- * @param tools The current surface client tools (name + handler). Descriptions/schemas ride the
880
- * app-context manifest separately; only the executable handler is needed here.
1488
+ * The client tools in effect right now, resolved through the same unified resolver the server uses
1489
+ * (`override → session → app → static`, first match wins): the host's registered tools (described
1490
+ * by the manifest the host streams when it did not describe them itself) first, then the app and
1491
+ * agent tiers the server resolved at mint. Only the host-registered tools can RUN here; the
1492
+ * other tiers describe what the app declares.
881
1493
  */
882
- RegisterAppClientTools(tools: ReadonlyArray<{
883
- Name: string;
884
- Handler: (params: Record<string, unknown>) => Promise<unknown> | unknown;
885
- }>): void;
1494
+ private resolveAppClientTools;
886
1495
  /**
887
1496
  * Executes a host-registered surface client tool by name (the {@link RealtimeChannelContext.ExecuteClientTool}
888
1497
  * implementation). Tolerant: an unknown tool or a thrown handler resolves to a structured
889
- * `Success: false` result the channel narrates — never throws.
1498
+ * `Success: false` result the channel narrates — never throws. A tool the app declares but the host has
1499
+ * not registered a handler for says so, instead of reading like a typo. A handler that returns an object with
1500
+ * `Success: false` also gives `Success: false`, with the handler's `ErrorMessage` and its object as `Result`.
890
1501
  *
891
1502
  * @param name The tool name (the model's `action`).
892
1503
  * @param params The tool parameters.
893
1504
  * @returns A structured result for the channel to serialize back to the model.
894
1505
  */
895
1506
  private executeAppClientTool;
1507
+ /** The model-readable reason a client tool could not run. */
1508
+ private describeMissingClientTool;
896
1509
  /**
897
1510
  * Runs a channel-specific GraphQL operation through the live session's provider (the
898
1511
  * {@link RealtimeChannelContext.ExecuteServerAction} implementation). Best-effort: any
@@ -914,6 +1527,8 @@ export declare class RealtimeSessionRuntime {
914
1527
  * never affected.
915
1528
  */
916
1529
  private applyPriorChannelStates;
1530
+ /** Offers one plugin its prior-session state, if there is one. Rejections and throws are logged, never fatal. */
1531
+ private restorePriorChannelState;
917
1532
  /**
918
1533
  * Persists a channel's state as a first-class versioned artifact (`MJ: Artifacts`) via the
919
1534
  * `SaveSessionChannelArtifact` mutation — the channel-context capability behind e.g. the
@@ -945,8 +1560,10 @@ export declare class RealtimeSessionRuntime {
945
1560
  private createRealtimeClient;
946
1561
  /**
947
1562
  * Builds the client-direct session config the realtime client connects with.
948
- * Aggregates tracks sourced by active channels under {@link REQUESTED_TRACKS_SESSION_KEY} so the driver
949
- * can negotiate them (e.g., establishing inbound video streaming for Whiteboard / RemoteBrowser).
1563
+ * Aggregates the tracks active channels source and sink under {@link REQUESTED_TRACKS_SESSION_KEY} so the
1564
+ * driver can negotiate them (e.g., inbound video for Whiteboard / RemoteBrowser, outbound video for a
1565
+ * channel that shows the agent's video). A track the model does not support resolves to `'unsupported'`.
1566
+ * The session's transport and relay URL go to the driver as minted (an unknown transport is left out: direct).
950
1567
  */
951
1568
  BuildClientConfig(session: StartRealtimeClientSessionResult): ClientRealtimeSessionConfig;
952
1569
  /** @deprecated Use {@link BuildClientConfig}. */
@@ -959,6 +1576,14 @@ export declare class RealtimeSessionRuntime {
959
1576
  private parseSessionConfig;
960
1577
  /** Subscribes this service's policy handlers to the realtime client's events. */
961
1578
  private wireClientHandlers;
1579
+ /**
1580
+ * The model's video arrived: publish it on {@link AgentVideo$}, and mark each channel that shows it (one that sinks
1581
+ * outbound video) as used, so the host shows its surface. A client the session has already let go of is ignored, so a
1582
+ * late frame cannot bring the video back after the call.
1583
+ */
1584
+ private onAgentVideo;
1585
+ /** Clears {@link AgentVideo$} when the call's client goes. */
1586
+ private clearAgentVideo;
962
1587
  /** Maps a client state event onto the UI connection state. */
963
1588
  private onClientStateChange;
964
1589
  /**
@@ -969,6 +1594,11 @@ export declare class RealtimeSessionRuntime {
969
1594
  private mapClientState;
970
1595
  /** True when the live control channel is usable (open and not torn down / failed). */
971
1596
  private isSessionLive;
1597
+ /**
1598
+ * Moves {@link ConnectionState$} to `state`, and {@link Resuming$} first: a `'connecting'` while the call is live, or
1599
+ * already resuming, is a resume; any other state ends one. Every state change goes through here.
1600
+ */
1601
+ private setConnectionState;
972
1602
  /**
973
1603
  * Applies transcript policy to client transcript events. Interim deltas don't become
974
1604
  * captions/turns (the client already drives the speaking state) but DO mark this turn's
@@ -1040,6 +1670,14 @@ export declare class RealtimeSessionRuntime {
1040
1670
  * for the surface panel's artifact tabs.
1041
1671
  */
1042
1672
  private emitDelegationResult;
1673
+ /**
1674
+ * Offers a delegated run's artifacts to the channels that host artifacts (an Interactive Component channel
1675
+ * shows a component artifact, or swaps in a newer version of one it already shows). Each channel is asked
1676
+ * first whether it wants them; an advertised, unopened channel that does is mounted before it is handed
1677
+ * them. One channel failing never stops the others, and nothing here can fail the delegation result,
1678
+ * which has already been emitted.
1679
+ */
1680
+ private offerDelegationArtifacts;
1043
1681
  /**
1044
1682
  * Cancels ONE in-flight delegated tool call — the overlay's per-card ✕ affordance.
1045
1683
  *
@@ -1073,7 +1711,12 @@ export declare class RealtimeSessionRuntime {
1073
1711
  * 0 on a structured failure or a thrown transport error (both logged, never thrown).
1074
1712
  */
1075
1713
  private cancelSessionTool;
1076
- /** Calls the `StartRealtimeClientSession` mutation to obtain an ephemeral token + config. */
1714
+ /**
1715
+ * Mints a session through the installed {@link Launcher} — by default the stock
1716
+ * `StartRealtimeClientSession` mutation (see {@link DefaultRealtimeSessionLauncher}). `showsAgentVideo` is whether
1717
+ * one of the session's channels could show the agent's video; without one the server asks the model for no avatar.
1718
+ * `conversationBranchId` is the fork the session starts on; `null` is Main.
1719
+ */
1077
1720
  private mintSession;
1078
1721
  /** Calls the `ExecuteRealtimeSessionTool` mutation; returns the ResultJson string. */
1079
1722
  private executeSessionTool;
@@ -1121,9 +1764,14 @@ export declare class RealtimeSessionRuntime {
1121
1764
  /**
1122
1765
  * Accumulates one usage DELTA from the realtime client (per-response token counts —
1123
1766
  * the `OnUsage` contract shape) and schedules the debounced relay. Negative / non-finite
1124
- * values are clamped to 0; an all-zero delta is dropped without arming the timer.
1767
+ * values are clamped to 0; an all-zero delta is dropped without arming the timer. The
1768
+ * update's input and output detail blocks add into the pending details by the record's
1769
+ * rules (amounts add up, inbound video running totals keep the larger value), so an update
1770
+ * that carries only avatar video seconds still counts.
1125
1771
  */
1126
1772
  private onUsageDelta;
1773
+ /** Schedules the debounced usage relay unless one is already pending. */
1774
+ private armUsageFlush;
1127
1775
  /** Clamps a driver-reported token delta: undefined / negative / non-finite become 0. */
1128
1776
  private clampUsageDelta;
1129
1777
  /**
@@ -1228,6 +1876,11 @@ export declare class RealtimeSessionRuntime {
1228
1876
  private teardown;
1229
1877
  /** The body of {@link teardown}; never called concurrently with itself. */
1230
1878
  private runTeardown;
1879
+ /**
1880
+ * Calls the host's {@link IRealtimeMediaHost.ReleaseMicrophone}. Best-effort by contract: a failure
1881
+ * is logged and never thrown, because it must never block ending a call.
1882
+ */
1883
+ private releaseMicrophone;
1231
1884
  /** Calls the `CloseAgentSession` mutation (provisioned in P4b). */
1232
1885
  private closeServerSession;
1233
1886
  /** Pushes a caption onto the live list (immutable update for change detection). */