@opengeni/db 0.23.0 → 0.27.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 (35) hide show
  1. package/dist/{chunk-3UCHDMKG.js → chunk-EX7CDSGH.js} +196 -2
  2. package/dist/chunk-EX7CDSGH.js.map +1 -0
  3. package/dist/{chunk-L6ADMZHE.js → chunk-IHPCI4GV.js} +5 -1
  4. package/dist/chunk-IHPCI4GV.js.map +1 -0
  5. package/dist/connection-token-resolver.d.ts +24 -2
  6. package/dist/index.d.ts +68 -2
  7. package/dist/index.js +1457 -338
  8. package/dist/index.js.map +1 -1
  9. package/dist/preference-registry.d.ts +14 -0
  10. package/dist/provision-roles.js +1 -1
  11. package/dist/runtime-posture.d.ts +3 -3
  12. package/dist/schema.d.ts +262 -4
  13. package/dist/schema.js +5 -1
  14. package/dist/session-realtime-mirror.d.ts +22 -1
  15. package/dist/workspace-instruction-policies-schema.d.ts +449 -0
  16. package/dist/workspace-instruction-policies.d.ts +88 -8
  17. package/drizzle/0165_document_authority_foundation.sql +259 -0
  18. package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
  19. package/drizzle/0167_document_index_replay_authority.sql +61 -0
  20. package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
  21. package/drizzle/0169_workspace_instruction_policy_onboarding_proposals.sql +202 -0
  22. package/package.json +3 -3
  23. package/src/connection-token-resolver.ts +79 -16
  24. package/src/index.ts +419 -23
  25. package/src/preference-registry.ts +103 -0
  26. package/src/runtime-posture.ts +4 -0
  27. package/src/schema.ts +86 -0
  28. package/src/session-control.ts +48 -1
  29. package/src/session-queue-commands.ts +45 -11
  30. package/src/session-realtime-mirror.ts +117 -1
  31. package/src/session-realtime.ts +49 -1
  32. package/src/workspace-instruction-policies-schema.ts +116 -0
  33. package/src/workspace-instruction-policies.ts +819 -25
  34. package/dist/chunk-3UCHDMKG.js.map +0 -1
  35. package/dist/chunk-L6ADMZHE.js.map +0 -1
package/src/schema.ts CHANGED
@@ -720,6 +720,45 @@ export const connections = pgTable(
720
720
  }),
721
721
  );
722
722
 
