@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.
Files changed (48) hide show
  1. package/dist/{chunk-CYGFLLMN.js → chunk-3UCHDMKG.js} +656 -282
  2. package/dist/chunk-3UCHDMKG.js.map +1 -0
  3. package/dist/{chunk-BNGEN5QZ.js → chunk-L6ADMZHE.js} +24 -2
  4. package/dist/chunk-L6ADMZHE.js.map +1 -0
  5. package/dist/index.d.ts +42 -3
  6. package/dist/index.js +6919 -3760
  7. package/dist/index.js.map +1 -1
  8. package/dist/provision-roles.js +1 -1
  9. package/dist/runtime-posture.d.ts +3 -3
  10. package/dist/schema.d.ts +1416 -92
  11. package/dist/schema.js +11 -1
  12. package/dist/session-control.d.ts +7 -1
  13. package/dist/session-queue-commands.d.ts +8 -0
  14. package/dist/session-realtime-context.d.ts +56 -0
  15. package/dist/session-realtime-ledger.d.ts +188 -0
  16. package/dist/session-realtime-mirror.d.ts +30 -0
  17. package/dist/session-realtime-state.d.ts +2 -0
  18. package/dist/session-realtime-terminal.d.ts +40 -0
  19. package/dist/session-realtime.d.ts +59 -0
  20. package/dist/workspace-instruction-policies-schema.d.ts +239 -0
  21. package/dist/workspace-instruction-policies.d.ts +44 -0
  22. package/drizzle/0156_slack_reaction_trigger.sql +49 -0
  23. package/drizzle/0157_session_policy_role_snapshots.sql +1146 -0
  24. package/drizzle/0158_session_realtime_mode.sql +88 -0
  25. package/drizzle/0159_session_realtime_ledger.sql +198 -0
  26. package/drizzle/0160_session_realtime_delegation_terminal.sql +38 -0
  27. package/drizzle/0161_session_realtime_context_projection.sql +82 -0
  28. package/drizzle/0162_session_realtime_connection_promotion.sql +53 -0
  29. package/drizzle/0163_session_realtime_delegation_progress.sql +35 -0
  30. package/drizzle/0164_session_realtime_models.sql +28 -0
  31. package/package.json +4 -4
  32. package/src/index.ts +670 -79
  33. package/src/preference-registry.ts +11 -6
  34. package/src/provision-roles.ts +12 -0
  35. package/src/runtime-posture.ts +10 -0
  36. package/src/schema.ts +400 -43
  37. package/src/session-control.ts +596 -21
  38. package/src/session-queue-commands.ts +76 -7
  39. package/src/session-realtime-context.ts +393 -0
  40. package/src/session-realtime-ledger.ts +1790 -0
  41. package/src/session-realtime-mirror.ts +160 -0
  42. package/src/session-realtime-state.ts +25 -0
  43. package/src/session-realtime-terminal.ts +306 -0
  44. package/src/session-realtime.ts +611 -0
  45. package/src/workspace-instruction-policies-schema.ts +41 -0
  46. package/src/workspace-instruction-policies.ts +131 -2
  47. package/dist/chunk-BNGEN5QZ.js.map +0 -1
  48. package/dist/chunk-CYGFLLMN.js.map +0 -1
@@ -31,6 +31,11 @@ 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
+ } from "./session-realtime-mirror";
34
39
  import * as schema from "./schema";
35
40
  import {
36
41
  frozenInitiatorForCommandActor,
@@ -529,7 +534,7 @@ export async function moveQueuedTurnInTransaction(
529
534
  workspaceId: input.workspaceId,
530
535
  controlLock: "already_locked",
531
536
  });
