@opengeni/db 0.22.2 → 0.26.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.
Files changed (55) hide show
  1. package/dist/{chunk-CYGFLLMN.js → chunk-7WTDI7Y3.js} +764 -283
  2. package/dist/chunk-7WTDI7Y3.js.map +1 -0
  3. package/dist/{chunk-BNGEN5QZ.js → chunk-KW6U54V2.js} +26 -2
  4. package/dist/chunk-KW6U54V2.js.map +1 -0
  5. package/dist/connection-token-resolver.d.ts +24 -2
  6. package/dist/index.d.ts +107 -5
  7. package/dist/index.js +7857 -3898
  8. package/dist/index.js.map +1 -1
  9. package/dist/preference-registry.d.ts +14 -0
  10. package/dist/provision-roles.js +1 -1
  11. package/dist/runtime-posture.d.ts +3 -3
  12. package/dist/schema.d.ts +1659 -77
  13. package/dist/schema.js +13 -1
  14. package/dist/session-control.d.ts +7 -1
  15. package/dist/session-queue-commands.d.ts +8 -0
  16. package/dist/session-realtime-context.d.ts +56 -0
  17. package/dist/session-realtime-ledger.d.ts +188 -0
  18. package/dist/session-realtime-mirror.d.ts +51 -0
  19. package/dist/session-realtime-state.d.ts +2 -0
  20. package/dist/session-realtime-terminal.d.ts +40 -0
  21. package/dist/session-realtime.d.ts +59 -0
  22. package/dist/workspace-instruction-policies-schema.d.ts +307 -0
  23. package/dist/workspace-instruction-policies.d.ts +97 -7
  24. package/drizzle/0156_slack_reaction_trigger.sql +49 -0
  25. package/drizzle/0157_session_policy_role_snapshots.sql +1146 -0
  26. package/drizzle/0158_session_realtime_mode.sql +88 -0
  27. package/drizzle/0159_session_realtime_ledger.sql +198 -0
  28. package/drizzle/0160_session_realtime_delegation_terminal.sql +38 -0
  29. package/drizzle/0161_session_realtime_context_projection.sql +82 -0
  30. package/drizzle/0162_session_realtime_connection_promotion.sql +53 -0
  31. package/drizzle/0163_session_realtime_delegation_progress.sql +35 -0
  32. package/drizzle/0164_session_realtime_models.sql +28 -0
  33. package/drizzle/0165_document_authority_foundation.sql +259 -0
  34. package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
  35. package/drizzle/0167_document_index_replay_authority.sql +61 -0
  36. package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
  37. package/package.json +4 -4
  38. package/src/connection-token-resolver.ts +79 -16
  39. package/src/index.ts +1033 -101
  40. package/src/preference-registry.ts +114 -6
  41. package/src/provision-roles.ts +12 -0
  42. package/src/runtime-posture.ts +12 -0
  43. package/src/schema.ts +486 -43
  44. package/src/session-control.ts +643 -21
  45. package/src/session-queue-commands.ts +121 -18
  46. package/src/session-realtime-context.ts +393 -0
  47. package/src/session-realtime-ledger.ts +1790 -0
  48. package/src/session-realtime-mirror.ts +276 -0
  49. package/src/session-realtime-state.ts +25 -0
  50. package/src/session-realtime-terminal.ts +306 -0
  51. package/src/session-realtime.ts +659 -0
  52. package/src/workspace-instruction-policies-schema.ts +71 -0
  53. package/src/workspace-instruction-policies.ts +568 -25
  54. package/dist/chunk-BNGEN5QZ.js.map +0 -1
  55. package/dist/chunk-CYGFLLMN.js.map +0 -1
@@ -31,6 +31,12 @@ import {
31
31
  SessionControlInvariantError,
32
32
  updateSessionCommandReceiptResult,
33
33
  } from "./session-control";
34
+ import { sessionRealtimeIsActiveInTransaction } from "./session-realtime-state";
35
+ import {
36
+ mirrorSessionRealtimeContextInTransaction,
37
+ renderRealtimeHumanInputContext,
38
+ renderRealtimeHumanInputResponseContext,
39
+ } from "./session-realtime-mirror";
34
40
  import * as schema from "./schema";
