@opengeni/sdk 0.52.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +67 -7
  2. package/dist/artifacts.js +5 -5
  3. package/dist/{chunk-FRJFNDIR.js → chunk-4DV37UUA.js} +6 -2
  4. package/dist/chunk-4DV37UUA.js.map +1 -0
  5. package/dist/{chunk-KIHGPM7H.js → chunk-A5DC5WEM.js} +288 -45
  6. package/dist/chunk-A5DC5WEM.js.map +1 -0
  7. package/dist/{chunk-V5F253OG.js → chunk-GU6JF75T.js} +6 -3
  8. package/dist/chunk-GU6JF75T.js.map +1 -0
  9. package/dist/{chunk-MZWTWOX6.js → chunk-ST5DDKJP.js} +327 -1
  10. package/dist/chunk-ST5DDKJP.js.map +1 -0
  11. package/dist/{chunk-FHI4DFIG.js → chunk-TYZ4JL4J.js} +2 -2
  12. package/dist/{chunk-ILQZR5N6.js → chunk-YGH5P47G.js} +693 -34
  13. package/dist/chunk-YGH5P47G.js.map +1 -0
  14. package/dist/{chunk-DXDL7EEW.js → chunk-YKZME56C.js} +197 -113
  15. package/dist/chunk-YKZME56C.js.map +1 -0
  16. package/dist/client.d.ts +108 -18
  17. package/dist/codex-realtime-controller.d.ts +5 -0
  18. package/dist/codex-realtime-controller.js +2 -2
  19. package/dist/codex-realtime-v3.d.ts +13 -2
  20. package/dist/core.js +7 -7
  21. package/dist/desktop.d.ts +8 -0
  22. package/dist/editable-artifacts.js +4 -4
  23. package/dist/gateway-realtime-transport.d.ts +1 -0
  24. package/dist/gateway-realtime-transport.js +5 -3
  25. package/dist/index.d.ts +6 -2
  26. package/dist/index.js +68 -34
  27. package/dist/index.js.map +1 -1
  28. package/dist/interaction-revision-stream.d.ts +11 -0
  29. package/dist/interaction.d.ts +604 -5
  30. package/dist/interaction.js +27 -1
  31. package/dist/realtime.d.ts +3 -3
  32. package/dist/realtime.js +11 -7
  33. package/dist/realtime.js.map +1 -1
  34. package/dist/types.d.ts +283 -69
  35. package/dist/workspace-live-stream.d.ts +14 -0
  36. package/package.json +2 -2
  37. package/src/client.ts +863 -62
  38. package/src/codex-realtime-controller.ts +239 -12
  39. package/src/codex-realtime-lifecycle.ts +1 -0
  40. package/src/codex-realtime-v3.ts +162 -31
  41. package/src/desktop.ts +11 -1
  42. package/src/errors.ts +16 -2
  43. package/src/gateway-realtime-transport.ts +252 -109
  44. package/src/index.ts +42 -13
  45. package/src/interaction-revision-stream.ts +117 -0
  46. package/src/interaction.ts +1049 -5
  47. package/src/realtime.ts +14 -4
  48. package/src/types.ts +353 -72
  49. package/src/workspace-live-stream.ts +137 -0
  50. package/dist/chunk-DXDL7EEW.js.map +0 -1
  51. package/dist/chunk-FRJFNDIR.js.map +0 -1
  52. package/dist/chunk-ILQZR5N6.js.map +0 -1
  53. package/dist/chunk-KIHGPM7H.js.map +0 -1
  54. package/dist/chunk-MZWTWOX6.js.map +0 -1
  55. package/dist/chunk-V5F253OG.js.map +0 -1
  56. /package/dist/{chunk-FHI4DFIG.js.map → chunk-TYZ4JL4J.js.map} +0 -0
