@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
@@ -1,5 +1,6 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import {
3
+ PreferenceRegistrySnapshot,
3
4
  ResolvedWorkspaceInstructionPolicySnapshot,
4
5
  WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS,
5
6
  WorkspaceInstructionPolicySnapshot,
@@ -14,19 +15,23 @@ import type {
14
15
  WorkspaceInstructionPolicyKind,
15
16
  WorkspaceInstructionPolicyListQuery,
16
17
  WorkspaceInstructionPolicyListResponse,
18
+ WorkspaceInstructionPolicyOnboardingProposal,
19
+ WorkspaceInstructionPolicyOnboardingProposalListQuery,
20
+ WorkspaceInstructionPolicyOnboardingProposalListResponse,
17
21
  WorkspaceInstructionPolicyProvenanceSource,
18
22
  WorkspaceInstructionPolicyRevision,
19
23
  WorkspaceInstructionPolicyScope,
20
24
  WorkspaceInstructionPolicySnapshotEntry,
21
25
  WorkspaceInstructionPolicyTarget,
22
26
  } from "@opengeni/contracts";
23
- import { and, asc, desc, eq, inArray, isNull, lt, sql, type SQL } from "drizzle-orm";
27
+ import { and, asc, desc, eq, inArray, isNull, lt, or, sql, type SQL } from "drizzle-orm";
24
28
  import type { Database } from "./index";
25
- import { withRlsContext, withWorkspaceRls } from "./index";
29
+ import { withRlsContext, withWorkspaceRls, withWorkspaceSubjectRls } from "./index";
26
30
  import { nestedPostgresSqlState } from "./persistence-errors";
27
31
  import * as schema from "./schema";
28
32
 
29
- type DraftInput = WorkspaceInstructionPolicyTarget & {
33
+ type DraftRequestInput = WorkspaceInstructionPolicyTarget & {
34
+ operationId: string;
30
35
  accountId: string;
31
36
  workspaceId: string;
32
37
  content: string;
@@ -36,10 +41,52 @@ type DraftInput = WorkspaceInstructionPolicyTarget & {
36
41
  createdBySubjectId: string;
37
42
  };
38
43
 
39
- type CallerDraftInput = Omit<DraftInput, "provenanceSource"> & {
44
+ type DraftInput = DraftRequestInput & {
45
+ requestFingerprint: string;
46
+ };
47
+
48
+ type CallerDraftInput = Omit<DraftRequestInput, "operationId" | "provenanceSource"> & {
49
+ operationId?: string;
40
50
  provenanceSource: WorkspaceInstructionPolicyDraftProvenanceSource;
41
51
  };
42
52
 
53
+ type ImportLegacyRequestInput = {
54
+ operationId: string;
55
+ accountId: string;
56
+ workspaceId: string;
57
+ createdBySubjectId: string;
58
+ supersedesRevisionId: string | null;
59
+ };
60
+
61
+ type ActiveRevisionInput = {
62
+ operationId: string;
63
+ accountId: string;
64
+ workspaceId: string;
65
+ targetRevisionId: string;
66
+ expectedCurrentRevisionId: string | null;
67
+ expectedActivationVersion?: number;
68
+ actorSubjectId: string;
69
+ reason: string;
70
+ type: WorkspaceInstructionPolicyActivationType;
71
+ };
72
+
73
+ type OnboardingProposalRequestInput = WorkspaceInstructionPolicyTarget & {
74
+ operationId: string;
75
+ accountId: string;
76
+ workspaceId: string;
77
+ content: string;
78
+ sourceId: string;
79
+ sourceVersion: string;
80
+ confidenceBps: number;
81
+ expectedCurrentRevisionId: string | null;
82
+ expectedActivationVersion: number;
83
+ createdBySubjectId: string;
84
+ };
85
+
86
+ type OnboardingProposalInput = OnboardingProposalRequestInput & {
87
+ requestFingerprint: string;
88
+ };
89
+
43
90
  export class WorkspaceInstructionPolicyConflictError extends Error {
44
91
  readonly name = "WorkspaceInstructionPolicyConflictError";
45
92
  readonly code = "WORKSPACE_INSTRUCTION_POLICY_CONFLICT";
@@ -49,6 +96,15 @@ export class WorkspaceInstructionPolicyConflictError extends Error {
49
96
  }
50
97
  }
51
98
 
99
+ export class WorkspaceInstructionPolicyOperationReuseError extends Error {
100
+ readonly name = "WorkspaceInstructionPolicyOperationReuseError";
101
+ readonly code = "WORKSPACE_INSTRUCTION_POLICY_OPERATION_REUSED";
102
+
103
+ constructor() {
104
+ super("The workspace instruction-policy operation id was already used for another request");
105
+ }
106
+ }
107
+
52
108
  export class WorkspaceInstructionPolicyNotFoundError extends Error {
53
109
  readonly name = "WorkspaceInstructionPolicyNotFoundError";
54
110
 
@@ -69,6 +125,43 @@ export class WorkspaceInstructionPolicyLegacyUnavailableError extends Error {
69
125
  }
70
126
  }
71
127
 
128
+ export class WorkspaceInstructionPolicyOnboardingProposalContentError extends Error {
129
+ readonly name = "WorkspaceInstructionPolicyOnboardingProposalContentError";
130
+
131
+ constructor(
132
+ readonly code:
133
+ | "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_EMPTY"
134
+ | "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_OVERSIZED",
135
+ ) {
136
+ super(
137
+ code === "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_EMPTY"
138
+ ? "An onboarding proposal must contain non-blank instruction-policy content"
139
+ : `An onboarding proposal must not exceed ${WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters`,
140
+ );
141
+ }
142
+ }
143
+
144
+ export class WorkspaceInstructionPolicyOnboardingProposalStaleError extends Error {
145
+ readonly name = "WorkspaceInstructionPolicyOnboardingProposalStaleError";
146
+ readonly code = "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_STALE";
147
+
148
+ constructor(readonly currentHead: WorkspaceInstructionPolicyHead | null) {
149
+ super("The onboarding proposal was prepared against a stale active policy baseline");
150
+ }
151
+ }
152
+
153
+ export class WorkspaceInstructionPolicyOnboardingProposalConflictError extends Error {
154
+ readonly name = "WorkspaceInstructionPolicyOnboardingProposalConflictError";
155
+ readonly code = "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_CONFLICT";
156
+
157
+ constructor(
158
+ readonly existingProposalId: string,
159
+ readonly existingDraftRevisionId: string,
160
+ ) {
161
+ super("This onboarding source version already proposed a draft for the policy target");
162
+ }
163
+ }
164
+
72
165
  export class WorkspaceInstructionPolicySnapshotAuthorityError extends Error {
73
166
  readonly name = "WorkspaceInstructionPolicySnapshotAuthorityError";
74
167
  }
@@ -82,21 +175,258 @@ export type WorkspaceInstructionPolicyAttemptClaims = {
82
175
  executionGeneration: number;
83
176
  };
84
177
 
178
+ export type WorkspaceStateAcceptedAttemptGovernance = {
179
+ attemptId: string;
180
+ executionGeneration: number;
181
+ acceptedAt: string;
182
+ policySnapshot: WorkspaceInstructionPolicySnapshot | null;
183
+ preferenceSnapshot: {
184
+ id: string;
185
+ descriptorHash: string;
186
+ descriptors: Array<{
187
+ id: string;
188
+ revisionId: string;
189
+ contentHash: string;
190
+ activeVersion: number;
191
+ scope: "organization" | "workspace" | "user";
192
+ }>;
193
+ truncated: boolean;
194
+ createdAt: string;
195
+ } | null;
196
+ };
197
+
85
198
  function contentHash(content: string): string {
86
199
  return createHash("sha256").update(content, "utf8").digest("hex");
87
200
  }
88
201
 
202
+ type OperationFingerprintField = readonly [
203
+ name: string,
204
+ present: boolean,
205
+ value: string | number | null,
206
+ ];
207
+
208
+ function hasOwnField(value: object, field: PropertyKey): boolean {
209
+ return Object.prototype.hasOwnProperty.call(value, field);
210
+ }
211
+
212
+ function operationRequestFingerprint(
213
+ operation: string,
214
+ fields: readonly OperationFingerprintField[],
215
+ ): string {
216
+ const canonicalRequest = JSON.stringify([
217
+ "workspace_instruction_policy_operation",
218
+ 1,
219
+ operation,
220
+ fields,
221
+ ]);
222
+ return createHash("sha256").update(canonicalRequest, "utf8").digest("hex");
223
+ }
224
+
225
+ function draftRequestFingerprint(input: DraftRequestInput): string {
226
+ return operationRequestFingerprint("create_draft", [
227
+ ["accountId", true, input.accountId],
228
+ ["workspaceId", true, input.workspaceId],
229
+ ["kind", true, input.kind],
230
+ ["scope", true, input.scope],
231
+ ["roleKey", true, input.roleKey],
232
+ ["content", true, input.content],
233
+ ["provenanceSource", true, input.provenanceSource],
234
+ ["provenanceSourceId", true, input.provenanceSourceId],
235
+ ["supersedesRevisionId", true, input.supersedesRevisionId],
236
+ ["createdBySubjectId", true, input.createdBySubjectId],
237
+ ]);
238
+ }
239
+
240
+ function importLegacyRequestFingerprint(input: ImportLegacyRequestInput): string {
241
+ return operationRequestFingerprint("import_legacy", [
242
+ ["accountId", true, input.accountId],
243
+ ["workspaceId", true, input.workspaceId],
244
+ ["createdBySubjectId", true, input.createdBySubjectId],
245
+ ["supersedesRevisionId", true, input.supersedesRevisionId],
246
+ ]);
247
+ }
248
+
249
+ function activeRevisionRequestFingerprint(input: ActiveRevisionInput): string {
250
+ const expectedCurrentRevisionIdPresent = hasOwnField(input, "expectedCurrentRevisionId");
251
+ const expectedActivationVersionPresent = hasOwnField(input, "expectedActivationVersion");
252
+ return operationRequestFingerprint(`change_active_revision:${input.type}`, [
253
+ ["accountId", true, input.accountId],
254
+ ["workspaceId", true, input.workspaceId],
255
+ ["targetRevisionId", true, input.targetRevisionId],
256
+ [
257
+ "expectedCurrentRevisionId",
258
+ expectedCurrentRevisionIdPresent,
259
+ input.expectedCurrentRevisionId ?? null,
260
+ ],
261
+ [
262
+ "expectedActivationVersion",
263
+ expectedActivationVersionPresent,
264
+ input.expectedActivationVersion ?? null,
265
+ ],
266
+ ["actorSubjectId", true, input.actorSubjectId],
267
+ ["reason", true, input.reason],
268
+ ]);
269
+ }
270
+
271
+ function onboardingProposalRequestFingerprint(input: OnboardingProposalRequestInput): string {
272
+ return operationRequestFingerprint("create_onboarding_proposal", [
273
+ ["accountId", true, input.accountId],
274
+ ["workspaceId", true, input.workspaceId],
275
+ ["kind", true, input.kind],
276
+ ["scope", true, input.scope],
277
+ ["roleKey", true, input.roleKey],
278
+ ["content", true, input.content],
279
+ ["sourceId", true, input.sourceId],
280
+ ["sourceVersion", true, input.sourceVersion],
281
+ ["confidenceBps", true, input.confidenceBps],
282
+ ["expectedCurrentRevisionId", true, input.expectedCurrentRevisionId],
283
+ ["expectedActivationVersion", true, input.expectedActivationVersion],
284
+ ["createdBySubjectId", true, input.createdBySubjectId],
285
+ ]);
286
+ }
287
+
89
288
  function iso(value: Date | string): string {
90
289
  return (value instanceof Date ? value : new Date(value)).toISOString();
91
290
  }
92
291
 
292
+ /**
293
+ * Inspect one immutable accepted attempt only when the authenticated caller is
294
+ * exactly its frozen initiating human. The null result deliberately conflates
295
+ * absence, cross-account/workspace lookup, and another subject's attempt.
296
+ */
297
+ export async function getWorkspaceStateAcceptedAttemptGovernance(
298
+ db: Database,
299
+ input: { accountId: string; workspaceId: string; subjectId: string; attemptId: string },
300
+ ): Promise<WorkspaceStateAcceptedAttemptGovernance | null> {
301
+ return await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (scopedDb) => {
302
+ const initiatingSubject = sql<string>`coalesce(
303
+ ${schema.sessionTurns.initiatingHumanSubjectId},
304
+ case when ${schema.sessionTurns.initiatorKind} = 'subject'
305
+ then ${schema.sessionTurns.initiatorSubjectId}
306
+ end
307
+ )`;
308
+ const [attempt] = await scopedDb
309
+ .select({
310
+ attemptId: schema.sessionTurnAttempts.id,
311
+ executionGeneration: schema.sessionTurnAttempts.executionGeneration,
312
+ acceptedAt: schema.sessionTurns.createdAt,
313
+ })
314
+ .from(schema.sessionTurnAttempts)
315
+ .innerJoin(
316
+ schema.sessionTurns,
317
+ and(
318
+ eq(schema.sessionTurns.accountId, schema.sessionTurnAttempts.accountId),
319
+ eq(schema.sessionTurns.workspaceId, schema.sessionTurnAttempts.workspaceId),
320
+ eq(schema.sessionTurns.sessionId, schema.sessionTurnAttempts.sessionId),
321
+ eq(schema.sessionTurns.id, schema.sessionTurnAttempts.turnId),
322
+ ),
323
+ )
324
+ .where(
325
+ and(
326
+ eq(schema.sessionTurnAttempts.accountId, input.accountId),
327
+ eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
328
+ eq(schema.sessionTurnAttempts.id, input.attemptId),
329
+ eq(initiatingSubject, input.subjectId),
330
+ ),
331
+ )
332
+ .limit(1);
333
+ if (!attempt) return null;
334
+
335
+ const [policyRows, preferenceRows] = await Promise.all([
336
+ scopedDb
337
+ .select()
338
+ .from(schema.workspaceInstructionPolicySnapshots)
339
+ .where(
340
+ and(
341
+ eq(schema.workspaceInstructionPolicySnapshots.accountId, input.accountId),
342
+ eq(schema.workspaceInstructionPolicySnapshots.workspaceId, input.workspaceId),
343
+ eq(schema.workspaceInstructionPolicySnapshots.attemptId, input.attemptId),
344
+ eq(
345
+ schema.workspaceInstructionPolicySnapshots.executionGeneration,
346
+ attempt.executionGeneration,
347
+ ),
348
+ ),
349
+ )
350
+ .limit(1),
351
+ scopedDb
352
+ .select()
353
+ .from(schema.preferenceRegistrySnapshots)
354
+ .where(
355
+ and(
356
+ eq(schema.preferenceRegistrySnapshots.accountId, input.accountId),
357
+ eq(schema.preferenceRegistrySnapshots.workspaceId, input.workspaceId),
358
+ eq(schema.preferenceRegistrySnapshots.attemptId, input.attemptId),
359
+ eq(schema.preferenceRegistrySnapshots.executionGeneration, attempt.executionGeneration),
360
+ eq(schema.preferenceRegistrySnapshots.initiatingHumanSubjectId, input.subjectId),
361
+ ),
362
+ )
363
+ .limit(1),
364
+ ]);
365
+ const policyRow = policyRows[0] ?? null;
366
+ const preferenceRow = preferenceRows[0] ?? null;
367
+ return {
368
+ attemptId: attempt.attemptId,
369
+ executionGeneration: attempt.executionGeneration,
370
+ acceptedAt: iso(attempt.acceptedAt),
371
+ policySnapshot: policyRow
372
+ ? WorkspaceInstructionPolicySnapshot.parse({
373
+ id: policyRow.id,
374
+ workspaceId: policyRow.workspaceId,
375
+ sessionId: policyRow.sessionId,
376
+ turnId: policyRow.turnId,
377
+ attemptId: policyRow.attemptId,
378
+ executionGeneration: policyRow.executionGeneration,
379
+ policyRole: policyRow.policyRole,
380
+ roleSource: policyRow.roleSource,
381
+ entries: policyRow.entries,
382
+ entryHash: policyRow.entryHash,
383
+ createdAt: iso(policyRow.createdAt),
384
+ })
385
+ : null,
386
+ preferenceSnapshot: preferenceRow
387
+ ? (() => {
388
+ const snapshot = PreferenceRegistrySnapshot.parse({
389
+ id: preferenceRow.id,
390
+ workspaceId: preferenceRow.workspaceId,
391
+ sessionId: preferenceRow.sessionId,
392
+ turnId: preferenceRow.turnId,
393
+ attemptId: preferenceRow.attemptId,
394
+ executionGeneration: preferenceRow.executionGeneration,
395
+ initiatingHumanSubjectId: preferenceRow.initiatingHumanSubjectId,
396
+ descriptorHash: preferenceRow.descriptorHash,
397
+ descriptors: preferenceRow.descriptors,
398
+ truncated: preferenceRow.truncated,
399
+ createdAt: iso(preferenceRow.createdAt),
400
+ });
401
+ return {
402
+ id: snapshot.id,
403
+ descriptorHash: snapshot.descriptorHash,
404
+ descriptors: snapshot.descriptors.map((descriptor) => ({
405
+ id: descriptor.id,
406
+ revisionId: descriptor.revisionId,
407
+ contentHash: descriptor.contentHash,
408
+ activeVersion: descriptor.activeVersion,
409
+ scope: descriptor.scope,
410
+ })),
411
+ truncated: snapshot.truncated,
412
+ createdAt: snapshot.createdAt,
413
+ };
414
+ })()
415
+ : null,
416
+ };
417
+ });
418
+ }
419
+
93
420
  type RevisionRow = typeof schema.workspaceInstructionPolicyRevisions.$inferSelect;
94
421
  type HeadRow = typeof schema.workspaceInstructionPolicyHeads.$inferSelect;
95
422
  type EventRow = typeof schema.workspaceInstructionPolicyActivationEvents.$inferSelect;
423
+ type OnboardingProposalRow =
424
+ typeof schema.workspaceInstructionPolicyOnboardingProposals.$inferSelect;
96
425
 
97
426
  function revisionFromRow(row: RevisionRow): WorkspaceInstructionPolicyRevision {
98
427
  return {
99
428
  id: row.id,
429
+ operationId: row.operationId ?? row.id,
100
430
  accountId: row.accountId,
101
431
  workspaceId: row.workspaceId,
102
432
  revision: row.revision,
@@ -132,6 +462,7 @@ function headFromRow(row: HeadRow): WorkspaceInstructionPolicyHead {
132
462
  function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent {
133
463
  return {
134
464
  id: row.id,
465
+ operationId: row.operationId ?? row.id,
135
466
  accountId: row.accountId,
136
467
  workspaceId: row.workspaceId,
137
468
  kind: row.kind as WorkspaceInstructionPolicyKind,
@@ -158,6 +489,58 @@ function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent
158
489
  };
159
490
  }
160
491
 
492
+ function onboardingProposalFromRow(
493
+ row: OnboardingProposalRow,
494
+ draftRow: RevisionRow,
495
+ ): WorkspaceInstructionPolicyOnboardingProposal {
496
+ return {
497
+ id: row.id,
498
+ operationId: row.operationId,
499
+ accountId: row.accountId,
500
+ workspaceId: row.workspaceId,
501
+ kind: row.kind as WorkspaceInstructionPolicyKind,
502
+ scope: row.scope as WorkspaceInstructionPolicyScope,
503
+ roleKey: row.roleKey,
504
+ source: {
505
+ id: row.sourceId,
506
+ version: row.sourceVersion,
507
+ confidenceBps: row.confidenceBps,
508
+ },
509
+ baseline:
510
+ row.baselineRevisionId === null
511
+ ? null
512
+ : {
513
+ workspaceId: row.workspaceId,
514
+ kind: row.kind as WorkspaceInstructionPolicyKind,
515
+ scope: row.scope as WorkspaceInstructionPolicyScope,
516
+ roleKey: row.roleKey,
517
+ revisionId: row.baselineRevisionId,
518
+ revision: row.baselineRevision!,
519
+ contentHash: row.baselineContentHash!,
520
+ activationVersion: row.baselineActivationVersion,
521
+ activatedAt: iso(row.baselineActivatedAt!),
522
+ },
523
+ draft: revisionFromRow(draftRow),
524
+ status: "proposed",
525
+ createdBySubjectId: row.createdBySubjectId,
526
+ createdAt: iso(row.createdAt),
527
+ };
528
+ }
529
+
530
+ function headFromEventRow(row: EventRow): WorkspaceInstructionPolicyHead {
531
+ return {
532
+ workspaceId: row.workspaceId,
533
+ kind: row.kind as WorkspaceInstructionPolicyKind,
534
+ scope: row.scope as WorkspaceInstructionPolicyScope,
535
+ roleKey: row.roleKey,
536
+ revisionId: row.newRevisionId,
537
+ revision: row.newRevision,
538
+ contentHash: row.newContentHash,
539
+ activationVersion: row.activationVersion,
540
+ activatedAt: iso(row.createdAt),
541
+ };
542
+ }
543
+
161
544
  function headTargetConditions(
162
545
  workspaceId: string,
163
546
  target: WorkspaceInstructionPolicyTarget,
@@ -225,6 +608,109 @@ async function getRevisionInTransaction(
225
608
  return row ?? null;
226
609
  }
227
610
 
611
+ async function getRevisionByOperationInTransaction(
612
+ db: Database,
613
+ workspaceId: string,
614
+ operationId: string,
615
+ ): Promise<RevisionRow | null> {
616
+ const [row] = await db
617
+ .select()
618
+ .from(schema.workspaceInstructionPolicyRevisions)
619
+ .where(
620
+ and(
621
+ eq(schema.workspaceInstructionPolicyRevisions.workspaceId, workspaceId),
622
+ or(
623
+ eq(schema.workspaceInstructionPolicyRevisions.operationId, operationId),
624
+ and(
625
+ isNull(schema.workspaceInstructionPolicyRevisions.operationId),
626
+ eq(schema.workspaceInstructionPolicyRevisions.id, operationId),
627
+ ),
628
+ ),
629
+ ),
630
+ )
631
+ .limit(1);
632
+ return row ?? null;
633
+ }
634
+
635
+ async function getEventByOperationInTransaction(
636
+ db: Database,
637
+ workspaceId: string,
638
+ operationId: string,
639
+ ): Promise<EventRow | null> {
640
+ const [row] = await db
641
+ .select()
642
+ .from(schema.workspaceInstructionPolicyActivationEvents)
643
+ .where(
644
+ and(
645
+ eq(schema.workspaceInstructionPolicyActivationEvents.workspaceId, workspaceId),
646
+ or(
647
+ eq(schema.workspaceInstructionPolicyActivationEvents.operationId, operationId),
648
+ and(
649
+ isNull(schema.workspaceInstructionPolicyActivationEvents.operationId),
650
+ eq(schema.workspaceInstructionPolicyActivationEvents.id, operationId),
651
+ ),
652
+ ),
653
+ ),
654
+ )
655
+ .limit(1);
656
+ return row ?? null;
657
+ }
658
+
659
+ async function getOnboardingProposalByOperationInTransaction(
660
+ db: Database,
661
+ workspaceId: string,
662
+ operationId: string,
663
+ ): Promise<OnboardingProposalRow | null> {
664
+ const [row] = await db
665
+ .select()
666
+ .from(schema.workspaceInstructionPolicyOnboardingProposals)
667
+ .where(
668
+ and(
669
+ eq(schema.workspaceInstructionPolicyOnboardingProposals.workspaceId, workspaceId),
670
+ eq(schema.workspaceInstructionPolicyOnboardingProposals.operationId, operationId),
671
+ ),
672
+ )
673
+ .limit(1);
674
+ return row ?? null;
675
+ }
676
+
677
+ async function getOnboardingProposalBySourceVersionInTransaction(
678
+ db: Database,
679
+ input: Pick<
680
+ OnboardingProposalRequestInput,
681
+ "workspaceId" | "kind" | "scope" | "roleKey" | "sourceId" | "sourceVersion"
682
+ >,
683
+ ): Promise<OnboardingProposalRow | null> {
684
+ const [row] = await db
685
+ .select()
686
+ .from(schema.workspaceInstructionPolicyOnboardingProposals)
687
+ .where(
688
+ and(
689
+ eq(schema.workspaceInstructionPolicyOnboardingProposals.workspaceId, input.workspaceId),
690
+ eq(schema.workspaceInstructionPolicyOnboardingProposals.kind, input.kind),
691
+ eq(schema.workspaceInstructionPolicyOnboardingProposals.scope, input.scope),
692
+ input.roleKey === null
693
+ ? isNull(schema.workspaceInstructionPolicyOnboardingProposals.roleKey)
694
+ : eq(schema.workspaceInstructionPolicyOnboardingProposals.roleKey, input.roleKey),
695
+ eq(schema.workspaceInstructionPolicyOnboardingProposals.sourceId, input.sourceId),
696
+ eq(schema.workspaceInstructionPolicyOnboardingProposals.sourceVersion, input.sourceVersion),
697
+ ),
698
+ )
699
+ .limit(1);
700
+ return row ?? null;
701
+ }
702
+
703
+ async function resolveOnboardingProposalInTransaction(
704
+ db: Database,
705
+ row: OnboardingProposalRow,
706
+ ): Promise<WorkspaceInstructionPolicyOnboardingProposal> {
707
+ const draftRow = await getRevisionInTransaction(db, row.workspaceId, row.draftRevisionId);
708
+ if (!draftRow) {
709
+ throw new Error("Workspace instruction-policy onboarding proposal lost its draft revision");
710
+ }
711
+ return onboardingProposalFromRow(row, draftRow);
712
+ }
713
+
228
714
  async function getHeadInTransaction(
229
715
  db: Database,
230
716
  workspaceId: string,
@@ -246,10 +732,41 @@ function sameTarget(
246
732
  return left.kind === right.kind && left.scope === right.scope && left.roleKey === right.roleKey;
247
733
  }
248
734
 
735
+ function draftReceiptMatches(row: RevisionRow, input: DraftInput): boolean {
736
+ return (
737
+ row.createdBySubjectId === input.createdBySubjectId &&
738
+ row.kind === input.kind &&
739
+ row.scope === input.scope &&
740
+ row.roleKey === input.roleKey &&
741
+ row.content === input.content &&
742
+ row.provenanceSource === input.provenanceSource &&
743
+ row.provenanceSourceId === input.provenanceSourceId &&
744
+ row.supersedesRevisionId === input.supersedesRevisionId
745
+ );
746
+ }
747
+
249
748
  async function createDraftInTransaction(
250
749
  db: Database,
251
750
  input: DraftInput,
252
751
  ): Promise<WorkspaceInstructionPolicyRevision> {
752
+ if (await getEventByOperationInTransaction(db, input.workspaceId, input.operationId)) {
753
+ throw new WorkspaceInstructionPolicyOperationReuseError();
754
+ }
755
+ const existing = await getRevisionByOperationInTransaction(
756
+ db,
757
+ input.workspaceId,
758
+ input.operationId,
759
+ );
760
+ if (existing) {
761
+ if (
762
+ existing.requestFingerprint === null
763
+ ? !draftReceiptMatches(existing, input)
764
+ : existing.requestFingerprint !== input.requestFingerprint
765
+ ) {
766
+ throw new WorkspaceInstructionPolicyOperationReuseError();
767
+ }
768
+ return revisionFromRow(existing);
769
+ }
253
770
  if (input.supersedesRevisionId !== null) {
254
771
  const superseded = await getRevisionInTransaction(
255
772
  db,
@@ -265,6 +782,8 @@ async function createDraftInTransaction(
265
782
  const [created] = await db
266
783
  .insert(schema.workspaceInstructionPolicyRevisions)
267
784
  .values({
785
+ operationId: input.operationId,
786
+ requestFingerprint: input.requestFingerprint,
268
787
  accountId: input.accountId,
269
788
  workspaceId: input.workspaceId,
270
789
  kind: input.kind,
@@ -277,8 +796,19 @@ async function createDraftInTransaction(
277
796
  supersedesRevisionId: input.supersedesRevisionId,
278
797
  createdBySubjectId: input.createdBySubjectId,
279
798
  })
799
+ .onConflictDoNothing()
280
800
  .returning();
281
- if (!created) throw new Error("Workspace instruction-policy draft was not created");
801
+ if (!created) {
802
+ const concurrent = await getRevisionByOperationInTransaction(
803
+ db,
804
+ input.workspaceId,
805
+ input.operationId,
806
+ );
807
+ if (!concurrent || concurrent.requestFingerprint !== input.requestFingerprint) {
808
+ throw new WorkspaceInstructionPolicyOperationReuseError();
809
+ }
810
+ return revisionFromRow(concurrent);
811
+ }
282
812
  return revisionFromRow(created);
283
813
  }
284
814
 
@@ -286,12 +816,14 @@ export async function createWorkspaceInstructionPolicyDraft(
286
816
  db: Database,
287
817
  input: CallerDraftInput,
288
818
  ): Promise<WorkspaceInstructionPolicyRevision> {
819
+ const request = { ...input, operationId: input.operationId ?? randomUUID() };
820
+ const normalized = { ...request, requestFingerprint: draftRequestFingerprint(request) };
289
821
  return await withRlsContext(
290
822
  db,
291
823
  { accountId: input.accountId, workspaceId: input.workspaceId },
292
824
  async (scopedDb) => {
293
825
  await lockWorkspace(scopedDb, input);
294
- return await createDraftInTransaction(scopedDb, input);
826
+ return await createDraftInTransaction(scopedDb, normalized);
295
827
  },
296
828
  );
297
829
  }
@@ -299,18 +831,44 @@ export async function createWorkspaceInstructionPolicyDraft(
299
831
  /** Import only the stored legacy override; never materialize a deployment default or activate it. */
300
832
  export async function importLegacyWorkspaceInstructionPolicyDraft(
301
833
  db: Database,
302
- input: {
303
- accountId: string;
304
- workspaceId: string;
305
- createdBySubjectId: string;
306
- supersedesRevisionId: string | null;
307
- },
834
+ input: Omit<ImportLegacyRequestInput, "operationId"> & { operationId?: string },
308
835
  ): Promise<WorkspaceInstructionPolicyRevision> {
836
+ const request = { ...input, operationId: input.operationId ?? randomUUID() };
837
+ const normalized = {
838
+ ...request,
839
+ requestFingerprint: importLegacyRequestFingerprint(request),
840
+ };
309
841
  return await withRlsContext(
310
842
  db,
311
843
  { accountId: input.accountId, workspaceId: input.workspaceId },
312
844
  async (scopedDb) => {
313
845
  const workspace = await lockWorkspace(scopedDb, input);
846
+ if (
847
+ await getEventByOperationInTransaction(scopedDb, input.workspaceId, normalized.operationId)
848
+ ) {
849
+ throw new WorkspaceInstructionPolicyOperationReuseError();
850
+ }
851
+ const existing = await getRevisionByOperationInTransaction(
852
+ scopedDb,
853
+ input.workspaceId,
854
+ normalized.operationId,
855
+ );
856
+ if (existing) {
857
+ if (
858
+ existing.requestFingerprint === null
859
+ ? existing.createdBySubjectId !== input.createdBySubjectId ||
860
+ existing.kind !== "charter" ||
861
+ existing.scope !== "global" ||
862
+ existing.roleKey !== null ||
863
+ existing.provenanceSource !== "legacy_import" ||
864
+ existing.provenanceSourceId !== "workspaces.agent_instructions" ||
865
+ existing.supersedesRevisionId !== input.supersedesRevisionId
866
+ : existing.requestFingerprint !== normalized.requestFingerprint
867
+ ) {
868
+ throw new WorkspaceInstructionPolicyOperationReuseError();
869
+ }
870
+ return revisionFromRow(existing);
871
+ }
314
872
  if (workspace.agentInstructions === null) {
315
873
  throw new WorkspaceInstructionPolicyLegacyUnavailableError();
316
874
  }
@@ -323,7 +881,7 @@ export async function importLegacyWorkspaceInstructionPolicyDraft(
323
881
  );
324
882
  }
325
883
  return await createDraftInTransaction(scopedDb, {
326
- ...input,
884
+ ...normalized,
327
885
  kind: "charter",
328
886
  scope: "global",
329
887
  roleKey: null,
@@ -335,6 +893,206 @@ export async function importLegacyWorkspaceInstructionPolicyDraft(
335
893
  );
336
894
  }
337
895
 
896
+ export async function createWorkspaceInstructionPolicyOnboardingProposal(
897
+ db: Database,
898
+ input: Omit<OnboardingProposalRequestInput, "operationId"> & { operationId?: string },
899
+ ): Promise<WorkspaceInstructionPolicyOnboardingProposal> {
900
+ if (input.content.trim().length === 0) {
901
+ throw new WorkspaceInstructionPolicyOnboardingProposalContentError(
902
+ "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_EMPTY",
903
+ );
904
+ }
905
+ if (input.content.length > WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS) {
906
+ throw new WorkspaceInstructionPolicyOnboardingProposalContentError(
907
+ "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_OVERSIZED",
908
+ );
909
+ }
910
+ const request: OnboardingProposalRequestInput = {
911
+ ...input,
912
+ operationId: input.operationId ?? randomUUID(),
913
+ sourceId: input.sourceId.trim(),
914
+ sourceVersion: input.sourceVersion.trim(),
915
+ };
916
+ const normalized: OnboardingProposalInput = {
917
+ ...request,
918
+ requestFingerprint: onboardingProposalRequestFingerprint(request),
919
+ };
920
+ return await withRlsContext(
921
+ db,
922
+ { accountId: input.accountId, workspaceId: input.workspaceId },
923
+ async (scopedDb) => {
924
+ await lockWorkspace(scopedDb, input);
925
+ const existing = await getOnboardingProposalByOperationInTransaction(
926
+ scopedDb,
927
+ input.workspaceId,
928
+ normalized.operationId,
929
+ );
930
+ if (existing) {
931
+ if (existing.requestFingerprint !== normalized.requestFingerprint) {
932
+ throw new WorkspaceInstructionPolicyOperationReuseError();
933
+ }
934
+ return await resolveOnboardingProposalInTransaction(scopedDb, existing);
935
+ }
936
+ if (
937
+ (await getEventByOperationInTransaction(
938
+ scopedDb,
939
+ input.workspaceId,
940
+ normalized.operationId,
941
+ )) ||
942
+ (await getRevisionByOperationInTransaction(
943
+ scopedDb,
944
+ input.workspaceId,
945
+ normalized.operationId,
946
+ ))
947
+ ) {
948
+ throw new WorkspaceInstructionPolicyOperationReuseError();
949
+ }
950
+
951
+ const target: WorkspaceInstructionPolicyTarget = {
952
+ kind: normalized.kind,
953
+ scope: normalized.scope,
954
+ roleKey: normalized.roleKey,
955
+ };
956
+ const currentRow = await getHeadInTransaction(scopedDb, input.workspaceId, target);
957
+ const currentHead = currentRow ? headFromRow(currentRow) : null;
958
+ if (
959
+ (currentHead?.revisionId ?? null) !== normalized.expectedCurrentRevisionId ||
960
+ (currentHead?.activationVersion ?? 0) !== normalized.expectedActivationVersion
961
+ ) {
962
+ throw new WorkspaceInstructionPolicyOnboardingProposalStaleError(currentHead);
963
+ }
964
+
965
+ const sourceConflict = await getOnboardingProposalBySourceVersionInTransaction(
966
+ scopedDb,
967
+ normalized,
968
+ );
969
+ if (sourceConflict) {
970
+ throw new WorkspaceInstructionPolicyOnboardingProposalConflictError(
971
+ sourceConflict.id,
972
+ sourceConflict.draftRevisionId,
973
+ );
974
+ }
975
+
976
+ const proposalId = randomUUID();
977
+ const draft = await createDraftInTransaction(scopedDb, {
978
+ operationId: normalized.operationId,
979
+ requestFingerprint: normalized.requestFingerprint,
980
+ accountId: normalized.accountId,
981
+ workspaceId: normalized.workspaceId,
982
+ kind: normalized.kind,
983
+ scope: normalized.scope,
984
+ roleKey: normalized.roleKey,
985
+ content: normalized.content,
986
+ provenanceSource: "onboarding",
987
+ provenanceSourceId: proposalId,
988
+ supersedesRevisionId: currentHead?.revisionId ?? null,
989
+ createdBySubjectId: normalized.createdBySubjectId,
990
+ });
991
+ const createdAt = new Date();
992
+ const [created] = await scopedDb
993
+ .insert(schema.workspaceInstructionPolicyOnboardingProposals)
994
+ .values({
995
+ id: proposalId,
996
+ operationId: normalized.operationId,
997
+ requestFingerprint: normalized.requestFingerprint,
998
+ accountId: normalized.accountId,
999
+ workspaceId: normalized.workspaceId,
1000
+ kind: normalized.kind,
1001
+ scope: normalized.scope,
1002
+ roleKey: normalized.roleKey,
1003
+ sourceId: normalized.sourceId,
1004
+ sourceVersion: normalized.sourceVersion,
1005
+ confidenceBps: normalized.confidenceBps,
1006
+ baselineRevisionId: currentHead?.revisionId ?? null,
1007
+ baselineRevision: currentHead?.revision ?? null,
1008
+ baselineContentHash: currentHead?.contentHash ?? null,
1009
+ baselineActivationVersion: currentHead?.activationVersion ?? 0,
1010
+ baselineActivatedAt: currentHead ? new Date(currentHead.activatedAt) : null,
1011
+ draftRevisionId: draft.id,
1012
+ draftRevision: draft.revision,
1013
+ draftContentHash: draft.contentHash,
1014
+ status: "proposed",
1015
+ createdBySubjectId: normalized.createdBySubjectId,
1016
+ createdAt,
1017
+ })
1018
+ .onConflictDoNothing()
1019
+ .returning();
1020
+ if (created) {
1021
+ const draftRow = await getRevisionInTransaction(scopedDb, input.workspaceId, draft.id);
1022
+ if (!draftRow) throw new Error("Onboarding proposal draft was not recorded");
1023
+ return onboardingProposalFromRow(created, draftRow);
1024
+ }
1025
+
1026
+ const concurrentOperation = await getOnboardingProposalByOperationInTransaction(
1027
+ scopedDb,
1028
+ input.workspaceId,
1029
+ normalized.operationId,
1030
+ );
1031
+ if (concurrentOperation) {
1032
+ if (concurrentOperation.requestFingerprint !== normalized.requestFingerprint) {
1033
+ throw new WorkspaceInstructionPolicyOperationReuseError();
1034
+ }
1035
+ return await resolveOnboardingProposalInTransaction(scopedDb, concurrentOperation);
1036
+ }
1037
+ const concurrentSource = await getOnboardingProposalBySourceVersionInTransaction(
1038
+ scopedDb,
1039
+ normalized,
1040
+ );
1041
+ if (concurrentSource) {
1042
+ throw new WorkspaceInstructionPolicyOnboardingProposalConflictError(
1043
+ concurrentSource.id,
1044
+ concurrentSource.draftRevisionId,
1045
+ );
1046
+ }
1047
+ throw new Error("Workspace instruction-policy onboarding proposal was not created");
1048
+ },
1049
+ );
1050
+ }
1051
+
1052
+ export async function listWorkspaceInstructionPolicyOnboardingProposals(
1053
+ db: Database,
1054
+ workspaceId: string,
1055
+ query: WorkspaceInstructionPolicyOnboardingProposalListQuery,
1056
+ ): Promise<WorkspaceInstructionPolicyOnboardingProposalListResponse> {
1057
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
1058
+ const rows = await scopedDb
1059
+ .select()
1060
+ .from(schema.workspaceInstructionPolicyOnboardingProposals)
1061
+ .where(eq(schema.workspaceInstructionPolicyOnboardingProposals.workspaceId, workspaceId))
1062
+ .orderBy(
1063
+ desc(schema.workspaceInstructionPolicyOnboardingProposals.createdAt),
1064
+ desc(schema.workspaceInstructionPolicyOnboardingProposals.id),
1065
+ )
1066
+ .limit(query.limit + 1);
1067
+ const truncated = rows.length > query.limit;
1068
+ const page = rows.slice(0, query.limit);
1069
+ if (page.length === 0) return { proposals: [], truncated };
1070
+ const revisions = await scopedDb
1071
+ .select()
1072
+ .from(schema.workspaceInstructionPolicyRevisions)
1073
+ .where(
1074
+ and(
1075
+ eq(schema.workspaceInstructionPolicyRevisions.workspaceId, workspaceId),
1076
+ inArray(
1077
+ schema.workspaceInstructionPolicyRevisions.id,
1078
+ page.map((proposal) => proposal.draftRevisionId),
1079
+ ),
1080
+ ),
1081
+ );
1082
+ const revisionsById = new Map(revisions.map((revision) => [revision.id, revision]));
1083
+ return {
1084
+ proposals: page.map((proposal) => {
1085
+ const draft = revisionsById.get(proposal.draftRevisionId);
1086
+ if (!draft) {
1087
+ throw new Error("Workspace instruction-policy onboarding proposal lost its draft");
1088
+ }
1089
+ return onboardingProposalFromRow(proposal, draft);
1090
+ }),
1091
+ truncated,
1092
+ };
1093
+ });
1094
+ }
1095
+
338
1096
  function listFilterConditions(
339
1097
  workspaceId: string,
340
1098
  query: WorkspaceInstructionPolicyListQuery,
@@ -493,16 +1251,9 @@ export async function diffWorkspaceInstructionPolicyRevisions(
493
1251
 
494
1252
  async function changeActiveRevision(
495
1253
  db: Database,
496
- input: {
497
- accountId: string;
498
- workspaceId: string;
499
- targetRevisionId: string;
500
- expectedCurrentRevisionId: string | null;
501
- actorSubjectId: string;
502
- reason: string;
503
- type: WorkspaceInstructionPolicyActivationType;
504
- },
1254
+ input: ActiveRevisionInput,
505
1255
  ): Promise<WorkspaceInstructionPolicyActivationResponse> {
1256
+ const requestFingerprint = activeRevisionRequestFingerprint(input);
506
1257
  return await withRlsContext(
507
1258
  db,
508
1259
  { accountId: input.accountId, workspaceId: input.workspaceId },
@@ -510,6 +1261,34 @@ async function changeActiveRevision(
510
1261
  // This row lock serializes both an absent-head first activation and later
511
1262
  // updates, so every loser can return the authoritative typed conflict.
512
1263
  await lockWorkspace(scopedDb, input);
1264
+ if (
1265
+ await getRevisionByOperationInTransaction(scopedDb, input.workspaceId, input.operationId)
1266
+ ) {
1267
+ throw new WorkspaceInstructionPolicyOperationReuseError();
1268
+ }
1269
+ const existingEvent = await getEventByOperationInTransaction(
1270
+ scopedDb,
1271
+ input.workspaceId,
1272
+ input.operationId,
1273
+ );
1274
+ if (existingEvent) {
1275
+ const legacyRequestMatches =
1276
+ hasOwnField(input, "expectedCurrentRevisionId") &&
1277
+ !hasOwnField(input, "expectedActivationVersion") &&
1278
+ existingEvent.type === input.type &&
1279
+ existingEvent.newRevisionId === input.targetRevisionId &&
1280
+ existingEvent.oldRevisionId === input.expectedCurrentRevisionId &&
1281
+ existingEvent.actorSubjectId === input.actorSubjectId &&
1282
+ existingEvent.reason === input.reason;
1283
+ if (
1284
+ existingEvent.requestFingerprint === null
1285
+ ? !legacyRequestMatches
1286
+ : existingEvent.requestFingerprint !== requestFingerprint
1287
+ ) {
1288
+ throw new WorkspaceInstructionPolicyOperationReuseError();
1289
+ }
1290
+ return { head: headFromEventRow(existingEvent), event: eventFromRow(existingEvent) };
1291
+ }
513
1292
  const targetRow = await getRevisionInTransaction(
514
1293
  scopedDb,
515
1294
  input.workspaceId,
@@ -524,7 +1303,11 @@ async function changeActiveRevision(
524
1303
  };
525
1304
  const currentRow = await getHeadInTransaction(scopedDb, input.workspaceId, targetIdentity);
526
1305
  const currentHead = currentRow ? headFromRow(currentRow) : null;
527
- if ((currentHead?.revisionId ?? null) !== input.expectedCurrentRevisionId) {
1306
+ if (
1307
+ (currentHead?.revisionId ?? null) !== input.expectedCurrentRevisionId ||
1308
+ (input.expectedActivationVersion !== undefined &&
1309
+ (currentHead?.activationVersion ?? 0) !== input.expectedActivationVersion)
1310
+ ) {
528
1311
  throw new WorkspaceInstructionPolicyConflictError(currentHead);
529
1312
  }
530
1313
  if (currentHead?.revisionId === target.id) {
@@ -559,6 +1342,8 @@ async function changeActiveRevision(
559
1342
  const [eventRow] = await scopedDb
560
1343
  .insert(schema.workspaceInstructionPolicyActivationEvents)
561
1344
  .values({
1345
+ operationId: input.operationId,
1346
+ requestFingerprint,
562
1347
  accountId: input.accountId,
563
1348
  workspaceId: input.workspaceId,
564
1349
  ...targetIdentity,
@@ -613,16 +1398,19 @@ async function changeActiveRevision(
613
1398
  export async function activateWorkspaceInstructionPolicyRevision(
614
1399
  db: Database,
615
1400
  input: {
1401
+ operationId?: string;
616
1402
  accountId: string;
617
1403
  workspaceId: string;
618
1404
  revisionId: string;
619
1405
  expectedCurrentRevisionId: string | null;
1406
+ expectedActivationVersion?: number;
620
1407
  actorSubjectId: string;
621
1408
  reason: string;
622
1409
  },
623
1410
  ): Promise<WorkspaceInstructionPolicyActivationResponse> {
624
1411
  return await changeActiveRevision(db, {
625
1412
  ...input,
1413
+ operationId: input.operationId ?? randomUUID(),
626
1414
  targetRevisionId: input.revisionId,
627
1415
  type: "activate",
628
1416
  });
@@ -631,15 +1419,21 @@ export async function activateWorkspaceInstructionPolicyRevision(
631
1419
  export async function rollbackWorkspaceInstructionPolicyRevision(
632
1420
  db: Database,
633
1421
  input: {
1422
+ operationId?: string;
634
1423
  accountId: string;
635
1424
  workspaceId: string;
636
1425
  targetRevisionId: string;
637
1426
  expectedCurrentRevisionId: string;
1427
+ expectedActivationVersion?: number;
638
1428
  actorSubjectId: string;
639
1429
  reason: string;
640
1430
  },
641
1431
  ): Promise<WorkspaceInstructionPolicyActivationResponse> {
642
- return await changeActiveRevision(db, { ...input, type: "rollback" });
1432
+ return await changeActiveRevision(db, {
1433
+ ...input,
1434
+ operationId: input.operationId ?? randomUUID(),
1435
+ type: "rollback",
1436
+ });
643
1437
  }
644
1438
 
645
1439
  /**