@opengeni/sdk 0.57.0 → 2.2.0-canary.0
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 +123 -2
- package/dist/artifacts.js +5 -5
- package/dist/automations-client.d.ts +17 -0
- package/dist/automations.d.ts +3 -0
- package/dist/automations.js +78 -0
- package/dist/automations.js.map +1 -0
- package/dist/{chunk-JDLDDRNE.js → chunk-C2H7HBNP.js} +290 -33
- package/dist/chunk-C2H7HBNP.js.map +1 -0
- package/dist/{chunk-GU6JF75T.js → chunk-C4DJPZRU.js} +1 -1
- package/dist/{chunk-GU6JF75T.js.map → chunk-C4DJPZRU.js.map} +1 -1
- package/dist/{chunk-JRPFWI6T.js → chunk-G6LBVWKT.js} +2 -2
- package/dist/{chunk-HJ3HHUT5.js → chunk-GLDV5QY7.js} +16 -2
- package/dist/chunk-GLDV5QY7.js.map +1 -0
- package/dist/{chunk-ST5DDKJP.js → chunk-GW3EJ2GP.js} +52 -1
- package/dist/chunk-GW3EJ2GP.js.map +1 -0
- package/dist/{chunk-VWE2QIVO.js → chunk-XNG7YGR3.js} +23 -6
- package/dist/chunk-XNG7YGR3.js.map +1 -0
- package/dist/{chunk-PKQ377ED.js → chunk-YBQTKNTL.js} +696 -57
- package/dist/chunk-YBQTKNTL.js.map +1 -0
- package/dist/client.d.ts +151 -26
- package/dist/codex-realtime-controller.d.ts +3 -0
- package/dist/codex-realtime-controller.js +2 -2
- package/dist/codex-realtime-v3.d.ts +13 -2
- package/dist/company-brain.d.ts +32 -0
- package/dist/company-profile.d.ts +1 -1
- package/dist/core.js +4 -4
- package/dist/editable-artifacts/browser-session.d.ts +7 -3
- package/dist/editable-artifacts/controller.d.ts +4 -1
- package/dist/editable-artifacts/errors.d.ts +1 -1
- package/dist/editable-artifacts/http-live-transport.d.ts +6 -2
- package/dist/editable-artifacts/index.d.ts +2 -0
- package/dist/editable-artifacts/open-failure.d.ts +9 -0
- package/dist/editable-artifacts/types.d.ts +4 -3
- package/dist/editable-artifacts/worker/kernel-adapter.d.ts +1 -1
- package/dist/editable-artifacts-worker.js +44 -17
- package/dist/editable-artifacts-worker.js.map +1 -1
- package/dist/editable-artifacts.js +222 -119
- package/dist/editable-artifacts.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +61 -12
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +23 -0
- package/dist/interaction.js +4 -1
- package/dist/organization-private-session-settings.d.ts +6 -0
- package/dist/organization-private-session-settings.js +22 -0
- package/dist/organization-private-session-settings.js.map +1 -0
- package/dist/pr-review-client.d.ts +15 -0
- package/dist/pr-review.d.ts +4 -0
- package/dist/pr-review.js +56 -0
- package/dist/pr-review.js.map +1 -0
- package/dist/preference-registry.d.ts +8 -0
- package/dist/realtime.js +2 -2
- package/dist/retained-artifacts.d.ts +5 -1
- package/dist/types.d.ts +976 -17
- package/dist/workspace-instruction-policies.d.ts +22 -0
- package/dist/workspace-learning.d.ts +124 -0
- package/package.json +14 -2
- package/src/automations-client.ts +118 -0
- package/src/automations.ts +15 -0
- package/src/client.ts +974 -71
- package/src/codex-realtime-controller.ts +235 -5
- package/src/codex-realtime-v3.ts +162 -31
- package/src/company-brain.ts +99 -0
- package/src/company-profile.ts +4 -1
- package/src/editable-artifacts/browser-session.ts +36 -22
- package/src/editable-artifacts/controller.ts +66 -45
- package/src/editable-artifacts/errors.ts +2 -0
- package/src/editable-artifacts/http-live-transport.ts +62 -37
- package/src/editable-artifacts/index.ts +6 -0
- package/src/editable-artifacts/open-failure.ts +79 -0
- package/src/editable-artifacts/storage.ts +31 -40
- package/src/editable-artifacts/types.ts +4 -3
- package/src/editable-artifacts/worker/kernel-adapter.ts +23 -7
- package/src/editable-artifacts/worker/runtime.ts +26 -13
- package/src/editable-artifacts/worker/wire-codec.ts +23 -5
- package/src/errors.ts +1 -1
- package/src/index.ts +131 -0
- package/src/interaction.ts +95 -0
- package/src/organization-private-session-settings.ts +33 -0
- package/src/pr-review-client.ts +84 -0
- package/src/pr-review.ts +15 -0
- package/src/preference-registry.ts +9 -0
- package/src/retained-artifacts.ts +101 -0
- package/src/stream.ts +13 -2
- package/src/types.ts +1117 -16
- package/src/workspace-instruction-policies.ts +24 -0
- package/src/workspace-learning.ts +138 -0
- package/dist/chunk-HJ3HHUT5.js.map +0 -1
- package/dist/chunk-JDLDDRNE.js.map +0 -1
- package/dist/chunk-PKQ377ED.js.map +0 -1
- package/dist/chunk-ST5DDKJP.js.map +0 -1
- package/dist/chunk-VWE2QIVO.js.map +0 -1
- /package/dist/{chunk-JRPFWI6T.js.map → chunk-G6LBVWKT.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,
|
|
@@ -49,7 +50,29 @@ export const CODEX_REALTIME_NEGOTIATION_TIMEOUT_MS = 20_000;
|
|
|
49
50
|
// OpenGeni policy: rotate conservatively without asserting an upstream lifetime.
|
|
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;
|
|
53
|
+
const MAX_BROWSER_TIMEOUT_MS = 2_147_483_647;
|
|
52
54
|
const OWNER_RECORD_VERSION = 1;
|
|
55
|
+
const OWNER_DELEGATION_REPLAY_VERSION = 1;
|
|
56
|
+
const OWNER_DELEGATION_REPLAY_MAX_CALLS = 4_096;
|
|
57
|
+
const OWNER_DELEGATION_REPLAY_MAX_BYTES = 4 * 1024 * 1024;
|
|
58
|
+
const SESSION_REALTIME_INBOUND_ENTRY_KEYS = new Set([
|
|
59
|
+
"operationId",
|
|
60
|
+
"kind",
|
|
61
|
+
"role",
|
|
62
|
+
"providerEventId",
|
|
63
|
+
"delegationItemId",
|
|
64
|
+
"text",
|
|
65
|
+
"payload",
|
|
66
|
+
"modelContext",
|
|
67
|
+
]);
|
|
68
|
+
const SESSION_REALTIME_INBOUND_KINDS = new Set([
|
|
69
|
+
"user_transcript",
|
|
70
|
+
"assistant_transcript",
|
|
71
|
+
"delegation_call",
|
|
72
|
+
"interruption",
|
|
73
|
+
"error",
|
|
74
|
+
]);
|
|
75
|
+
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
76
|
|
|
54
77
|
export type CodexRealtimeControllerStatus =
|
|
55
78
|
| "idle"
|
|
@@ -202,6 +225,7 @@ export type CreateCodexRealtimeControllerOptions = {
|
|
|
202
225
|
createPeerConnection?: (() => RTCPeerConnection) | undefined;
|
|
203
226
|
getUserMedia?: ((constraints: MediaStreamConstraints) => Promise<MediaStream>) | undefined;
|
|
204
227
|
randomUUID?: (() => string) | undefined;
|
|
228
|
+
now?: (() => Date) | undefined;
|
|
205
229
|
setInterval?: ((callback: () => void, delayMs: number) => unknown) | undefined;
|
|
206
230
|
clearInterval?: ((handle: unknown) => void) | undefined;
|
|
207
231
|
setTimeout?: ((callback: () => void, delayMs: number) => unknown) | undefined;
|
|
@@ -213,6 +237,8 @@ export type CreateCodexRealtimeControllerOptions = {
|
|
|
213
237
|
model?: SessionRealtimeModel | undefined;
|
|
214
238
|
ownerStorageNamespace?: string | undefined;
|
|
215
239
|
startTransport?: RealtimeControllerTransportStarter | undefined;
|
|
240
|
+
/** Model-visible application context captured with each durable realtime message. */
|
|
241
|
+
getModelContext?: (() => string | undefined) | undefined;
|
|
216
242
|
};
|
|
217
243
|
|
|
218
244
|
export type RealtimeControllerTransportStarter = (input: {
|
|
@@ -258,6 +284,13 @@ type OwnerRecord = {
|
|
|
258
284
|
operationId: string;
|
|
259
285
|
browserInstanceId: string;
|
|
260
286
|
ownerKey: string;
|
|
287
|
+
delegationReplay?: OwnerDelegationReplay | undefined;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
type OwnerDelegationReplay = {
|
|
291
|
+
version: typeof OWNER_DELEGATION_REPLAY_VERSION;
|
|
292
|
+
acceptedDelegationItemIds: string[];
|
|
293
|
+
pendingDelegations: SessionRealtimeInboundEntry[];
|
|
261
294
|
};
|
|
262
295
|
|
|
263
296
|
type ConnectionRuntime = {
|
|
@@ -285,6 +318,7 @@ export function createCodexRealtimeController(
|
|
|
285
318
|
);
|
|
286
319
|
const model = options.model ?? "gpt-live-1-boulder-alpha";
|
|
287
320
|
const randomUUID = options.randomUUID ?? defaultRandomUUID;
|
|
321
|
+
const now = options.now ?? (() => new Date());
|
|
288
322
|
const scheduleInterval =
|
|
289
323
|
options.setInterval ?? ((callback, delay) => globalThis.setInterval(callback, delay));
|
|
290
324
|
const unscheduleInterval =
|
|
@@ -331,6 +365,7 @@ export function createCodexRealtimeController(
|
|
|
331
365
|
let rotationTimer: unknown = null;
|
|
332
366
|
let reconnectTimer: unknown = null;
|
|
333
367
|
let negotiationTimer: unknown = null;
|
|
368
|
+
let lostOwnerExpiryTimer: unknown = null;
|
|
334
369
|
let closed = false;
|
|
335
370
|
let stopping = false;
|
|
336
371
|
let generation = 0;
|
|
@@ -338,6 +373,59 @@ export function createCodexRealtimeController(
|
|
|
338
373
|
let recoveryTerminal = false;
|
|
339
374
|
let mutationTail = Promise.resolve();
|
|
340
375
|
let connectionTask: Promise<void> | null = null;
|
|
376
|
+
const acceptedDelegationItemIds = new Set(
|
|
377
|
+
owner?.delegationReplay?.acceptedDelegationItemIds ?? [],
|
|
378
|
+
);
|
|
379
|
+
const pendingDelegations = new Map(
|
|
380
|
+
(owner?.delegationReplay?.pendingDelegations ?? []).flatMap((entry) =>
|
|
381
|
+
entry.delegationItemId ? [[entry.delegationItemId, entry] as const] : [],
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const invalidateStoredOwner = (): void => {
|
|
386
|
+
try {
|
|
387
|
+
storage?.removeItem(storageKey);
|
|
388
|
+
} catch {
|
|
389
|
+
// Best effort only. The bridge still fails closed and retains the exact
|
|
390
|
+
// in-memory snapshot for this controller's bounded recovery attempts.
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const persistDelegationReplay = (input: {
|
|
395
|
+
acceptedDelegationItemIds: ReadonlySet<string>;
|
|
396
|
+
pendingDelegations: ReadonlyMap<string, SessionRealtimeInboundEntry>;
|
|
397
|
+
}): void => {
|
|
398
|
+
if (!owner || !storage) return;
|
|
399
|
+
const delegationReplay: OwnerDelegationReplay = {
|
|
400
|
+
version: OWNER_DELEGATION_REPLAY_VERSION,
|
|
401
|
+
acceptedDelegationItemIds: [...input.acceptedDelegationItemIds],
|
|
402
|
+
pendingDelegations: [...input.pendingDelegations.values()],
|
|
403
|
+
};
|
|
404
|
+
if (
|
|
405
|
+
delegationReplay.pendingDelegations.length > CODEX_REALTIME_V3_PENDING_MAX_ENTRIES ||
|
|
406
|
+
delegationReplay.acceptedDelegationItemIds.length +
|
|
407
|
+
delegationReplay.pendingDelegations.length >
|
|
408
|
+
OWNER_DELEGATION_REPLAY_MAX_CALLS
|
|
409
|
+
) {
|
|
410
|
+
invalidateStoredOwner();
|
|
411
|
+
throw new Error("Realtime delegation replay journal exceeded its call limit");
|
|
412
|
+
}
|
|
413
|
+
const next: OwnerRecord = { ...owner, delegationReplay };
|
|
414
|
+
const serialized = JSON.stringify(next);
|
|
415
|
+
if (new TextEncoder().encode(serialized).byteLength > OWNER_DELEGATION_REPLAY_MAX_BYTES) {
|
|
416
|
+
invalidateStoredOwner();
|
|
417
|
+
throw new Error("Realtime delegation replay journal exceeded its byte limit");
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
storage.setItem(storageKey, serialized);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
// Never leave older ownership proof reloadable without the delegation
|
|
423
|
+
// snapshot that the active bridge just froze.
|
|
424
|
+
invalidateStoredOwner();
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
owner = next;
|
|
428
|
+
};
|
|
341
429
|
|
|
342
430
|
const publish = (patch: Partial<CodexRealtimeControllerSnapshot>): void => {
|
|
343
431
|
state = { ...state, ...patch };
|
|
@@ -372,11 +460,13 @@ export function createCodexRealtimeController(
|
|
|
372
460
|
if (rotationTimer !== null) unscheduleTimeout(rotationTimer);
|
|
373
461
|
if (reconnectTimer !== null) unscheduleTimeout(reconnectTimer);
|
|
374
462
|
if (negotiationTimer !== null) unscheduleTimeout(negotiationTimer);
|
|
463
|
+
if (lostOwnerExpiryTimer !== null) unscheduleTimeout(lostOwnerExpiryTimer);
|
|
375
464
|
heartbeatTimer = null;
|
|
376
465
|
syncTimer = null;
|
|
377
466
|
rotationTimer = null;
|
|
378
467
|
reconnectTimer = null;
|
|
379
468
|
negotiationTimer = null;
|
|
469
|
+
lostOwnerExpiryTimer = null;
|
|
380
470
|
};
|
|
381
471
|
|
|
382
472
|
const stopNegotiationTimers = (): void => {
|
|
@@ -422,6 +512,8 @@ export function createCodexRealtimeController(
|
|
|
422
512
|
const clearOwner = (): void => {
|
|
423
513
|
owner = null;
|
|
424
514
|
storage?.removeItem(storageKey);
|
|
515
|
+
acceptedDelegationItemIds.clear();
|
|
516
|
+
pendingDelegations.clear();
|
|
425
517
|
};
|
|
426
518
|
|
|
427
519
|
const transitionEnded = (message = "Realtime mode ended"): void => {
|
|
@@ -524,7 +616,10 @@ export function createCodexRealtimeController(
|
|
|
524
616
|
});
|
|
525
617
|
return;
|
|
526
618
|
}
|
|
527
|
-
publish({
|
|
619
|
+
publish({
|
|
620
|
+
audibleOutput: next,
|
|
621
|
+
...(next === "audible" ? { error: null } : {}),
|
|
622
|
+
});
|
|
528
623
|
};
|
|
529
624
|
|
|
530
625
|
const startActiveIntervals = (): void => {
|
|
@@ -911,6 +1006,10 @@ export function createCodexRealtimeController(
|
|
|
911
1006
|
sync: async (request) =>
|
|
912
1007
|
await syncForGeneration(targetGeneration, activated.mode.id, request),
|
|
913
1008
|
randomUUID,
|
|
1009
|
+
...(options.getModelContext ? { getModelContext: options.getModelContext } : {}),
|
|
1010
|
+
acceptedDelegationItemIds,
|
|
1011
|
+
pendingDelegations,
|
|
1012
|
+
onDelegationReplayStateChange: persistDelegationReplay,
|
|
914
1013
|
onSnapshot: (nextBridge) => {
|
|
915
1014
|
if (active?.generation === targetGeneration) publish({ bridge: nextBridge });
|
|
916
1015
|
},
|
|
@@ -1097,7 +1196,12 @@ export function createCodexRealtimeController(
|
|
|
1097
1196
|
await handleConnectionFailure(error, "reconnect", false);
|
|
1098
1197
|
} else {
|
|
1099
1198
|
clearOwner();
|
|
1100
|
-
publish({
|
|
1199
|
+
publish({
|
|
1200
|
+
status: "error",
|
|
1201
|
+
realtimeId: null,
|
|
1202
|
+
mode: null,
|
|
1203
|
+
error: safeError(error),
|
|
1204
|
+
});
|
|
1101
1205
|
}
|
|
1102
1206
|
throw error;
|
|
1103
1207
|
}
|
|
@@ -1157,6 +1261,12 @@ export function createCodexRealtimeController(
|
|
|
1157
1261
|
if (!record || record.operationId !== lifecycle.operationId) {
|
|
1158
1262
|
closeBrowserResources();
|
|
1159
1263
|
owner = null;
|
|
1264
|
+
const leaseExpiresAt = Date.parse(lifecycle.leaseExpiresAt);
|
|
1265
|
+
const remainingLeaseMs = leaseExpiresAt - now().getTime();
|
|
1266
|
+
if (Number.isFinite(remainingLeaseMs) && remainingLeaseMs <= 0) {
|
|
1267
|
+
transitionEnded("Realtime lease expired");
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1160
1270
|
const message =
|
|
1161
1271
|
"Realtime is active in another browser owner. It can resume after that owner stops or its lease expires.";
|
|
1162
1272
|
publish({
|
|
@@ -1167,13 +1277,26 @@ export function createCodexRealtimeController(
|
|
|
1167
1277
|
diagnostic: diagnostic("lost_owner", message, false),
|
|
1168
1278
|
error: message,
|
|
1169
1279
|
});
|
|
1280
|
+
if (Number.isFinite(remainingLeaseMs) && remainingLeaseMs <= MAX_BROWSER_TIMEOUT_MS) {
|
|
1281
|
+
const observedRealtimeId = lifecycle.realtimeId;
|
|
1282
|
+
lostOwnerExpiryTimer = scheduleTimeout(() => {
|
|
1283
|
+
lostOwnerExpiryTimer = null;
|
|
1284
|
+
if (state.status === "lost_owner" && state.realtimeId === observedRealtimeId) {
|
|
1285
|
+
transitionEnded("Realtime lease expired");
|
|
1286
|
+
}
|
|
1287
|
+
}, remainingLeaseMs);
|
|
1288
|
+
}
|
|
1170
1289
|
return;
|
|
1171
1290
|
}
|
|
1172
1291
|
if (connectionTask || state.status === "starting") return;
|
|
1173
1292
|
closed = false;
|
|
1174
1293
|
stopping = false;
|
|
1175
1294
|
owner = record;
|
|
1176
|
-
publish({
|
|
1295
|
+
publish({
|
|
1296
|
+
status: "recovering",
|
|
1297
|
+
realtimeId: lifecycle.realtimeId,
|
|
1298
|
+
error: null,
|
|
1299
|
+
});
|
|
1177
1300
|
connectionTask = begin(record, true)
|
|
1178
1301
|
.then(() => undefined)
|
|
1179
1302
|
.catch(async (error) => {
|
|
@@ -1377,8 +1500,13 @@ function readOwnerRecord(
|
|
|
1377
1500
|
): OwnerRecord | null {
|
|
1378
1501
|
const raw = storage?.getItem(key);
|
|
1379
1502
|
if (!raw) return null;
|
|
1503
|
+
if (new TextEncoder().encode(raw).byteLength > OWNER_DELEGATION_REPLAY_MAX_BYTES) {
|
|
1504
|
+
storage?.removeItem(key);
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1380
1507
|
try {
|
|
1381
1508
|
const parsed = recordValue(JSON.parse(raw));
|
|
1509
|
+
const delegationReplay = readOwnerDelegationReplay(parsed?.delegationReplay);
|
|
1382
1510
|
if (
|
|
1383
1511
|
parsed?.version !== OWNER_RECORD_VERSION ||
|
|
1384
1512
|
parsed.workspaceId !== scope.workspaceId ||
|
|
@@ -1386,18 +1514,120 @@ function readOwnerRecord(
|
|
|
1386
1514
|
!stringValue(parsed.operationId) ||
|
|
1387
1515
|
!stringValue(parsed.browserInstanceId) ||
|
|
1388
1516
|
!stringValue(parsed.ownerKey) ||
|
|
1389
|
-
String(parsed.ownerKey).length < 32
|
|
1517
|
+
String(parsed.ownerKey).length < 32 ||
|
|
1518
|
+
delegationReplay === null
|
|
1390
1519
|
) {
|
|
1391
1520
|
storage?.removeItem(key);
|
|
1392
1521
|
return null;
|
|
1393
1522
|
}
|
|
1394
|
-
return
|
|
1523
|
+
return {
|
|
1524
|
+
...(parsed as Omit<OwnerRecord, "delegationReplay">),
|
|
1525
|
+
...(delegationReplay ? { delegationReplay } : {}),
|
|
1526
|
+
};
|
|
1395
1527
|
} catch {
|
|
1396
1528
|
storage?.removeItem(key);
|
|
1397
1529
|
return null;
|
|
1398
1530
|
}
|
|
1399
1531
|
}
|
|
1400
1532
|
|
|
1533
|
+
function readOwnerDelegationReplay(value: unknown): OwnerDelegationReplay | undefined | null {
|
|
1534
|
+
if (value === undefined) return undefined;
|
|
1535
|
+
const record = recordValue(value);
|
|
1536
|
+
if (
|
|
1537
|
+
record?.version !== OWNER_DELEGATION_REPLAY_VERSION ||
|
|
1538
|
+
!Array.isArray(record.acceptedDelegationItemIds) ||
|
|
1539
|
+
!Array.isArray(record.pendingDelegations) ||
|
|
1540
|
+
record.acceptedDelegationItemIds.length + record.pendingDelegations.length >
|
|
1541
|
+
OWNER_DELEGATION_REPLAY_MAX_CALLS ||
|
|
1542
|
+
record.pendingDelegations.length > CODEX_REALTIME_V3_PENDING_MAX_ENTRIES
|
|
1543
|
+
) {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
const acceptedDelegationItemIds: string[] = [];
|
|
1547
|
+
const accepted = new Set<string>();
|
|
1548
|
+
for (const candidate of record.acceptedDelegationItemIds) {
|
|
1549
|
+
if (typeof candidate !== "string" || candidate.length < 1 || candidate.length > 1_024) {
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
if (!accepted.has(candidate)) {
|
|
1553
|
+
accepted.add(candidate);
|
|
1554
|
+
acceptedDelegationItemIds.push(candidate);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
const pendingDelegations: SessionRealtimeInboundEntry[] = [];
|
|
1558
|
+
const pendingIds = new Set<string>();
|
|
1559
|
+
for (const candidate of record.pendingDelegations) {
|
|
1560
|
+
const parsed = readSessionRealtimeInboundEntry(candidate);
|
|
1561
|
+
if (
|
|
1562
|
+
!parsed ||
|
|
1563
|
+
parsed.kind !== "delegation_call" ||
|
|
1564
|
+
!parsed.delegationItemId ||
|
|
1565
|
+
accepted.has(parsed.delegationItemId) ||
|
|
1566
|
+
pendingIds.has(parsed.delegationItemId)
|
|
1567
|
+
) {
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
pendingIds.add(parsed.delegationItemId);
|
|
1571
|
+
pendingDelegations.push(parsed);
|
|
1572
|
+
}
|
|
1573
|
+
const replay: OwnerDelegationReplay = {
|
|
1574
|
+
version: OWNER_DELEGATION_REPLAY_VERSION,
|
|
1575
|
+
acceptedDelegationItemIds,
|
|
1576
|
+
pendingDelegations,
|
|
1577
|
+
};
|
|
1578
|
+
if (
|
|
1579
|
+
new TextEncoder().encode(JSON.stringify(replay)).byteLength > OWNER_DELEGATION_REPLAY_MAX_BYTES
|
|
1580
|
+
) {
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
return replay;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function readSessionRealtimeInboundEntry(value: unknown): SessionRealtimeInboundEntry | null {
|
|
1587
|
+
const record = recordValue(value);
|
|
1588
|
+
if (
|
|
1589
|
+
!record ||
|
|
1590
|
+
Object.keys(record).some((key) => !SESSION_REALTIME_INBOUND_ENTRY_KEYS.has(key)) ||
|
|
1591
|
+
typeof record.operationId !== "string" ||
|
|
1592
|
+
!UUID_PATTERN.test(record.operationId) ||
|
|
1593
|
+
typeof record.kind !== "string" ||
|
|
1594
|
+
!SESSION_REALTIME_INBOUND_KINDS.has(record.kind) ||
|
|
1595
|
+
!optionalNullableEnum(record.role, ["user", "assistant"]) ||
|
|
1596
|
+
!optionalNullableBoundedString(record.providerEventId, 1_024) ||
|
|
1597
|
+
!optionalNullableBoundedString(record.delegationItemId, 1_024) ||
|
|
1598
|
+
!optionalNullableBoundedString(record.text, 131_072) ||
|
|
1599
|
+
(record.payload !== undefined && recordValue(record.payload) === null) ||
|
|
1600
|
+
!optionalModelContext(record.modelContext)
|
|
1601
|
+
) {
|
|
1602
|
+
return null;
|
|
1603
|
+
}
|
|
1604
|
+
return record as SessionRealtimeInboundEntry;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function optionalNullableEnum(value: unknown, allowed: readonly string[]): boolean {
|
|
1608
|
+
return (
|
|
1609
|
+
value === undefined || value === null || (typeof value === "string" && allowed.includes(value))
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function optionalNullableBoundedString(value: unknown, maxLength: number): boolean {
|
|
1614
|
+
return (
|
|
1615
|
+
value === undefined ||
|
|
1616
|
+
value === null ||
|
|
1617
|
+
(typeof value === "string" && value.length <= maxLength)
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function optionalModelContext(value: unknown): boolean {
|
|
1622
|
+
return (
|
|
1623
|
+
value === undefined ||
|
|
1624
|
+
(typeof value === "string" &&
|
|
1625
|
+
value.length >= 1 &&
|
|
1626
|
+
value.length <= 32_768 &&
|
|
1627
|
+
value.trim() === value)
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1401
1631
|
function defaultStorage(): CodexRealtimeOwnerStorage | undefined {
|
|
1402
1632
|
return typeof sessionStorage === "undefined" ? undefined : sessionStorage;
|
|
1403
1633
|
}
|
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
|
|