35
41
  import {
36
42
  frozenInitiatorForCommandActor,
@@ -279,23 +285,56 @@ export async function supersedeSessionCurrentDirectionInTransaction(
279
285
  eq(schema.sessionHumanInputRequests.status, "pending"),
280
286
  ),
281
287
  )
282
- .returning({ id: schema.sessionHumanInputRequests.id });
288
+ .returning({
289
+ id: schema.sessionHumanInputRequests.id,
290
+ questions: schema.sessionHumanInputRequests.questions,
291
+ });
283
292
  let lastSequence = closedTools.sequence;
284
293
  if (cancelledHumanInputs.length > 0) {
285
- await db.insert(schema.sessionEvents).values(
286
- cancelledHumanInputs.map((request) => ({
294
+ const cancelledHumanInputEvents = await db
295
+ .insert(schema.sessionEvents)
296
+ .values(
297
+ cancelledHumanInputs.map((request) => ({
298
+ accountId: input.accountId,
299
+ workspaceId: input.workspaceId,
300
+ sessionId: input.sessionId,
301
+ sequence: ++lastSequence,
302
+ type: "user.humanInputResponse",
303
+ turnId: current.id,
304
+ turnGeneration: current.executionGeneration,
305
+ turnAssociation: "current",
306
+ payload: { requestId: request.id, response: { outcome: "cancelled" } },
307
+ occurredAt: now,
308
+ })),
309
+ )
310
+ .returning();
311
+ const requestsById = new Map(cancelledHumanInputs.map((request) => [request.id, request]));
312
+ for (const event of cancelledHumanInputEvents) {
313
+ const payload = event.payload as { requestId?: unknown };
314
+ const request =
315
+ typeof payload.requestId === "string" ? requestsById.get(payload.requestId) : null;
316
+ if (!request) continue;
317
+ await mirrorSessionRealtimeContextInTransaction(db, {
287
318
  accountId: input.accountId,
288
319
  workspaceId: input.workspaceId,
289
320
  sessionId: input.sessionId,
290
- sequence: ++lastSequence,
291
- type: "user.humanInputResponse",
321
+ sourceKind: "human_input_response",
322
+ sourceId: event.id,
292
323
  turnId: current.id,
293
- turnGeneration: current.executionGeneration,
294
- turnAssociation: "current",
295
- payload: { requestId: request.id, response: { outcome: "cancelled" } },
296
- occurredAt: now,
297
- })),
298
- );
324
+ channel: null,
325
+ text: renderRealtimeHumanInputResponseContext({
326
+ requestId: request.id,
327
+ questions: request.questions,
328
+ response: { outcome: "cancelled" },
329
+ }),
330
+ payload: {
331
+ requestId: request.id,
332
+ outcome: "cancelled",
333
+ sourceEventId: event.id,
334
+ },
335
+ now,
336
+ });
337
+ }
299
338
  }
300
339
  await db
301
340
  .update(schema.sessionTurns)
@@ -529,7 +568,7 @@ export async function moveQueuedTurnInTransaction(
529
568
  workspaceId: input.workspaceId,
530
569
  controlLock: "already_locked",
531
570
  });