532
- const session = await lockSession(db, input.workspaceId, input.sessionId);
537
+ let session = await lockSession(db, input.workspaceId, input.sessionId);
533
538
  const requestHash = canonicalSessionCommandHash({
534
539
  beforeTurnId: input.beforeTurnId,
535
540
  expectedQueueVersion: input.expectedQueueVersion,
@@ -659,7 +664,7 @@ export async function deleteSessionQueueItemInTransaction(
659
664
  workspaceId: input.workspaceId,
660
665
  controlLock: "already_locked",
661
666
  });
662
- const session = await lockSession(db, input.workspaceId, input.sessionId);
667
+ let session = await lockSession(db, input.workspaceId, input.sessionId);
663
668
  const requestHash = canonicalSessionCommandHash({
664
669
  expectedTurnVersion: input.expectedTurnVersion,
665
670
  reason: input.reason ?? null,
@@ -786,7 +791,7 @@ export async function editQueuedTurnInTransaction(
786
791
  workspaceId: input.workspaceId,
787
792
  controlLock: "already_locked",
788
793
  });
789
- const session = await lockSession(db, input.workspaceId, input.sessionId);
794
+ let session = await lockSession(db, input.workspaceId, input.sessionId);
790
795
  const requestHash = canonicalSessionCommandHash({
791
796
  expectedTurnVersion: input.expectedTurnVersion,
792
797
  expectedDraftRevision: input.expectedDraftRevision,
@@ -995,6 +1000,7 @@ export async function steerQueuedTurnInTransaction(
995
1000
  replay: true,
996
1001
  };
997
1002
  }
1003
+ await lockSession(db, input.workspaceId, input.sessionId);
998
1004
  if (input.actor.type === "agent_attempt") {
999
1005
  await assertAgentCommandAuthorityInTransaction(db, {
1000
1006
  workspaceId: input.workspaceId,
@@ -1185,6 +1191,14 @@ export async function submitHumanPromptInTransaction(
1185
1191
  reasoningEffortFallback: ReasoningEffort;
1186
1192
  /** Trusted API/core admission snapshot. Omitted only by legacy low-level callers. */
1187
1193
  turnExecutionPolicy?: TurnExecutionPolicyV1;
1194
+ /** Trusted core-only metadata attached to the admitted turn. */
1195
+ turnMetadata?: Record<string, unknown>;
1196
+ /** Trusted display projection; the durable turn still receives `text`. */
1197
+ messagePresentation?: {
1198
+ kind: "realtime_voice" | "realtime_voice_handoff";
1199
+ text: string;
1200
+ context: string;
1201
+ };
1188
1202
  source: "user" | "api";
1189
1203
  personalConnectionDelegations?: McpPersonalConnectionDelegation[];
1190
1204
  mcpCredentialUpdates?: Array<{
@@ -1215,6 +1229,8 @@ export async function submitHumanPromptInTransaction(
1215
1229
  reasoningEffort: input.reasoningEffort ?? null,
1216
1230
  latencyMode: input.latencyMode ?? null,
1217
1231
  source: input.source,
1232
+ turnMetadata: input.turnMetadata ?? {},
1233
+ messagePresentation: input.messagePresentation ?? null,
1218
1234
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1219
1235
  ...(input.actor.type === "service"
1220
1236
  ? {
@@ -1444,7 +1460,15 @@ export async function submitHumanPromptInTransaction(
1444
1460
  type: "user.message",
1445
1461
  clientEventId: input.operationKey,
1446
1462
  payload: sanitizeEventPayload({
1447
- text: input.text,
1463
+ text: input.messagePresentation?.text ?? input.text,
1464
+ ...(input.messagePresentation
1465
+ ? {
1466
+ presentation: {
1467
+ kind: input.messagePresentation.kind,
1468
+ context: input.messagePresentation.context,
1469
+ },
1470
+ }
1471
+ : {}),
1448
1472
  ...(input.resources.length ? { resources: input.resources } : {}),
1449
1473
  ...(input.model ? { model: input.model } : {}),
1450
1474
  ...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
@@ -1481,13 +1505,23 @@ export async function submitHumanPromptInTransaction(
1481
1505
  latencyMode: input.turnExecutionPolicy?.latencyMode ?? input.latencyMode ?? "standard",
1482
1506
  sandboxBackend: session.sandboxBackend,
1483
1507
  metadata: input.turnExecutionPolicy
1484
- ? metadataWithTurnExecutionPolicyV1({}, input.turnExecutionPolicy)
1485
- : {},
1508
+ ? metadataWithTurnExecutionPolicyV1(input.turnMetadata ?? {}, input.turnExecutionPolicy)
1509
+ : (input.turnMetadata ?? {}),
1486
1510
  lineage: { actor: input.actor.type },
1487
1511
  ...initiatorColumns(frozenInitiator),
1512
+ initiatingHumanSubjectId: editedSourceTurn
1513
+ ? (editedSourceTurn.initiatingHumanSubjectId ??
1514
+ (editedSourceTurn.initiatorKind === "subject"
1515
+ ? editedSourceTurn.initiatorSubjectId
1516
+ : null))
1517
+ : frozenInitiator.initiator.kind === "subject"
1518
+ ? frozenInitiator.initiator.subjectId
1519
+ : null,
1488
1520
  personalConnectionDelegations: editedSourceTurn
1489
1521
  ? editedSourceTurn.personalConnectionDelegations
1490
1522
  : (input.personalConnectionDelegations ?? []),
1523
+ createdAt: now,
1524
+ updatedAt: now,
1491
1525
  })
1492
1526
  .returning();
1493
1527
  if (!turn) throw new SessionControlInvariantError("Prompt turn was not inserted");
@@ -1618,6 +1652,35 @@ export async function submitHumanPromptInTransaction(
1618
1652
  });
1619
1653
  }
1620
1654
  const eventRows = await db.insert(schema.sessionEvents).values(eventValues).returning();
1655
+ if (input.actor.type === "human") {
1656
+ const realtimeRouting =
1657
+ input.delivery === "steer"
1658
+ ? "accepted_for_steering"
1659
+ : session.activeTurnId || existingQueued.length > 0
1660
+ ? "queued_for_execution"
1661
+ : "accepted_for_execution";
1662
+ await mirrorSessionRealtimeContextInTransaction(db, {
1663
+ accountId: input.accountId,
1664
+ workspaceId: input.workspaceId,
1665
+ sessionId: input.sessionId,
1666
+ sourceKind: "human_input",
1667
+ sourceId: acceptedEventId,
1668
+ turnId,
1669
+ channel: null,
1670
+ text: renderRealtimeHumanInputContext({
1671
+ delivery: input.delivery,
1672
+ routing: realtimeRouting,
1673
+ text: input.text,
1674
+ }),
1675
+ payload: {
1676
+ delivery: input.delivery,
1677
+ routing: realtimeRouting,
1678
+ acceptedEventId,
1679
+ instruction: "OpenGeni accepted and routed this user input; do not delegate it again.",
1680
+ },
1681
+ now,
1682
+ });
1683
+ }
1621
1684
  const queueVersion = session.queueVersion + 1;
1622
1685
  await db
1623
1686
  .update(schema.sessions)
@@ -1772,6 +1835,11 @@ export async function sendAgentMessageInTransaction(
1772
1835
  const effective = await evaluateSessionControl(db, input.workspaceId, input.targetSessionId, {
1773
1836
  workspaceControl,
1774
1837
  });
1838
+ const realtimeActive = await sessionRealtimeIsActiveInTransaction(
1839
+ db,
1840
+ input.workspaceId,
1841
+ input.targetSessionId,
1842
+ );
1775
1843
  const now = new Date();
1776
1844
  const [update] = await db
1777
1845
  .insert(schema.sessionSystemUpdates)
@@ -1818,7 +1886,7 @@ export async function sendAgentMessageInTransaction(
1818
1886
  .returning({ id: schema.sessionEvents.id });
1819
1887
  if (!event) throw new SessionControlInvariantError("Agent message event was not inserted");
1820
1888
  const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
1821
- const runnable = session.activeTurnId === null && effective.state === "active";
1889
+ const runnable = !realtimeActive && session.activeTurnId === null && effective.state === "active";
1822
1890
  const wake = runnable
1823
1891
  ? await registerInternalUpdateWakeInTransaction(db, {
1824
1892
  accountId: input.accountId,
@@ -1929,6 +1997,7 @@ export async function steerAgentSessionInTransaction(
1929
1997
  replay: true,
1930
1998
  };
1931
1999
  }
2000
+ await lockSession(db, input.workspaceId, input.targetSessionId);
1932
2001
  await assertAgentCommandAuthorityInTransaction(db, {
1933
2002
  workspaceId: input.workspaceId,
1934
2003
  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
+ }