@opengeni/db 0.22.1 → 0.23.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/dist/{chunk-CYGFLLMN.js → chunk-3UCHDMKG.js} +656 -282
- package/dist/chunk-3UCHDMKG.js.map +1 -0
- package/dist/{chunk-BNGEN5QZ.js → chunk-L6ADMZHE.js} +24 -2
- package/dist/chunk-L6ADMZHE.js.map +1 -0
- package/dist/index.d.ts +42 -3
- package/dist/index.js +6919 -3760
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +1416 -92
- package/dist/schema.js +11 -1
- package/dist/session-control.d.ts +7 -1
- package/dist/session-queue-commands.d.ts +8 -0
- package/dist/session-realtime-context.d.ts +56 -0
- package/dist/session-realtime-ledger.d.ts +188 -0
- package/dist/session-realtime-mirror.d.ts +30 -0
- package/dist/session-realtime-state.d.ts +2 -0
- package/dist/session-realtime-terminal.d.ts +40 -0
- package/dist/session-realtime.d.ts +59 -0
- package/dist/workspace-instruction-policies-schema.d.ts +239 -0
- package/dist/workspace-instruction-policies.d.ts +44 -0
- package/drizzle/0156_slack_reaction_trigger.sql +49 -0
- package/drizzle/0157_session_policy_role_snapshots.sql +1146 -0
- package/drizzle/0158_session_realtime_mode.sql +88 -0
- package/drizzle/0159_session_realtime_ledger.sql +198 -0
- package/drizzle/0160_session_realtime_delegation_terminal.sql +38 -0
- package/drizzle/0161_session_realtime_context_projection.sql +82 -0
- package/drizzle/0162_session_realtime_connection_promotion.sql +53 -0
- package/drizzle/0163_session_realtime_delegation_progress.sql +35 -0
- package/drizzle/0164_session_realtime_models.sql +28 -0
- package/package.json +4 -4
- package/src/index.ts +670 -79
- package/src/preference-registry.ts +11 -6
- package/src/provision-roles.ts +12 -0
- package/src/runtime-posture.ts +10 -0
- package/src/schema.ts +400 -43
- package/src/session-control.ts +596 -21
- package/src/session-queue-commands.ts +76 -7
- package/src/session-realtime-context.ts +393 -0
- package/src/session-realtime-ledger.ts +1790 -0
- package/src/session-realtime-mirror.ts +160 -0
- package/src/session-realtime-state.ts +25 -0
- package/src/session-realtime-terminal.ts +306 -0
- package/src/session-realtime.ts +611 -0
- package/src/workspace-instruction-policies-schema.ts +41 -0
- package/src/workspace-instruction-policies.ts +131 -2
- package/dist/chunk-BNGEN5QZ.js.map +0 -1
- package/dist/chunk-CYGFLLMN.js.map +0 -1
|
@@ -0,0 +1,1790 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { LatencyMode, ReasoningEffort, type SessionRealtimeMode } from "@opengeni/contracts";
|
|
4
|
+
import { and, asc, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
|
5
|
+
|
|
6
|
+
import { sanitizeEventPayload } from "./event-payload-sanitizer";
|
|
7
|
+
import type { Database } from "./index";
|
|
8
|
+
import * as schema from "./schema";
|
|
9
|
+
import {
|
|
10
|
+
assertSessionRealtimeOwnerInTransaction,
|
|
11
|
+
SessionRealtimeConflictError,
|
|
12
|
+
type AssertSessionRealtimeOwnerInput,
|
|
13
|
+
} from "./session-realtime";
|
|
14
|
+
import { lockSessionEventWriteRows, lockWorkspaceInferenceControl } from "./session-control";
|
|
15
|
+
import { submitHumanPromptInTransaction } from "./session-queue-commands";
|
|
16
|
+
import { mirrorSessionRealtimeContextInTransaction } from "./session-realtime-mirror";
|
|
17
|
+
|
|
18
|
+
export const SESSION_REALTIME_LEDGER_MAX_BATCH = 64;
|
|
19
|
+
export const SESSION_REALTIME_LEDGER_MAX_TEXT_BYTES = 131_072;
|
|
20
|
+
export const SESSION_REALTIME_LEDGER_MAX_PAYLOAD_BYTES = 131_072;
|
|
21
|
+
export const SESSION_REALTIME_LEDGER_MAX_OUTBOUND = 100;
|
|
22
|
+
export const SESSION_REALTIME_STARTUP_MAX_ENTRIES = 100;
|
|
23
|
+
const SESSION_REALTIME_SOURCE_EVENT_IDS_MAX = 64;
|
|
24
|
+
|
|
25
|
+
export type SessionRealtimeConnectionState =
|
|
26
|
+
| "negotiating"
|
|
27
|
+
| "ready"
|
|
28
|
+
| "active"
|
|
29
|
+
| "failed"
|
|
30
|
+
| "closed";
|
|
31
|
+
|
|
32
|
+
export type SessionRealtimeConnectionPromotionMode = "legacy" | "staged";
|
|
33
|
+
|
|
34
|
+
export type SessionRealtimeConnection = {
|
|
35
|
+
id: string;
|
|
36
|
+
realtimeId: string;
|
|
37
|
+
operationId: string;
|
|
38
|
+
connectionEpoch: number;
|
|
39
|
+
startupFenceSequence: number;
|
|
40
|
+
promotionMode: SessionRealtimeConnectionPromotionMode;
|
|
41
|
+
state: SessionRealtimeConnectionState;
|
|
42
|
+
sdpAnswer: string | null;
|
|
43
|
+
failureCode: string | null;
|
|
44
|
+
providerSessionId: string | null;
|
|
45
|
+
startupEventId: string | null;
|
|
46
|
+
startupAcknowledgedAt: string | null;
|
|
47
|
+
negotiatedAt: string | null;
|
|
48
|
+
closedAt: string | null;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type ClaimSessionRealtimeConnectionInput = AssertSessionRealtimeOwnerInput & {
|
|
54
|
+
operationId: string;
|
|
55
|
+
expectedConnectionEpoch: number;
|
|
56
|
+
rotate: boolean;
|
|
57
|
+
promotionMode?: SessionRealtimeConnectionPromotionMode | undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type ClaimSessionRealtimeConnectionResult = {
|
|
61
|
+
connection: SessionRealtimeConnection;
|
|
62
|
+
startupEntries: SessionRealtimeLedgerEntry[];
|
|
63
|
+
mode: SessionRealtimeMode;
|
|
64
|
+
modeVersion: number;
|
|
65
|
+
replay: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type CompleteSessionRealtimeConnectionInput = {
|
|
69
|
+
workspaceId: string;
|
|
70
|
+
sessionId: string;
|
|
71
|
+
realtimeId: string;
|
|
72
|
+
connectionId: string;
|
|
73
|
+
operationId: string;
|
|
74
|
+
connectionEpoch: number;
|
|
75
|
+
sdpAnswer: string;
|
|
76
|
+
now?: Date;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type CompleteSessionRealtimeConnectionResult = {
|
|
80
|
+
connection: SessionRealtimeConnection;
|
|
81
|
+
replay: boolean;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type ActivateSessionRealtimeConnectionInput = AssertSessionRealtimeOwnerInput & {
|
|
85
|
+
operationId: string;
|
|
86
|
+
connectionId: string;
|
|
87
|
+
connectionEpoch: number;
|
|
88
|
+
expectedConnectionEpoch: number;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type ActivateSessionRealtimeConnectionResult = {
|
|
92
|
+
connection: SessionRealtimeConnection;
|
|
93
|
+
mode: SessionRealtimeMode;
|
|
94
|
+
replay: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type FailSessionRealtimeConnectionInput = Omit<
|
|
98
|
+
CompleteSessionRealtimeConnectionInput,
|
|
99
|
+
"sdpAnswer"
|
|
100
|
+
> & {
|
|
101
|
+
failureCode: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type FailSessionRealtimeConnectionResult = {
|
|
105
|
+
connection: SessionRealtimeConnection;
|
|
106
|
+
replay: boolean;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type SessionRealtimeLedgerDirection = "provider_in" | "provider_out";
|
|
110
|
+
|
|
111
|
+
export type SessionRealtimeLedgerKind =
|
|
112
|
+
| "user_transcript"
|
|
113
|
+
| "assistant_transcript"
|
|
114
|
+
| "delegation_call"
|
|
115
|
+
| "delegation_progress"
|
|
116
|
+
| "delegation_result"
|
|
117
|
+
| "interruption"
|
|
118
|
+
| "session_update"
|
|
119
|
+
| "error";
|
|
120
|
+
|
|
121
|
+
export type SessionRealtimeLedgerEntry = {
|
|
122
|
+
id: string;
|
|
123
|
+
realtimeId: string;
|
|
124
|
+
operationId: string;
|
|
125
|
+
connectionEpoch: number;
|
|
126
|
+
sequence: number;
|
|
127
|
+
direction: SessionRealtimeLedgerDirection;
|
|
128
|
+
kind: SessionRealtimeLedgerKind;
|
|
129
|
+
role: "user" | "assistant" | null;
|
|
130
|
+
providerEventId: string | null;
|
|
131
|
+
delegationItemId: string | null;
|
|
132
|
+
sourceUpdateId: string | null;
|
|
133
|
+
historyItemId: string | null;
|
|
134
|
+
turnId: string | null;
|
|
135
|
+
text: string | null;
|
|
136
|
+
payload: Record<string, unknown>;
|
|
137
|
+
clientAckedAt: string | null;
|
|
138
|
+
providerAckedAt: string | null;
|
|
139
|
+
createdAt: string;
|
|
140
|
+
updatedAt: string;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export type SessionRealtimeInboundEntryInput = {
|
|
144
|
+
operationId: string;
|
|
145
|
+
kind: Exclude<
|
|
146
|
+
SessionRealtimeLedgerKind,
|
|
147
|
+
"delegation_progress" | "delegation_result" | "session_update"
|
|
148
|
+
>;
|
|
149
|
+
role?: "user" | "assistant" | null | undefined;
|
|
150
|
+
providerEventId?: string | null | undefined;
|
|
151
|
+
delegationItemId?: string | null | undefined;
|
|
152
|
+
text?: string | null | undefined;
|
|
153
|
+
payload?: Record<string, unknown> | undefined;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export type SyncSessionRealtimeLedgerInput = AssertSessionRealtimeOwnerInput & {
|
|
157
|
+
connectionId: string;
|
|
158
|
+
connectionEpoch: number;
|
|
159
|
+
entries?: SessionRealtimeInboundEntryInput[] | undefined;
|
|
160
|
+
clientAckThroughSequence?: number | null | undefined;
|
|
161
|
+
providerAckSequences?: number[] | undefined;
|
|
162
|
+
providerStarted?:
|
|
163
|
+
| { providerSessionId: string; providerEventId?: string | null | undefined }
|
|
164
|
+
| undefined;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export type SyncSessionRealtimeLedgerResult = {
|
|
168
|
+
accepted: Array<{ entry: SessionRealtimeLedgerEntry; replay: boolean }>;
|
|
169
|
+
outbound: SessionRealtimeLedgerEntry[];
|
|
170
|
+
eventIds: string[];
|
|
171
|
+
workflowWakeRevision: number | null;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export type SyncSessionRealtimeLedgerHooks = {
|
|
175
|
+
/** Test-only failure injection after both durable admission rows exist. */
|
|
176
|
+
afterDelegationAdmission?:
|
|
177
|
+
| ((admission: { entryId: string; turnId: string }) => void | Promise<void>)
|
|
178
|
+
| undefined;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type AppendSessionRealtimeOutboundInput = {
|
|
182
|
+
workspaceId: string;
|
|
183
|
+
sessionId: string;
|
|
184
|
+
realtimeId: string;
|
|
185
|
+
operationId: string;
|
|
186
|
+
connectionEpoch: number;
|
|
187
|
+
kind: Extract<SessionRealtimeLedgerKind, "delegation_result" | "error">;
|
|
188
|
+
delegationItemId?: string | null;
|
|
189
|
+
text?: string | null;
|
|
190
|
+
payload?: Record<string, unknown>;
|
|
191
|
+
now?: Date;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export type AppendSessionRealtimeOutboundResult = {
|
|
195
|
+
entry: SessionRealtimeLedgerEntry;
|
|
196
|
+
replay: boolean;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export {
|
|
200
|
+
projectSessionRealtimeDelegationTerminalInTransaction,
|
|
201
|
+
type ProjectSessionRealtimeDelegationTerminalInput,
|
|
202
|
+
type ProjectSessionRealtimeDelegationTerminalResult,
|
|
203
|
+
} from "./session-realtime-terminal";
|
|
204
|
+
|
|
205
|
+
export type ProjectSessionRealtimeDelegationProgressInput = {
|
|
206
|
+
accountId: string;
|
|
207
|
+
workspaceId: string;
|
|
208
|
+
sessionId: string;
|
|
209
|
+
turnId: string;
|
|
210
|
+
events: ReadonlyArray<{
|
|
211
|
+
id: string;
|
|
212
|
+
sequence: number;
|
|
213
|
+
type: string;
|
|
214
|
+
payload: unknown;
|
|
215
|
+
}>;
|
|
216
|
+
now?: Date;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export type ProjectSessionRealtimeDelegationProgressResult = {
|
|
220
|
+
entries: SessionRealtimeLedgerEntry[];
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
type ConnectionRow = typeof schema.sessionRealtimeConnections.$inferSelect;
|
|
224
|
+
type EntryRow = typeof schema.sessionRealtimeEntries.$inferSelect;
|
|
225
|
+
|
|
226
|
+
function mapConnection(row: ConnectionRow): SessionRealtimeConnection {
|
|
227
|
+
return {
|
|
228
|
+
id: row.id,
|
|
229
|
+
realtimeId: row.realtimeId,
|
|
230
|
+
operationId: row.operationId,
|
|
231
|
+
connectionEpoch: row.connectionEpoch,
|
|
232
|
+
startupFenceSequence: row.startupFenceSequence,
|
|
233
|
+
promotionMode: row.promotionMode as SessionRealtimeConnectionPromotionMode,
|
|
234
|
+
state: row.state as SessionRealtimeConnectionState,
|
|
235
|
+
sdpAnswer: row.sdpAnswer,
|
|
236
|
+
failureCode: row.failureCode,
|
|
237
|
+
providerSessionId: row.providerSessionId,
|
|
238
|
+
startupEventId: row.startupEventId,
|
|
239
|
+
startupAcknowledgedAt: row.startupAcknowledgedAt?.toISOString() ?? null,
|
|
240
|
+
negotiatedAt: row.negotiatedAt?.toISOString() ?? null,
|
|
241
|
+
closedAt: row.closedAt?.toISOString() ?? null,
|
|
242
|
+
createdAt: row.createdAt.toISOString(),
|
|
243
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function mapEntry(row: EntryRow): SessionRealtimeLedgerEntry {
|
|
248
|
+
return {
|
|
249
|
+
id: row.id,
|
|
250
|
+
realtimeId: row.realtimeId,
|
|
251
|
+
operationId: row.operationId,
|
|
252
|
+
connectionEpoch: row.connectionEpoch,
|
|
253
|
+
sequence: row.sequence,
|
|
254
|
+
direction: row.direction as SessionRealtimeLedgerDirection,
|
|
255
|
+
kind: row.kind as SessionRealtimeLedgerKind,
|
|
256
|
+
role: row.role as "user" | "assistant" | null,
|
|
257
|
+
providerEventId: row.providerEventId,
|
|
258
|
+
delegationItemId: row.delegationItemId,
|
|
259
|
+
sourceUpdateId: row.sourceUpdateId,
|
|
260
|
+
historyItemId: row.historyItemId,
|
|
261
|
+
turnId: row.turnId,
|
|
262
|
+
text: row.text,
|
|
263
|
+
payload: row.payload,
|
|
264
|
+
clientAckedAt: row.clientAckedAt?.toISOString() ?? null,
|
|
265
|
+
providerAckedAt: row.providerAckedAt?.toISOString() ?? null,
|
|
266
|
+
createdAt: row.createdAt.toISOString(),
|
|
267
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function assertConnectionEpoch(value: number): void {
|
|
272
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
273
|
+
throw new SessionRealtimeConflictError(
|
|
274
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
275
|
+
"Realtime connection epoch is invalid",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function assertBoundedString(
|
|
281
|
+
value: string | null | undefined,
|
|
282
|
+
maximum: number,
|
|
283
|
+
label: string,
|
|
284
|
+
): void {
|
|
285
|
+
if (value !== null && value !== undefined && Buffer.byteLength(value, "utf8") > maximum) {
|
|
286
|
+
throw new Error(`${label} exceeds the durable realtime ledger limit`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function boundedPayload(input: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
291
|
+
const payload = sanitizeEventPayload(input ?? {});
|
|
292
|
+
if (
|
|
293
|
+
Buffer.byteLength(JSON.stringify(payload), "utf8") > SESSION_REALTIME_LEDGER_MAX_PAYLOAD_BYTES
|
|
294
|
+
) {
|
|
295
|
+
throw new Error("Realtime payload exceeds the durable ledger limit");
|
|
296
|
+
}
|
|
297
|
+
return payload;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function realtimeDelegationMetadata(metadata: Record<string, unknown>): {
|
|
301
|
+
realtimeId: string;
|
|
302
|
+
connectionEpoch: number;
|
|
303
|
+
delegationItemId: string;
|
|
304
|
+
ledgerEntryId: string;
|
|
305
|
+
} | null {
|
|
306
|
+
const value = metadata.realtimeDelegation;
|
|
307
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
308
|
+
const delegation = value as Record<string, unknown>;
|
|
309
|
+
if (
|
|
310
|
+
typeof delegation.realtimeId !== "string" ||
|
|
311
|
+
typeof delegation.connectionEpoch !== "number" ||
|
|
312
|
+
!Number.isSafeInteger(delegation.connectionEpoch) ||
|
|
313
|
+
delegation.connectionEpoch < 1 ||
|
|
314
|
+
typeof delegation.delegationItemId !== "string" ||
|
|
315
|
+
delegation.delegationItemId.length === 0 ||
|
|
316
|
+
typeof delegation.ledgerEntryId !== "string"
|
|
317
|
+
) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
realtimeId: delegation.realtimeId,
|
|
322
|
+
connectionEpoch: delegation.connectionEpoch,
|
|
323
|
+
delegationItemId: delegation.delegationItemId,
|
|
324
|
+
ledgerEntryId: delegation.ledgerEntryId,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** True only for an exact provider-call-linked ordinary turn shape. */
|
|
329
|
+
export function isSessionRealtimeDelegationTurnMetadata(
|
|
330
|
+
metadata: Record<string, unknown>,
|
|
331
|
+
): boolean {
|
|
332
|
+
return realtimeDelegationMetadata(metadata) !== null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function deterministicUuid(seed: string): string {
|
|
336
|
+
const bytes = createHash("sha256").update(seed, "utf8").digest().subarray(0, 16);
|
|
337
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
|
|
338
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
339
|
+
const hex = bytes.toString("hex");
|
|
340
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function sourceEventProvenance(ids: readonly string[]): {
|
|
344
|
+
identity: string;
|
|
345
|
+
ids: string[];
|
|
346
|
+
count: number;
|
|
347
|
+
truncated: boolean;
|
|
348
|
+
} {
|
|
349
|
+
const identity = createHash("sha256").update(ids.join(":"), "utf8").digest("hex");
|
|
350
|
+
if (ids.length <= SESSION_REALTIME_SOURCE_EVENT_IDS_MAX) {
|
|
351
|
+
return { identity, ids: [...ids], count: ids.length, truncated: false };
|
|
352
|
+
}
|
|
353
|
+
const half = SESSION_REALTIME_SOURCE_EVENT_IDS_MAX / 2;
|
|
354
|
+
return {
|
|
355
|
+
identity,
|
|
356
|
+
ids: [...ids.slice(0, half), ...ids.slice(-half)],
|
|
357
|
+
count: ids.length,
|
|
358
|
+
truncated: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function utf8Chunks(value: string, maximumBytes: number): string[] {
|
|
363
|
+
const bytes = Buffer.from(value, "utf8");
|
|
364
|
+
if (bytes.length <= maximumBytes) return [value];
|
|
365
|
+
const chunks: string[] = [];
|
|
366
|
+
let start = 0;
|
|
367
|
+
while (start < bytes.length) {
|
|
368
|
+
let end = Math.min(start + maximumBytes, bytes.length);
|
|
369
|
+
if (end < bytes.length) {
|
|
370
|
+
while (end > start && (bytes[end]! & 0xc0) === 0x80) end -= 1;
|
|
371
|
+
}
|
|
372
|
+
if (end === start) throw new Error("Realtime progress chunk boundary is invalid");
|
|
373
|
+
chunks.push(bytes.subarray(start, end).toString("utf8"));
|
|
374
|
+
start = end;
|
|
375
|
+
}
|
|
376
|
+
return chunks;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function expectedRole(input: SessionRealtimeInboundEntryInput): "user" | "assistant" | null {
|
|
380
|
+
if (input.kind === "user_transcript") {
|
|
381
|
+
if (input.role !== undefined && input.role !== "user") {
|
|
382
|
+
throw new Error("User transcript must use the user role");
|
|
383
|
+
}
|
|
384
|
+
return "user";
|
|
385
|
+
}
|
|
386
|
+
if (input.kind === "assistant_transcript") {
|
|
387
|
+
if (input.role !== undefined && input.role !== "assistant") {
|
|
388
|
+
throw new Error("Assistant transcript must use the assistant role");
|
|
389
|
+
}
|
|
390
|
+
return "assistant";
|
|
391
|
+
}
|
|
392
|
+
const role = input.role ?? null;
|
|
393
|
+
if (!["user_transcript", "assistant_transcript"].includes(input.kind) && role !== null) {
|
|
394
|
+
throw new Error("Only realtime transcripts may carry a role");
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function claimSessionRealtimeConnectionInTransaction(
|
|
400
|
+
db: Database,
|
|
401
|
+
input: ClaimSessionRealtimeConnectionInput,
|
|
402
|
+
): Promise<ClaimSessionRealtimeConnectionResult> {
|
|
403
|
+
assertConnectionEpoch(input.expectedConnectionEpoch);
|
|
404
|
+
const promotionMode = input.promotionMode ?? "staged";
|
|
405
|
+
let staleRotationError: SessionRealtimeConflictError | null = null;
|
|
406
|
+
let mode;
|
|
407
|
+
try {
|
|
408
|
+
mode = await assertSessionRealtimeOwnerInTransaction(db, input);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (
|
|
411
|
+
!input.rotate ||
|
|
412
|
+
!(error instanceof SessionRealtimeConflictError) ||
|
|
413
|
+
error.code !== "REALTIME_VERSION_CHANGED"
|
|
414
|
+
) {
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
staleRotationError = error;
|
|
418
|
+
mode = await assertSessionRealtimeOwnerInTransaction(db, {
|
|
419
|
+
...input,
|
|
420
|
+
expectedVersion: input.expectedVersion + 1,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const [existing] = await db
|
|
424
|
+
.select()
|
|
425
|
+
.from(schema.sessionRealtimeConnections)
|
|
426
|
+
.where(
|
|
427
|
+
and(
|
|
428
|
+
eq(schema.sessionRealtimeConnections.realtimeId, input.realtimeId),
|
|
429
|
+
eq(schema.sessionRealtimeConnections.operationId, input.operationId),
|
|
430
|
+
),
|
|
431
|
+
)
|
|
432
|
+
.limit(1);
|
|
433
|
+
if (existing) {
|
|
434
|
+
if (
|
|
435
|
+
existing.promotionMode !== promotionMode ||
|
|
436
|
+
(input.rotate && existing.connectionEpoch <= input.expectedConnectionEpoch) ||
|
|
437
|
+
(!input.rotate && existing.connectionEpoch !== input.expectedConnectionEpoch)
|
|
438
|
+
) {
|
|
439
|
+
throw new SessionRealtimeConflictError(
|
|
440
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
441
|
+
"Realtime connection operation was already used with different input",
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
connection: mapConnection(existing),
|
|
446
|
+
startupEntries: await startupEntriesForConnection(db, existing),
|
|
447
|
+
mode,
|
|
448
|
+
modeVersion: mode.version,
|
|
449
|
+
replay: true,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (staleRotationError) throw staleRotationError;
|
|
453
|
+
if (mode.connectionEpoch !== input.expectedConnectionEpoch) {
|
|
454
|
+
throw new SessionRealtimeConflictError(
|
|
455
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
456
|
+
"Realtime connection epoch changed",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const connections = await db
|
|
461
|
+
.select()
|
|
462
|
+
.from(schema.sessionRealtimeConnections)
|
|
463
|
+
.where(eq(schema.sessionRealtimeConnections.realtimeId, input.realtimeId))
|
|
464
|
+
.for("update");
|
|
465
|
+
const now = input.now ?? new Date();
|
|
466
|
+
const active = connections.find((connection) => connection.state === "active");
|
|
467
|
+
const preparing = connections.find(
|
|
468
|
+
(connection) => connection.state === "negotiating" || connection.state === "ready",
|
|
469
|
+
);
|
|
470
|
+
if (!input.rotate && (active || preparing)) {
|
|
471
|
+
throw new SessionRealtimeConflictError(
|
|
472
|
+
"REALTIME_CONNECTION_ACTIVE",
|
|
473
|
+
"Realtime mode already has an open provider connection",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
if (input.rotate) {
|
|
477
|
+
if (promotionMode === "staged") {
|
|
478
|
+
if (active?.promotionMode === "legacy" || preparing?.promotionMode === "legacy") {
|
|
479
|
+
throw new SessionRealtimeConflictError(
|
|
480
|
+
"REALTIME_CONNECTION_ACTIVE",
|
|
481
|
+
"Legacy realtime connection must finish rotating before staged promotion",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (preparing) {
|
|
485
|
+
await db
|
|
486
|
+
.update(schema.sessionRealtimeConnections)
|
|
487
|
+
.set({ state: "closed", closedAt: now, updatedAt: now })
|
|
488
|
+
.where(eq(schema.sessionRealtimeConnections.id, preparing.id));
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
await db
|
|
492
|
+
.update(schema.sessionRealtimeConnections)
|
|
493
|
+
.set({ state: "closed", closedAt: now, updatedAt: now })
|
|
494
|
+
.where(
|
|
495
|
+
and(
|
|
496
|
+
eq(schema.sessionRealtimeConnections.realtimeId, input.realtimeId),
|
|
497
|
+
inArray(schema.sessionRealtimeConnections.state, ["negotiating", "ready", "active"]),
|
|
498
|
+
),
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const highestEpoch = connections.reduce(
|
|
504
|
+
(maximum, connection) => Math.max(maximum, connection.connectionEpoch),
|
|
505
|
+
input.expectedConnectionEpoch,
|
|
506
|
+
);
|
|
507
|
+
const targetEpoch = input.rotate ? highestEpoch + 1 : input.expectedConnectionEpoch;
|
|
508
|
+
|
|
509
|
+
let modeVersion = mode.version;
|
|
510
|
+
if (input.rotate && promotionMode === "legacy") {
|
|
511
|
+
const [rotated] = await db
|
|
512
|
+
.update(schema.sessionRealtimeModes)
|
|
513
|
+
.set({
|
|
514
|
+
connectionEpoch: targetEpoch,
|
|
515
|
+
version: mode.version + 1,
|
|
516
|
+
updatedAt: now,
|
|
517
|
+
})
|
|
518
|
+
.where(
|
|
519
|
+
and(
|
|
520
|
+
eq(schema.sessionRealtimeModes.id, input.realtimeId),
|
|
521
|
+
eq(schema.sessionRealtimeModes.state, "active"),
|
|
522
|
+
eq(schema.sessionRealtimeModes.version, mode.version),
|
|
523
|
+
eq(schema.sessionRealtimeModes.connectionEpoch, input.expectedConnectionEpoch),
|
|
524
|
+
),
|
|
525
|
+
)
|
|
526
|
+
.returning({ version: schema.sessionRealtimeModes.version });
|
|
527
|
+
if (!rotated) {
|
|
528
|
+
throw new SessionRealtimeConflictError(
|
|
529
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
530
|
+
"Realtime connection changed while rotating",
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
modeVersion = rotated.version;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const [modeRow] = await db
|
|
537
|
+
.select({ accountId: schema.sessionRealtimeModes.accountId })
|
|
538
|
+
.from(schema.sessionRealtimeModes)
|
|
539
|
+
.where(eq(schema.sessionRealtimeModes.id, input.realtimeId))
|
|
540
|
+
.limit(1);
|
|
541
|
+
if (!modeRow) throw new Error("Realtime mode disappeared while claiming connection");
|
|
542
|
+
|
|
543
|
+
const startupEntries = await db
|
|
544
|
+
.select()
|
|
545
|
+
.from(schema.sessionRealtimeEntries)
|
|
546
|
+
.where(
|
|
547
|
+
and(
|
|
548
|
+
eq(schema.sessionRealtimeEntries.realtimeId, input.realtimeId),
|
|
549
|
+
eq(schema.sessionRealtimeEntries.direction, "provider_out"),
|
|
550
|
+
isNull(schema.sessionRealtimeEntries.providerAckedAt),
|
|
551
|
+
),
|
|
552
|
+
)
|
|
553
|
+
.orderBy(asc(schema.sessionRealtimeEntries.sequence))
|
|
554
|
+
.limit(SESSION_REALTIME_STARTUP_MAX_ENTRIES);
|
|
555
|
+
const startupFenceSequence = startupEntries.at(-1)?.sequence ?? 0;
|
|
556
|
+
|
|
557
|
+
const [connection] = await db
|
|
558
|
+
.insert(schema.sessionRealtimeConnections)
|
|
559
|
+
.values({
|
|
560
|
+
accountId: modeRow.accountId,
|
|
561
|
+
workspaceId: input.workspaceId,
|
|
562
|
+
sessionId: input.sessionId,
|
|
563
|
+
realtimeId: input.realtimeId,
|
|
564
|
+
operationId: input.operationId,
|
|
565
|
+
connectionEpoch: targetEpoch,
|
|
566
|
+
startupFenceSequence,
|
|
567
|
+
promotionMode,
|
|
568
|
+
state: "negotiating",
|
|
569
|
+
createdAt: now,
|
|
570
|
+
updatedAt: now,
|
|
571
|
+
})
|
|
572
|
+
.returning();
|
|
573
|
+
if (!connection) throw new Error("Failed to claim realtime connection");
|
|
574
|
+
return {
|
|
575
|
+
connection: mapConnection(connection),
|
|
576
|
+
startupEntries: startupEntries.map(mapEntry),
|
|
577
|
+
mode,
|
|
578
|
+
modeVersion,
|
|
579
|
+
replay: false,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function startupEntriesForConnection(
|
|
584
|
+
db: Database,
|
|
585
|
+
connection: ConnectionRow,
|
|
586
|
+
): Promise<SessionRealtimeLedgerEntry[]> {
|
|
587
|
+
if (connection.startupFenceSequence === 0) return [];
|
|
588
|
+
const rows = await db
|
|
589
|
+
.select()
|
|
590
|
+
.from(schema.sessionRealtimeEntries)
|
|
591
|
+
.where(
|
|
592
|
+
and(
|
|
593
|
+
eq(schema.sessionRealtimeEntries.realtimeId, connection.realtimeId),
|
|
594
|
+
eq(schema.sessionRealtimeEntries.direction, "provider_out"),
|
|
595
|
+
isNull(schema.sessionRealtimeEntries.providerAckedAt),
|
|
596
|
+
sql`${schema.sessionRealtimeEntries.sequence} <= ${connection.startupFenceSequence}`,
|
|
597
|
+
),
|
|
598
|
+
)
|
|
599
|
+
.orderBy(asc(schema.sessionRealtimeEntries.sequence))
|
|
600
|
+
.limit(SESSION_REALTIME_STARTUP_MAX_ENTRIES);
|
|
601
|
+
return rows.map(mapEntry);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function lockConnectionFinalizationMode(
|
|
605
|
+
db: Database,
|
|
606
|
+
input: Pick<
|
|
607
|
+
CompleteSessionRealtimeConnectionInput,
|
|
608
|
+
"workspaceId" | "sessionId" | "realtimeId" | "connectionEpoch" | "now"
|
|
609
|
+
>,
|
|
610
|
+
) {
|
|
611
|
+
await lockSessionEventWriteRows(db, {
|
|
612
|
+
workspaceId: input.workspaceId,
|
|
613
|
+
controlLock: "share",
|
|
614
|
+
sessionIds: [input.sessionId],
|
|
615
|
+
});
|
|
616
|
+
const [mode] = await db
|
|
617
|
+
.select()
|
|
618
|
+
.from(schema.sessionRealtimeModes)
|
|
619
|
+
.where(
|
|
620
|
+
and(
|
|
621
|
+
eq(schema.sessionRealtimeModes.workspaceId, input.workspaceId),
|
|
622
|
+
eq(schema.sessionRealtimeModes.sessionId, input.sessionId),
|
|
623
|
+
eq(schema.sessionRealtimeModes.id, input.realtimeId),
|
|
624
|
+
),
|
|
625
|
+
)
|
|
626
|
+
.for("update")
|
|
627
|
+
.limit(1);
|
|
628
|
+
if (!mode || mode.state !== "active" || mode.leaseExpiresAt <= (input.now ?? new Date())) {
|
|
629
|
+
throw new SessionRealtimeConflictError(
|
|
630
|
+
"REALTIME_NOT_ACTIVE",
|
|
631
|
+
"Realtime mode is no longer active",
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (mode.connectionEpoch > input.connectionEpoch) {
|
|
635
|
+
throw new SessionRealtimeConflictError(
|
|
636
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
637
|
+
"Realtime connection epoch changed before negotiation completed",
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return mode;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export async function completeSessionRealtimeConnectionInTransaction(
|
|
644
|
+
db: Database,
|
|
645
|
+
input: CompleteSessionRealtimeConnectionInput,
|
|
646
|
+
): Promise<CompleteSessionRealtimeConnectionResult> {
|
|
647
|
+
assertConnectionEpoch(input.connectionEpoch);
|
|
648
|
+
assertBoundedString(input.sdpAnswer, 1_048_576, "Realtime SDP answer");
|
|
649
|
+
if (input.sdpAnswer.length === 0) throw new Error("Realtime SDP answer is empty");
|
|
650
|
+
await lockConnectionFinalizationMode(db, input);
|
|
651
|
+
const [connection] = await db
|
|
652
|
+
.select()
|
|
653
|
+
.from(schema.sessionRealtimeConnections)
|
|
654
|
+
.where(
|
|
655
|
+
and(
|
|
656
|
+
eq(schema.sessionRealtimeConnections.workspaceId, input.workspaceId),
|
|
657
|
+
eq(schema.sessionRealtimeConnections.sessionId, input.sessionId),
|
|
658
|
+
eq(schema.sessionRealtimeConnections.realtimeId, input.realtimeId),
|
|
659
|
+
eq(schema.sessionRealtimeConnections.id, input.connectionId),
|
|
660
|
+
),
|
|
661
|
+
)
|
|
662
|
+
.for("update")
|
|
663
|
+
.limit(1);
|
|
664
|
+
if (!connection) {
|
|
665
|
+
throw new SessionRealtimeConflictError(
|
|
666
|
+
"REALTIME_CONNECTION_NOT_FOUND",
|
|
667
|
+
"Realtime connection claim not found",
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (
|
|
671
|
+
connection.operationId !== input.operationId ||
|
|
672
|
+
connection.connectionEpoch !== input.connectionEpoch
|
|
673
|
+
) {
|
|
674
|
+
throw new SessionRealtimeConflictError(
|
|
675
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
676
|
+
"Realtime connection claim changed",
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (connection.state === "ready" || connection.state === "active") {
|
|
680
|
+
if (connection.sdpAnswer !== input.sdpAnswer) {
|
|
681
|
+
throw new SessionRealtimeConflictError(
|
|
682
|
+
"REALTIME_CONNECTION_STATE_CHANGED",
|
|
683
|
+
"Realtime connection was already completed with another answer",
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return { connection: mapConnection(connection), replay: true };
|
|
687
|
+
}
|
|
688
|
+
if (connection.state !== "negotiating") {
|
|
689
|
+
throw new SessionRealtimeConflictError(
|
|
690
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
691
|
+
"Realtime connection is no longer current",
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
const now = input.now ?? new Date();
|
|
695
|
+
const completedState = connection.promotionMode === "legacy" ? "active" : "ready";
|
|
696
|
+
const [completed] = await db
|
|
697
|
+
.update(schema.sessionRealtimeConnections)
|
|
698
|
+
.set({
|
|
699
|
+
state: completedState,
|
|
700
|
+
sdpAnswer: input.sdpAnswer,
|
|
701
|
+
negotiatedAt: now,
|
|
702
|
+
updatedAt: now,
|
|
703
|
+
})
|
|
704
|
+
.where(
|
|
705
|
+
and(
|
|
706
|
+
eq(schema.sessionRealtimeConnections.id, input.connectionId),
|
|
707
|
+
eq(schema.sessionRealtimeConnections.state, "negotiating"),
|
|
708
|
+
),
|
|
709
|
+
)
|
|
710
|
+
.returning();
|
|
711
|
+
if (!completed) {
|
|
712
|
+
throw new SessionRealtimeConflictError(
|
|
713
|
+
"REALTIME_CONNECTION_STATE_CHANGED",
|
|
714
|
+
"Realtime connection changed while negotiation completed",
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
return { connection: mapConnection(completed), replay: false };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export async function activateSessionRealtimeConnectionInTransaction(
|
|
721
|
+
db: Database,
|
|
722
|
+
input: ActivateSessionRealtimeConnectionInput,
|
|
723
|
+
): Promise<ActivateSessionRealtimeConnectionResult> {
|
|
724
|
+
assertConnectionEpoch(input.connectionEpoch);
|
|
725
|
+
assertConnectionEpoch(input.expectedConnectionEpoch);
|
|
726
|
+
let mode;
|
|
727
|
+
try {
|
|
728
|
+
mode = await assertSessionRealtimeOwnerInTransaction(db, input);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if (
|
|
731
|
+
!(error instanceof SessionRealtimeConflictError) ||
|
|
732
|
+
error.code !== "REALTIME_VERSION_CHANGED"
|
|
733
|
+
) {
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
mode = await assertSessionRealtimeOwnerInTransaction(db, {
|
|
737
|
+
...input,
|
|
738
|
+
expectedVersion: input.expectedVersion + 1,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const [connection] = await db
|
|
743
|
+
.select()
|
|
744
|
+
.from(schema.sessionRealtimeConnections)
|
|
745
|
+
.where(
|
|
746
|
+
and(
|
|
747
|
+
eq(schema.sessionRealtimeConnections.workspaceId, input.workspaceId),
|
|
748
|
+
eq(schema.sessionRealtimeConnections.sessionId, input.sessionId),
|
|
749
|
+
eq(schema.sessionRealtimeConnections.realtimeId, input.realtimeId),
|
|
750
|
+
eq(schema.sessionRealtimeConnections.id, input.connectionId),
|
|
751
|
+
),
|
|
752
|
+
)
|
|
753
|
+
.for("update")
|
|
754
|
+
.limit(1);
|
|
755
|
+
if (!connection) {
|
|
756
|
+
throw new SessionRealtimeConflictError(
|
|
757
|
+
"REALTIME_CONNECTION_NOT_FOUND",
|
|
758
|
+
"Realtime connection claim not found",
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
if (
|
|
762
|
+
connection.operationId !== input.operationId ||
|
|
763
|
+
connection.connectionEpoch !== input.connectionEpoch
|
|
764
|
+
) {
|
|
765
|
+
throw new SessionRealtimeConflictError(
|
|
766
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
767
|
+
"Realtime connection claim changed",
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
if (connection.state === "active") {
|
|
771
|
+
if (mode.connectionEpoch !== connection.connectionEpoch) {
|
|
772
|
+
throw new SessionRealtimeConflictError(
|
|
773
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
774
|
+
"Realtime connection is no longer current",
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
connection: mapConnection(connection),
|
|
779
|
+
mode,
|
|
780
|
+
replay: true,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (
|
|
784
|
+
connection.promotionMode !== "staged" ||
|
|
785
|
+
connection.state !== "ready" ||
|
|
786
|
+
mode.connectionEpoch !== input.expectedConnectionEpoch ||
|
|
787
|
+
connection.connectionEpoch < mode.connectionEpoch
|
|
788
|
+
) {
|
|
789
|
+
throw new SessionRealtimeConflictError(
|
|
790
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
791
|
+
"Realtime replacement is no longer ready for activation",
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const now = input.now ?? new Date();
|
|
796
|
+
await db
|
|
797
|
+
.update(schema.sessionRealtimeConnections)
|
|
798
|
+
.set({ state: "closed", closedAt: now, updatedAt: now })
|
|
799
|
+
.where(
|
|
800
|
+
and(
|
|
801
|
+
eq(schema.sessionRealtimeConnections.realtimeId, input.realtimeId),
|
|
802
|
+
eq(schema.sessionRealtimeConnections.state, "active"),
|
|
803
|
+
),
|
|
804
|
+
);
|
|
805
|
+
const [activated] = await db
|
|
806
|
+
.update(schema.sessionRealtimeConnections)
|
|
807
|
+
.set({ state: "active", updatedAt: now })
|
|
808
|
+
.where(
|
|
809
|
+
and(
|
|
810
|
+
eq(schema.sessionRealtimeConnections.id, connection.id),
|
|
811
|
+
eq(schema.sessionRealtimeConnections.state, "ready"),
|
|
812
|
+
),
|
|
813
|
+
)
|
|
814
|
+
.returning();
|
|
815
|
+
if (!activated) {
|
|
816
|
+
throw new SessionRealtimeConflictError(
|
|
817
|
+
"REALTIME_CONNECTION_STATE_CHANGED",
|
|
818
|
+
"Realtime replacement changed while activating",
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
let modeVersion = mode.version;
|
|
823
|
+
if (mode.connectionEpoch !== connection.connectionEpoch) {
|
|
824
|
+
const [promoted] = await db
|
|
825
|
+
.update(schema.sessionRealtimeModes)
|
|
826
|
+
.set({
|
|
827
|
+
connectionEpoch: connection.connectionEpoch,
|
|
828
|
+
version: mode.version + 1,
|
|
829
|
+
updatedAt: now,
|
|
830
|
+
})
|
|
831
|
+
.where(
|
|
832
|
+
and(
|
|
833
|
+
eq(schema.sessionRealtimeModes.id, input.realtimeId),
|
|
834
|
+
eq(schema.sessionRealtimeModes.state, "active"),
|
|
835
|
+
eq(schema.sessionRealtimeModes.version, mode.version),
|
|
836
|
+
eq(schema.sessionRealtimeModes.connectionEpoch, input.expectedConnectionEpoch),
|
|
837
|
+
),
|
|
838
|
+
)
|
|
839
|
+
.returning({ version: schema.sessionRealtimeModes.version });
|
|
840
|
+
if (!promoted) {
|
|
841
|
+
throw new SessionRealtimeConflictError(
|
|
842
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
843
|
+
"Realtime connection changed while activating replacement",
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
modeVersion = promoted.version;
|
|
847
|
+
}
|
|
848
|
+
return {
|
|
849
|
+
connection: mapConnection(activated),
|
|
850
|
+
mode: {
|
|
851
|
+
...mode,
|
|
852
|
+
version: modeVersion,
|
|
853
|
+
connectionEpoch: connection.connectionEpoch,
|
|
854
|
+
},
|
|
855
|
+
replay: false,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function failSessionRealtimeConnectionInTransaction(
|
|
860
|
+
db: Database,
|
|
861
|
+
input: FailSessionRealtimeConnectionInput,
|
|
862
|
+
): Promise<FailSessionRealtimeConnectionResult> {
|
|
863
|
+
assertConnectionEpoch(input.connectionEpoch);
|
|
864
|
+
assertBoundedString(input.failureCode, 128, "Realtime connection failure code");
|
|
865
|
+
if (!/^[a-z0-9_.:-]+$/i.test(input.failureCode)) {
|
|
866
|
+
throw new Error("Realtime connection failure code is invalid");
|
|
867
|
+
}
|
|
868
|
+
await lockConnectionFinalizationMode(db, input);
|
|
869
|
+
const [connection] = await db
|
|
870
|
+
.select()
|
|
871
|
+
.from(schema.sessionRealtimeConnections)
|
|
872
|
+
.where(
|
|
873
|
+
and(
|
|
874
|
+
eq(schema.sessionRealtimeConnections.workspaceId, input.workspaceId),
|
|
875
|
+
eq(schema.sessionRealtimeConnections.sessionId, input.sessionId),
|
|
876
|
+
eq(schema.sessionRealtimeConnections.realtimeId, input.realtimeId),
|
|
877
|
+
eq(schema.sessionRealtimeConnections.id, input.connectionId),
|
|
878
|
+
),
|
|
879
|
+
)
|
|
880
|
+
.for("update")
|
|
881
|
+
.limit(1);
|
|
882
|
+
if (!connection) {
|
|
883
|
+
throw new SessionRealtimeConflictError(
|
|
884
|
+
"REALTIME_CONNECTION_NOT_FOUND",
|
|
885
|
+
"Realtime connection claim not found",
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
if (
|
|
889
|
+
connection.operationId !== input.operationId ||
|
|
890
|
+
connection.connectionEpoch !== input.connectionEpoch
|
|
891
|
+
) {
|
|
892
|
+
throw new SessionRealtimeConflictError(
|
|
893
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
894
|
+
"Realtime connection claim changed",
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (connection.state === "failed") {
|
|
898
|
+
if (connection.failureCode !== input.failureCode) {
|
|
899
|
+
throw new SessionRealtimeConflictError(
|
|
900
|
+
"REALTIME_CONNECTION_STATE_CHANGED",
|
|
901
|
+
"Realtime connection was already failed with another code",
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
return { connection: mapConnection(connection), replay: true };
|
|
905
|
+
}
|
|
906
|
+
if (connection.state !== "negotiating") {
|
|
907
|
+
throw new SessionRealtimeConflictError(
|
|
908
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
909
|
+
"Realtime connection is no longer negotiating",
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
const now = input.now ?? new Date();
|
|
913
|
+
const [failed] = await db
|
|
914
|
+
.update(schema.sessionRealtimeConnections)
|
|
915
|
+
.set({
|
|
916
|
+
state: "failed",
|
|
917
|
+
failureCode: input.failureCode,
|
|
918
|
+
closedAt: now,
|
|
919
|
+
updatedAt: now,
|
|
920
|
+
})
|
|
921
|
+
.where(
|
|
922
|
+
and(
|
|
923
|
+
eq(schema.sessionRealtimeConnections.id, input.connectionId),
|
|
924
|
+
eq(schema.sessionRealtimeConnections.state, "negotiating"),
|
|
925
|
+
),
|
|
926
|
+
)
|
|
927
|
+
.returning();
|
|
928
|
+
if (!failed) {
|
|
929
|
+
throw new SessionRealtimeConflictError(
|
|
930
|
+
"REALTIME_CONNECTION_STATE_CHANGED",
|
|
931
|
+
"Realtime connection changed while negotiation failed",
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
return { connection: mapConnection(failed), replay: false };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async function nextLedgerSequence(db: Database, realtimeId: string): Promise<number> {
|
|
938
|
+
const [row] = await db
|
|
939
|
+
.select({
|
|
940
|
+
next: sql<number>`coalesce(max(${schema.sessionRealtimeEntries.sequence}), 0) + 1`,
|
|
941
|
+
})
|
|
942
|
+
.from(schema.sessionRealtimeEntries)
|
|
943
|
+
.where(eq(schema.sessionRealtimeEntries.realtimeId, realtimeId));
|
|
944
|
+
return Number(row?.next ?? 1);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
async function validateActiveConnection(
|
|
948
|
+
db: Database,
|
|
949
|
+
input: {
|
|
950
|
+
workspaceId: string;
|
|
951
|
+
sessionId: string;
|
|
952
|
+
realtimeId: string;
|
|
953
|
+
connectionId: string;
|
|
954
|
+
connectionEpoch: number;
|
|
955
|
+
},
|
|
956
|
+
) {
|
|
957
|
+
const [connection] = await db
|
|
958
|
+
.select()
|
|
959
|
+
.from(schema.sessionRealtimeConnections)
|
|
960
|
+
.where(
|
|
961
|
+
and(
|
|
962
|
+
eq(schema.sessionRealtimeConnections.workspaceId, input.workspaceId),
|
|
963
|
+
eq(schema.sessionRealtimeConnections.sessionId, input.sessionId),
|
|
964
|
+
eq(schema.sessionRealtimeConnections.realtimeId, input.realtimeId),
|
|
965
|
+
eq(schema.sessionRealtimeConnections.id, input.connectionId),
|
|
966
|
+
),
|
|
967
|
+
)
|
|
968
|
+
.for("update")
|
|
969
|
+
.limit(1);
|
|
970
|
+
if (!connection) {
|
|
971
|
+
throw new SessionRealtimeConflictError(
|
|
972
|
+
"REALTIME_CONNECTION_NOT_FOUND",
|
|
973
|
+
"Realtime connection not found",
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
if (connection.connectionEpoch !== input.connectionEpoch || connection.state !== "active") {
|
|
977
|
+
throw new SessionRealtimeConflictError(
|
|
978
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
979
|
+
"Realtime connection is no longer active",
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
return connection;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function delegationCallFailure(
|
|
986
|
+
input: SessionRealtimeInboundEntryInput,
|
|
987
|
+
): { code: "invalid_delegation_call"; message: string } | null {
|
|
988
|
+
if (!input.delegationItemId?.trim()) {
|
|
989
|
+
return {
|
|
990
|
+
code: "invalid_delegation_call",
|
|
991
|
+
message: "Realtime delegation call is missing its provider item identity",
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
if (!input.text?.trim()) {
|
|
995
|
+
return {
|
|
996
|
+
code: "invalid_delegation_call",
|
|
997
|
+
message: "Realtime delegation call is missing ordinary work instructions",
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function canonicalJsonValue(value: unknown): unknown {
|
|
1004
|
+
if (Array.isArray(value)) return value.map(canonicalJsonValue);
|
|
1005
|
+
if (value && typeof value === "object") {
|
|
1006
|
+
return Object.fromEntries(
|
|
1007
|
+
Object.entries(value as Record<string, unknown>)
|
|
1008
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
1009
|
+
.map(([key, entry]) => [key, canonicalJsonValue(entry)]),
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
return value;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function inboundReplayMatches(
|
|
1016
|
+
row: EntryRow,
|
|
1017
|
+
input: SessionRealtimeInboundEntryInput,
|
|
1018
|
+
role: "user" | "assistant" | null,
|
|
1019
|
+
text: string | null,
|
|
1020
|
+
payload: Record<string, unknown>,
|
|
1021
|
+
): boolean {
|
|
1022
|
+
return (
|
|
1023
|
+
row.direction === "provider_in" &&
|
|
1024
|
+
row.kind === input.kind &&
|
|
1025
|
+
row.role === role &&
|
|
1026
|
+
row.providerEventId === (input.providerEventId ?? null) &&
|
|
1027
|
+
row.delegationItemId === (input.delegationItemId ?? null) &&
|
|
1028
|
+
row.sourceUpdateId === null &&
|
|
1029
|
+
row.text === text &&
|
|
1030
|
+
JSON.stringify(canonicalJsonValue(row.payload)) === JSON.stringify(canonicalJsonValue(payload))
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
async function appendInvalidDelegationFailure(
|
|
1035
|
+
db: Database,
|
|
1036
|
+
input: Pick<
|
|
1037
|
+
SyncSessionRealtimeLedgerInput,
|
|
1038
|
+
"workspaceId" | "sessionId" | "realtimeId" | "connectionEpoch"
|
|
1039
|
+
>,
|
|
1040
|
+
accountId: string,
|
|
1041
|
+
incoming: SessionRealtimeInboundEntryInput,
|
|
1042
|
+
failure: NonNullable<ReturnType<typeof delegationCallFailure>>,
|
|
1043
|
+
sequence: number,
|
|
1044
|
+
now: Date,
|
|
1045
|
+
): Promise<void> {
|
|
1046
|
+
await db.insert(schema.sessionRealtimeEntries).values({
|
|
1047
|
+
accountId,
|
|
1048
|
+
workspaceId: input.workspaceId,
|
|
1049
|
+
sessionId: input.sessionId,
|
|
1050
|
+
realtimeId: input.realtimeId,
|
|
1051
|
+
operationId: crypto.randomUUID(),
|
|
1052
|
+
connectionEpoch: input.connectionEpoch,
|
|
1053
|
+
sequence,
|
|
1054
|
+
direction: "provider_out",
|
|
1055
|
+
kind: "error",
|
|
1056
|
+
delegationItemId: incoming.delegationItemId ?? null,
|
|
1057
|
+
text: failure.message,
|
|
1058
|
+
payload: {
|
|
1059
|
+
code: failure.code,
|
|
1060
|
+
callOperationId: incoming.operationId,
|
|
1061
|
+
},
|
|
1062
|
+
createdAt: now,
|
|
1063
|
+
updatedAt: now,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async function admitRealtimeDelegationInTransaction(
|
|
1068
|
+
db: Database,
|
|
1069
|
+
input: Pick<
|
|
1070
|
+
SyncSessionRealtimeLedgerInput,
|
|
1071
|
+
"workspaceId" | "sessionId" | "realtimeId" | "connectionEpoch" | "ownerSubjectId"
|
|
1072
|
+
>,
|
|
1073
|
+
accountId: string,
|
|
1074
|
+
incoming: SessionRealtimeInboundEntryInput,
|
|
1075
|
+
entryId: string,
|
|
1076
|
+
): Promise<{ turnId: string; eventIds: string[]; wakeRevision: number }> {
|
|
1077
|
+
const [session] = await db
|
|
1078
|
+
.select()
|
|
1079
|
+
.from(schema.sessions)
|
|
1080
|
+
.where(
|
|
1081
|
+
and(
|
|
1082
|
+
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
1083
|
+
eq(schema.sessions.id, input.sessionId),
|
|
1084
|
+
),
|
|
1085
|
+
)
|
|
1086
|
+
.limit(1);
|
|
1087
|
+
if (!session || session.accountId !== accountId || session.status === "cancelled") {
|
|
1088
|
+
throw new SessionRealtimeConflictError("REALTIME_NOT_FOUND", "Session not found");
|
|
1089
|
+
}
|
|
1090
|
+
const reasoning = ReasoningEffort.safeParse(session.metadata.reasoningEffort);
|
|
1091
|
+
const [latestStarted] = await db
|
|
1092
|
+
.select({
|
|
1093
|
+
model: schema.sessionTurns.model,
|
|
1094
|
+
reasoningEffort: schema.sessionTurns.reasoningEffort,
|
|
1095
|
+
latencyMode: schema.sessionTurns.latencyMode,
|
|
1096
|
+
})
|
|
1097
|
+
.from(schema.sessionTurns)
|
|
1098
|
+
.where(
|
|
1099
|
+
and(
|
|
1100
|
+
eq(schema.sessionTurns.workspaceId, input.workspaceId),
|
|
1101
|
+
eq(schema.sessionTurns.sessionId, input.sessionId),
|
|
1102
|
+
sql`${schema.sessionTurns.startedAt} is not null`,
|
|
1103
|
+
),
|
|
1104
|
+
)
|
|
1105
|
+
.orderBy(desc(schema.sessionTurns.startedAt), desc(schema.sessionTurns.createdAt))
|
|
1106
|
+
.limit(1);
|
|
1107
|
+
const latestReasoning = ReasoningEffort.safeParse(latestStarted?.reasoningEffort);
|
|
1108
|
+
const latestLatency = LatencyMode.safeParse(latestStarted?.latencyMode);
|
|
1109
|
+
const provenance = {
|
|
1110
|
+
source: "realtime_provider_delegation",
|
|
1111
|
+
realtimeId: input.realtimeId,
|
|
1112
|
+
connectionEpoch: input.connectionEpoch,
|
|
1113
|
+
delegationItemId: incoming.delegationItemId!,
|
|
1114
|
+
ledgerEntryId: entryId,
|
|
1115
|
+
};
|
|
1116
|
+
const inputTranscript = incoming.payload?.inputTranscript;
|
|
1117
|
+
if (typeof inputTranscript !== "string" || inputTranscript.trim().length === 0) {
|
|
1118
|
+
throw new Error("Realtime delegation input transcript is required");
|
|
1119
|
+
}
|
|
1120
|
+
const admitted = await submitHumanPromptInTransaction(db, {
|
|
1121
|
+
accountId,
|
|
1122
|
+
workspaceId: input.workspaceId,
|
|
1123
|
+
sessionId: input.sessionId,
|
|
1124
|
+
subjectId: input.ownerSubjectId,
|
|
1125
|
+
subjectLabel: "Realtime",
|
|
1126
|
+
actor: {
|
|
1127
|
+
type: "service",
|
|
1128
|
+
subjectId: input.ownerSubjectId,
|
|
1129
|
+
subjectLabel: "Realtime",
|
|
1130
|
+
context: provenance,
|
|
1131
|
+
},
|
|
1132
|
+
operationKey: incoming.operationId,
|
|
1133
|
+
delivery: "steer",
|
|
1134
|
+
text: incoming.text!,
|
|
1135
|
+
messagePresentation: {
|
|
1136
|
+
kind: "realtime_voice",
|
|
1137
|
+
text: inputTranscript,
|
|
1138
|
+
context: incoming.text!,
|
|
1139
|
+
},
|
|
1140
|
+
resources: [],
|
|
1141
|
+
model: latestStarted?.model ?? session.model,
|
|
1142
|
+
reasoningEffort: latestReasoning.success
|
|
1143
|
+
? latestReasoning.data
|
|
1144
|
+
: reasoning.success
|
|
1145
|
+
? reasoning.data
|
|
1146
|
+
: "medium",
|
|
1147
|
+
latencyMode: latestLatency.success ? latestLatency.data : "standard",
|
|
1148
|
+
reasoningEffortFallback: reasoning.success ? reasoning.data : "medium",
|
|
1149
|
+
turnMetadata: {
|
|
1150
|
+
realtimeDelegation: { ...provenance, inputTranscript },
|
|
1151
|
+
},
|
|
1152
|
+
source: "api",
|
|
1153
|
+
});
|
|
1154
|
+
return {
|
|
1155
|
+
turnId: admitted.turnId,
|
|
1156
|
+
eventIds: admitted.eventIds,
|
|
1157
|
+
wakeRevision: admitted.wakeRevision,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
async function materializeRealtimeUpdates(
|
|
1162
|
+
db: Database,
|
|
1163
|
+
input: Pick<
|
|
1164
|
+
SyncSessionRealtimeLedgerInput,
|
|
1165
|
+
"workspaceId" | "sessionId" | "realtimeId" | "connectionEpoch"
|
|
1166
|
+
>,
|
|
1167
|
+
accountId: string,
|
|
1168
|
+
allocateSequence: () => number,
|
|
1169
|
+
now: Date,
|
|
1170
|
+
): Promise<void> {
|
|
1171
|
+
const rows = await db
|
|
1172
|
+
.select({ update: schema.sessionSystemUpdates })
|
|
1173
|
+
.from(schema.sessionSystemUpdates)
|
|
1174
|
+
.leftJoin(
|
|
1175
|
+
schema.sessionRealtimeEntries,
|
|
1176
|
+
and(
|
|
1177
|
+
eq(schema.sessionRealtimeEntries.realtimeId, input.realtimeId),
|
|
1178
|
+
eq(schema.sessionRealtimeEntries.sourceUpdateId, schema.sessionSystemUpdates.id),
|
|
1179
|
+
),
|
|
1180
|
+
)
|
|
1181
|
+
.where(
|
|
1182
|
+
and(
|
|
1183
|
+
eq(schema.sessionSystemUpdates.workspaceId, input.workspaceId),
|
|
1184
|
+
eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
|
|
1185
|
+
inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
|
|
1186
|
+
inArray(schema.sessionSystemUpdates.kind, ["agent_message", "child_terminal_result"]),
|
|
1187
|
+
isNull(schema.sessionRealtimeEntries.id),
|
|
1188
|
+
),
|
|
1189
|
+
)
|
|
1190
|
+
.orderBy(asc(schema.sessionSystemUpdates.createdAt), asc(schema.sessionSystemUpdates.id))
|
|
1191
|
+
.limit(SESSION_REALTIME_LEDGER_MAX_OUTBOUND);
|
|
1192
|
+
const updates = rows.map(({ update }) => update);
|
|
1193
|
+
if (updates.length === 0) return;
|
|
1194
|
+
await db.insert(schema.sessionRealtimeEntries).values(
|
|
1195
|
+
updates.map((update) => ({
|
|
1196
|
+
accountId,
|
|
1197
|
+
workspaceId: input.workspaceId,
|
|
1198
|
+
sessionId: input.sessionId,
|
|
1199
|
+
realtimeId: input.realtimeId,
|
|
1200
|
+
operationId: update.id,
|
|
1201
|
+
connectionEpoch: input.connectionEpoch,
|
|
1202
|
+
sequence: allocateSequence(),
|
|
1203
|
+
direction: "provider_out",
|
|
1204
|
+
kind: "session_update",
|
|
1205
|
+
sourceUpdateId: update.id,
|
|
1206
|
+
text: update.summary,
|
|
1207
|
+
payload: boundedPayload({
|
|
1208
|
+
updateId: update.id,
|
|
1209
|
+
kind: update.kind,
|
|
1210
|
+
classification: update.classification,
|
|
1211
|
+
sourceId: update.sourceId,
|
|
1212
|
+
summary: update.summary,
|
|
1213
|
+
payload: update.payload,
|
|
1214
|
+
lineage: update.lineage,
|
|
1215
|
+
}),
|
|
1216
|
+
createdAt: now,
|
|
1217
|
+
updatedAt: now,
|
|
1218
|
+
})),
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function assertAck(value: number | null | undefined, maximum: number): number | null {
|
|
1223
|
+
if (value === null || value === undefined) return null;
|
|
1224
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
|
|
1225
|
+
throw new SessionRealtimeConflictError(
|
|
1226
|
+
"REALTIME_ACK_INVALID",
|
|
1227
|
+
"Realtime acknowledgment is outside the durable ledger",
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
return value;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function assertProviderAckSequences(values: number[] | undefined, maximum: number): number[] {
|
|
1234
|
+
if (!values || values.length === 0) return [];
|
|
1235
|
+
if (values.length > SESSION_REALTIME_LEDGER_MAX_OUTBOUND) {
|
|
1236
|
+
throw new SessionRealtimeConflictError(
|
|
1237
|
+
"REALTIME_ACK_INVALID",
|
|
1238
|
+
"Realtime provider acknowledgment batch exceeds the server limit",
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
const unique = [...new Set(values)].sort((left, right) => left - right);
|
|
1242
|
+
if (
|
|
1243
|
+
unique.length !== values.length ||
|
|
1244
|
+
unique.some((value) => !Number.isSafeInteger(value) || value < 1 || value > maximum)
|
|
1245
|
+
) {
|
|
1246
|
+
throw new SessionRealtimeConflictError(
|
|
1247
|
+
"REALTIME_ACK_INVALID",
|
|
1248
|
+
"Realtime provider acknowledgment is outside the durable ledger",
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
return unique;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
export async function syncSessionRealtimeLedgerInTransaction(
|
|
1255
|
+
db: Database,
|
|
1256
|
+
input: SyncSessionRealtimeLedgerInput,
|
|
1257
|
+
hooks: SyncSessionRealtimeLedgerHooks = {},
|
|
1258
|
+
): Promise<SyncSessionRealtimeLedgerResult> {
|
|
1259
|
+
assertConnectionEpoch(input.connectionEpoch);
|
|
1260
|
+
if ((input.entries?.length ?? 0) > SESSION_REALTIME_LEDGER_MAX_BATCH) {
|
|
1261
|
+
throw new Error("Realtime ledger batch exceeds the server limit");
|
|
1262
|
+
}
|
|
1263
|
+
// Realtime sync may admit canonical Steer work. Preserve the global lock
|
|
1264
|
+
// order before owner proof locks the session row.
|
|
1265
|
+
await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
|
|
1266
|
+
const mode = await assertSessionRealtimeOwnerInTransaction(db, input);
|
|
1267
|
+
if (mode.connectionEpoch !== input.connectionEpoch) {
|
|
1268
|
+
throw new SessionRealtimeConflictError(
|
|
1269
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
1270
|
+
"Realtime connection epoch changed",
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
const connection = await validateActiveConnection(db, input);
|
|
1274
|
+
if (connection.id !== input.connectionId) {
|
|
1275
|
+
throw new SessionRealtimeConflictError(
|
|
1276
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
1277
|
+
"Realtime connection changed",
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const [modeRow] = await db
|
|
1282
|
+
.select({ accountId: schema.sessionRealtimeModes.accountId })
|
|
1283
|
+
.from(schema.sessionRealtimeModes)
|
|
1284
|
+
.where(eq(schema.sessionRealtimeModes.id, input.realtimeId))
|
|
1285
|
+
.limit(1);
|
|
1286
|
+
if (!modeRow) throw new Error("Realtime mode disappeared while syncing ledger");
|
|
1287
|
+
|
|
1288
|
+
let nextSequence = await nextLedgerSequence(db, input.realtimeId);
|
|
1289
|
+
let providerStartupAcknowledged = connection.startupAcknowledgedAt !== null;
|
|
1290
|
+
const eventIds: string[] = [];
|
|
1291
|
+
let workflowWakeRevision: number | null = null;
|
|
1292
|
+
const now = input.now ?? new Date();
|
|
1293
|
+
if (input.providerStarted) {
|
|
1294
|
+
assertBoundedString(
|
|
1295
|
+
input.providerStarted.providerSessionId,
|
|
1296
|
+
1024,
|
|
1297
|
+
"Realtime provider session id",
|
|
1298
|
+
);
|
|
1299
|
+
assertBoundedString(input.providerStarted.providerEventId, 1024, "Realtime startup event id");
|
|
1300
|
+
if (connection.startupAcknowledgedAt) {
|
|
1301
|
+
if (
|
|
1302
|
+
connection.providerSessionId !== input.providerStarted.providerSessionId ||
|
|
1303
|
+
connection.startupEventId !== (input.providerStarted.providerEventId ?? null)
|
|
1304
|
+
) {
|
|
1305
|
+
throw new SessionRealtimeConflictError(
|
|
1306
|
+
"REALTIME_CONNECTION_STATE_CHANGED",
|
|
1307
|
+
"Realtime provider startup was already acknowledged with different proof",
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
} else {
|
|
1311
|
+
const [acknowledged] = await db
|
|
1312
|
+
.update(schema.sessionRealtimeConnections)
|
|
1313
|
+
.set({
|
|
1314
|
+
providerSessionId: input.providerStarted.providerSessionId,
|
|
1315
|
+
startupEventId: input.providerStarted.providerEventId ?? null,
|
|
1316
|
+
startupAcknowledgedAt: now,
|
|
1317
|
+
updatedAt: now,
|
|
1318
|
+
})
|
|
1319
|
+
.where(
|
|
1320
|
+
and(
|
|
1321
|
+
eq(schema.sessionRealtimeConnections.id, connection.id),
|
|
1322
|
+
isNull(schema.sessionRealtimeConnections.startupAcknowledgedAt),
|
|
1323
|
+
),
|
|
1324
|
+
)
|
|
1325
|
+
.returning({ id: schema.sessionRealtimeConnections.id });
|
|
1326
|
+
if (!acknowledged) {
|
|
1327
|
+
throw new SessionRealtimeConflictError(
|
|
1328
|
+
"REALTIME_CONNECTION_STATE_CHANGED",
|
|
1329
|
+
"Realtime provider startup changed while acknowledging it",
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
providerStartupAcknowledged = true;
|
|
1334
|
+
}
|
|
1335
|
+
const accepted: SyncSessionRealtimeLedgerResult["accepted"] = [];
|
|
1336
|
+
for (const incoming of input.entries ?? []) {
|
|
1337
|
+
if (incoming.kind === "delegation_call" && !providerStartupAcknowledged) {
|
|
1338
|
+
throw new SessionRealtimeConflictError(
|
|
1339
|
+
"REALTIME_PROVIDER_NOT_STARTED",
|
|
1340
|
+
"Realtime provider startup proof is required before delegation",
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
const payload = boundedPayload(incoming.payload);
|
|
1344
|
+
const role = expectedRole(incoming);
|
|
1345
|
+
const text = incoming.text ?? null;
|
|
1346
|
+
assertBoundedString(text, SESSION_REALTIME_LEDGER_MAX_TEXT_BYTES, "Realtime text");
|
|
1347
|
+
if (
|
|
1348
|
+
(incoming.kind === "user_transcript" || incoming.kind === "assistant_transcript") &&
|
|
1349
|
+
!text
|
|
1350
|
+
) {
|
|
1351
|
+
throw new Error("Finalized realtime transcript text is required");
|
|
1352
|
+
}
|
|
1353
|
+
if (incoming.kind === "user_transcript" || incoming.kind === "assistant_transcript") {
|
|
1354
|
+
const turnId = payload.turnId;
|
|
1355
|
+
if (typeof turnId !== "string" || turnId.length === 0) {
|
|
1356
|
+
throw new Error("Finalized realtime transcript turn id is required");
|
|
1357
|
+
}
|
|
1358
|
+
assertBoundedString(turnId, 1_024, "Realtime transcript turn id");
|
|
1359
|
+
}
|
|
1360
|
+
const [existing] = await db
|
|
1361
|
+
.select()
|
|
1362
|
+
.from(schema.sessionRealtimeEntries)
|
|
1363
|
+
.where(
|
|
1364
|
+
and(
|
|
1365
|
+
eq(schema.sessionRealtimeEntries.realtimeId, input.realtimeId),
|
|
1366
|
+
incoming.kind === "delegation_call" && incoming.delegationItemId
|
|
1367
|
+
? or(
|
|
1368
|
+
eq(schema.sessionRealtimeEntries.operationId, incoming.operationId),
|
|
1369
|
+
and(
|
|
1370
|
+
eq(schema.sessionRealtimeEntries.kind, "delegation_call"),
|
|
1371
|
+
eq(schema.sessionRealtimeEntries.delegationItemId, incoming.delegationItemId),
|
|
1372
|
+
),
|
|
1373
|
+
)
|
|
1374
|
+
: eq(schema.sessionRealtimeEntries.operationId, incoming.operationId),
|
|
1375
|
+
),
|
|
1376
|
+
)
|
|
1377
|
+
.limit(1);
|
|
1378
|
+
if (existing) {
|
|
1379
|
+
if (!inboundReplayMatches(existing, incoming, role, text, payload)) {
|
|
1380
|
+
throw new SessionRealtimeConflictError(
|
|
1381
|
+
incoming.kind === "delegation_call"
|
|
1382
|
+
? "REALTIME_DELEGATION_CHANGED"
|
|
1383
|
+
: "REALTIME_ENTRY_CHANGED",
|
|
1384
|
+
"Realtime ledger operation was already used with different input",
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
accepted.push({ entry: mapEntry(existing), replay: true });
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
let [entry] = await db
|
|
1391
|
+
.insert(schema.sessionRealtimeEntries)
|
|
1392
|
+
.values({
|
|
1393
|
+
accountId: modeRow.accountId,
|
|
1394
|
+
workspaceId: input.workspaceId,
|
|
1395
|
+
sessionId: input.sessionId,
|
|
1396
|
+
realtimeId: input.realtimeId,
|
|
1397
|
+
operationId: incoming.operationId,
|
|
1398
|
+
connectionEpoch: input.connectionEpoch,
|
|
1399
|
+
sequence: nextSequence++,
|
|
1400
|
+
direction: "provider_in",
|
|
1401
|
+
kind: incoming.kind,
|
|
1402
|
+
role,
|
|
1403
|
+
providerEventId: incoming.providerEventId ?? null,
|
|
1404
|
+
delegationItemId: incoming.delegationItemId ?? null,
|
|
1405
|
+
historyItemId: null,
|
|
1406
|
+
text,
|
|
1407
|
+
payload,
|
|
1408
|
+
createdAt: now,
|
|
1409
|
+
updatedAt: now,
|
|
1410
|
+
})
|
|
1411
|
+
.returning();
|
|
1412
|
+
if (!entry) throw new Error("Failed to append realtime ledger entry");
|
|
1413
|
+
if (incoming.kind === "delegation_call") {
|
|
1414
|
+
const failure = delegationCallFailure(incoming);
|
|
1415
|
+
if (failure) {
|
|
1416
|
+
await appendInvalidDelegationFailure(
|
|
1417
|
+
db,
|
|
1418
|
+
input,
|
|
1419
|
+
modeRow.accountId,
|
|
1420
|
+
incoming,
|
|
1421
|
+
failure,
|
|
1422
|
+
nextSequence++,
|
|
1423
|
+
now,
|
|
1424
|
+
);
|
|
1425
|
+
} else {
|
|
1426
|
+
const admission = await admitRealtimeDelegationInTransaction(
|
|
1427
|
+
db,
|
|
1428
|
+
input,
|
|
1429
|
+
modeRow.accountId,
|
|
1430
|
+
incoming,
|
|
1431
|
+
entry.id,
|
|
1432
|
+
);
|
|
1433
|
+
const [linked] = await db
|
|
1434
|
+
.update(schema.sessionRealtimeEntries)
|
|
1435
|
+
.set({ turnId: admission.turnId, updatedAt: now })
|
|
1436
|
+
.where(eq(schema.sessionRealtimeEntries.id, entry.id))
|
|
1437
|
+
.returning();
|
|
1438
|
+
if (!linked) throw new Error("Failed to link realtime delegation turn");
|
|
1439
|
+
entry = linked;
|
|
1440
|
+
eventIds.push(...admission.eventIds);
|
|
1441
|
+
workflowWakeRevision = Math.max(workflowWakeRevision ?? 0, admission.wakeRevision);
|
|
1442
|
+
await hooks.afterDelegationAdmission?.({ entryId: entry.id, turnId: admission.turnId });
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
accepted.push({ entry: mapEntry(entry), replay: false });
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
await materializeRealtimeUpdates(db, input, modeRow.accountId, () => nextSequence++, now);
|
|
1449
|
+
const maximum = nextSequence - 1;
|
|
1450
|
+
const clientAck = assertAck(input.clientAckThroughSequence, maximum);
|
|
1451
|
+
if (clientAck !== null && clientAck > 0) {
|
|
1452
|
+
await db
|
|
1453
|
+
.update(schema.sessionRealtimeEntries)
|
|
1454
|
+
.set({ clientAckedAt: now, updatedAt: now })
|
|
1455
|
+
.where(
|
|
1456
|
+
and(
|
|
1457
|
+
eq(schema.sessionRealtimeEntries.workspaceId, input.workspaceId),
|
|
1458
|
+
eq(schema.sessionRealtimeEntries.sessionId, input.sessionId),
|
|
1459
|
+
eq(schema.sessionRealtimeEntries.realtimeId, input.realtimeId),
|
|
1460
|
+
eq(schema.sessionRealtimeEntries.direction, "provider_out"),
|
|
1461
|
+
isNull(schema.sessionRealtimeEntries.clientAckedAt),
|
|
1462
|
+
sql`${schema.sessionRealtimeEntries.sequence} <= ${clientAck}`,
|
|
1463
|
+
),
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
const providerAckSequences = assertProviderAckSequences(input.providerAckSequences, maximum);
|
|
1467
|
+
if (providerAckSequences.length > 0) {
|
|
1468
|
+
const ackable = await db
|
|
1469
|
+
.select({
|
|
1470
|
+
id: schema.sessionRealtimeEntries.id,
|
|
1471
|
+
sequence: schema.sessionRealtimeEntries.sequence,
|
|
1472
|
+
})
|
|
1473
|
+
.from(schema.sessionRealtimeEntries)
|
|
1474
|
+
.where(
|
|
1475
|
+
and(
|
|
1476
|
+
eq(schema.sessionRealtimeEntries.workspaceId, input.workspaceId),
|
|
1477
|
+
eq(schema.sessionRealtimeEntries.sessionId, input.sessionId),
|
|
1478
|
+
eq(schema.sessionRealtimeEntries.realtimeId, input.realtimeId),
|
|
1479
|
+
eq(schema.sessionRealtimeEntries.direction, "provider_out"),
|
|
1480
|
+
inArray(schema.sessionRealtimeEntries.sequence, providerAckSequences),
|
|
1481
|
+
sql`${schema.sessionRealtimeEntries.clientAckedAt} is not null`,
|
|
1482
|
+
),
|
|
1483
|
+
)
|
|
1484
|
+
.for("update");
|
|
1485
|
+
if (
|
|
1486
|
+
ackable.length !== providerAckSequences.length ||
|
|
1487
|
+
ackable.some((entry) => !providerAckSequences.includes(entry.sequence))
|
|
1488
|
+
) {
|
|
1489
|
+
throw new SessionRealtimeConflictError(
|
|
1490
|
+
"REALTIME_ACK_INVALID",
|
|
1491
|
+
"Realtime provider acknowledgment does not match client-received outbound entries",
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
await db
|
|
1495
|
+
.update(schema.sessionRealtimeEntries)
|
|
1496
|
+
.set({ providerAckedAt: now, updatedAt: now })
|
|
1497
|
+
.where(
|
|
1498
|
+
and(
|
|
1499
|
+
eq(schema.sessionRealtimeEntries.workspaceId, input.workspaceId),
|
|
1500
|
+
eq(schema.sessionRealtimeEntries.sessionId, input.sessionId),
|
|
1501
|
+
eq(schema.sessionRealtimeEntries.realtimeId, input.realtimeId),
|
|
1502
|
+
inArray(
|
|
1503
|
+
schema.sessionRealtimeEntries.id,
|
|
1504
|
+
ackable.map((entry) => entry.id),
|
|
1505
|
+
),
|
|
1506
|
+
isNull(schema.sessionRealtimeEntries.providerAckedAt),
|
|
1507
|
+
),
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
const outbound = await db
|
|
1511
|
+
.select()
|
|
1512
|
+
.from(schema.sessionRealtimeEntries)
|
|
1513
|
+
.where(
|
|
1514
|
+
and(
|
|
1515
|
+
eq(schema.sessionRealtimeEntries.workspaceId, input.workspaceId),
|
|
1516
|
+
eq(schema.sessionRealtimeEntries.sessionId, input.sessionId),
|
|
1517
|
+
eq(schema.sessionRealtimeEntries.realtimeId, input.realtimeId),
|
|
1518
|
+
eq(schema.sessionRealtimeEntries.direction, "provider_out"),
|
|
1519
|
+
isNull(schema.sessionRealtimeEntries.providerAckedAt),
|
|
1520
|
+
),
|
|
1521
|
+
)
|
|
1522
|
+
.orderBy(
|
|
1523
|
+
sql`case when ${schema.sessionRealtimeEntries.clientAckedAt} is null then 0 else 1 end`,
|
|
1524
|
+
asc(schema.sessionRealtimeEntries.sequence),
|
|
1525
|
+
)
|
|
1526
|
+
.limit(SESSION_REALTIME_LEDGER_MAX_OUTBOUND);
|
|
1527
|
+
return {
|
|
1528
|
+
accepted,
|
|
1529
|
+
outbound: outbound.map(mapEntry),
|
|
1530
|
+
eventIds,
|
|
1531
|
+
workflowWakeRevision,
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
export async function appendSessionRealtimeOutboundInTransaction(
|
|
1536
|
+
db: Database,
|
|
1537
|
+
input: AppendSessionRealtimeOutboundInput,
|
|
1538
|
+
): Promise<AppendSessionRealtimeOutboundResult> {
|
|
1539
|
+
assertConnectionEpoch(input.connectionEpoch);
|
|
1540
|
+
await lockSessionEventWriteRows(db, {
|
|
1541
|
+
workspaceId: input.workspaceId,
|
|
1542
|
+
controlLock: "share",
|
|
1543
|
+
sessionIds: [input.sessionId],
|
|
1544
|
+
});
|
|
1545
|
+
const [mode] = await db
|
|
1546
|
+
.select()
|
|
1547
|
+
.from(schema.sessionRealtimeModes)
|
|
1548
|
+
.where(
|
|
1549
|
+
and(
|
|
1550
|
+
eq(schema.sessionRealtimeModes.workspaceId, input.workspaceId),
|
|
1551
|
+
eq(schema.sessionRealtimeModes.sessionId, input.sessionId),
|
|
1552
|
+
eq(schema.sessionRealtimeModes.id, input.realtimeId),
|
|
1553
|
+
),
|
|
1554
|
+
)
|
|
1555
|
+
.for("update")
|
|
1556
|
+
.limit(1);
|
|
1557
|
+
if (
|
|
1558
|
+
!mode ||
|
|
1559
|
+
mode.state !== "active" ||
|
|
1560
|
+
mode.connectionEpoch !== input.connectionEpoch ||
|
|
1561
|
+
mode.leaseExpiresAt <= (input.now ?? new Date())
|
|
1562
|
+
) {
|
|
1563
|
+
throw new SessionRealtimeConflictError(
|
|
1564
|
+
"REALTIME_CONNECTION_CHANGED",
|
|
1565
|
+
"Realtime connection changed before outbound delivery",
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
const [activeConnection] = await db
|
|
1569
|
+
.select({ id: schema.sessionRealtimeConnections.id })
|
|
1570
|
+
.from(schema.sessionRealtimeConnections)
|
|
1571
|
+
.where(
|
|
1572
|
+
and(
|
|
1573
|
+
eq(schema.sessionRealtimeConnections.realtimeId, input.realtimeId),
|
|
1574
|
+
eq(schema.sessionRealtimeConnections.connectionEpoch, input.connectionEpoch),
|
|
1575
|
+
eq(schema.sessionRealtimeConnections.state, "active"),
|
|
1576
|
+
),
|
|
1577
|
+
)
|
|
1578
|
+
.limit(1);
|
|
1579
|
+
if (!activeConnection) {
|
|
1580
|
+
throw new SessionRealtimeConflictError(
|
|
1581
|
+
"REALTIME_CONNECTION_NOT_FOUND",
|
|
1582
|
+
"Active realtime connection not found",
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
const [existing] = await db
|
|
1586
|
+
.select()
|
|
1587
|
+
.from(schema.sessionRealtimeEntries)
|
|
1588
|
+
.where(
|
|
1589
|
+
and(
|
|
1590
|
+
eq(schema.sessionRealtimeEntries.realtimeId, input.realtimeId),
|
|
1591
|
+
eq(schema.sessionRealtimeEntries.operationId, input.operationId),
|
|
1592
|
+
),
|
|
1593
|
+
)
|
|
1594
|
+
.limit(1);
|
|
1595
|
+
if (existing) return { entry: mapEntry(existing), replay: true };
|
|
1596
|
+
const text = input.text ?? null;
|
|
1597
|
+
assertBoundedString(text, SESSION_REALTIME_LEDGER_MAX_TEXT_BYTES, "Realtime text");
|
|
1598
|
+
const payload = boundedPayload(input.payload);
|
|
1599
|
+
const nextSequence = await nextLedgerSequence(db, input.realtimeId);
|
|
1600
|
+
const now = input.now ?? new Date();
|
|
1601
|
+
const [entry] = await db
|
|
1602
|
+
.insert(schema.sessionRealtimeEntries)
|
|
1603
|
+
.values({
|
|
1604
|
+
accountId: mode.accountId,
|
|
1605
|
+
workspaceId: input.workspaceId,
|
|
1606
|
+
sessionId: input.sessionId,
|
|
1607
|
+
realtimeId: input.realtimeId,
|
|
1608
|
+
operationId: input.operationId,
|
|
1609
|
+
connectionEpoch: input.connectionEpoch,
|
|
1610
|
+
sequence: Number(nextSequence),
|
|
1611
|
+
direction: "provider_out",
|
|
1612
|
+
kind: input.kind,
|
|
1613
|
+
delegationItemId: input.delegationItemId,
|
|
1614
|
+
text,
|
|
1615
|
+
payload,
|
|
1616
|
+
createdAt: now,
|
|
1617
|
+
updatedAt: now,
|
|
1618
|
+
})
|
|
1619
|
+
.returning();
|
|
1620
|
+
if (!entry) throw new Error("Failed to append realtime outbound entry");
|
|
1621
|
+
return { entry: mapEntry(entry), replay: false };
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Under the caller's canonical session/turn fence, mirror accepted assistant
|
|
1626
|
+
* text deltas into the active delegation's durable provider-out stream. One
|
|
1627
|
+
* append batch becomes one or more bounded progress rows, preserving the
|
|
1628
|
+
* ordinary event order without making the browser's live SSE authoritative.
|
|
1629
|
+
*/
|
|
1630
|
+
export async function projectSessionRealtimeDelegationProgressInTransaction(
|
|
1631
|
+
db: Database,
|
|
1632
|
+
input: ProjectSessionRealtimeDelegationProgressInput,
|
|
1633
|
+
): Promise<ProjectSessionRealtimeDelegationProgressResult> {
|
|
1634
|
+
const progressEvents = input.events.flatMap((event) => {
|
|
1635
|
+
if (
|
|
1636
|
+
event.type !== "agent.message.delta" ||
|
|
1637
|
+
!event.payload ||
|
|
1638
|
+
typeof event.payload !== "object" ||
|
|
1639
|
+
Array.isArray(event.payload)
|
|
1640
|
+
) {
|
|
1641
|
+
return [];
|
|
1642
|
+
}
|
|
1643
|
+
const text = (event.payload as Record<string, unknown>).text;
|
|
1644
|
+
return typeof text === "string" && text.length > 0 ? [{ ...event, text }] : [];
|
|
1645
|
+
});
|
|
1646
|
+
if (progressEvents.length === 0) return { entries: [] };
|
|
1647
|
+
|
|
1648
|
+
const [call] = await db
|
|
1649
|
+
.select()
|
|
1650
|
+
.from(schema.sessionRealtimeEntries)
|
|
1651
|
+
.where(
|
|
1652
|
+
and(
|
|
1653
|
+
eq(schema.sessionRealtimeEntries.accountId, input.accountId),
|
|
1654
|
+
eq(schema.sessionRealtimeEntries.workspaceId, input.workspaceId),
|
|
1655
|
+
eq(schema.sessionRealtimeEntries.sessionId, input.sessionId),
|
|
1656
|
+
eq(schema.sessionRealtimeEntries.turnId, input.turnId),
|
|
1657
|
+
eq(schema.sessionRealtimeEntries.direction, "provider_in"),
|
|
1658
|
+
eq(schema.sessionRealtimeEntries.kind, "delegation_call"),
|
|
1659
|
+
),
|
|
1660
|
+
)
|
|
1661
|
+
.for("update")
|
|
1662
|
+
.limit(1);
|
|
1663
|
+
const text = progressEvents.map((event) => event.text).join("");
|
|
1664
|
+
const source = sourceEventProvenance(progressEvents.map((event) => event.id));
|
|
1665
|
+
const sourceEventIds = source.ids;
|
|
1666
|
+
const sourceEventSequences = progressEvents.map((event) => event.sequence);
|
|
1667
|
+
const now = input.now ?? new Date();
|
|
1668
|
+
if (!call) {
|
|
1669
|
+
const mirrored = await mirrorSessionRealtimeContextInTransaction(db, {
|
|
1670
|
+
accountId: input.accountId,
|
|
1671
|
+
workspaceId: input.workspaceId,
|
|
1672
|
+
sessionId: input.sessionId,
|
|
1673
|
+
sourceKind: "assistant_progress",
|
|
1674
|
+
sourceId: source.identity,
|
|
1675
|
+
turnId: input.turnId,
|
|
1676
|
+
channel: "commentary",
|
|
1677
|
+
text,
|
|
1678
|
+
payload: {
|
|
1679
|
+
route: "session_context",
|
|
1680
|
+
status: "running",
|
|
1681
|
+
sourceEventIds,
|
|
1682
|
+
sourceEventCount: source.count,
|
|
1683
|
+
sourceEventIdsTruncated: source.truncated,
|
|
1684
|
+
firstSourceEventSequence: sourceEventSequences[0],
|
|
1685
|
+
lastSourceEventSequence: sourceEventSequences.at(-1),
|
|
1686
|
+
},
|
|
1687
|
+
now,
|
|
1688
|
+
});
|
|
1689
|
+
return { entries: mirrored ? [mapEntry(mirrored.entry)] : [] };
|
|
1690
|
+
}
|
|
1691
|
+
if (!call.delegationItemId) {
|
|
1692
|
+
throw new Error(`Delegation turn ${input.turnId} has no provider item identity`);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const [mode] = await db
|
|
1696
|
+
.select({
|
|
1697
|
+
connectionEpoch: schema.sessionRealtimeModes.connectionEpoch,
|
|
1698
|
+
state: schema.sessionRealtimeModes.state,
|
|
1699
|
+
leaseExpiresAt: schema.sessionRealtimeModes.leaseExpiresAt,
|
|
1700
|
+
})
|
|
1701
|
+
.from(schema.sessionRealtimeModes)
|
|
1702
|
+
.where(
|
|
1703
|
+
and(
|
|
1704
|
+
eq(schema.sessionRealtimeModes.id, call.realtimeId),
|
|
1705
|
+
eq(schema.sessionRealtimeModes.accountId, input.accountId),
|
|
1706
|
+
eq(schema.sessionRealtimeModes.workspaceId, input.workspaceId),
|
|
1707
|
+
eq(schema.sessionRealtimeModes.sessionId, input.sessionId),
|
|
1708
|
+
),
|
|
1709
|
+
)
|
|
1710
|
+
.limit(1);
|
|
1711
|
+
if (!mode) {
|
|
1712
|
+
throw new Error(`Delegation turn ${input.turnId} lost its realtime mode ownership`);
|
|
1713
|
+
}
|
|
1714
|
+
if (mode.state !== "active" || mode.leaseExpiresAt <= now) {
|
|
1715
|
+
const mirrored = await mirrorSessionRealtimeContextInTransaction(db, {
|
|
1716
|
+
accountId: input.accountId,
|
|
1717
|
+
workspaceId: input.workspaceId,
|
|
1718
|
+
sessionId: input.sessionId,
|
|
1719
|
+
sourceKind: "assistant_progress",
|
|
1720
|
+
sourceId: source.identity,
|
|
1721
|
+
turnId: input.turnId,
|
|
1722
|
+
channel: "commentary",
|
|
1723
|
+
text,
|
|
1724
|
+
payload: {
|
|
1725
|
+
route: "session_context",
|
|
1726
|
+
status: "running",
|
|
1727
|
+
priorRealtimeId: call.realtimeId,
|
|
1728
|
+
sourceEventIds,
|
|
1729
|
+
sourceEventCount: source.count,
|
|
1730
|
+
sourceEventIdsTruncated: source.truncated,
|
|
1731
|
+
firstSourceEventSequence: sourceEventSequences[0],
|
|
1732
|
+
lastSourceEventSequence: sourceEventSequences.at(-1),
|
|
1733
|
+
},
|
|
1734
|
+
now,
|
|
1735
|
+
});
|
|
1736
|
+
return { entries: mirrored ? [mapEntry(mirrored.entry)] : [] };
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
const chunks = utf8Chunks(text, SESSION_REALTIME_LEDGER_MAX_TEXT_BYTES);
|
|
1740
|
+
const operationSeed = source.identity;
|
|
1741
|
+
let nextSequence = await nextLedgerSequence(db, call.realtimeId);
|
|
1742
|
+
const inserted = await db
|
|
1743
|
+
.insert(schema.sessionRealtimeEntries)
|
|
1744
|
+
.values(
|
|
1745
|
+
chunks.map((chunk, chunkIndex) => ({
|
|
1746
|
+
accountId: input.accountId,
|
|
1747
|
+
workspaceId: input.workspaceId,
|
|
1748
|
+
sessionId: input.sessionId,
|
|
1749
|
+
realtimeId: call.realtimeId,
|
|
1750
|
+
operationId: deterministicUuid(
|
|
1751
|
+
`opengeni:session-realtime-delegation-progress:${input.turnId}:${operationSeed}:${chunkIndex}`,
|
|
1752
|
+
),
|
|
1753
|
+
connectionEpoch: mode.connectionEpoch,
|
|
1754
|
+
sequence: nextSequence++,
|
|
1755
|
+
direction: "provider_out" as const,
|
|
1756
|
+
kind: "delegation_progress" as const,
|
|
1757
|
+
delegationItemId: call.delegationItemId,
|
|
1758
|
+
turnId: input.turnId,
|
|
1759
|
+
text: chunk,
|
|
1760
|
+
payload: boundedPayload({
|
|
1761
|
+
route: "delegation_context",
|
|
1762
|
+
channel: "commentary",
|
|
1763
|
+
status: "running",
|
|
1764
|
+
turnId: input.turnId,
|
|
1765
|
+
callOperationId: call.operationId,
|
|
1766
|
+
callLedgerEntryId: call.id,
|
|
1767
|
+
sourceEventIds,
|
|
1768
|
+
sourceEventCount: source.count,
|
|
1769
|
+
sourceEventIdsTruncated: source.truncated,
|
|
1770
|
+
firstSourceEventSequence: sourceEventSequences[0],
|
|
1771
|
+
lastSourceEventSequence: sourceEventSequences.at(-1),
|
|
1772
|
+
chunkIndex,
|
|
1773
|
+
chunkCount: chunks.length,
|
|
1774
|
+
}),
|
|
1775
|
+
createdAt: now,
|
|
1776
|
+
updatedAt: now,
|
|
1777
|
+
})),
|
|
1778
|
+
)
|
|
1779
|
+
.returning();
|
|
1780
|
+
return { entries: inserted.map(mapEntry) };
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Project the authoritative terminal event of an accepted same-session
|
|
1785
|
+
* delegation into exactly one outbound ledger row. The caller already owns the
|
|
1786
|
+
* canonical session/turn settlement locks; this helper deliberately does not
|
|
1787
|
+
* require the original provider connection (or even the mode) to remain
|
|
1788
|
+
* active. A later valid rotation may therefore replay the same durable row,
|
|
1789
|
+
* while the immutable realtime id prevents projection into another mode.
|
|
1790
|
+
*/
|