@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.
- package/dist/{chunk-CYGFLLMN.js → chunk-7WTDI7Y3.js} +764 -283
- package/dist/chunk-7WTDI7Y3.js.map +1 -0
- package/dist/{chunk-BNGEN5QZ.js → chunk-KW6U54V2.js} +26 -2
- package/dist/chunk-KW6U54V2.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +24 -2
- package/dist/index.d.ts +107 -5
- package/dist/index.js +7857 -3898
- package/dist/index.js.map +1 -1
- package/dist/preference-registry.d.ts +14 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +1659 -77
- package/dist/schema.js +13 -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 +51 -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 +307 -0
- package/dist/workspace-instruction-policies.d.ts +97 -7
- 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/drizzle/0165_document_authority_foundation.sql +259 -0
- package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
- package/drizzle/0167_document_index_replay_authority.sql +61 -0
- package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
- package/package.json +4 -4
- package/src/connection-token-resolver.ts +79 -16
- package/src/index.ts +1033 -101
- package/src/preference-registry.ts +114 -6
- package/src/provision-roles.ts +12 -0
- package/src/runtime-posture.ts +12 -0
- package/src/schema.ts +486 -43
- package/src/session-control.ts +643 -21
- package/src/session-queue-commands.ts +121 -18
- package/src/session-realtime-context.ts +393 -0
- package/src/session-realtime-ledger.ts +1790 -0
- package/src/session-realtime-mirror.ts +276 -0
- package/src/session-realtime-state.ts +25 -0
- package/src/session-realtime-terminal.ts +306 -0
- package/src/session-realtime.ts +659 -0
- package/src/workspace-instruction-policies-schema.ts +71 -0
- package/src/workspace-instruction-policies.ts +568 -25
- package/dist/chunk-BNGEN5QZ.js.map +0 -1
- 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({
|
|
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
|
|
286
|
-
|
|
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
|
-
|
|
291
|
-
|
|
321
|
+
sourceKind: "human_input_response",
|
|
322
|
+
sourceId: event.id,
|
|
292
323
|
turnId: current.id,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("&", "&").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
|
+
}
|