723
+ export const connectionDisconnectOperations = pgTable(
724
+ "connection_disconnect_operations",
725
+ {
726
+ id: uuid("id").primaryKey().defaultRandom(),
727
+ accountId: uuid("account_id")
728
+ .notNull()
729
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
730
+ workspaceId: uuid("workspace_id")
731
+ .notNull()
732
+ .references(() => workspaces.id, { onDelete: "cascade" }),
733
+ connectionId: uuid("connection_id")
734
+ .notNull()
735
+ .references(() => connections.id, { onDelete: "cascade" }),
736
+ subjectId: text("subject_id").notNull(),
737
+ idempotencyKey: text("idempotency_key").notNull(),
738
+ expectedVersion: integer("expected_version").notNull(),
739
+ resultVersion: integer("result_version").notNull(),
740
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
741
+ },
742
+ (table) => ({
743
+ workspaceSubjectKey: uniqueIndex("connection_disconnect_operations_subject_key_uq").on(
744
+ table.workspaceId,
745
+ table.subjectId,
746
+ table.idempotencyKey,
747
+ ),
748
+ connectionGeneration: uniqueIndex(
749
+ "connection_disconnect_operations_connection_generation_uq",
750
+ ).on(table.workspaceId, table.connectionId, table.expectedVersion),
751
+ identityValid: check(
752
+ "connection_disconnect_operations_identity_check",
753
+ sql`length(${table.subjectId}) between 1 and 512
754
+ and length(${table.idempotencyKey}) between 1 and 200
755
+ and ${table.idempotencyKey} = btrim(${table.idempotencyKey})
756
+ and ${table.expectedVersion} > 0
757
+ and ${table.resultVersion} = ${table.expectedVersion} + 1`,
758
+ ),
759
+ }),
760
+ );
761
+
723
762
  export const connectorActionPolicies = pgTable(
724
763
  "connector_action_policies",
725
764
  {
@@ -2150,6 +2189,11 @@ export const documents = pgTable(
2150
2189
  sourceUpdatedAt: timestamp("source_updated_at", { withTimezone: true }),
2151
2190
  sourceVersion: text("source_version"),
2152
2191
  aclTags: jsonb("acl_tags").$type<string[]>().notNull().default([]),
2192
+ // Durable authorization tuple. The workspace_id above remains ingestion
2193
+ // provenance; organization authority deliberately has no workspace owner.
2194
+ authorityKind: text("authority_kind").notNull().default("workspace"),
2195
+ authorityWorkspaceId: uuid("authority_workspace_id"),
2196
+ authoritySubjectId: text("authority_subject_id"),
2153
2197
  // Per-document access controls. visibility 'private' restricts human reads to
2154
2198
  // created_by (a grant subject id, not a uuid); agent_access=false hides the
2155
2199
  // document from agent retrieval surfaces (docs MCP) while humans keep REST.
@@ -2187,6 +2231,28 @@ export const documents = pgTable(
2187
2231
  table.workspaceId,
2188
2232
  table.curationStatus,
2189
2233
  ),
2234
+ authority: index("documents_authority_idx").on(
2235
+ table.accountId,
2236
+ table.authorityKind,
2237
+ table.authorityWorkspaceId,
2238
+ table.authoritySubjectId,
2239
+ table.status,
2240
+ ),
2241
+ authorityWorkspaceAccount: foreignKey({
2242
+ name: "documents_authority_workspace_fk",
2243
+ columns: [table.authorityWorkspaceId, table.accountId],
2244
+ foreignColumns: [workspaces.id, workspaces.accountId],
2245
+ }).onDelete("restrict"),
2246
+ authorityState: check(
2247
+ "documents_authority_chk",
2248
+ sql`(${table.authorityKind} = 'organization' and ${table.authorityWorkspaceId} is null and ${table.authoritySubjectId} is null)
2249
+ or (${table.authorityKind} = 'workspace' and ${table.authorityWorkspaceId} = ${table.workspaceId} and ${table.authoritySubjectId} is null)
2250
+ or (${table.authorityKind} = 'personal' and ${table.authorityWorkspaceId} = ${table.workspaceId} and nullif(btrim(${table.authoritySubjectId}), '') is not null and octet_length(convert_to(${table.authoritySubjectId}, 'UTF8')) <= 1024 and ${table.authoritySubjectId} = ${table.createdBy})`,
2251
+ ),
2252
+ authorityVisibility: check(
2253
+ "documents_authority_visibility_chk",
2254
+ sql`(${table.authorityKind} = 'personal') = (${table.visibility} = 'private')`,
2255
+ ),
2190
2256
  visibilityState: check(
2191
2257
  "documents_visibility_chk",
2192
2258
  sql`${table.visibility} in ('workspace', 'private')`,
@@ -2229,6 +2295,9 @@ export const documentChunks = pgTable(
2229
2295
  chunkIndex: integer("chunk_index").notNull(),
2230
2296
  text: text("text").notNull(),
2231
2297
  metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
2298
+ authorityKind: text("authority_kind").notNull().default("workspace"),
2299
+ authorityWorkspaceId: uuid("authority_workspace_id"),
2300
+ authoritySubjectId: text("authority_subject_id"),
2232
2301
  embedding: vector("embedding").notNull(),
2233
2302
  embeddingModel: text("embedding_model").notNull(),
2234
2303
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
@@ -2240,6 +2309,23 @@ export const documentChunks = pgTable(
2240
2309
  table.chunkIndex,
2241
2310
  ),
2242
2311
  base: index("document_chunks_workspace_base_idx").on(table.workspaceId, table.baseId),
2312
+ authority: index("document_chunks_authority_idx").on(
2313
+ table.accountId,
2314
+ table.authorityKind,
2315
+ table.authorityWorkspaceId,
2316
+ table.authoritySubjectId,
2317
+ ),
2318
+ authorityWorkspaceAccount: foreignKey({
2319
+ name: "document_chunks_authority_workspace_fk",
2320
+ columns: [table.authorityWorkspaceId, table.accountId],
2321
+ foreignColumns: [workspaces.id, workspaces.accountId],
2322
+ }).onDelete("restrict"),
2323
+ authorityState: check(
2324
+ "document_chunks_authority_chk",
2325
+ sql`(${table.authorityKind} = 'organization' and ${table.authorityWorkspaceId} is null and ${table.authoritySubjectId} is null)
2326
+ or (${table.authorityKind} = 'workspace' and ${table.authorityWorkspaceId} = ${table.workspaceId} and ${table.authoritySubjectId} is null)
2327
+ or (${table.authorityKind} = 'personal' and ${table.authorityWorkspaceId} = ${table.workspaceId} and nullif(btrim(${table.authoritySubjectId}), '') is not null and octet_length(convert_to(${table.authoritySubjectId}, 'UTF8')) <= 1024)`,
2328
+ ),
2243
2329
  }),
2244
2330
  );
2245
2331
 
@@ -10,6 +10,10 @@ import { and, eq, inArray, sql } from "drizzle-orm";
10
10
  import type { Database } from "./index";
11
11
  import * as schema from "./schema";
12
12
  import { closePendingSessionToolCallsInTransaction } from "./session-tool-call-settlement";
13
+ import {
14
+ mirrorSessionRealtimeContextInTransaction,
15
+ renderRealtimeHumanInputResponseContext,
16
+ } from "./session-realtime-mirror";
13
17
 
14
18
  export const SESSION_ANCESTRY_LIMIT = 10_000;
15
19
 
@@ -2088,7 +2092,12 @@ async function cancelSessionSubtreeInTransaction(
2088
2092
  }
2089
2093
  const immediatelyCancelledTurnIds = immediatelyCancelledTurns.map((turn) => turn.id);
2090
2094
  const now = new Date();
2091
- let cancelledHumanInputs: Array<{ id: string; sessionId: string; turnId: string }> = [];
2095
+ let cancelledHumanInputs: Array<{
2096
+ id: string;
2097
+ sessionId: string;
2098
+ turnId: string;
2099
+ questions: (typeof schema.sessionHumanInputRequests.$inferSelect)["questions"];
2100
+ }> = [];
2092
2101
  if (immediatelyCancelledTurnIds.length > 0) {
2093
2102
  await db
2094
2103
  .update(schema.sessionTurns)
@@ -2122,6 +2131,7 @@ async function cancelSessionSubtreeInTransaction(
2122
2131
  id: schema.sessionHumanInputRequests.id,
2123
2132
  sessionId: schema.sessionHumanInputRequests.sessionId,
2124
2133
  turnId: schema.sessionHumanInputRequests.turnId,
2134
+ questions: schema.sessionHumanInputRequests.questions,
2125
2135
  });
2126
2136
  await db
2127
2137
  .update(schema.codexCapacityWaiters)
@@ -2247,8 +2257,45 @@ async function cancelSessionSubtreeInTransaction(
2247
2257
  ? await db.insert(schema.sessionEvents).values(eventValues).returning({
2248
2258
  id: schema.sessionEvents.id,
2249
2259
  sequence: schema.sessionEvents.sequence,
2260
+ type: schema.sessionEvents.type,
2261
+ turnId: schema.sessionEvents.turnId,
2262
+ payload: schema.sessionEvents.payload,
2250
2263
  })
2251
2264
  : [];
2265
+ const cancelledHumanInputsById = new Map(
2266
+ cancelledHumanInputs
2267
+ .filter((request) => request.sessionId === session.id)
2268
+ .map((request) => [request.id, request]),
2269
+ );
2270
+ for (const event of inserted) {
2271
+ if (event.type !== "user.humanInputResponse") continue;
2272
+ const payload = event.payload as { requestId?: unknown };
2273
+ const request =
2274
+ typeof payload.requestId === "string"
2275
+ ? cancelledHumanInputsById.get(payload.requestId)
2276
+ : null;
2277
+ if (!request) continue;
2278
+ await mirrorSessionRealtimeContextInTransaction(db, {
2279
+ accountId: input.accountId,
2280
+ workspaceId: input.workspaceId,
2281
+ sessionId: session.id,
2282
+ sourceKind: "human_input_response",
2283
+ sourceId: event.id,
2284
+ turnId: event.turnId,
2285
+ channel: null,
2286
+ text: renderRealtimeHumanInputResponseContext({
2287
+ requestId: request.id,
2288
+ questions: request.questions,
2289
+ response: { outcome: "cancelled" },
2290
+ }),
2291
+ payload: {
2292
+ requestId: request.id,
2293
+ outcome: "cancelled",
2294
+ sourceEventId: event.id,
2295
+ },
2296
+ now,
2297
+ });
2298
+ }
2252
2299
  const liveTurnId =
2253
2300
  session.activeTurnId && liveTurnIds.has(session.activeTurnId) ? session.activeTurnId : null;
2254
2301
  await db
@@ -35,6 +35,7 @@ import { sessionRealtimeIsActiveInTransaction } from "./session-realtime-state";
35
35
  import {
36
36
  mirrorSessionRealtimeContextInTransaction,
37
37
  renderRealtimeHumanInputContext,
38
+ renderRealtimeHumanInputResponseContext,
38
39
  } from "./session-realtime-mirror";
39
40
  import * as schema from "./schema";
40
41
  import {
@@ -284,23 +285,56 @@ export async function supersedeSessionCurrentDirectionInTransaction(
284
285
  eq(schema.sessionHumanInputRequests.status, "pending"),
285
286
  ),
286
287
  )
287
- .returning({ id: schema.sessionHumanInputRequests.id });
288
+ .returning({
289
+ id: schema.sessionHumanInputRequests.id,
290
+ questions: schema.sessionHumanInputRequests.questions,
291
+ });
288
292
  let lastSequence = closedTools.sequence;
289
293
  if (cancelledHumanInputs.length > 0) {
290
- await db.insert(schema.sessionEvents).values(
291
- cancelledHumanInputs.map((request) => ({
294
+ const cancelledHumanInputEvents = await db
295
+ .insert(schema.sessionEvents)
296
+ .values(
297
+ cancelledHumanInputs.map((request) => ({
298
+ accountId: input.accountId,
299
+ workspaceId: input.workspaceId,
300
+ sessionId: input.sessionId,
301
+ sequence: ++lastSequence,
302
+ type: "user.humanInputResponse",
303
+ turnId: current.id,
304
+ turnGeneration: current.executionGeneration,
305
+ turnAssociation: "current",
306
+ payload: { requestId: request.id, response: { outcome: "cancelled" } },
307
+ occurredAt: now,
308
+ })),
309
+ )
310
+ .returning();
311
+ const requestsById = new Map(cancelledHumanInputs.map((request) => [request.id, request]));
312
+ for (const event of cancelledHumanInputEvents) {
313
+ const payload = event.payload as { requestId?: unknown };
314
+ const request =
315
+ typeof payload.requestId === "string" ? requestsById.get(payload.requestId) : null;
316
+ if (!request) continue;
317
+ await mirrorSessionRealtimeContextInTransaction(db, {
292
318
  accountId: input.accountId,
293
319
  workspaceId: input.workspaceId,
294
320
  sessionId: input.sessionId,
295
- sequence: ++lastSequence,
296
- type: "user.humanInputResponse",
321
+ sourceKind: "human_input_response",
322
+ sourceId: event.id,
297
323
  turnId: current.id,
298
- turnGeneration: current.executionGeneration,
299
- turnAssociation: "current",
300
- payload: { requestId: request.id, response: { outcome: "cancelled" } },
301
- occurredAt: now,
302
- })),
303
- );
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
+ }
304
338
  }
305
339
  await db
306
340
  .update(schema.sessionTurns)
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
+ import type { HumanInputQuestion, HumanInputResponse } from "@opengeni/contracts";
3
4
  import { and, desc, eq, gt, sql } from "drizzle-orm";
4
5
 
5
6
  import { sanitizeEventPayload } from "./event-payload-sanitizer";
@@ -16,7 +17,12 @@ export type MirrorSessionRealtimeContextInput = {
16
17
  accountId: string;
17
18
  workspaceId: string;
18
19
  sessionId: string;
19
- sourceKind: "human_input" | "assistant_progress" | "assistant_terminal";
20
+ sourceKind:
21
+ | "human_input"
22
+ | "human_input_request"
23
+ | "human_input_response"
24
+ | "assistant_progress"
25
+ | "assistant_terminal";
20
26
  sourceId: string;
21
27
  text: string;
22
28
  channel: SessionRealtimeMirrorChannel;
@@ -155,6 +161,116 @@ export function renderRealtimeHumanInputContext(input: {
155
161
  ].join("\n");
156
162
  }
157
163
 
164
+ export type RealtimeHumanInputRequestContext = {
165
+ id: string;
166
+ questions: HumanInputQuestion[];
167
+ allowSkip: boolean;
168
+ expiresAt?: Date | string | null;
169
+ };
170
+
171
+ /**
172
+ * Render the exact durable question contract for the conversational surface.
173
+ * The structured form remains authoritative, while a conversational answer is
174
+ * ordinary new user input that the realtime model delegates once with enough
175
+ * question context for the next underlying turn.
176
+ */
177
+ export function renderRealtimeHumanInputRequestContext(input: {
178
+ requests: RealtimeHumanInputRequestContext[];
179
+ }): string {
180
+ const lines = ["<session_human_input_request>", " <status>waiting_for_user</status>"];
181
+ for (const request of input.requests) {
182
+ lines.push(
183
+ " <request>",
184
+ ` <id>${escapeXmlText(request.id)}</id>`,
185
+ ` <allow_skip>${String(request.allowSkip)}</allow_skip>`,
186
+ ` <expires_at>${escapeXmlText(renderExpiry(request.expiresAt))}</expires_at>`,
187
+ );
188
+ for (const question of request.questions) {
189
+ lines.push(
190
+ " <question>",
191
+ ` <id>${escapeXmlText(question.id)}</id>`,
192
+ ` <kind>${escapeXmlText(question.kind)}</kind>`,
193
+ ` <required>${String(question.required)}</required>`,
194
+ );
195
+ if (question.label) lines.push(` <label>${escapeXmlText(question.label)}</label>`);
196
+ lines.push(` <prompt>${escapeXmlText(question.prompt)}</prompt>`);
197
+ if (question.helpText) {
198
+ lines.push(` <help_text>${escapeXmlText(question.helpText)}</help_text>`);
199
+ }
200
+ if (question.options.length > 0) {
201
+ lines.push(" <options>");
202
+ for (const option of question.options) {
203
+ lines.push(
204
+ " <option>",
205
+ ` <id>${escapeXmlText(option.id)}</id>`,
206
+ ` <label>${escapeXmlText(option.label)}</label>`,
207
+ );
208
+ if (option.description) {
209
+ lines.push(` <description>${escapeXmlText(option.description)}</description>`);
210
+ }
211
+ lines.push(" </option>");
212
+ }
213
+ lines.push(" </options>");
214
+ }
215
+ lines.push(
216
+ ` <allow_other>${String(question.allowOther)}</allow_other>`,
217
+ " </question>",
218
+ );
219
+ }
220
+ lines.push(" </request>");
221
+ }
222
+ lines.push(
223
+ " <instruction>The current work is waiting for the user's input. Ask the questions naturally, one at a time when useful, without changing their meaning or answering for the user. The user may answer in the visible form or answer conversationally here. If the user answers here, delegate exactly once with a complete message containing the relevant question and the user's answer. If the user changes direction instead, delegate that new direction normally. Do not claim the work resumed until a later session update confirms it.</instruction>",
224
+ "</session_human_input_request>",
225
+ );
226
+ return lines.join("\n");
227
+ }
228
+
229
+ export function renderRealtimeHumanInputResponseContext(input: {
230
+ requestId: string;
231
+ questions: HumanInputQuestion[];
232
+ response: HumanInputResponse;
233
+ }): string {
234
+ const lines = [
235
+ "<session_human_input_response>",
236
+ ` <request_id>${escapeXmlText(input.requestId)}</request_id>`,
237
+ ` <outcome>${escapeXmlText(input.response.outcome)}</outcome>`,
238
+ ];
239
+ if (input.response.outcome === "answered") {
240
+ const questions = new Map(input.questions.map((question) => [question.id, question]));
241
+ lines.push(" <answers>");
242
+ for (const answer of input.response.answers) {
243
+ const question = questions.get(answer.questionId);
244
+ const optionLabels = new Map(
245
+ question?.options.map((option) => [option.id, option.label]) ?? [],
246
+ );
247
+ lines.push(
248
+ " <answer>",
249
+ ` <question_id>${escapeXmlText(answer.questionId)}</question_id>`,
250
+ );
251
+ if (question) lines.push(` <question>${escapeXmlText(question.prompt)}</question>`);
252
+ for (const value of answer.values) {
253
+ lines.push(` <value>${escapeXmlText(optionLabels.get(value) ?? value)}</value>`);
254
+ }
255
+ if (answer.other) lines.push(` <other>${escapeXmlText(answer.other)}</other>`);
256
+ lines.push(" </answer>");
257
+ }
258
+ lines.push(" </answers>");
259
+ }
260
+ lines.push(
261
+ input.response.outcome === "answered" || input.response.outcome === "skipped"
262
+ ? " <instruction>This structured response was accepted through the session UI and the same work can resume. Treat it as authoritative user context, do not delegate it again, and acknowledge briefly only if useful.</instruction>"
263
+ : " <instruction>This pending question is no longer active. Do not ask it again unless the user raises it.</instruction>",
264
+ "</session_human_input_response>",
265
+ );
266
+ return lines.join("\n");
267
+ }
268
+
269
+ function renderExpiry(value: Date | string | null | undefined): string {
270
+ if (value === null || value === undefined) return "none";
271
+ return value instanceof Date ? value.toISOString() : value;
272
+ }
273
+
158
274
  function escapeXmlText(value: string): string {
159
275
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
160
276
  }
@@ -6,7 +6,7 @@ import type {
6
6
  SessionRealtimeMode,
7
7
  SessionRealtimeModel,
8
8
  } from "@opengeni/contracts";
9
- import { and, asc, eq, inArray } from "drizzle-orm";
9
+ import { and, asc, eq, gt, inArray, isNull, or } from "drizzle-orm";
10
10
 
11
11
  import { sanitizeEventPayload } from "./event-payload-sanitizer";
12
12
  import type { Database } from "./index";
@@ -16,6 +16,10 @@ import {
16
16
  lockSessionEventWriteRows,
17
17
  registerSessionWorkflowWakeInTransaction,
18
18
  } from "./session-control";
19
+ import {
20
+ mirrorSessionRealtimeContextInTransaction,
21
+ renderRealtimeHumanInputRequestContext,
22
+ } from "./session-realtime-mirror";
19
23
  import * as schema from "./schema";
20
24
 
21
25
  export { sessionRealtimeIsActiveInTransaction } from "./session-realtime-state";
@@ -445,6 +449,50 @@ export async function beginSessionRealtimeInTransaction(
445
449
  now,
446
450
  );
447
451
  eventIds.push(event.id);
452
+ const pendingHumanInputs = await db
453
+ .select({
454
+ id: schema.sessionHumanInputRequests.id,
455
+ turnId: schema.sessionHumanInputRequests.turnId,
456
+ questions: schema.sessionHumanInputRequests.questions,
457
+ allowSkip: schema.sessionHumanInputRequests.allowSkip,
458
+ expiresAt: schema.sessionHumanInputRequests.expiresAt,
459
+ })
460
+ .from(schema.sessionHumanInputRequests)
461
+ .where(
462
+ and(
463
+ eq(schema.sessionHumanInputRequests.accountId, input.accountId),
464
+ eq(schema.sessionHumanInputRequests.workspaceId, input.workspaceId),
465
+ eq(schema.sessionHumanInputRequests.sessionId, input.sessionId),
466
+ eq(schema.sessionHumanInputRequests.status, "pending"),
467
+ or(
468
+ isNull(schema.sessionHumanInputRequests.expiresAt),
469
+ gt(schema.sessionHumanInputRequests.expiresAt, now),
470
+ ),
471
+ ),
472
+ )
473
+ .orderBy(
474
+ asc(schema.sessionHumanInputRequests.createdAt),
475
+ asc(schema.sessionHumanInputRequests.id),
476
+ );
477
+ if (pendingHumanInputs.length > 0) {
478
+ const sourceTurnIds = new Set(pendingHumanInputs.map((request) => request.turnId));
479
+ await mirrorSessionRealtimeContextInTransaction(db, {
480
+ accountId: input.accountId,
481
+ workspaceId: input.workspaceId,
482
+ sessionId: input.sessionId,
483
+ sourceKind: "human_input_request",
484
+ sourceId: `startup:${pendingHumanInputs.map((request) => request.id).join(":")}`,
485
+ turnId: sourceTurnIds.size === 1 ? pendingHumanInputs[0]!.turnId : null,
486
+ channel: "speakable",
487
+ text: renderRealtimeHumanInputRequestContext({ requests: pendingHumanInputs }),
488
+ payload: {
489
+ status: "waiting_for_user",
490
+ requestIds: pendingHumanInputs.map((request) => request.id),
491
+ trigger: "realtime_start",
492
+ },
493
+ now,
494
+ });
495
+ }
448
496
  return {
449
497
  mode: mapRealtimeMode(row),
450
498
  replay: false,
@@ -21,6 +21,8 @@ export const workspaceInstructionPolicyRevisions = pgTable(
21
21
  "workspace_instruction_policy_revisions",
22
22
  {
23
23
  id: uuid("id").primaryKey().defaultRandom(),
24
+ operationId: uuid("operation_id"),
25
+ requestFingerprint: text("request_fingerprint"),
24
26
  accountId: uuid("account_id").notNull(),
25
27
  workspaceId: uuid("workspace_id").notNull(),
26
28
  revision: bigint("revision", { mode: "number" })
@@ -38,6 +40,19 @@ export const workspaceInstructionPolicyRevisions = pgTable(
38
40
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
39
41
  },
40
42
  (table) => ({
43
+ workspaceOperation: uniqueIndex("workspace_instruction_policy_revisions_workspace_operation_uq")
44
+ .on(table.workspaceId, table.operationId)
45
+ .where(sql`${table.operationId} is not null`),
46
+ operationReceipt: check(
47
+ "workspace_instruction_policy_revisions_operation_receipt_chk",
48
+ sql`(
49
+ (${table.operationId} is null and ${table.requestFingerprint} is null)
50
+ or (
51
+ ${table.operationId} is not null
52
+ and ${table.requestFingerprint} ~ '^[0-9a-f]{64}$'
53
+ )
54
+ )`,
55
+ ),
41
56
  workspaceRevision: uniqueIndex(
42
57
  "workspace_instruction_policy_revisions_workspace_revision_uq",
43
58
  ).on(table.workspaceId, table.revision),
@@ -99,6 +114,8 @@ export const workspaceInstructionPolicyActivationEvents = pgTable(
99
114
  "workspace_instruction_policy_activation_events",
100
115
  {
101
116
  id: uuid("id").primaryKey().defaultRandom(),
117
+ operationId: uuid("operation_id"),
118
+ requestFingerprint: text("request_fingerprint"),
102
119
  accountId: uuid("account_id").notNull(),
103
120
  workspaceId: uuid("workspace_id").notNull(),
104
121
  kind: text("kind").notNull(),
@@ -117,6 +134,19 @@ export const workspaceInstructionPolicyActivationEvents = pgTable(
117
134
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
118
135
  },
119
136
  (table) => ({
137
+ workspaceOperation: uniqueIndex("workspace_instruction_policy_events_workspace_operation_uq")
138
+ .on(table.workspaceId, table.operationId)
139
+ .where(sql`${table.operationId} is not null`),
140
+ operationReceipt: check(
141
+ "workspace_instruction_policy_events_operation_receipt_chk",
142
+ sql`(
143
+ (${table.operationId} is null and ${table.requestFingerprint} is null)
144
+ or (
145
+ ${table.operationId} is not null
146
+ and ${table.requestFingerprint} ~ '^[0-9a-f]{64}$'
147
+ )
148
+ )`,
149
+ ),
120
150
  workspaceActivationVersion: uniqueIndex(
121
151
  "workspace_instruction_policy_events_target_version_uq",
122
152
  ).on(
@@ -142,6 +172,92 @@ export const workspaceInstructionPolicyActivationEvents = pgTable(
142
172
  }),
143
173
  );
144
174
 
175
+ export const workspaceInstructionPolicyOnboardingProposals = pgTable(
176
+ "workspace_instruction_policy_onboarding_proposals",
177
+ {
178
+ id: uuid("id").primaryKey().defaultRandom(),
179
+ operationId: uuid("operation_id").notNull(),
180
+ requestFingerprint: text("request_fingerprint").notNull(),
181
+ accountId: uuid("account_id").notNull(),
182
+ workspaceId: uuid("workspace_id").notNull(),
183
+ kind: text("kind").notNull(),
184
+ scope: text("scope").notNull(),
185
+ roleKey: text("role_key"),
186
+ sourceId: text("source_id").notNull(),
187
+ sourceVersion: text("source_version").notNull(),
188
+ confidenceBps: integer("confidence_bps").notNull(),
189
+ baselineRevisionId: uuid("baseline_revision_id"),
190
+ baselineRevision: bigint("baseline_revision", { mode: "number" }),
191
+ baselineContentHash: text("baseline_content_hash"),
192
+ baselineActivationVersion: bigint("baseline_activation_version", { mode: "number" }).notNull(),
193
+ baselineActivatedAt: timestamp("baseline_activated_at", { withTimezone: true }),
194
+ draftRevisionId: uuid("draft_revision_id").notNull(),
195
+ draftRevision: bigint("draft_revision", { mode: "number" }).notNull(),
196
+ draftContentHash: text("draft_content_hash").notNull(),
197
+ status: text("status").notNull().default("proposed"),
198
+ createdBySubjectId: text("created_by_subject_id").notNull(),
199
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
200
+ },
201
+ (table) => ({
202
+ workspaceOperation: uniqueIndex(
203
+ "workspace_instruction_policy_onboarding_proposals_workspace_operation_uq",
204
+ ).on(table.workspaceId, table.operationId),
205
+ sourceVersionTarget: uniqueIndex(
206
+ "workspace_instruction_policy_onboarding_proposals_source_version_target_uq",
207
+ ).on(
208
+ table.workspaceId,
209
+ table.kind,
210
+ table.scope,
211
+ sql`coalesce(${table.roleKey}, '')`,
212
+ table.sourceId,
213
+ table.sourceVersion,
214
+ ),
215
+ workspaceTimeline: index(
216
+ "workspace_instruction_policy_onboarding_proposals_workspace_time_idx",
217
+ ).on(table.workspaceId, table.createdAt, table.id),
218
+ operationReceipt: check(
219
+ "workspace_instruction_policy_onboarding_proposals_operation_receipt_chk",
220
+ sql`${table.requestFingerprint} ~ '^[0-9a-f]{64}$'`,
221
+ ),
222
+ target: check(
223
+ "workspace_instruction_policy_onboarding_proposals_target_chk",
224
+ sql`(
225
+ (${table.kind} = 'charter' and ${table.scope} = 'global' and ${table.roleKey} is null)
226
+ or (${table.kind} = 'policy' and ${table.scope} = 'global' and ${table.roleKey} is null)
227
+ or (${table.kind} = 'policy' and ${table.scope} = 'role' and ${table.roleKey} is not null)
228
+ )`,
229
+ ),
230
+ source: check(
231
+ "workspace_instruction_policy_onboarding_proposals_source_chk",
232
+ sql`length(btrim(${table.sourceId})) between 1 and 512
233
+ and length(btrim(${table.sourceVersion})) between 1 and 256
234
+ and ${table.confidenceBps} between 0 and 10000`,
235
+ ),
236
+ baseline: check(
237
+ "workspace_instruction_policy_onboarding_proposals_baseline_chk",
238
+ sql`(
239
+ ${table.baselineRevisionId} is null
240
+ and ${table.baselineRevision} is null
241
+ and ${table.baselineContentHash} is null
242
+ and ${table.baselineActivationVersion} = 0
243
+ and ${table.baselineActivatedAt} is null
244
+ ) or (
245
+ ${table.baselineRevisionId} is not null
246
+ and ${table.baselineRevision} > 0
247
+ and ${table.baselineContentHash} ~ '^[0-9a-f]{64}$'
248
+ and ${table.baselineActivationVersion} > 0
249
+ and ${table.baselineActivatedAt} is not null
250
+ )`,
251
+ ),
252
+ draft: check(
253
+ "workspace_instruction_policy_onboarding_proposals_draft_chk",
254
+ sql`${table.draftRevision} > 0
255
+ and ${table.draftContentHash} ~ '^[0-9a-f]{64}$'
256
+ and ${table.status} = 'proposed'`,
257
+ ),
258
+ }),
259
+ );
260
+
145
261
  export const workspaceInstructionPolicySnapshots = pgTable(
146
262
  "workspace_instruction_policy_snapshots",
147
263
  {