@@ -33,6 +33,7 @@ import type {
33
33
  GatewayRealtimeConnectResponse,
34
34
  EndSessionRealtimeRequest,
35
35
  RenewSessionRealtimeRequest,
36
+ SessionRealtimeInboundEntry,
36
37
  SessionRealtimeMode,
37
38
  SessionRealtimeModel,
38
39
  SessionRealtimeMutationResponse,
@@ -50,6 +51,27 @@ export const CODEX_REALTIME_NEGOTIATION_TIMEOUT_MS = 20_000;
50
51
  const DEFAULT_CONNECTION_ROTATION_INTERVAL_MS = 15 * 60_000;
51
52
  const DEFAULT_RECONNECT_BACKOFF_MS = [250, 1_000, 2_000, 5_000] as const;
52
53
  const OWNER_RECORD_VERSION = 1;
54
+ const OWNER_DELEGATION_REPLAY_VERSION = 1;
55
+ const OWNER_DELEGATION_REPLAY_MAX_CALLS = 4_096;
56
+ const OWNER_DELEGATION_REPLAY_MAX_BYTES = 4 * 1024 * 1024;
57
+ const SESSION_REALTIME_INBOUND_ENTRY_KEYS = new Set([
58
+ "operationId",
59
+ "kind",
60
+ "role",
61
+ "providerEventId",
62
+ "delegationItemId",
63
+ "text",
64
+ "payload",
65
+ "modelContext",
66
+ ]);
67
+ const SESSION_REALTIME_INBOUND_KINDS = new Set([
68
+ "user_transcript",
69
+ "assistant_transcript",
70
+ "delegation_call",
71
+ "interruption",
72
+ "error",
73
+ ]);
74
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
53
75
 
54
76
  export type CodexRealtimeControllerStatus =
55
77
  | "idle"
@@ -123,6 +145,12 @@ export type CodexRealtimeControllerClient = {
123
145
  request: GatewayRealtimeConnectRequest,
124
146
  options?: { signal?: AbortSignal | undefined },
125
147
  ): Promise<GatewayRealtimeConnectResponse>;
148
+ negotiateXaiSubscriptionRealtime?(
149
+ workspaceId: string,
150
+ sessionId: string,
151
+ request: GatewayRealtimeConnectRequest,
152
+ options?: { signal?: AbortSignal | undefined },
153
+ ): Promise<GatewayRealtimeConnectResponse>;
126
154
  activateCodexRealtimeConnection(
127
155
  workspaceId: string,
128
156
  sessionId: string,
@@ -149,7 +177,9 @@ export type CodexRealtimeOwnerStorage = Pick<Storage, "getItem" | "setItem" | "r
149
177
 
150
178
  /** Canonical browser-owner storage namespace for a public realtime model. */
151
179
  export function sessionRealtimeOwnerStorageNamespace(model: SessionRealtimeModel): string {
152
- return model === "gpt-live-1-boulder-alpha" ? "codex-realtime-owner" : "gateway-realtime-owner";
180
+ if (model === "gpt-live-1-boulder-alpha") return "codex-realtime-owner";
181
+ if (model === "supergrok/grok-voice-think-fast-2.0") return "xai-realtime-owner";
182
+ return "gateway-realtime-owner";
153
183
  }
154
184
 
155
185
  /** Canonical browser-owner storage key shared by the SDK controller and React facade. */
@@ -205,6 +235,8 @@ export type CreateCodexRealtimeControllerOptions = {
205
235
  model?: SessionRealtimeModel | undefined;
206
236
  ownerStorageNamespace?: string | undefined;
207
237
  startTransport?: RealtimeControllerTransportStarter | undefined;
238
+ /** Model-visible application context captured with each durable realtime message. */
239
+ getModelContext?: (() => string | undefined) | undefined;
208
240
  };
209
241
 
210
242
  export type RealtimeControllerTransportStarter = (input: {
@@ -250,6 +282,13 @@ type OwnerRecord = {
250
282
  operationId: string;
251
283
  browserInstanceId: string;
252
284
  ownerKey: string;
285
+ delegationReplay?: OwnerDelegationReplay | undefined;
286
+ };
287
+
288
+ type OwnerDelegationReplay = {
289
+ version: typeof OWNER_DELEGATION_REPLAY_VERSION;
290
+ acceptedDelegationItemIds: string[];
291
+ pendingDelegations: SessionRealtimeInboundEntry[];
253
292
  };
254
293
 
255
294
  type ConnectionRuntime = {
@@ -330,6 +369,59 @@ export function createCodexRealtimeController(
330
369
  let recoveryTerminal = false;
331
370
  let mutationTail = Promise.resolve();
332
371
  let connectionTask: Promise<void> | null = null;
372
+ const acceptedDelegationItemIds = new Set(
373
+ owner?.delegationReplay?.acceptedDelegationItemIds ?? [],
374
+ );
375
+ const pendingDelegations = new Map(
376
+ (owner?.delegationReplay?.pendingDelegations ?? []).flatMap((entry) =>
377
+ entry.delegationItemId ? [[entry.delegationItemId, entry] as const] : [],
378
+ ),
379
+ );
380
+
381
+ const invalidateStoredOwner = (): void => {
382
+ try {
383
+ storage?.removeItem(storageKey);
384
+ } catch {
385
+ // Best effort only. The bridge still fails closed and retains the exact
386
+ // in-memory snapshot for this controller's bounded recovery attempts.
387
+ }
388
+ };
389
+
390
+ const persistDelegationReplay = (input: {
391
+ acceptedDelegationItemIds: ReadonlySet<string>;
392
+ pendingDelegations: ReadonlyMap<string, SessionRealtimeInboundEntry>;
393
+ }): void => {
394
+ if (!owner || !storage) return;
395
+ const delegationReplay: OwnerDelegationReplay = {
396
+ version: OWNER_DELEGATION_REPLAY_VERSION,
397
+ acceptedDelegationItemIds: [...input.acceptedDelegationItemIds],
398
+ pendingDelegations: [...input.pendingDelegations.values()],
399
+ };
400
+ if (
401
+ delegationReplay.pendingDelegations.length > CODEX_REALTIME_V3_PENDING_MAX_ENTRIES ||
402
+ delegationReplay.acceptedDelegationItemIds.length +
403
+ delegationReplay.pendingDelegations.length >
404
+ OWNER_DELEGATION_REPLAY_MAX_CALLS
405
+ ) {
406
+ invalidateStoredOwner();
407
+ throw new Error("Realtime delegation replay journal exceeded its call limit");
408
+ }
409
+ const next: OwnerRecord = { ...owner, delegationReplay };
410
+ const serialized = JSON.stringify(next);
411
+ if (new TextEncoder().encode(serialized).byteLength > OWNER_DELEGATION_REPLAY_MAX_BYTES) {
412
+ invalidateStoredOwner();
413
+ throw new Error("Realtime delegation replay journal exceeded its byte limit");
414
+ }
415
+ try {
416
+ storage.setItem(storageKey, serialized);
417
+ } catch (error) {
418
+ // Never leave older ownership proof reloadable without the delegation
419
+ // snapshot that the active bridge just froze.
420
+ invalidateStoredOwner();
421
+ throw error;
422
+ }
423
+ owner = next;
424
+ };
333
425
 
334
426
  const publish = (patch: Partial<CodexRealtimeControllerSnapshot>): void => {
335
427
  state = { ...state, ...patch };
@@ -414,6 +506,8 @@ export function createCodexRealtimeController(
414
506
  const clearOwner = (): void => {
415
507
  owner = null;
416
508
  storage?.removeItem(storageKey);
509
+ acceptedDelegationItemIds.clear();
510
+ pendingDelegations.clear();
417
511
  };
418
512
 
419
513
  const transitionEnded = (message = "Realtime mode ended"): void => {
@@ -454,16 +548,26 @@ export function createCodexRealtimeController(
454
548
  publish({ microphone: "active" });
455
549
  return acquired;
456
550
  } catch (error) {
457
- if (error instanceof CodexRealtimeMicrophoneError) {
458
- const kind = error.code === "permission_denied" ? "permission_failure" : "device_failure";
551
+ const microphoneError =
552
+ error instanceof CodexRealtimeMicrophoneError
553
+ ? error
554
+ : signal.aborted && state.microphone === "acquiring"
555
+ ? new CodexRealtimeMicrophoneError(
556
+ "acquisition_failed",
557
+ "Microphone did not become available before voice startup timed out",
558
+ )
559
+ : null;
560
+ if (microphoneError) {
561
+ const kind =
562
+ microphoneError.code === "permission_denied" ? "permission_failure" : "device_failure";
459
563
  publish({
460
- microphone: error.code,
564
+ microphone: microphoneError.code,
461
565
  status: state.mode?.state === "active" ? "recovering" : "error",
462
- diagnostic: diagnostic(kind, error.message, true),
463
- error: error.message,
566
+ diagnostic: diagnostic(kind, microphoneError.message, true),
567
+ error: microphoneError.message,
464
568
  });
465
569
  }
466
- throw error;
570
+ throw microphoneError ?? error;
467
571
  }
468
572
  };
469
573
 
@@ -506,7 +610,10 @@ export function createCodexRealtimeController(
506
610
  });
507
611
  return;
508
612
  }
509
- publish({ audibleOutput: next, ...(next === "audible" ? { error: null } : {}) });
613
+ publish({
614
+ audibleOutput: next,
615
+ ...(next === "audible" ? { error: null } : {}),
616
+ });
510
617
  };
511
618
 
512
619
  const startActiveIntervals = (): void => {
@@ -893,6 +1000,10 @@ export function createCodexRealtimeController(
893
1000
  sync: async (request) =>
894
1001
  await syncForGeneration(targetGeneration, activated.mode.id, request),
895
1002
  randomUUID,
1003
+ ...(options.getModelContext ? { getModelContext: options.getModelContext } : {}),
1004
+ acceptedDelegationItemIds,
1005
+ pendingDelegations,
1006
+ onDelegationReplayStateChange: persistDelegationReplay,
896
1007
  onSnapshot: (nextBridge) => {
897
1008
  if (active?.generation === targetGeneration) publish({ bridge: nextBridge });
898
1009
  },
@@ -1079,7 +1190,12 @@ export function createCodexRealtimeController(
1079
1190
  await handleConnectionFailure(error, "reconnect", false);
1080
1191
  } else {
1081
1192
  clearOwner();
1082
- publish({ status: "error", realtimeId: null, mode: null, error: safeError(error) });
1193
+ publish({
1194
+ status: "error",
1195
+ realtimeId: null,
1196
+ mode: null,
1197
+ error: safeError(error),
1198
+ });
1083
1199
  }
1084
1200
  throw error;
1085
1201
  }
@@ -1155,7 +1271,11 @@ export function createCodexRealtimeController(
1155
1271
  closed = false;
1156
1272
  stopping = false;
1157
1273
  owner = record;
1158
- publish({ status: "recovering", realtimeId: lifecycle.realtimeId, error: null });
1274
+ publish({
1275
+ status: "recovering",
1276
+ realtimeId: lifecycle.realtimeId,
1277
+ error: null,
1278
+ });
1159
1279
  connectionTask = begin(record, true)
1160
1280
  .then(() => undefined)
1161
1281
  .catch(async (error) => {
@@ -1359,8 +1479,13 @@ function readOwnerRecord(
1359
1479
  ): OwnerRecord | null {
1360
1480
  const raw = storage?.getItem(key);
1361
1481
  if (!raw) return null;
1482
+ if (new TextEncoder().encode(raw).byteLength > OWNER_DELEGATION_REPLAY_MAX_BYTES) {
1483
+ storage?.removeItem(key);
1484
+ return null;
1485
+ }
1362
1486
  try {
1363
1487
  const parsed = recordValue(JSON.parse(raw));
1488
+ const delegationReplay = readOwnerDelegationReplay(parsed?.delegationReplay);
1364
1489
  if (
1365
1490
  parsed?.version !== OWNER_RECORD_VERSION ||
1366
1491
  parsed.workspaceId !== scope.workspaceId ||
@@ -1368,18 +1493,120 @@ function readOwnerRecord(
1368
1493
  !stringValue(parsed.operationId) ||
1369
1494
  !stringValue(parsed.browserInstanceId) ||
1370
1495
  !stringValue(parsed.ownerKey) ||
1371
- String(parsed.ownerKey).length < 32
1496
+ String(parsed.ownerKey).length < 32 ||
1497
+ delegationReplay === null
1372
1498
  ) {
1373
1499
  storage?.removeItem(key);
1374
1500
  return null;
1375
1501
  }
1376
- return parsed as OwnerRecord;
1502
+ return {
1503
+ ...(parsed as Omit<OwnerRecord, "delegationReplay">),
1504
+ ...(delegationReplay ? { delegationReplay } : {}),
1505
+ };
1377
1506
  } catch {
1378
1507
  storage?.removeItem(key);
1379
1508
  return null;
1380
1509
  }
1381
1510
  }
1382
1511
 
1512
+ function readOwnerDelegationReplay(value: unknown): OwnerDelegationReplay | undefined | null {
1513
+ if (value === undefined) return undefined;
1514
+ const record = recordValue(value);
1515
+ if (
1516
+ record?.version !== OWNER_DELEGATION_REPLAY_VERSION ||
1517
+ !Array.isArray(record.acceptedDelegationItemIds) ||
1518
+ !Array.isArray(record.pendingDelegations) ||
1519
+ record.acceptedDelegationItemIds.length + record.pendingDelegations.length >
1520
+ OWNER_DELEGATION_REPLAY_MAX_CALLS ||
1521
+ record.pendingDelegations.length > CODEX_REALTIME_V3_PENDING_MAX_ENTRIES
1522
+ ) {
1523
+ return null;
1524
+ }
1525
+ const acceptedDelegationItemIds: string[] = [];
1526
+ const accepted = new Set<string>();
1527
+ for (const candidate of record.acceptedDelegationItemIds) {
1528
+ if (typeof candidate !== "string" || candidate.length < 1 || candidate.length > 1_024) {
1529
+ return null;
1530
+ }
1531
+ if (!accepted.has(candidate)) {
1532
+ accepted.add(candidate);
1533
+ acceptedDelegationItemIds.push(candidate);
1534
+ }
1535
+ }
1536
+ const pendingDelegations: SessionRealtimeInboundEntry[] = [];
1537
+ const pendingIds = new Set<string>();
1538
+ for (const candidate of record.pendingDelegations) {
1539
+ const parsed = readSessionRealtimeInboundEntry(candidate);
1540
+ if (
1541
+ !parsed ||
1542
+ parsed.kind !== "delegation_call" ||
1543
+ !parsed.delegationItemId ||
1544
+ accepted.has(parsed.delegationItemId) ||
1545
+ pendingIds.has(parsed.delegationItemId)
1546
+ ) {
1547
+ return null;
1548
+ }
1549
+ pendingIds.add(parsed.delegationItemId);
1550
+ pendingDelegations.push(parsed);
1551
+ }
1552
+ const replay: OwnerDelegationReplay = {
1553
+ version: OWNER_DELEGATION_REPLAY_VERSION,
1554
+ acceptedDelegationItemIds,
1555
+ pendingDelegations,
1556
+ };
1557
+ if (
1558
+ new TextEncoder().encode(JSON.stringify(replay)).byteLength > OWNER_DELEGATION_REPLAY_MAX_BYTES
1559
+ ) {
1560
+ return null;
1561
+ }
1562
+ return replay;
1563
+ }
1564
+
1565
+ function readSessionRealtimeInboundEntry(value: unknown): SessionRealtimeInboundEntry | null {
1566
+ const record = recordValue(value);
1567
+ if (
1568
+ !record ||
1569
+ Object.keys(record).some((key) => !SESSION_REALTIME_INBOUND_ENTRY_KEYS.has(key)) ||
1570
+ typeof record.operationId !== "string" ||
1571
+ !UUID_PATTERN.test(record.operationId) ||
1572
+ typeof record.kind !== "string" ||
1573
+ !SESSION_REALTIME_INBOUND_KINDS.has(record.kind) ||
1574
+ !optionalNullableEnum(record.role, ["user", "assistant"]) ||
1575
+ !optionalNullableBoundedString(record.providerEventId, 1_024) ||
1576
+ !optionalNullableBoundedString(record.delegationItemId, 1_024) ||
1577
+ !optionalNullableBoundedString(record.text, 131_072) ||
1578
+ (record.payload !== undefined && recordValue(record.payload) === null) ||
1579
+ !optionalModelContext(record.modelContext)
1580
+ ) {
1581
+ return null;
1582
+ }
1583
+ return record as SessionRealtimeInboundEntry;
1584
+ }
1585
+
1586
+ function optionalNullableEnum(value: unknown, allowed: readonly string[]): boolean {
1587
+ return (
1588
+ value === undefined || value === null || (typeof value === "string" && allowed.includes(value))
1589
+ );
1590
+ }
1591
+
1592
+ function optionalNullableBoundedString(value: unknown, maxLength: number): boolean {
1593
+ return (
1594
+ value === undefined ||
1595
+ value === null ||
1596
+ (typeof value === "string" && value.length <= maxLength)
1597
+ );
1598
+ }
1599
+
1600
+ function optionalModelContext(value: unknown): boolean {
1601
+ return (
1602
+ value === undefined ||
1603
+ (typeof value === "string" &&
1604
+ value.length >= 1 &&
1605
+ value.length <= 32_768 &&
1606
+ value.trim() === value)
1607
+ );
1608
+ }
1609
+
1383
1610
  function defaultStorage(): CodexRealtimeOwnerStorage | undefined {
1384
1611
  return typeof sessionStorage === "undefined" ? undefined : sessionStorage;
1385
1612
  }
@@ -66,6 +66,7 @@ export function projectSessionRealtimeLifecycle(
66
66
 
67
67
  function realtimeModel(value: unknown): SessionRealtimeModel | null {
68
68
  return value === "gpt-live-1-boulder-alpha" ||
69
+ value === "supergrok/grok-voice-think-fast-2.0" ||
69
70
  value === "opengeni-gateway/openai/gpt-realtime-2.1" ||
70
71
  value === "opengeni-gateway/openai/gpt-realtime-mini" ||
71
72
  value === "opengeni-gateway/xai/grok-voice-think-fast-2.0" ||
@@ -35,9 +35,10 @@ export const CODEX_REALTIME_V3_PENDING_MAX_ENTRIES = 256;
35
35
  export const CODEX_REALTIME_V3_PENDING_MAX_BYTES = 16 * 1024 * 1024;
36
36
  const REALTIME_DELEGATION_TRANSCRIPT_MAX_BYTES = 65_536;
37
37
  const REALTIME_DELEGATION_INPUT_MAX_BYTES = 65_536;
38
+ const REALTIME_MODEL_CONTEXT_MAX_CHARACTERS = 32_768;
38
39
 
39
40
  export type CodexRealtimeV3BridgeFatal = {
40
- code: "pending_overflow";
41
+ code: "pending_overflow" | "replay_journal_failed";
41
42
  message: string;
42
43
  };
43
44
 
@@ -72,6 +73,19 @@ export type CodexRealtimeV3BridgeOptions = {
72
73
  >;
73
74
  sync(request: SyncSessionRealtimeLedgerRequest): Promise<SyncSessionRealtimeLedgerResponse>;
74
75
  randomUUID?: (() => string) | undefined;
76
+ /** Model-visible application context captured once for each durable message-bearing entry. */
77
+ getModelContext?: (() => string | undefined) | undefined;
78
+ /** Controller-lifetime delegation identities shared across provider connection rotations. */
79
+ acceptedDelegationItemIds?: Set<string> | undefined;
80
+ /** Unsynced delegation snapshots retained exactly across provider connection rotations. */
81
+ pendingDelegations?: Map<string, SessionRealtimeInboundEntry> | undefined;
82
+ /** Persist exact delegation replay state before it becomes browser-reload-sensitive. */
83
+ onDelegationReplayStateChange?:
84
+ | ((state: {
85
+ acceptedDelegationItemIds: ReadonlySet<string>;
86
+ pendingDelegations: ReadonlyMap<string, SessionRealtimeInboundEntry>;
87
+ }) => void)
88
+ | undefined;
75
89
  /** The controller installs its activation FIFO first, then enables this listener synchronously. */
76
90
  listen?: boolean | undefined;
77
91
  onSnapshot?: ((snapshot: CodexRealtimeV3BridgeSnapshot) => void) | undefined;
@@ -125,9 +139,32 @@ export function createCodexRealtimeV3Bridge(
125
139
  const clientReceivedSequences = new Set<number>();
126
140
  const sentSequences = new Set<number>();
127
141
  const finalizedTurnIds = new Set<string>();
142
+ const acceptedDelegationItemIds = options.acceptedDelegationItemIds ?? new Set<string>();
143
+ const pendingDelegations = options.pendingDelegations ?? new Map();
144
+ const locallyQueuedDelegationItemIds = new Set<string>();
128
145
  let transcriptSinceDelegation: FinalizedTranscript[] = [];
129
- let pendingDelegationUserTranscript: { delegationItemId: string; text: string } | null = null;
146
+ let pendingDelegationUserTranscript: {
147
+ delegationItemId: string;
148
+ text: string;
149
+ } | null = null;
130
150
  const randomUUID = options.randomUUID ?? defaultRandomUUID;
151
+ const currentModelContext = (): string | undefined => {
152
+ let context: string | undefined;
153
+ try {
154
+ context = options.getModelContext?.()?.trim();
155
+ } catch {
156
+ lastError =
157
+ "Realtime model context callback failed; the provider message continued without application context";
158
+ return undefined;
159
+ }
160
+ if (!context) return undefined;
161
+ if (context.length > REALTIME_MODEL_CONTEXT_MAX_CHARACTERS) {
162
+ lastError =
163
+ "Realtime model context exceeded the 32768-character limit; the provider message continued without application context";
164
+ return undefined;
165
+ }
166
+ return context;
167
+ };
131
168
 
132
169
  const snapshot = (): CodexRealtimeV3BridgeSnapshot => ({
133
170
  connectionId: options.connectionId,
@@ -148,9 +185,9 @@ export function createCodexRealtimeV3Bridge(
148
185
  });
149
186
  const publish = (): void => options.onSnapshot?.(snapshot());
150
187
 
151
- const triggerFatal = (message: string): void => {
188
+ const triggerFatalCode = (code: CodexRealtimeV3BridgeFatal["code"], message: string): void => {
152
189
  if (closed || fatal) return;
153
- fatal = { code: "pending_overflow", message };
190
+ fatal = { code, message };
154
191
  lastError = message;
155
192
  publish();
156
193
  try {
@@ -161,6 +198,10 @@ export function createCodexRealtimeV3Bridge(
161
198
  }
162
199
  };
163
200
 
201
+ const triggerFatal = (message: string): void => {
202
+ triggerFatalCode("pending_overflow", message);
203
+ };
204
+
164
205
  const enqueue = (entry: SessionRealtimeInboundEntry): boolean => {
165
206
  if (closed || sealed || fatal) return false;
166
207
  const bytes = utf8ByteLength(JSON.stringify(entry));
@@ -177,6 +218,14 @@ export function createCodexRealtimeV3Bridge(
177
218
  return true;
178
219
  };
179
220
 
221
+ // Same-browser reload reconstructs this exact map from the persisted owner
222
+ // journal. Queue those first-frozen calls before listening to the replacement
223
+ // provider connection; startup proof or a duplicate call drives the normal
224
+ // flush path without resampling application context.
225
+ for (const [delegationItemId, entry] of pendingDelegations) {
226
+ if (enqueue(entry)) locallyQueuedDelegationItemIds.add(delegationItemId);
227
+ }
228
+
180
229
  const hasWork = (): boolean =>
181
230
  pendingInbound.length > 0 ||
182
231
  (!providerStartedAccepted && providerStarted !== undefined) ||
@@ -213,7 +262,46 @@ export function createCodexRealtimeV3Bridge(
213
262
  throw error;
214
263
  }
215
264
 
265
+ const acceptedAfterSync = new Set(acceptedDelegationItemIds);
266
+ const pendingAfterSync = new Map(pendingDelegations);
267
+ let delegationReplayChanged = false;
268
+ for (const item of batch) {
269
+ if (item.entry.kind === "delegation_call" && item.entry.delegationItemId) {
270
+ delegationReplayChanged = true;
271
+ acceptedAfterSync.add(item.entry.delegationItemId);
272
+ if (pendingAfterSync.get(item.entry.delegationItemId) === item.entry) {
273
+ pendingAfterSync.delete(item.entry.delegationItemId);
274
+ }
275
+ }
276
+ }
277
+ if (delegationReplayChanged) {
278
+ try {
279
+ options.onDelegationReplayStateChange?.({
280
+ acceptedDelegationItemIds: acceptedAfterSync,
281
+ pendingDelegations: pendingAfterSync,
282
+ });
283
+ } catch (error) {
284
+ // The server may already have admitted this exact batch. Keep it
285
+ // queued with the original operation identity and stop this bridge.
286
+ // Recovery can safely replay it because the prior pending journal
287
+ // state remains authoritative until the accepted transition writes.
288
+ pendingInbound = [...batch, ...pendingInbound];
289
+ triggerFatalCode(
290
+ "replay_journal_failed",
291
+ `Codex realtime delegation replay journal failed: ${safeError(error)}`,
292
+ );
293
+ return;
294
+ }
295
+ }
296
+
216
297
  for (const item of batch) {
298
+ if (item.entry.kind === "delegation_call" && item.entry.delegationItemId) {
299
+ acceptedDelegationItemIds.add(item.entry.delegationItemId);
300
+ if (pendingDelegations.get(item.entry.delegationItemId) === item.entry) {
301
+ pendingDelegations.delete(item.entry.delegationItemId);
302
+ }
303
+ locallyQueuedDelegationItemIds.delete(item.entry.delegationItemId);
304
+ }
217
305
  pendingInboundCount -= 1;
218
306
  pendingInboundBytes -= item.bytes;
219
307
  }
@@ -345,31 +433,67 @@ export function createCodexRealtimeV3Bridge(
345
433
  // These events are provider UI deltas. `turn.done` is the single
346
434
  // authoritative finalized transcript persisted below.
347
435
  } else if (event.type === "delegation.created") {
348
- activeDelegationId = event.delegationItemId;
349
- const transcript = delegationTranscript(transcriptSinceDelegation, event.inputTranscript);
350
- const coveredTurnIds = transcriptSinceDelegation.map((entry) => entry.turnId);
351
- durable = enqueue({
352
- operationId: randomUUID(),
353
- kind: "delegation_call",
354
- providerEventId: event.providerEventId,
355
- delegationItemId: event.delegationItemId,
356
- text: renderRealtimeDelegationInput(event.inputTranscript, transcript),
357
- payload: {
358
- offsetMs: event.offsetMs,
359
- inputTranscript: event.inputTranscript,
360
- transcriptFenceTurnIds: coveredTurnIds,
361
- },
362
- });
363
- if (durable) {
364
- const alreadyFinalized = transcriptSinceDelegation.some(
365
- (entry) =>
366
- entry.role === "user" &&
367
- normalizedTranscript(entry.text) === normalizedTranscript(event.inputTranscript),
368
- );
369
- pendingDelegationUserTranscript = alreadyFinalized
370
- ? null
371
- : { delegationItemId: event.delegationItemId, text: event.inputTranscript };
372
- transcriptSinceDelegation = [];
436
+ if (acceptedDelegationItemIds.has(event.delegationItemId)) {
437
+ ignoredEventCount += 1;
438
+ lastIgnoredEventType = event.type;
439
+ } else if (locallyQueuedDelegationItemIds.has(event.delegationItemId)) {
440
+ ignoredEventCount += 1;
441
+ lastIgnoredEventType = event.type;
442
+ durable = true;
443
+ } else {
444
+ let entry = pendingDelegations.get(event.delegationItemId);
445
+ if (!entry) {
446
+ const transcript = delegationTranscript(transcriptSinceDelegation, event.inputTranscript);
447
+ const coveredTurnIds = transcriptSinceDelegation.map((item) => item.turnId);
448
+ const modelContext = currentModelContext();
449
+ entry = {
450
+ operationId: randomUUID(),
451
+ kind: "delegation_call",
452
+ providerEventId: event.providerEventId,
453
+ delegationItemId: event.delegationItemId,
454
+ text: renderRealtimeDelegationInput(event.inputTranscript, transcript),
455
+ payload: {
456
+ offsetMs: event.offsetMs,
457
+ inputTranscript: event.inputTranscript,
458
+ transcriptFenceTurnIds: coveredTurnIds,
459
+ },
460
+ ...(modelContext ? { modelContext } : {}),
461
+ };
462
+ pendingDelegations.set(event.delegationItemId, entry);
463
+ }
464
+ try {
465
+ options.onDelegationReplayStateChange?.({
466
+ acceptedDelegationItemIds,
467
+ pendingDelegations,
468
+ });
469
+ } catch (error) {
470
+ triggerFatalCode(
471
+ "replay_journal_failed",
472
+ `Codex realtime delegation replay journal failed: ${safeError(error)}`,
473
+ );
474
+ return Promise.resolve();
475
+ }
476
+ durable = enqueue(entry);
477
+ if (durable) {
478
+ locallyQueuedDelegationItemIds.add(event.delegationItemId);
479
+ activeDelegationId = event.delegationItemId;
480
+ const frozenInputTranscript =
481
+ typeof entry.payload.inputTranscript === "string"
482
+ ? entry.payload.inputTranscript
483
+ : event.inputTranscript;
484
+ const alreadyFinalized = transcriptSinceDelegation.some(
485
+ (item) =>
486
+ item.role === "user" &&
487
+ normalizedTranscript(item.text) === normalizedTranscript(frozenInputTranscript),
488
+ );
489
+ pendingDelegationUserTranscript = alreadyFinalized
490
+ ? null
491
+ : {
492
+ delegationItemId: event.delegationItemId,
493
+ text: frozenInputTranscript,
494
+ };
495
+ transcriptSinceDelegation = [];
496
+ }
373
497
  }
374
498
  } else if (event.type === "output_audio.delta") {
375
499
  speaking = true;
@@ -383,7 +507,9 @@ export function createCodexRealtimeV3Bridge(
383
507
  normalizedTranscript(pendingDelegationUserTranscript.text)
384
508
  ? pendingDelegationUserTranscript.delegationItemId
385
509
  : null;
386
- durable = enqueue(finalTranscript(randomUUID, event, coveredByDelegationItemId));
510
+ durable = enqueue(
511
+ finalTranscript(randomUUID, event, coveredByDelegationItemId, currentModelContext()),
512
+ );
387
513
  if (durable) {
388
514
  finalizedTurnIds.add(event.turnId);
389
515
  if (coveredByDelegationItemId) {
@@ -446,6 +572,7 @@ function finalTranscript(
446
572
  randomUUID: () => string,
447
573
  event: Extract<CodexRealtimeV3Event, { type: "turn.done" }>,
448
574
  coveredByDelegationItemId: string | null,
575
+ modelContext: string | undefined,
449
576
  ): SessionRealtimeInboundEntry {
450
577
  return {
451
578
  operationId: randomUUID(),
@@ -456,6 +583,7 @@ function finalTranscript(
456
583
  turnId: event.turnId,
457
584
  ...(coveredByDelegationItemId ? { coveredByDelegationItemId } : {}),
458
585
  },
586
+ ...(modelContext ? { modelContext } : {}),
459
587
  };
460
588
  }
461
589
 
@@ -539,7 +667,10 @@ function sendOutbound(events: RTCDataChannel, entry: SessionRealtimeLedgerEntry)
539
667
  text,
540
668
  channel: payloadChannel ?? "speakable",
541
669
  })
542
- : encodeCodexRealtimeV3SessionContextAppend({ text, channel: payloadChannel });
670
+ : encodeCodexRealtimeV3SessionContextAppend({
671
+ text,
672
+ channel: payloadChannel,
673
+ });
543
674
  for (const message of messages) events.send(JSON.stringify(message));
544
675
  }
545
676
 
package/src/desktop.ts CHANGED
@@ -71,13 +71,23 @@ export interface DesktopRfbLike {
71
71
  type: "connect" | "disconnect" | "securityfailure",
72
72
  cb: (e?: unknown) => void,
73
73
  ) => void;
74
+ /** Send clipboard text over the already-open RFB connection. */
75
+ clipboardPasteFrom?(text: string): void;
76
+ /** Send one RFB key event. A missing `down` emits a complete key press. */
77
+ sendKey?(keysym: number, code: string, down?: boolean): void;
78
+ /** Focus or release the RFB keyboard sink without reconnecting. */
79
+ focus?(options?: FocusOptions): void;
80
+ blur?(): void;
74
81
  disconnect(): void;
75
82
  }
76
83
 
77
84
  export type DesktopRfbFactory = (
78
85
  target: HTMLElement,
79
86
  url: string,
80
- opts: { credentials?: { password?: string | undefined } | undefined },
87
+ opts: {
88
+ credentials?: { password?: string | undefined } | undefined;
89
+ wsProtocols?: string[] | undefined;
90
+ },
81
91
  ) => DesktopRfbLike;
82
92
 
83
93
  export type DesktopConnectionState =