@opengeni/db 0.22.2 → 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 +39 -3
- package/dist/index.js +7009 -3882
- 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 +614 -78
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
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
|
+
}
|