532
- const session = await lockSession(db, input.workspaceId, input.sessionId);
571
+ let session = await lockSession(db, input.workspaceId, input.sessionId);
533
572
  const requestHash = canonicalSessionCommandHash({
534
573
  beforeTurnId: input.beforeTurnId,
535
574
  expectedQueueVersion: input.expectedQueueVersion,
@@ -659,7 +698,7 @@ export async function deleteSessionQueueItemInTransaction(
659
698
  workspaceId: input.workspaceId,
660
699
  controlLock: "already_locked",
661
700
  });
662
- const session = await lockSession(db, input.workspaceId, input.sessionId);
701
+ let session = await lockSession(db, input.workspaceId, input.sessionId);
663
702
  const requestHash = canonicalSessionCommandHash({
664
703
  expectedTurnVersion: input.expectedTurnVersion,
665
704
  reason: input.reason ?? null,
@@ -786,7 +825,7 @@ export async function editQueuedTurnInTransaction(
786
825
  workspaceId: input.workspaceId,
787
826
  controlLock: "already_locked",
788
827
  });
789
- const session = await lockSession(db, input.workspaceId, input.sessionId);
828
+ let session = await lockSession(db, input.workspaceId, input.sessionId);
790
829
  const requestHash = canonicalSessionCommandHash({
791
830
  expectedTurnVersion: input.expectedTurnVersion,
792
831
  expectedDraftRevision: input.expectedDraftRevision,
@@ -995,6 +1034,7 @@ export async function steerQueuedTurnInTransaction(
995
1034
  replay: true,
996
1035
  };
997
1036
  }
1037
+ await lockSession(db, input.workspaceId, input.sessionId);
998
1038
  if (input.actor.type === "agent_attempt") {
999
1039
  await assertAgentCommandAuthorityInTransaction(db, {
1000
1040
  workspaceId: input.workspaceId,
@@ -1185,6 +1225,14 @@ export async function submitHumanPromptInTransaction(
1185
1225
  reasoningEffortFallback: ReasoningEffort;
1186
1226
  /** Trusted API/core admission snapshot. Omitted only by legacy low-level callers. */
1187
1227
  turnExecutionPolicy?: TurnExecutionPolicyV1;
1228
+ /** Trusted core-only metadata attached to the admitted turn. */
1229
+ turnMetadata?: Record<string, unknown>;
1230
+ /** Trusted display projection; the durable turn still receives `text`. */
1231
+ messagePresentation?: {
1232
+ kind: "realtime_voice" | "realtime_voice_handoff";
1233
+ text: string;
1234
+ context: string;
1235
+ };
1188
1236
  source: "user" | "api";
1189
1237
  personalConnectionDelegations?: McpPersonalConnectionDelegation[];
1190
1238
  mcpCredentialUpdates?: Array<{
@@ -1215,6 +1263,8 @@ export async function submitHumanPromptInTransaction(
1215
1263
  reasoningEffort: input.reasoningEffort ?? null,
1216
1264
  latencyMode: input.latencyMode ?? null,
1217
1265
  source: input.source,
1266
+ turnMetadata: input.turnMetadata ?? {},
1267
+ messagePresentation: input.messagePresentation ?? null,
1218
1268
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1219
1269
  ...(input.actor.type === "service"
1220
1270
  ? {
@@ -1444,7 +1494,15 @@ export async function submitHumanPromptInTransaction(
1444
1494
  type: "user.message",
1445
1495
  clientEventId: input.operationKey,
1446
1496
  payload: sanitizeEventPayload({
1447
- text: input.text,
1497
+ text: input.messagePresentation?.text ?? input.text,
1498
+ ...(input.messagePresentation
1499
+ ? {
1500
+ presentation: {
1501
+ kind: input.messagePresentation.kind,
1502
+ context: input.messagePresentation.context,
1503
+ },
1504
+ }
1505
+ : {}),
1448
1506
  ...(input.resources.length ? { resources: input.resources } : {}),
1449
1507
  ...(input.model ? { model: input.model } : {}),
1450
1508
  ...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
@@ -1481,13 +1539,23 @@ export async function submitHumanPromptInTransaction(
1481
1539
  latencyMode: input.turnExecutionPolicy?.latencyMode ?? input.latencyMode ?? "standard",
1482
1540
  sandboxBackend: session.sandboxBackend,
1483
1541
  metadata: input.turnExecutionPolicy
1484
- ? metadataWithTurnExecutionPolicyV1({}, input.turnExecutionPolicy)
1485
- : {},
1542
+ ? metadataWithTurnExecutionPolicyV1(input.turnMetadata ?? {}, input.turnExecutionPolicy)
1543
+ : (input.turnMetadata ?? {}),
1486
1544
  lineage: { actor: input.actor.type },
1487
1545
  ...initiatorColumns(frozenInitiator),
1546
+ initiatingHumanSubjectId: editedSourceTurn
1547
+ ? (editedSourceTurn.initiatingHumanSubjectId ??
1548
+ (editedSourceTurn.initiatorKind === "subject"
1549
+ ? editedSourceTurn.initiatorSubjectId
1550
+ : null))
1551
+ : frozenInitiator.initiator.kind === "subject"
1552
+ ? frozenInitiator.initiator.subjectId
1553
+ : null,
1488
1554
  personalConnectionDelegations: editedSourceTurn
1489
1555
  ? editedSourceTurn.personalConnectionDelegations
1490
1556
  : (input.personalConnectionDelegations ?? []),
1557
+ createdAt: now,
1558
+ updatedAt: now,
1491
1559
  })
1492
1560
  .returning();
1493
1561
  if (!turn) throw new SessionControlInvariantError("Prompt turn was not inserted");
@@ -1618,6 +1686,35 @@ export async function submitHumanPromptInTransaction(
1618
1686
  });
1619
1687
  }
1620
1688
  const eventRows = await db.insert(schema.sessionEvents).values(eventValues).returning();
1689
+ if (input.actor.type === "human") {
1690
+ const realtimeRouting =
1691
+ input.delivery === "steer"
1692
+ ? "accepted_for_steering"
1693
+ : session.activeTurnId || existingQueued.length > 0
1694
+ ? "queued_for_execution"
1695
+ : "accepted_for_execution";
1696
+ await mirrorSessionRealtimeContextInTransaction(db, {
1697
+ accountId: input.accountId,
1698
+ workspaceId: input.workspaceId,
1699
+ sessionId: input.sessionId,
1700
+ sourceKind: "human_input",
1701
+ sourceId: acceptedEventId,
1702
+ turnId,
1703
+ channel: null,
1704
+ text: renderRealtimeHumanInputContext({
1705
+ delivery: input.delivery,
1706
+ routing: realtimeRouting,
1707
+ text: input.text,
1708
+ }),
1709
+ payload: {
1710
+ delivery: input.delivery,
1711
+ routing: realtimeRouting,
1712
+ acceptedEventId,
1713
+ instruction: "OpenGeni accepted and routed this user input; do not delegate it again.",
1714
+ },
1715
+ now,
1716
+ });
1717
+ }
1621
1718
  const queueVersion = session.queueVersion + 1;
1622
1719
  await db
1623
1720
  .update(schema.sessions)
@@ -1772,6 +1869,11 @@ export async function sendAgentMessageInTransaction(
1772
1869
  const effective = await evaluateSessionControl(db, input.workspaceId, input.targetSessionId, {
1773
1870
  workspaceControl,
1774
1871
  });
1872
+ const realtimeActive = await sessionRealtimeIsActiveInTransaction(
1873
+ db,
1874
+ input.workspaceId,
1875
+ input.targetSessionId,
1876
+ );
1775
1877
  const now = new Date();
1776
1878
  const [update] = await db
1777
1879
  .insert(schema.sessionSystemUpdates)
@@ -1818,7 +1920,7 @@ export async function sendAgentMessageInTransaction(
1818
1920
  .returning({ id: schema.sessionEvents.id });
1819
1921
  if (!event) throw new SessionControlInvariantError("Agent message event was not inserted");
1820
1922
  const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
1821
- const runnable = session.activeTurnId === null && effective.state === "active";
1923
+ const runnable = !realtimeActive && session.activeTurnId === null && effective.state === "active";
1822
1924
  const wake = runnable
1823
1925
  ? await registerInternalUpdateWakeInTransaction(db, {
1824
1926
  accountId: input.accountId,
@@ -1929,6 +2031,7 @@ export async function steerAgentSessionInTransaction(
1929
2031
  replay: true,
1930
2032
  };
1931
2033
  }
2034
+ await lockSession(db, input.workspaceId, input.targetSessionId);
1932
2035
  await assertAgentCommandAuthorityInTransaction(db, {
1933
2036
  workspaceId: input.workspaceId,
1934
2037
  actor: input.actor,
@@ -0,0 +1,393 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { ReasoningEffort } from "@opengeni/contracts";
4
+ import { and, asc, desc, eq, inArray, isNull, sql } from "drizzle-orm";
5
+
6
+ import type { Database } from "./index";
7
+ import * as schema from "./schema";
8
+ import { submitHumanPromptInTransaction } from "./session-queue-commands";
9
+
10
+ export const SESSION_REALTIME_CONTEXT_MAX_BYTES = 65_536;
11
+ export const SESSION_REALTIME_TAIL_SOURCE = "transcript_tail_flush";
12
+ export const SESSION_REALTIME_TAIL_INSTRUCTION =
13
+ "The user just ended their realtime session. Here is the remaining handoff/transcript tail. You probably do not have to do anything; acknowledge the handoff unless the transcript itself asks for something.";
14
+
15
+ export type SessionRealtimeContextProjection = {
16
+ id: string;
17
+ workspaceId: string;
18
+ sessionId: string;
19
+ turnId: string;
20
+ context: string | null;
21
+ sourceModeCount: number;
22
+ sourceEntryCount: number;
23
+ includedEntryCount: number;
24
+ omittedEntryCount: number;
25
+ createdAt: string;
26
+ };
27
+
28
+ export type SessionRealtimeContextSourceEntry = {
29
+ id: string;
30
+ realtimeId: string;
31
+ sequence: number;
32
+ role: string | null;
33
+ text: string | null;
34
+ payload: Record<string, unknown>;
35
+ };
36
+
37
+ export type SessionRealtimeContinuityEntry = {
38
+ realtimeId: string;
39
+ sequence: number;
40
+ role: "user" | "assistant";
41
+ text: string;
42
+ turnId: string;
43
+ createdAt: string;
44
+ };
45
+
46
+ function deterministicUuid(seed: string): string {
47
+ const bytes = createHash("sha256").update(seed, "utf8").digest().subarray(0, 16);
48
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
49
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
50
+ const hex = bytes.toString("hex");
51
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
52
+ }
53
+
54
+ function escapeXmlText(input: string): string {
55
+ return input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
56
+ }
57
+
58
+ function entryLine(entry: Pick<SessionRealtimeContextSourceEntry, "role" | "text">): string {
59
+ const role = entry.role === "assistant" ? "assistant" : "user";
60
+ return `${role}: ${entry.text ?? ""}`;
61
+ }
62
+
63
+ function utf8Bytes(value: string): number {
64
+ return Buffer.byteLength(value, "utf8");
65
+ }
66
+
67
+ function takeUtf8Head(value: string, maximumBytes: number): string {
68
+ const bytes = Buffer.from(value, "utf8");
69
+ if (bytes.length <= maximumBytes) return value;
70
+ let end = maximumBytes;
71
+ while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end -= 1;
72
+ return bytes.subarray(0, end).toString("utf8");
73
+ }
74
+
75
+ function boundedEscapedLine(
76
+ entry: Pick<SessionRealtimeContextSourceEntry, "role" | "text">,
77
+ maximumBytes: number,
78
+ ): string {
79
+ const role = entry.role === "assistant" ? "assistant" : "user";
80
+ const marker = "…[turn truncated]";
81
+ const raw = entry.text ?? "";
82
+ let low = 0;
83
+ let high = Buffer.byteLength(raw, "utf8");
84
+ let best = "";
85
+ while (low <= high) {
86
+ const middle = Math.floor((low + high) / 2);
87
+ const candidate = `${role}: ${escapeXmlText(takeUtf8Head(raw, middle))}${marker}`;
88
+ if (utf8Bytes(candidate) <= maximumBytes) {
89
+ best = candidate;
90
+ low = middle + 1;
91
+ } else {
92
+ high = middle - 1;
93
+ }
94
+ }
95
+ return best;
96
+ }
97
+
98
+ /** Render the exact finalized tail after the latest provider delegation fence. */
99
+ export function renderSessionRealtimeTail(entries: readonly SessionRealtimeContextSourceEntry[]): {
100
+ context: string | null;
101
+ includedEntryCount: number;
102
+ omittedEntryCount: number;
103
+ } {
104
+ if (entries.length === 0) {
105
+ return { context: null, includedEntryCount: 0, omittedEntryCount: 0 };
106
+ }
107
+ const prefix = [
108
+ "<realtime_delegation>",
109
+ ` <source>${SESSION_REALTIME_TAIL_SOURCE}</source>`,
110
+ ` <input>${escapeXmlText(SESSION_REALTIME_TAIL_INSTRUCTION)}</input>`,
111
+ " <transcript_delta>",
112
+ ].join("\n");
113
+ const suffix = "\n </transcript_delta>\n</realtime_delegation>";
114
+ const available = SESSION_REALTIME_CONTEXT_MAX_BYTES - utf8Bytes(prefix) - utf8Bytes(suffix) - 2;
115
+ const selected: string[] = [];
116
+ let selectedBytes = 0;
117
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
118
+ const line = escapeXmlText(entryLine(entries[index]!));
119
+ const lineBytes = utf8Bytes(line) + (selected.length > 0 ? 1 : 0);
120
+ if (selectedBytes + lineBytes > available) break;
121
+ selected.unshift(line);
122
+ selectedBytes += lineBytes;
123
+ }
124
+ if (selected.length === 0) {
125
+ const truncated = boundedEscapedLine(entries.at(-1)!, available);
126
+ if (!truncated) throw new Error("Realtime transcript tail cannot fit its durable wrapper");
127
+ selected.push(truncated);
128
+ selectedBytes = utf8Bytes(truncated);
129
+ }
130
+ let includedEntryCount = selected.length;
131
+ while (includedEntryCount > 1) {
132
+ const omitted = entries.length - includedEntryCount;
133
+ const markerBytes =
134
+ omitted > 0 ? utf8Bytes(`[${omitted} older transcript turns omitted]`) + 1 : 0;
135
+ if (selectedBytes + markerBytes <= available) break;
136
+ const removed = selected.shift()!;
137
+ selectedBytes -= utf8Bytes(removed) + (selected.length > 0 ? 1 : 0);
138
+ includedEntryCount -= 1;
139
+ }
140
+ const omittedEntryCount = entries.length - includedEntryCount;
141
+ if (omittedEntryCount > 0) {
142
+ const marker = `[${omittedEntryCount} older transcript turns omitted]`;
143
+ const markerBytes = utf8Bytes(marker) + 1;
144
+ if (selectedBytes + markerBytes > available) {
145
+ const truncated = boundedEscapedLine(entries.at(-1)!, available - markerBytes);
146
+ if (!truncated) throw new Error("Realtime transcript tail omission marker cannot fit");
147
+ selected.splice(0, selected.length, truncated);
148
+ }
149
+ selected.unshift(marker);
150
+ }
151
+ const context = `${prefix}\n${selected.join("\n")}${suffix}`;
152
+ if (utf8Bytes(context) > SESSION_REALTIME_CONTEXT_MAX_BYTES) {
153
+ throw new Error("Realtime transcript tail exceeded its durable UTF-8 bound");
154
+ }
155
+ return { context, includedEntryCount, omittedEntryCount };
156
+ }
157
+
158
+ function mapProjection(
159
+ row: typeof schema.sessionRealtimeContextProjections.$inferSelect,
160
+ ): SessionRealtimeContextProjection {
161
+ return {
162
+ id: row.id,
163
+ workspaceId: row.workspaceId,
164
+ sessionId: row.sessionId,
165
+ turnId: row.turnId,
166
+ context: row.context,
167
+ sourceModeCount: row.sourceModeCount,
168
+ sourceEntryCount: row.sourceEntryCount,
169
+ includedEntryCount: row.includedEntryCount,
170
+ omittedEntryCount: row.omittedEntryCount,
171
+ createdAt: row.createdAt.toISOString(),
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Codex-style end fence: route only finalized voice transcript after the last
177
+ * delegation into the same canonical Steer-or-start path used by a human.
178
+ */
179
+ export async function flushSessionRealtimeTranscriptTailInTransaction(
180
+ db: Database,
181
+ input: {
182
+ accountId: string;
183
+ workspaceId: string;
184
+ sessionId: string;
185
+ realtimeId: string;
186
+ ownerSubjectId: string;
187
+ now?: Date;
188
+ },
189
+ ): Promise<SessionRealtimeContextProjection | null> {
190
+ const [mode] = await db
191
+ .select()
192
+ .from(schema.sessionRealtimeModes)
193
+ .where(
194
+ and(
195
+ eq(schema.sessionRealtimeModes.accountId, input.accountId),
196
+ eq(schema.sessionRealtimeModes.workspaceId, input.workspaceId),
197
+ eq(schema.sessionRealtimeModes.sessionId, input.sessionId),
198
+ eq(schema.sessionRealtimeModes.id, input.realtimeId),
199
+ ),
200
+ )
201
+ .for("update")
202
+ .limit(1);
203
+ if (!mode || mode.state !== "ended") return null;
204
+ if (mode.contextProjectionId) {
205
+ const [existing] = await db
206
+ .select()
207
+ .from(schema.sessionRealtimeContextProjections)
208
+ .where(eq(schema.sessionRealtimeContextProjections.id, mode.contextProjectionId))
209
+ .limit(1);
210
+ if (!existing) throw new Error(`Realtime mode ${mode.id} lost its tail projection`);
211
+ return mapProjection(existing);
212
+ }
213
+
214
+ const [latestDelegation] = await db
215
+ .select({
216
+ sequence: schema.sessionRealtimeEntries.sequence,
217
+ delegationItemId: schema.sessionRealtimeEntries.delegationItemId,
218
+ })
219
+ .from(schema.sessionRealtimeEntries)
220
+ .where(
221
+ and(
222
+ eq(schema.sessionRealtimeEntries.realtimeId, mode.id),
223
+ eq(schema.sessionRealtimeEntries.direction, "provider_in"),
224
+ eq(schema.sessionRealtimeEntries.kind, "delegation_call"),
225
+ ),
226
+ )
227
+ .orderBy(desc(schema.sessionRealtimeEntries.sequence))
228
+ .limit(1);
229
+ const rows = await db
230
+ .select({
231
+ id: schema.sessionRealtimeEntries.id,
232
+ realtimeId: schema.sessionRealtimeEntries.realtimeId,
233
+ sequence: schema.sessionRealtimeEntries.sequence,
234
+ role: schema.sessionRealtimeEntries.role,
235
+ text: schema.sessionRealtimeEntries.text,
236
+ payload: schema.sessionRealtimeEntries.payload,
237
+ })
238
+ .from(schema.sessionRealtimeEntries)
239
+ .where(
240
+ and(
241
+ eq(schema.sessionRealtimeEntries.realtimeId, mode.id),
242
+ inArray(schema.sessionRealtimeEntries.kind, ["user_transcript", "assistant_transcript"]),
243
+ sql`jsonb_typeof(${schema.sessionRealtimeEntries.payload} -> 'turnId') = 'string'`,
244
+ latestDelegation
245
+ ? sql`${schema.sessionRealtimeEntries.sequence} > ${latestDelegation.sequence}`
246
+ : undefined,
247
+ isNull(sql`${schema.sessionRealtimeEntries.payload} ->> 'coveredByDelegationItemId'`),
248
+ ),
249
+ )
250
+ .orderBy(asc(schema.sessionRealtimeEntries.sequence), asc(schema.sessionRealtimeEntries.id));
251
+ const rendered = renderSessionRealtimeTail(rows);
252
+ if (!rendered.context) return null;
253
+
254
+ const [session] = await db
255
+ .select({ metadata: schema.sessions.metadata })
256
+ .from(schema.sessions)
257
+ .where(
258
+ and(
259
+ eq(schema.sessions.workspaceId, input.workspaceId),
260
+ eq(schema.sessions.id, input.sessionId),
261
+ ),
262
+ )
263
+ .limit(1);
264
+ if (!session) throw new Error(`Realtime tail session ${input.sessionId} disappeared`);
265
+ const reasoning = ReasoningEffort.safeParse(session.metadata.reasoningEffort);
266
+ const admitted = await submitHumanPromptInTransaction(db, {
267
+ accountId: input.accountId,
268
+ workspaceId: input.workspaceId,
269
+ sessionId: input.sessionId,
270
+ subjectId: input.ownerSubjectId,
271
+ subjectLabel: "Realtime",
272
+ actor: {
273
+ type: "service",
274
+ subjectId: input.ownerSubjectId,
275
+ subjectLabel: "Realtime",
276
+ context: { source: SESSION_REALTIME_TAIL_SOURCE, realtimeId: mode.id },
277
+ },
278
+ operationKey: deterministicUuid(`opengeni:session-realtime-tail-flush:${mode.id}`),
279
+ delivery: "steer",
280
+ text: rendered.context,
281
+ messagePresentation: {
282
+ kind: "realtime_voice_handoff",
283
+ text: "Voice session ended. Remaining conversation context was sent to the agent.",
284
+ context: rendered.context,
285
+ },
286
+ resources: [],
287
+ reasoningEffortFallback: reasoning.success ? reasoning.data : "medium",
288
+ turnMetadata: {
289
+ realtimeTailFlush: {
290
+ source: SESSION_REALTIME_TAIL_SOURCE,
291
+ realtimeId: mode.id,
292
+ lastDelegationItemId: latestDelegation?.delegationItemId ?? null,
293
+ sourceEntryIds: rows.map((entry) => entry.id),
294
+ },
295
+ },
296
+ source: "api",
297
+ });
298
+ const now = input.now ?? new Date();
299
+ const [projection] = await db
300
+ .insert(schema.sessionRealtimeContextProjections)
301
+ .values({
302
+ accountId: input.accountId,
303
+ workspaceId: input.workspaceId,
304
+ sessionId: input.sessionId,
305
+ turnId: admitted.turnId,
306
+ context: rendered.context,
307
+ sourceModeCount: 1,
308
+ sourceEntryCount: rows.length,
309
+ includedEntryCount: rendered.includedEntryCount,
310
+ omittedEntryCount: rendered.omittedEntryCount,
311
+ createdAt: now,
312
+ })
313
+ .returning();
314
+ if (!projection) throw new Error("Failed to persist realtime transcript tail projection");
315
+ const [marked] = await db
316
+ .update(schema.sessionRealtimeModes)
317
+ .set({ contextProjectionId: projection.id, contextProjectedAt: now, updatedAt: now })
318
+ .where(
319
+ and(
320
+ eq(schema.sessionRealtimeModes.id, mode.id),
321
+ eq(schema.sessionRealtimeModes.state, "ended"),
322
+ isNull(schema.sessionRealtimeModes.contextProjectionId),
323
+ ),
324
+ )
325
+ .returning({ id: schema.sessionRealtimeModes.id });
326
+ if (!marked) throw new Error("Realtime transcript tail projection lost its mode fence");
327
+ return mapProjection(projection);
328
+ }
329
+
330
+ export async function listSessionRealtimeContinuityEntriesInTransaction(
331
+ db: Database,
332
+ input: {
333
+ accountId: string;
334
+ workspaceId: string;
335
+ sessionId: string;
336
+ maximumEntries?: number;
337
+ },
338
+ ): Promise<SessionRealtimeContinuityEntry[]> {
339
+ const maximumEntries = input.maximumEntries ?? 20;
340
+ if (!Number.isSafeInteger(maximumEntries) || maximumEntries < 1 || maximumEntries > 128) {
341
+ throw new Error("Realtime continuity entry bound is invalid");
342
+ }
343
+ const rows = await db
344
+ .select({
345
+ realtimeId: schema.sessionRealtimeEntries.realtimeId,
346
+ sequence: schema.sessionRealtimeEntries.sequence,
347
+ role: schema.sessionRealtimeEntries.role,
348
+ text: schema.sessionRealtimeEntries.text,
349
+ payload: schema.sessionRealtimeEntries.payload,
350
+ createdAt: schema.sessionRealtimeEntries.createdAt,
351
+ modeStartedAt: schema.sessionRealtimeModes.startedAt,
352
+ })
353
+ .from(schema.sessionRealtimeEntries)
354
+ .innerJoin(
355
+ schema.sessionRealtimeModes,
356
+ eq(schema.sessionRealtimeModes.id, schema.sessionRealtimeEntries.realtimeId),
357
+ )
358
+ .where(
359
+ and(
360
+ eq(schema.sessionRealtimeEntries.accountId, input.accountId),
361
+ eq(schema.sessionRealtimeEntries.workspaceId, input.workspaceId),
362
+ eq(schema.sessionRealtimeEntries.sessionId, input.sessionId),
363
+ inArray(schema.sessionRealtimeEntries.kind, ["user_transcript", "assistant_transcript"]),
364
+ sql`jsonb_typeof(${schema.sessionRealtimeEntries.payload} -> 'turnId') = 'string'`,
365
+ ),
366
+ )
367
+ .orderBy(
368
+ desc(schema.sessionRealtimeModes.startedAt),
369
+ desc(schema.sessionRealtimeEntries.sequence),
370
+ desc(schema.sessionRealtimeEntries.id),
371
+ )
372
+ .limit(maximumEntries);
373
+ return rows.reverse().flatMap((row) => {
374
+ const turnId = row.payload.turnId;
375
+ if (
376
+ (row.role !== "user" && row.role !== "assistant") ||
377
+ typeof row.text !== "string" ||
378
+ typeof turnId !== "string"
379
+ ) {
380
+ return [];
381
+ }
382
+ return [
383
+ {
384
+ realtimeId: row.realtimeId,
385
+ sequence: row.sequence,
386
+ role: row.role,
387
+ text: takeUtf8Head(row.text, 16_000),
388
+ turnId,
389
+ createdAt: row.createdAt.toISOString(),
390
+ },
391
+ ];
392
+ });
393
+ }