@opengeni/db 0.36.0 → 1.0.1
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/canonical-human-identities.js +3 -3
- package/dist/{chunk-P3RE4D2B.js → chunk-4SR4HKKA.js} +2 -2
- package/dist/{chunk-5KTIEDX7.js → chunk-6ZYKCRFO.js} +124 -16
- package/dist/chunk-6ZYKCRFO.js.map +1 -0
- package/dist/{chunk-CQZ4QSP5.js → chunk-CUPM3FGB.js} +3 -3
- package/dist/{chunk-TAUTWOS5.js → chunk-LTG3S6ZY.js} +2 -2
- package/dist/{chunk-VKLB2ZGI.js → chunk-NXAFLCQX.js} +81 -2
- package/dist/chunk-NXAFLCQX.js.map +1 -0
- package/dist/{chunk-F32YT7MP.js → chunk-WD5ROSLL.js} +2 -2
- package/dist/{chunk-JC5QPLUM.js → chunk-ZWREYEWA.js} +48 -3
- package/dist/chunk-ZWREYEWA.js.map +1 -0
- package/dist/editable-artifact-durable-export.d.ts +10 -0
- package/dist/editable-artifact-durable-export.js +18 -3
- package/dist/editable-artifact-durable-export.js.map +1 -1
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +69 -8
- package/dist/index.js +869 -75
- package/dist/index.js.map +1 -1
- package/dist/knowledge-source-sync-schema.d.ts +730 -0
- package/dist/knowledge-source-sync.d.ts +60 -0
- package/dist/persistence-errors.d.ts +8 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +21 -4
- package/dist/schema.js +5 -1
- package/dist/session-queue-commands.d.ts +1 -1
- package/dist/session-realtime-ledger.d.ts +1 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/drizzle/0238_recover_unclaimed_session_turns.sql +307 -0
- package/drizzle/0240_model_context_user_messages.sql +204 -0
- package/drizzle/0243_google_drive_object_acl_authority.sql +554 -0
- package/package.json +5 -5
- package/src/editable-artifact-durable-export.ts +27 -0
- package/src/index.ts +680 -30
- package/src/knowledge-source-sync-schema.ts +83 -0
- package/src/knowledge-source-sync.ts +530 -0
- package/src/persistence-errors.ts +60 -1
- package/src/provision-roles.ts +36 -0
- package/src/runtime-posture.ts +49 -1
- package/src/schema.ts +30 -7
- package/src/session-queue-commands.ts +13 -14
- package/src/session-realtime-context.ts +16 -0
- package/src/session-realtime-ledger.ts +53 -1
- package/dist/chunk-5KTIEDX7.js.map +0 -1
- package/dist/chunk-JC5QPLUM.js.map +0 -1
- package/dist/chunk-VKLB2ZGI.js.map +0 -1
- /package/dist/{chunk-P3RE4D2B.js.map → chunk-4SR4HKKA.js.map} +0 -0
- /package/dist/{chunk-CQZ4QSP5.js.map → chunk-CUPM3FGB.js.map} +0 -0
- /package/dist/{chunk-TAUTWOS5.js.map → chunk-LTG3S6ZY.js.map} +0 -0
- /package/dist/{chunk-F32YT7MP.js.map → chunk-WD5ROSLL.js.map} +0 -0
|
@@ -25,6 +25,26 @@ export type PersistenceFailureDetails = {
|
|
|
25
25
|
|
|
26
26
|
const SQLSTATE_KEYS = ["sqlState", "sqlstate", "code"] as const;
|
|
27
27
|
const NESTED_ERROR_KEYS = ["cause", "original", "driverError", "error", "errors"] as const;
|
|
28
|
+
const RETRYABLE_DATABASE_TRANSPORT_CODES = new Set([
|
|
29
|
+
// postgres.js connection lifecycle failures.
|
|
30
|
+
"CONNECTION_CLOSED",
|
|
31
|
+
"CONNECTION_DESTROYED",
|
|
32
|
+
"CONNECTION_ENDED",
|
|
33
|
+
"CONNECT_TIMEOUT",
|
|
34
|
+
// Node socket/DNS failures surfaced unchanged by postgres.js.
|
|
35
|
+
"EAI_AGAIN",
|
|
36
|
+
"ECONNABORTED",
|
|
37
|
+
"ECONNREFUSED",
|
|
38
|
+
"ECONNRESET",
|
|
39
|
+
"EHOSTDOWN",
|
|
40
|
+
"EHOSTUNREACH",
|
|
41
|
+
"ENETDOWN",
|
|
42
|
+
"ENETRESET",
|
|
43
|
+
"ENETUNREACH",
|
|
44
|
+
"ENOTFOUND",
|
|
45
|
+
"EPIPE",
|
|
46
|
+
"ETIMEDOUT",
|
|
47
|
+
]);
|
|
28
48
|
const DATABASE_ERROR_NAMES = new Set(["DatabaseError", "DrizzleQueryError", "PostgresError"]);
|
|
29
49
|
const DATABASE_DIAGNOSTIC_KEYS = [
|
|
30
50
|
"severity",
|
|
@@ -72,6 +92,9 @@ export function nestedPostgresSqlState(error: unknown): string | null {
|
|
|
72
92
|
const value = current[key];
|
|
73
93
|
if (typeof value !== "string" || !/^[0-9A-Z]{5}$/i.test(value)) continue;
|
|
74
94
|
const normalized = value.toUpperCase();
|
|
95
|
+
// Node transport codes such as EPIPE happen to be five characters but
|
|
96
|
+
// are not PostgreSQL SQLSTATEs.
|
|
97
|
+
if (RETRYABLE_DATABASE_TRANSPORT_CODES.has(normalized)) continue;
|
|
75
98
|
if (normalized === "40P01" || normalized === "40001") return normalized;
|
|
76
99
|
fallback ??= normalized;
|
|
77
100
|
}
|
|
@@ -94,13 +117,49 @@ export function isRetryablePersistenceSqlState(sqlState: string | null): boolean
|
|
|
94
117
|
return sqlState === "40P01" || sqlState === "40001";
|
|
95
118
|
}
|
|
96
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Recognize only explicit postgres.js/Node transport codes, including nested
|
|
122
|
+
* driver causes. These failures can arrive as plain Errors with no SQLSTATE or
|
|
123
|
+
* database diagnostic fields when the connection dies before PostgreSQL can
|
|
124
|
+
* answer. Messages are intentionally ignored: they are unstable and may
|
|
125
|
+
* contain connection detail.
|
|
126
|
+
*/
|
|
127
|
+
export function isRetryableDatabaseTransportFailure(error: unknown): boolean {
|
|
128
|
+
const queue: unknown[] = [error];
|
|
129
|
+
const seen = new Set<unknown>();
|
|
130
|
+
while (queue.length > 0 && seen.size < 64) {
|
|
131
|
+
const current = queue.shift();
|
|
132
|
+
if (!isRecord(current) || seen.has(current)) continue;
|
|
133
|
+
seen.add(current);
|
|
134
|
+
|
|
135
|
+
for (const key of ["code", "errno"] as const) {
|
|
136
|
+
const value = current[key];
|
|
137
|
+
if (
|
|
138
|
+
typeof value === "string" &&
|
|
139
|
+
RETRYABLE_DATABASE_TRANSPORT_CODES.has(value.toUpperCase())
|
|
140
|
+
) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const key of NESTED_ERROR_KEYS) {
|
|
146
|
+
const nested = current[key];
|
|
147
|
+
if (Array.isArray(nested)) queue.push(...nested);
|
|
148
|
+
else if (nested !== undefined) queue.push(nested);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
97
154
|
/**
|
|
98
155
|
* Distinguish database/ORM failures from expected domain exceptions when a
|
|
99
156
|
* driver omitted SQLSTATE. This checks shape only; callers retain the original
|
|
100
157
|
* failure independently as canonical error evidence.
|
|
101
158
|
*/
|
|
102
159
|
export function isDatabasePersistenceFailure(error: unknown): boolean {
|
|
103
|
-
if (nestedPostgresSqlState(error) !== null)
|
|
160
|
+
if (isRetryableDatabaseTransportFailure(error) || nestedPostgresSqlState(error) !== null) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
104
163
|
|
|
105
164
|
const queue: unknown[] = [error];
|
|
106
165
|
const seen = new Set<unknown>();
|
package/src/provision-roles.ts
CHANGED
|
@@ -757,6 +757,42 @@ BEGIN
|
|
|
757
757
|
${literal(role)}
|
|
758
758
|
);
|
|
759
759
|
END IF;
|
|
760
|
+
-- Migration 0243 adds the exact Google Drive object authorization and
|
|
761
|
+
-- safe-citation projections. Re-converge both capabilities for the same
|
|
762
|
+
-- supported migrate-then-provision order without granting direct mutation
|
|
763
|
+
-- of their append-only ACL evidence tables.
|
|
764
|
+
IF to_regprocedure(
|
|
765
|
+
format(
|
|
766
|
+
'%I.google_drive_file_authorized(uuid,uuid,text,uuid)',
|
|
767
|
+
${literal(schema)}
|
|
768
|
+
)
|
|
769
|
+
) IS NOT NULL THEN
|
|
770
|
+
EXECUTE format(
|
|
771
|
+
'REVOKE ALL ON FUNCTION %I.google_drive_file_authorized(uuid, uuid, text, uuid) FROM PUBLIC',
|
|
772
|
+
${literal(schema)}
|
|
773
|
+
);
|
|
774
|
+
EXECUTE format(
|
|
775
|
+
'GRANT EXECUTE ON FUNCTION %I.google_drive_file_authorized(uuid, uuid, text, uuid) TO %I',
|
|
776
|
+
${literal(schema)},
|
|
777
|
+
${literal(role)}
|
|
778
|
+
);
|
|
779
|
+
END IF;
|
|
780
|
+
IF to_regprocedure(
|
|
781
|
+
format(
|
|
782
|
+
'%I.google_drive_document_citation(uuid,uuid,text,uuid,uuid)',
|
|
783
|
+
${literal(schema)}
|
|
784
|
+
)
|
|
785
|
+
) IS NOT NULL THEN
|
|
786
|
+
EXECUTE format(
|
|
787
|
+
'REVOKE ALL ON FUNCTION %I.google_drive_document_citation(uuid, uuid, text, uuid, uuid) FROM PUBLIC',
|
|
788
|
+
${literal(schema)}
|
|
789
|
+
);
|
|
790
|
+
EXECUTE format(
|
|
791
|
+
'GRANT EXECUTE ON FUNCTION %I.google_drive_document_citation(uuid, uuid, text, uuid, uuid) TO %I',
|
|
792
|
+
${literal(schema)},
|
|
793
|
+
${literal(role)}
|
|
794
|
+
);
|
|
795
|
+
END IF;
|
|
760
796
|
IF to_regprocedure(
|
|
761
797
|
format(
|
|
762
798
|
'%I.ensure_managed_human_personal_workspace(uuid,text,uuid)',
|
package/src/runtime-posture.ts
CHANGED
|
@@ -44,6 +44,22 @@ const DEDICATED_ARTIFACT_CAPABILITY_ROUTINES = new Set<string>([
|
|
|
44
44
|
|
|
45
45
|
const KNOWLEDGE_SOURCE_SYNC_LOCK_AUTHORITY_ROUTINE =
|
|
46
46
|
"knowledge_source_sync_lock_authority(uuid, uuid, uuid)";
|
|
47
|
+
const GOOGLE_DRIVE_FILE_AUTHORIZATION_ROUTINE =
|
|
48
|
+
"google_drive_file_authorized(uuid, uuid, text, uuid)";
|
|
49
|
+
const GOOGLE_DRIVE_DOCUMENT_CITATION_ROUTINE =
|
|
50
|
+
"google_drive_document_citation(uuid, uuid, text, uuid, uuid)";
|
|
51
|
+
const GOOGLE_DRIVE_AUTHORITY_TABLES = [
|
|
52
|
+
"connections",
|
|
53
|
+
"files",
|
|
54
|
+
"google_drive_object_acl_evidence",
|
|
55
|
+
"google_drive_object_acl_principals",
|
|
56
|
+
"knowledge_document_versions",
|
|
57
|
+
"knowledge_providers",
|
|
58
|
+
"knowledge_source_objects",
|
|
59
|
+
"knowledge_source_sync_index_obligations",
|
|
60
|
+
"knowledge_source_sync_states",
|
|
61
|
+
"knowledge_sources",
|
|
62
|
+
] as const;
|
|
47
63
|
const KNOWLEDGE_SOURCE_SYNC_LOCK_AUTHORITY_TABLES = [
|
|
48
64
|
"knowledge_sources",
|
|
49
65
|
"knowledge_source_objects",
|
|
@@ -110,6 +126,8 @@ export const RUNTIME_TARGET_SCHEMA_CAPABILITY_ROUTINES = [
|
|
|
110
126
|
XAI_AUTHORITY_LIVE_ROUTINE,
|
|
111
127
|
XAI_CREATE_CREDENTIAL_ROUTINE,
|
|
112
128
|
XAI_DISCONNECT_CREDENTIAL_ROUTINE,
|
|
129
|
+
GOOGLE_DRIVE_DOCUMENT_CITATION_ROUTINE,
|
|
130
|
+
GOOGLE_DRIVE_FILE_AUTHORIZATION_ROUTINE,
|
|
113
131
|
KNOWLEDGE_SOURCE_SYNC_LOCK_AUTHORITY_ROUTINE,
|
|
114
132
|
MANAGED_HUMAN_PERSONAL_WORKSPACE_ROUTINE,
|
|
115
133
|
...CANONICAL_HUMAN_IDENTITY_ROUTINES,
|
|
@@ -214,6 +232,8 @@ export const FORCE_RLS_TABLES = [
|
|
|
214
232
|
"generated_video_artifacts",
|
|
215
233
|
"github_installation_repositories",
|
|
216
234
|
"github_installations",
|
|
235
|
+
"google_drive_object_acl_evidence",
|
|
236
|
+
"google_drive_object_acl_principals",
|
|
217
237
|
"host_export_config",
|
|
218
238
|
"host_export_consumers",
|
|
219
239
|
"host_export_cursor_state",
|
|
@@ -579,6 +599,8 @@ export const RUNTIME_READ_INSERT_TABLES = [
|
|
|
579
599
|
"editable_artifact_transactions",
|
|
580
600
|
"editable_artifact_undo_claims",
|
|
581
601
|
"editable_artifact_versions",
|
|
602
|
+
"google_drive_object_acl_evidence",
|
|
603
|
+
"google_drive_object_acl_principals",
|
|
582
604
|
"knowledge_change_proposals",
|
|
583
605
|
"knowledge_claim_evidence",
|
|
584
606
|
"knowledge_claim_relations",
|
|
@@ -1260,7 +1282,33 @@ export function evaluateRuntimeDatabasePosture(
|
|
|
1260
1282
|
} else if (!routine.securityDefiner) {
|
|
1261
1283
|
violations.push(`target-schema runtime capability ${routine.name} is not SECURITY DEFINER`);
|
|
1262
1284
|
}
|
|
1263
|
-
if (
|
|
1285
|
+
if (
|
|
1286
|
+
routine.name === GOOGLE_DRIVE_FILE_AUTHORIZATION_ROUTINE ||
|
|
1287
|
+
routine.name === GOOGLE_DRIVE_DOCUMENT_CITATION_ROUTINE
|
|
1288
|
+
) {
|
|
1289
|
+
const missingAuthorityTables = GOOGLE_DRIVE_AUTHORITY_TABLES.filter(
|
|
1290
|
+
(tableName) => !tableByName.has(tableName),
|
|
1291
|
+
);
|
|
1292
|
+
if (missingAuthorityTables.length > 0) {
|
|
1293
|
+
violations.push(
|
|
1294
|
+
`target-schema runtime capability ${routine.name} authority tables are missing: ${missingAuthorityTables.join(", ")}`,
|
|
1295
|
+
);
|
|
1296
|
+
} else {
|
|
1297
|
+
const authorityTables = GOOGLE_DRIVE_AUTHORITY_TABLES.map(
|
|
1298
|
+
(tableName) => tableByName.get(tableName)!,
|
|
1299
|
+
);
|
|
1300
|
+
const authorityOwners = new Set(authorityTables.map((table) => table.owner));
|
|
1301
|
+
if (authorityOwners.size !== 1) {
|
|
1302
|
+
violations.push(
|
|
1303
|
+
`target-schema runtime capability ${routine.name} authority table owners do not match: ${authorityTables.map((table) => `${table.name}=${table.owner}`).join(", ")}`,
|
|
1304
|
+
);
|
|
1305
|
+
} else if (routine.owner !== authorityTables[0]!.owner) {
|
|
1306
|
+
violations.push(
|
|
1307
|
+
`target-schema runtime capability ${routine.name} owner ${routine.owner} does not match authority table owner ${authorityTables[0]!.owner}`,
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
} else if (routine.name === KNOWLEDGE_SOURCE_SYNC_LOCK_AUTHORITY_ROUTINE) {
|
|
1264
1312
|
const missingAuthorityTables = KNOWLEDGE_SOURCE_SYNC_LOCK_AUTHORITY_TABLES.filter(
|
|
1265
1313
|
(tableName) => !tableByName.has(tableName),
|
|
1266
1314
|
);
|
package/src/schema.ts
CHANGED
|
@@ -2464,10 +2464,10 @@ export const sessions = pgTable(
|
|
|
2464
2464
|
status: text("status").notNull().default("queued"),
|
|
2465
2465
|
initialMessage: losslessText("initial_message").notNull(),
|
|
2466
2466
|
initialMessageCodecVersion: losslessCodecVersion("initial_message_codec_version"),
|
|
2467
|
-
//
|
|
2468
|
-
// initial turn copies
|
|
2469
|
-
// retrying caller's different
|
|
2470
|
-
|
|
2467
|
+
// Model-visible application context frozen with the winning create. The
|
|
2468
|
+
// initial turn copies it into the canonical user message so an idempotent
|
|
2469
|
+
// repair cannot adopt a retrying caller's different message context.
|
|
2470
|
+
initialModelContext: text("initial_model_context"),
|
|
2471
2471
|
title: text("title"),
|
|
2472
2472
|
titleSource: text("title_source"),
|
|
2473
2473
|
// Per-session agent persona/system instructions supplied at create (the
|
|
@@ -2762,6 +2762,11 @@ export const sessions = pgTable(
|
|
|
2762
2762
|
table.rootSessionId,
|
|
2763
2763
|
table.nestedAgentDepth,
|
|
2764
2764
|
),
|
|
2765
|
+
initialModelContextValid: check(
|
|
2766
|
+
"sessions_initial_model_context_check",
|
|
2767
|
+
sql`${table.initialModelContext} is null
|
|
2768
|
+
or opengeni_private.model_context_value_valid(${table.initialModelContext})`,
|
|
2769
|
+
),
|
|
2765
2770
|
}),
|
|
2766
2771
|
);
|
|
2767
2772
|
|
|
@@ -3115,6 +3120,9 @@ export const sessionRealtimeEntries = pgTable(
|
|
|
3115
3120
|
textCodecVersion: losslessCodecVersion("text_codec_version"),
|
|
3116
3121
|
payload: losslessJsonb("payload").$type<Record<string, unknown>>().notNull().default({}),
|
|
3117
3122
|
payloadCodecVersion: losslessCodecVersion("payload_codec_version"),
|
|
3123
|
+
// Application context for an exact provider-in delegation or finalized
|
|
3124
|
+
// transcript. It is materialized as ordinary user-role message content.
|
|
3125
|
+
modelContext: text("model_context"),
|
|
3118
3126
|
clientAckedAt: timestamp("client_acked_at", { withTimezone: true }),
|
|
3119
3127
|
providerAckedAt: timestamp("provider_acked_at", { withTimezone: true }),
|
|
3120
3128
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
@@ -3190,6 +3198,15 @@ export const sessionRealtimeEntries = pgTable(
|
|
|
3190
3198
|
or (${table.kind} = 'assistant_transcript' and ${table.role} = 'assistant' and ${table.text} is not null)
|
|
3191
3199
|
or (${table.kind} not in ('user_transcript', 'assistant_transcript') and ${table.role} is null)`,
|
|
3192
3200
|
),
|
|
3201
|
+
modelContextValid: check(
|
|
3202
|
+
"session_realtime_entries_model_context_check",
|
|
3203
|
+
sql`${table.modelContext} is null
|
|
3204
|
+
or (
|
|
3205
|
+
${table.direction} = 'provider_in'
|
|
3206
|
+
and ${table.kind} in ('delegation_call', 'user_transcript', 'assistant_transcript')
|
|
3207
|
+
and opengeni_private.model_context_value_valid(${table.modelContext})
|
|
3208
|
+
)`,
|
|
3209
|
+
),
|
|
3193
3210
|
}),
|
|
3194
3211
|
);
|
|
3195
3212
|
|
|
@@ -4497,9 +4514,10 @@ export const sessionTurns = pgTable(
|
|
|
4497
4514
|
prompt: losslessText("prompt").notNull(),
|
|
4498
4515
|
promptCodecVersion: losslessCodecVersion("prompt_codec_version"),
|
|
4499
4516
|
annotations: jsonb("annotations").$type<TimelineAnnotation[]>().notNull().default([]),
|
|
4500
|
-
//
|
|
4501
|
-
//
|
|
4502
|
-
|
|
4517
|
+
// Application context for this exact user message. It is copied into the
|
|
4518
|
+
// canonical user-role history item at claim and omitted from public queue
|
|
4519
|
+
// projections; full event/audit data retains it.
|
|
4520
|
+
modelContext: text("model_context"),
|
|
4503
4521
|
resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
|
|
4504
4522
|
tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
|
|
4505
4523
|
// false = inherit the durable session policy; true = this turn explicitly
|
|
@@ -4579,6 +4597,11 @@ export const sessionTurns = pgTable(
|
|
|
4579
4597
|
"session_turns_latency_mode_check",
|
|
4580
4598
|
sql`${table.latencyMode} in ('standard', 'priority', 'fast')`,
|
|
4581
4599
|
),
|
|
4600
|
+
modelContextValid: check(
|
|
4601
|
+
"session_turns_model_context_check",
|
|
4602
|
+
sql`${table.modelContext} is null
|
|
4603
|
+
or opengeni_private.model_context_value_valid(${table.modelContext})`,
|
|
4604
|
+
),
|
|
4582
4605
|
}),
|
|
4583
4606
|
);
|
|
4584
4607
|
|
|
@@ -1414,7 +1414,7 @@ export async function submitHumanPromptInTransaction(
|
|
|
1414
1414
|
expectedDraftRevision?: number | null;
|
|
1415
1415
|
text: string;
|
|
1416
1416
|
annotations?: TimelineAnnotation[];
|
|
1417
|
-
|
|
1417
|
+
modelContext?: string | null;
|
|
1418
1418
|
resources: ResourceRef[];
|
|
1419
1419
|
model?: string | null;
|
|
1420
1420
|
reasoningEffort?: ReasoningEffort | null;
|
|
@@ -1458,7 +1458,7 @@ export async function submitHumanPromptInTransaction(
|
|
|
1458
1458
|
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
1459
1459
|
text: input.text,
|
|
1460
1460
|
annotations,
|
|
1461
|
-
|
|
1461
|
+
modelContext: input.modelContext ?? null,
|
|
1462
1462
|
resources: withCanonicalResourceMountPaths(input.resources),
|
|
1463
1463
|
model: input.model ?? null,
|
|
1464
1464
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
@@ -1626,7 +1626,7 @@ export async function submitHumanPromptInTransaction(
|
|
|
1626
1626
|
}
|
|
1627
1627
|
|
|
1628
1628
|
let editedSourceTurn: QueuedTurnRow | undefined;
|
|
1629
|
-
let
|
|
1629
|
+
let editedSourceModelContext: string | null | undefined;
|
|
1630
1630
|
if (draft?.sourceTurnId) {
|
|
1631
1631
|
const sourceLocks = await lockSessionEventWriteRows(db, {
|
|
1632
1632
|
workspaceId: input.workspaceId,
|
|
@@ -1661,14 +1661,11 @@ export async function submitHumanPromptInTransaction(
|
|
|
1661
1661
|
},
|
|
1662
1662
|
);
|
|
1663
1663
|
}
|
|
1664
|
-
//
|
|
1665
|
-
//
|
|
1666
|
-
//
|
|
1667
|
-
// source identity is fenced by its exact withdrawn row version, not by
|
|
1668
|
-
// comparing the replacement content with the old prompt. A client-supplied
|
|
1669
|
-
// instruction value must never override the private source value.
|
|
1664
|
+
// Preserve the non-rendered message segment when editing only the visible
|
|
1665
|
+
// draft. Source identity is fenced by the exact withdrawn row version; a
|
|
1666
|
+
// replacement request cannot accidentally detach or replace its context.
|
|
1670
1667
|
editedSourceTurn = sourceTurn;
|
|
1671
|
-
|
|
1668
|
+
editedSourceModelContext = sourceTurn.modelContext ?? null;
|
|
1672
1669
|
}
|
|
1673
1670
|
|
|
1674
1671
|
for (const update of input.mcpCredentialUpdates ?? []) {
|
|
@@ -1725,6 +1722,10 @@ export async function submitHumanPromptInTransaction(
|
|
|
1725
1722
|
const acceptedEventId = crypto.randomUUID();
|
|
1726
1723
|
const turnId = crypto.randomUUID();
|
|
1727
1724
|
const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
|
|
1725
|
+
const effectiveModelContext =
|
|
1726
|
+
editedSourceModelContext !== undefined
|
|
1727
|
+
? editedSourceModelContext
|
|
1728
|
+
: (input.modelContext ?? null);
|
|
1728
1729
|
let sequence = session.lastSequence;
|
|
1729
1730
|
const eventValues: SessionEventInsertWithPayload[] = [
|
|
1730
1731
|
{
|
|
@@ -1747,6 +1748,7 @@ export async function submitHumanPromptInTransaction(
|
|
|
1747
1748
|
}
|
|
1748
1749
|
: {}),
|
|
1749
1750
|
...(input.resources.length ? { resources: input.resources } : {}),
|
|
1751
|
+
...(effectiveModelContext ? { modelContext: effectiveModelContext } : {}),
|
|
1750
1752
|
...(input.model ? { model: input.model } : {}),
|
|
1751
1753
|
...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
|
|
1752
1754
|
...(input.latencyMode ? { latencyMode: input.latencyMode } : {}),
|
|
@@ -1773,10 +1775,7 @@ export async function submitHumanPromptInTransaction(
|
|
|
1773
1775
|
position: input.delivery === "steer" ? 0 : existingQueued.length + 1,
|
|
1774
1776
|
prompt: input.text,
|
|
1775
1777
|
annotations,
|
|
1776
|
-
|
|
1777
|
-
editedSourceTurnInstructions !== undefined
|
|
1778
|
-
? editedSourceTurnInstructions
|
|
1779
|
-
: (input.turnInstructions ?? null),
|
|
1778
|
+
modelContext: effectiveModelContext,
|
|
1780
1779
|
resources: input.resources,
|
|
1781
1780
|
tools: [],
|
|
1782
1781
|
toolsProvided: false,
|
|
@@ -237,6 +237,7 @@ export async function flushSessionRealtimeTranscriptTailInTransaction(
|
|
|
237
237
|
textCodecVersion: schema.sessionRealtimeEntries.textCodecVersion,
|
|
238
238
|
payload: schema.sessionRealtimeEntries.payload,
|
|
239
239
|
payloadCodecVersion: schema.sessionRealtimeEntries.payloadCodecVersion,
|
|
240
|
+
modelContext: schema.sessionRealtimeEntries.modelContext,
|
|
240
241
|
})
|
|
241
242
|
.from(schema.sessionRealtimeEntries)
|
|
242
243
|
.where(
|
|
@@ -258,6 +259,20 @@ export async function flushSessionRealtimeTranscriptTailInTransaction(
|
|
|
258
259
|
}));
|
|
259
260
|
const rendered = renderSessionRealtimeTail(decodedRows);
|
|
260
261
|
if (!rendered.context) return null;
|
|
262
|
+
const renderedRows = decodedRows.slice(decodedRows.length - rendered.includedEntryCount);
|
|
263
|
+
let foundUserTranscript = false;
|
|
264
|
+
let modelContext: string | null = null;
|
|
265
|
+
for (let index = renderedRows.length - 1; index >= 0; index -= 1) {
|
|
266
|
+
const entry = renderedRows[index]!;
|
|
267
|
+
if (entry.role === "user") {
|
|
268
|
+
foundUserTranscript = true;
|
|
269
|
+
modelContext = entry.modelContext;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!foundUserTranscript) {
|
|
274
|
+
modelContext = renderedRows.at(-1)?.modelContext ?? null;
|
|
275
|
+
}
|
|
261
276
|
|
|
262
277
|
const [session] = await db
|
|
263
278
|
.select({ metadata: schema.sessions.metadata })
|
|
@@ -286,6 +301,7 @@ export async function flushSessionRealtimeTranscriptTailInTransaction(
|
|
|
286
301
|
operationKey: deterministicUuid(`opengeni:session-realtime-tail-flush:${mode.id}`),
|
|
287
302
|
delivery: "steer",
|
|
288
303
|
text: rendered.context,
|
|
304
|
+
modelContext,
|
|
289
305
|
messagePresentation: {
|
|
290
306
|
kind: "realtime_voice_handoff",
|
|
291
307
|
text: "Voice session ended. Remaining conversation context was sent to the agent.",
|
|
@@ -155,6 +155,7 @@ export type SessionRealtimeInboundEntryInput = {
|
|
|
155
155
|
delegationItemId?: string | null | undefined;
|
|
156
156
|
text?: string | null | undefined;
|
|
157
157
|
payload?: Record<string, unknown> | undefined;
|
|
158
|
+
modelContext?: string | undefined;
|
|
158
159
|
};
|
|
159
160
|
|
|
160
161
|
export type SyncSessionRealtimeLedgerInput = AssertSessionRealtimeOwnerInput & {
|
|
@@ -1016,12 +1017,34 @@ function canonicalJsonValue(value: unknown): unknown {
|
|
|
1016
1017
|
return value;
|
|
1017
1018
|
}
|
|
1018
1019
|
|
|
1020
|
+
function normalizedModelContext(input: SessionRealtimeInboundEntryInput): string | null {
|
|
1021
|
+
const value = input.modelContext?.trim();
|
|
1022
|
+
if (!value) {
|
|
1023
|
+
if (input.modelContext !== undefined) {
|
|
1024
|
+
throw new Error("Realtime modelContext must not be empty");
|
|
1025
|
+
}
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
if (value.length > 32_768) {
|
|
1029
|
+
throw new Error("Realtime modelContext exceeds the server limit");
|
|
1030
|
+
}
|
|
1031
|
+
if (
|
|
1032
|
+
input.kind !== "delegation_call" &&
|
|
1033
|
+
input.kind !== "user_transcript" &&
|
|
1034
|
+
input.kind !== "assistant_transcript"
|
|
1035
|
+
) {
|
|
1036
|
+
throw new Error("Realtime modelContext requires a delegation or finalized transcript");
|
|
1037
|
+
}
|
|
1038
|
+
return value;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1019
1041
|
function inboundReplayMatches(
|
|
1020
1042
|
row: EntryRow,
|
|
1021
1043
|
input: SessionRealtimeInboundEntryInput,
|
|
1022
1044
|
role: "user" | "assistant" | null,
|
|
1023
1045
|
text: string | null,
|
|
1024
1046
|
payload: Record<string, unknown>,
|
|
1047
|
+
modelContext: string | null,
|
|
1025
1048
|
): boolean {
|
|
1026
1049
|
const storedText =
|
|
1027
1050
|
row.text === null ? null : fromPostgresLosslessText(row.text, row.textCodecVersion);
|
|
@@ -1034,11 +1057,32 @@ function inboundReplayMatches(
|
|
|
1034
1057
|
row.delegationItemId === (input.delegationItemId ?? null) &&
|
|
1035
1058
|
row.sourceUpdateId === null &&
|
|
1036
1059
|
storedText === text &&
|
|
1060
|
+
row.modelContext === modelContext &&
|
|
1037
1061
|
JSON.stringify(canonicalJsonValue(storedPayload)) ===
|
|
1038
1062
|
JSON.stringify(canonicalJsonValue(payload))
|
|
1039
1063
|
);
|
|
1040
1064
|
}
|
|
1041
1065
|
|
|
1066
|
+
async function delegationModelContextMatches(
|
|
1067
|
+
db: Database,
|
|
1068
|
+
row: EntryRow,
|
|
1069
|
+
modelContext: string | null,
|
|
1070
|
+
): Promise<boolean> {
|
|
1071
|
+
if (row.kind !== "delegation_call" || row.turnId === null) return true;
|
|
1072
|
+
const [turn] = await db
|
|
1073
|
+
.select({ modelContext: schema.sessionTurns.modelContext })
|
|
1074
|
+
.from(schema.sessionTurns)
|
|
1075
|
+
.where(
|
|
1076
|
+
and(
|
|
1077
|
+
eq(schema.sessionTurns.workspaceId, row.workspaceId),
|
|
1078
|
+
eq(schema.sessionTurns.sessionId, row.sessionId),
|
|
1079
|
+
eq(schema.sessionTurns.id, row.turnId),
|
|
1080
|
+
),
|
|
1081
|
+
)
|
|
1082
|
+
.limit(1);
|
|
1083
|
+
return turn?.modelContext === modelContext;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1042
1086
|
async function appendInvalidDelegationFailure(
|
|
1043
1087
|
db: Database,
|
|
1044
1088
|
input: Pick<
|
|
@@ -1083,6 +1127,7 @@ async function admitRealtimeDelegationInTransaction(
|
|
|
1083
1127
|
accountId: string,
|
|
1084
1128
|
incoming: SessionRealtimeInboundEntryInput,
|
|
1085
1129
|
entryId: string,
|
|
1130
|
+
modelContext: string | null,
|
|
1086
1131
|
): Promise<{ turnId: string; eventIds: string[]; wakeRevision: number }> {
|
|
1087
1132
|
const [session] = await db
|
|
1088
1133
|
.select()
|
|
@@ -1142,6 +1187,7 @@ async function admitRealtimeDelegationInTransaction(
|
|
|
1142
1187
|
operationKey: incoming.operationId,
|
|
1143
1188
|
delivery: "steer",
|
|
1144
1189
|
text: incoming.text!,
|
|
1190
|
+
modelContext,
|
|
1145
1191
|
messagePresentation: {
|
|
1146
1192
|
kind: "realtime_voice",
|
|
1147
1193
|
text: inputTranscript,
|
|
@@ -1359,6 +1405,7 @@ export async function syncSessionRealtimeLedgerInTransaction(
|
|
|
1359
1405
|
const payload = boundedPayload(incoming.payload);
|
|
1360
1406
|
const role = expectedRole(incoming);
|
|
1361
1407
|
const text = incoming.text ?? null;
|
|
1408
|
+
const modelContext = normalizedModelContext(incoming);
|
|
1362
1409
|
assertBoundedString(text, SESSION_REALTIME_LEDGER_MAX_TEXT_BYTES, "Realtime text");
|
|
1363
1410
|
if (
|
|
1364
1411
|
(incoming.kind === "user_transcript" || incoming.kind === "assistant_transcript") &&
|
|
@@ -1392,7 +1439,10 @@ export async function syncSessionRealtimeLedgerInTransaction(
|
|
|
1392
1439
|
)
|
|
1393
1440
|
.limit(1);
|
|
1394
1441
|
if (existing) {
|
|
1395
|
-
if (
|
|
1442
|
+
if (
|
|
1443
|
+
!inboundReplayMatches(existing, incoming, role, text, payload, modelContext) ||
|
|
1444
|
+
!(await delegationModelContextMatches(db, existing, modelContext))
|
|
1445
|
+
) {
|
|
1396
1446
|
throw new SessionRealtimeConflictError(
|
|
1397
1447
|
incoming.kind === "delegation_call"
|
|
1398
1448
|
? "REALTIME_DELEGATION_CHANGED"
|
|
@@ -1423,6 +1473,7 @@ export async function syncSessionRealtimeLedgerInTransaction(
|
|
|
1423
1473
|
textCodecVersion: LOSSLESS_CONTENT_CODEC_VERSION,
|
|
1424
1474
|
payload,
|
|
1425
1475
|
payloadCodecVersion: LOSSLESS_CONTENT_CODEC_VERSION,
|
|
1476
|
+
modelContext,
|
|
1426
1477
|
createdAt: now,
|
|
1427
1478
|
updatedAt: now,
|
|
1428
1479
|
})
|
|
@@ -1447,6 +1498,7 @@ export async function syncSessionRealtimeLedgerInTransaction(
|
|
|
1447
1498
|
modeRow.accountId,
|
|
1448
1499
|
incoming,
|
|
1449
1500
|
entry.id,
|
|
1501
|
+
modelContext,
|
|
1450
1502
|
);
|
|
1451
1503
|
const [linked] = await db
|
|
1452
1504
|
.update(schema.sessionRealtimeEntries)
|