@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.
- package/README.md +67 -7
- package/dist/artifacts.js +5 -5
- package/dist/{chunk-FRJFNDIR.js → chunk-4DV37UUA.js} +6 -2
- package/dist/chunk-4DV37UUA.js.map +1 -0
- package/dist/{chunk-KIHGPM7H.js → chunk-A5DC5WEM.js} +288 -45
- package/dist/chunk-A5DC5WEM.js.map +1 -0
- package/dist/{chunk-V5F253OG.js → chunk-GU6JF75T.js} +6 -3
- package/dist/chunk-GU6JF75T.js.map +1 -0
- package/dist/{chunk-MZWTWOX6.js → chunk-ST5DDKJP.js} +327 -1
- package/dist/chunk-ST5DDKJP.js.map +1 -0
- package/dist/{chunk-FHI4DFIG.js → chunk-TYZ4JL4J.js} +2 -2
- package/dist/{chunk-ILQZR5N6.js → chunk-YGH5P47G.js} +693 -34
- package/dist/chunk-YGH5P47G.js.map +1 -0
- package/dist/{chunk-DXDL7EEW.js → chunk-YKZME56C.js} +197 -113
- package/dist/chunk-YKZME56C.js.map +1 -0
- package/dist/client.d.ts +108 -18
- package/dist/codex-realtime-controller.d.ts +5 -0
- package/dist/codex-realtime-controller.js +2 -2
- package/dist/codex-realtime-v3.d.ts +13 -2
- package/dist/core.js +7 -7
- package/dist/desktop.d.ts +8 -0
- package/dist/editable-artifacts.js +4 -4
- package/dist/gateway-realtime-transport.d.ts +1 -0
- package/dist/gateway-realtime-transport.js +5 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.js +68 -34
- package/dist/index.js.map +1 -1
- package/dist/interaction-revision-stream.d.ts +11 -0
- package/dist/interaction.d.ts +604 -5
- package/dist/interaction.js +27 -1
- package/dist/realtime.d.ts +3 -3
- package/dist/realtime.js +11 -7
- package/dist/realtime.js.map +1 -1
- package/dist/types.d.ts +283 -69
- package/dist/workspace-live-stream.d.ts +14 -0
- package/package.json +2 -2
- package/src/client.ts +863 -62
- package/src/codex-realtime-controller.ts +239 -12
- package/src/codex-realtime-lifecycle.ts +1 -0
- package/src/codex-realtime-v3.ts +162 -31
- package/src/desktop.ts +11 -1
- package/src/errors.ts +16 -2
- package/src/gateway-realtime-transport.ts +252 -109
- package/src/index.ts +42 -13
- package/src/interaction-revision-stream.ts +117 -0
- package/src/interaction.ts +1049 -5
- package/src/realtime.ts +14 -4
- package/src/types.ts +353 -72
- package/src/workspace-live-stream.ts +137 -0
- package/dist/chunk-DXDL7EEW.js.map +0 -1
- package/dist/chunk-FRJFNDIR.js.map +0 -1
- package/dist/chunk-ILQZR5N6.js.map +0 -1
- package/dist/chunk-KIHGPM7H.js.map +0 -1
- package/dist/chunk-MZWTWOX6.js.map +0 -1
- package/dist/chunk-V5F253OG.js.map +0 -1
- /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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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:
|
|
564
|
+
microphone: microphoneError.code,
|
|
461
565
|
status: state.mode?.state === "active" ? "recovering" : "error",
|
|
462
|
-
diagnostic: diagnostic(kind,
|
|
463
|
-
error:
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
|
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" ||
|
package/src/codex-realtime-v3.ts
CHANGED
|
@@ -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: {
|
|
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
|
|
188
|
+
const triggerFatalCode = (code: CodexRealtimeV3BridgeFatal["code"], message: string): void => {
|
|
152
189
|
if (closed || fatal) return;
|
|
153
|
-
fatal = { code
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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(
|
|
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({
|
|
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: {
|
|
87
|
+
opts: {
|
|
88
|
+
credentials?: { password?: string | undefined } | undefined;
|
|
89
|
+
wsProtocols?: string[] | undefined;
|
|
90
|
+
},
|
|
81
91
|
) => DesktopRfbLike;
|
|
82
92
|
|
|
83
93
|
export type DesktopConnectionState =
|