@opengeni/db 0.28.9 → 0.31.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.
Files changed (163) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +91 -0
  4. package/dist/browser-identities.d.ts +128 -0
  5. package/dist/browser-sessions.d.ts +272 -0
  6. package/dist/browser-state-artifacts.d.ts +17 -0
  7. package/dist/capability-components.d.ts +37 -0
  8. package/dist/capability-integrations.d.ts +192 -0
  9. package/dist/chunk-F5FT7BEZ.js +3535 -0
  10. package/dist/chunk-F5FT7BEZ.js.map +1 -0
  11. package/dist/chunk-ISI2TIUG.js +639 -0
  12. package/dist/chunk-ISI2TIUG.js.map +1 -0
  13. package/dist/chunk-MWDVXQOL.js +11639 -0
  14. package/dist/chunk-MWDVXQOL.js.map +1 -0
  15. package/dist/chunk-NBE6CHRI.js +955 -0
  16. package/dist/chunk-NBE6CHRI.js.map +1 -0
  17. package/dist/{chunk-VHBFHMPC.js → chunk-YHZXQ7HP.js} +754 -11
  18. package/dist/chunk-YHZXQ7HP.js.map +1 -0
  19. package/dist/codemode-operations.d.ts +117 -0
  20. package/dist/codex-token-resolver.d.ts +2 -2
  21. package/dist/company-profile-schema.d.ts +798 -0
  22. package/dist/company-profile.d.ts +268 -0
  23. package/dist/computer-sessions.d.ts +109 -0
  24. package/dist/connection-token-resolver.d.ts +9 -4
  25. package/dist/database.d.ts +52 -3
  26. package/dist/editable-artifact-durable-export.d.ts +153 -0
  27. package/dist/editable-artifact-durable-export.js +788 -0
  28. package/dist/editable-artifact-durable-export.js.map +1 -0
  29. package/dist/editable-artifact-materialization.d.ts +123 -0
  30. package/dist/editable-artifacts-schema.d.ts +4152 -0
  31. package/dist/editable-artifacts.d.ts +610 -0
  32. package/dist/editable-artifacts.js +26 -0
  33. package/dist/editable-artifacts.js.map +1 -0
  34. package/dist/generated-images.d.ts +139 -0
  35. package/dist/index.d.ts +698 -111
  36. package/dist/index.js +27573 -10322
  37. package/dist/index.js.map +1 -1
  38. package/dist/insights.d.ts +51 -4
  39. package/dist/integration-bindings.d.ts +70 -0
  40. package/dist/integration-features.d.ts +99 -0
  41. package/dist/interaction-revisions.d.ts +3 -0
  42. package/dist/interaction-schema.d.ts +5372 -0
  43. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  44. package/dist/knowledge-source-sync.d.ts +274 -0
  45. package/dist/memory-governance.d.ts +2 -2
  46. package/dist/memory-slack-delivery.d.ts +101 -0
  47. package/dist/new-session-drafts.d.ts +2 -2
  48. package/dist/pack-components.d.ts +76 -0
  49. package/dist/pack-installations.d.ts +86 -0
  50. package/dist/persistence-errors.d.ts +3 -3
  51. package/dist/plugin-packages.d.ts +126 -0
  52. package/dist/preference-registry.d.ts +1 -1
  53. package/dist/provision-roles.d.ts +9 -1
  54. package/dist/provision-roles.js +1 -1
  55. package/dist/runtime-posture.d.ts +18 -6
  56. package/dist/schema.d.ts +18342 -9099
  57. package/dist/schema.js +165 -1
  58. package/dist/scoped-knowledge.d.ts +97 -1
  59. package/dist/session-control.d.ts +4 -4
  60. package/dist/session-queue-commands.d.ts +19 -11
  61. package/dist/session-realtime-context.d.ts +2 -2
  62. package/dist/session-realtime-ledger.d.ts +4 -4
  63. package/dist/session-realtime-mirror.d.ts +2 -2
  64. package/dist/session-realtime-state.d.ts +1 -1
  65. package/dist/session-realtime-terminal.d.ts +1 -1
  66. package/dist/session-realtime.d.ts +7 -7
  67. package/dist/session-tool-call-settlement.d.ts +1 -1
  68. package/dist/slack-user-link-access.d.ts +54 -0
  69. package/dist/transcription-recordings.d.ts +2 -2
  70. package/dist/turn-initiator.d.ts +2 -2
  71. package/dist/video-generation.d.ts +216 -0
  72. package/dist/video-generation.js +66 -0
  73. package/dist/video-generation.js.map +1 -0
  74. package/dist/workspace-artifacts.d.ts +1 -1
  75. package/dist/workspace-instruction-policies.d.ts +1 -1
  76. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  77. package/dist/workspace-learning-policy.d.ts +161 -0
  78. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  79. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  80. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  81. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  82. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  83. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  84. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  85. package/drizzle/0190_timeline_annotations.sql +31 -0
  86. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  87. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  88. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  89. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  90. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  91. package/drizzle/0196_rig_provider_images.sql +152 -0
  92. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  93. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  94. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  95. package/drizzle/0201_company_profile_authority.sql +849 -0
  96. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  97. package/drizzle/0203_durable_video_generation.sql +353 -0
  98. package/drizzle/0204_video_generation_funding.sql +44 -0
  99. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  100. package/drizzle/0206_browser_sessions.sql +646 -0
  101. package/drizzle/0207_browser_identities.sql +379 -0
  102. package/drizzle/0208_attached_browser_devices.sql +165 -0
  103. package/drizzle/0209_computer_sessions.sql +575 -0
  104. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  105. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  106. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  107. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  108. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  109. package/drizzle/0215_capabilities_platform.sql +980 -0
  110. package/drizzle/0216_pack_component_ownership.sql +235 -0
  111. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  112. package/package.json +18 -5
  113. package/src/attached-browser-devices.ts +371 -0
  114. package/src/attempt-tool-catalogs.ts +131 -0
  115. package/src/browser-auth.ts +1896 -0
  116. package/src/browser-identities.ts +1168 -0
  117. package/src/browser-sessions.ts +2005 -0
  118. package/src/browser-state-artifacts.ts +59 -0
  119. package/src/capability-components.ts +166 -0
  120. package/src/capability-integrations.ts +1663 -0
  121. package/src/codemode-operations.ts +674 -0
  122. package/src/company-profile-schema.ts +143 -0
  123. package/src/company-profile.ts +968 -0
  124. package/src/computer-sessions.ts +1130 -0
  125. package/src/connection-token-resolver.ts +270 -25
  126. package/src/database.ts +427 -23
  127. package/src/editable-artifact-durable-export.ts +1201 -0
  128. package/src/editable-artifact-materialization.ts +893 -0
  129. package/src/editable-artifacts-schema.ts +879 -0
  130. package/src/editable-artifacts.ts +5404 -0
  131. package/src/generated-images.ts +740 -0
  132. package/src/index.ts +5917 -1182
  133. package/src/insights.ts +160 -13
  134. package/src/integration-bindings.ts +455 -0
  135. package/src/integration-features.ts +859 -0
  136. package/src/interaction-revisions.ts +40 -0
  137. package/src/interaction-schema.ts +1303 -0
  138. package/src/knowledge-source-sync-schema.ts +192 -0
  139. package/src/knowledge-source-sync.ts +1922 -0
  140. package/src/memory-slack-delivery.ts +801 -0
  141. package/src/pack-components.ts +954 -0
  142. package/src/pack-installations.ts +929 -0
  143. package/src/persistence-errors.ts +4 -7
  144. package/src/plugin-packages.ts +1186 -0
  145. package/src/provision-roles.ts +302 -1
  146. package/src/runtime-posture-cli.ts +9 -0
  147. package/src/runtime-posture.ts +508 -5
  148. package/src/schema.ts +5117 -3119
  149. package/src/scoped-knowledge-schema.ts +1 -1
  150. package/src/scoped-knowledge.ts +655 -3
  151. package/src/session-control.ts +8 -5
  152. package/src/session-queue-commands.ts +187 -14
  153. package/src/session-realtime-context.ts +2 -2
  154. package/src/session-realtime-ledger.ts +3 -3
  155. package/src/session-realtime.ts +7 -7
  156. package/src/session-tool-call-settlement.ts +9 -0
  157. package/src/slack-user-link-access.ts +702 -0
  158. package/src/video-generation.ts +1452 -0
  159. package/src/workspace-learning-policy-schema.ts +139 -0
  160. package/src/workspace-learning-policy.ts +595 -0
  161. package/dist/chunk-P6CUHXQZ.js +0 -7248
  162. package/dist/chunk-P6CUHXQZ.js.map +0 -1
  163. package/dist/chunk-VHBFHMPC.js.map +0 -1
@@ -0,0 +1,929 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import {
4
+ CapabilityPack as CapabilityPackSchema,
5
+ stableJson,
6
+ type CapabilityPack,
7
+ type PackInstallation,
8
+ type PackInstallationStatus,
9
+ } from "@opengeni/contracts";
10
+ import { and, eq, inArray, ne, sql } from "drizzle-orm";
11
+
12
+ import { withRlsContext, type Database } from "./database";
13
+ import * as schema from "./schema";
14
+
15
+ export class PackOperationIdempotencyError extends Error {
16
+ readonly name = "PackOperationIdempotencyError";
17
+ }
18
+
19
+ export class PackOperationInProgressError extends Error {
20
+ readonly name = "PackOperationInProgressError";
21
+ }
22
+
23
+ export class PackOperationClaimLostError extends Error {
24
+ readonly name = "PackOperationClaimLostError";
25
+ }
26
+
27
+ export class PackManifestChangedError extends Error {
28
+ readonly name = "PackManifestChangedError";
29
+ }
30
+
31
+ export class PackInstallationVersionConflictError extends Error {
32
+ readonly name = "PackInstallationVersionConflictError";
33
+
34
+ constructor(
35
+ readonly expectedVersion: number,
36
+ readonly actualVersion: number,
37
+ ) {
38
+ super(
39
+ `Pack installation changed: expected version ${expectedVersion}, current version ${actualVersion}`,
40
+ );
41
+ }
42
+ }
43
+
44
+ export class PackInstallationVersionRequiredError extends Error {
45
+ readonly name = "PackInstallationVersionRequiredError";
46
+ }
47
+
48
+ export type PreparedPackInstallation = {
49
+ installation: PackInstallation;
50
+ operationId: string;
51
+ operationVersion: number;
52
+ replayResult: Record<string, unknown> | null;
53
+ };
54
+
55
+ export async function preparePackInstallationOperation(
56
+ db: Database,
57
+ input: {
58
+ accountId: string;
59
+ workspaceId: string;
60
+ subjectId: string;
61
+ pack: CapabilityPack;
62
+ manifestDigest: string;
63
+ selectedRigId: string | null;
64
+ metadata: Record<string, unknown>;
65
+ idempotencyKey: string;
66
+ requestDigest: string;
67
+ registeredManifestDigest?: string;
68
+ expectedInstallationVersion?: number;
69
+ },
70
+ ): Promise<PreparedPackInstallation> {
71
+ return await withRlsContext(
72
+ db,
73
+ { accountId: input.accountId, workspaceId: input.workspaceId },
74
+ async (scopedDb) =>
75
+ await scopedDb.transaction(async (tx) => {
76
+ await tx.execute(
77
+ sql`select pg_advisory_xact_lock(hashtextextended(${`capability-operation:${input.workspaceId}:${input.idempotencyKey}`}, 0))`,
78
+ );
79
+ await tx.execute(
80
+ sql`select pg_advisory_xact_lock(hashtextextended(${`pack-installation:${input.workspaceId}:${input.pack.id}`}, 0))`,
81
+ );
82
+ let [operation] = await tx
83
+ .select()
84
+ .from(schema.capabilityOperations)
85
+ .where(
86
+ and(
87
+ eq(schema.capabilityOperations.workspaceId, input.workspaceId),
88
+ eq(schema.capabilityOperations.idempotencyKey, input.idempotencyKey),
89
+ ),
90
+ )
91
+ .for("update")
92
+ .limit(1);
93
+ let [existing] = await tx
94
+ .select()
95
+ .from(schema.packInstallations)
96
+ .where(
97
+ and(
98
+ eq(schema.packInstallations.workspaceId, input.workspaceId),
99
+ eq(schema.packInstallations.packId, input.pack.id),
100
+ ),
101
+ )
102
+ .for("update")
103
+ .limit(1);
104
+ if (operation && operation.requestDigest !== input.requestDigest) {
105
+ throw new PackOperationIdempotencyError("Pack idempotency key was reused");
106
+ }
107
+ if (operation?.status === "completed" && operation.result) {
108
+ if (!existing) throw new Error("Completed Pack operation lost its installation");
109
+ return {
110
+ installation: mapPackInstallation(existing),
111
+ operationId: operation.id,
112
+ operationVersion: operation.version,
113
+ replayResult: operation.result,
114
+ };
115
+ }
116
+ if (input.registeredManifestDigest) {
117
+ await assertRegisteredPackManifestCurrent(tx as unknown as Database, {
118
+ workspaceId: input.workspaceId,
119
+ packId: input.pack.id,
120
+ expectedDigest: input.registeredManifestDigest,
121
+ admittedDigest: input.manifestDigest,
122
+ });
123
+ }
124
+ assertPackOperationRetryableStatus(operation);
125
+ const resuming = Boolean(operation);
126
+ if (!resuming && existing) {
127
+ if (input.expectedInstallationVersion === undefined) {
128
+ throw new PackInstallationVersionRequiredError(
129
+ "Updating or repairing a Pack requires the previewed installation version",
130
+ );
131
+ }
132
+ if (existing.version !== input.expectedInstallationVersion) {
133
+ throw new PackInstallationVersionConflictError(
134
+ input.expectedInstallationVersion,
135
+ existing.version,
136
+ );
137
+ }
138
+ }
139
+ const otherOperation = await findOtherPackOperationInProgress(tx as unknown as Database, {
140
+ workspaceId: input.workspaceId,
141
+ packId: input.pack.id,
142
+ ...(operation ? { operationId: operation.id } : {}),
143
+ });
144
+ if (operation && otherOperation) {
145
+ throw new PackOperationInProgressError("Another Pack operation is already in progress");
146
+ }
147
+ if (!operation) {
148
+ if (otherOperation?.status === "running") {
149
+ if (
150
+ !existing ||
151
+ !["install", "update", "repair"].includes(otherOperation.kind) ||
152
+ !samePackInstallationIntent(existing, input)
153
+ ) {
154
+ throw new PackOperationInProgressError(
155
+ "Another Pack operation is already in progress",
156
+ );
157
+ }
158
+ await supersedeStaleRunningPackOperation(tx as unknown as Database, otherOperation);
159
+ } else if (otherOperation?.status === "pending") {
160
+ await supersedePendingPackOperation(tx as unknown as Database, otherOperation);
161
+ }
162
+ [operation] = await tx
163
+ .insert(schema.capabilityOperations)
164
+ .values({
165
+ accountId: input.accountId,
166
+ workspaceId: input.workspaceId,
167
+ idempotencyKey: input.idempotencyKey,
168
+ requestDigest: input.requestDigest,
169
+ kind: !existing
170
+ ? "install"
171
+ : existing.manifestDigest === input.manifestDigest
172
+ ? "repair"
173
+ : "update",
174
+ targetKind: "pack",
175
+ targetId: input.pack.id,
176
+ status: "running",
177
+ phase: "admitted",
178
+ createdBySubjectId: input.subjectId,
179
+ })
180
+ .returning();
181
+ } else {
182
+ operation = await resumePackOperation(tx as unknown as Database, operation);
183
+ }
184
+ if (!operation) throw new Error("Failed to admit Pack operation");
185
+ const now = new Date();
186
+ if (!existing) {
187
+ [existing] = await tx
188
+ .insert(schema.packInstallations)
189
+ .values({
190
+ accountId: input.accountId,
191
+ workspaceId: input.workspaceId,
192
+ packId: input.pack.id,
193
+ status: "installing",
194
+ manifestSnapshot: input.pack as unknown as Record<string, unknown>,
195
+ manifestDigest: input.manifestDigest,
196
+ selectedRigId: input.selectedRigId,
197
+ installedBySubjectId: input.subjectId,
198
+ metadata: input.metadata,
199
+ })
200
+ .returning();
201
+ } else if (!resuming) {
202
+ [existing] = await tx
203
+ .update(schema.packInstallations)
204
+ .set({
205
+ status: "needs_attention",
206
+ version: existing.version + 1,
207
+ manifestSnapshot: input.pack as unknown as Record<string, unknown>,
208
+ manifestDigest: input.manifestDigest,
209
+ selectedRigId: input.selectedRigId,
210
+ installedBySubjectId: input.subjectId,
211
+ metadata: input.metadata,
212
+ enabledAt: now,
213
+ updatedAt: now,
214
+ })
215
+ .where(eq(schema.packInstallations.id, existing.id))
216
+ .returning();
217
+ } else {
218
+ [existing] = await tx
219
+ .update(schema.packInstallations)
220
+ .set({
221
+ status: existing.status === "installing" ? "installing" : "needs_attention",
222
+ updatedAt: now,
223
+ })
224
+ .where(eq(schema.packInstallations.id, existing.id))
225
+ .returning();
226
+ }
227
+ if (!existing) throw new Error("Failed to prepare Pack installation");
228
+ return {
229
+ installation: mapPackInstallation(existing),
230
+ operationId: operation.id,
231
+ operationVersion: operation.version,
232
+ replayResult: null,
233
+ };
234
+ }),
235
+ );
236
+ }
237
+
238
+ export async function finalizePackInstallationOperation(
239
+ db: Database,
240
+ input: {
241
+ accountId: string;
242
+ workspaceId: string;
243
+ operationId: string;
244
+ operationVersion: number;
245
+ packInstallationId: string;
246
+ packId: string;
247
+ result: Record<string, unknown>;
248
+ },
249
+ ): Promise<PackInstallation> {
250
+ return await withRlsContext(
251
+ db,
252
+ { accountId: input.accountId, workspaceId: input.workspaceId },
253
+ async (scopedDb) =>
254
+ await scopedDb.transaction(async (tx) => {
255
+ await assertActivePackOperationClaim(tx as unknown as Database, {
256
+ accountId: input.accountId,
257
+ workspaceId: input.workspaceId,
258
+ operationId: input.operationId,
259
+ operationVersion: input.operationVersion,
260
+ packInstallationId: input.packInstallationId,
261
+ });
262
+ const now = new Date();
263
+ const [installation] = await tx
264
+ .update(schema.packInstallations)
265
+ .set({ status: "active", updatedAt: now })
266
+ .where(
267
+ and(
268
+ eq(schema.packInstallations.workspaceId, input.workspaceId),
269
+ eq(schema.packInstallations.id, input.packInstallationId),
270
+ eq(schema.packInstallations.packId, input.packId),
271
+ ),
272
+ )
273
+ .returning();
274
+ if (!installation) throw new Error("Pack installation was not found during finalize");
275
+ await tx
276
+ .insert(schema.capabilityInstallations)
277
+ .values({
278
+ accountId: input.accountId,
279
+ workspaceId: input.workspaceId,
280
+ capabilityId: `pack:${input.packId}`,
281
+ kind: "pack",
282
+ status: "active",
283
+ config: {},
284
+ metadata: {
285
+ platformVersion: 2,
286
+ packInstallationId: input.packInstallationId,
287
+ manifestDigest: installation.manifestDigest,
288
+ },
289
+ enabledAt: now,
290
+ updatedAt: now,
291
+ })
292
+ .onConflictDoUpdate({
293
+ target: [
294
+ schema.capabilityInstallations.workspaceId,
295
+ schema.capabilityInstallations.capabilityId,
296
+ ],
297
+ set: {
298
+ status: "active",
299
+ config: {},
300
+ metadata: {
301
+ platformVersion: 2,
302
+ packInstallationId: input.packInstallationId,
303
+ manifestDigest: installation.manifestDigest,
304
+ },
305
+ enabledAt: now,
306
+ updatedAt: now,
307
+ },
308
+ });
309
+ const [completed] = await tx
310
+ .update(schema.capabilityOperations)
311
+ .set({
312
+ status: "completed",
313
+ phase: "completed",
314
+ result: input.result,
315
+ errorCode: null,
316
+ version: sql`${schema.capabilityOperations.version} + 1`,
317
+ completedAt: now,
318
+ updatedAt: now,
319
+ })
320
+ .where(
321
+ and(
322
+ eq(schema.capabilityOperations.id, input.operationId),
323
+ eq(schema.capabilityOperations.status, "running"),
324
+ eq(schema.capabilityOperations.version, input.operationVersion),
325
+ ),
326
+ )
327
+ .returning({ id: schema.capabilityOperations.id });
328
+ if (!completed) throw new PackOperationClaimLostError("Pack operation claim was lost");
329
+ return mapPackInstallation(installation);
330
+ }),
331
+ );
332
+ }
333
+
334
+ export async function deferPackInstallationOperation(
335
+ db: Database,
336
+ input: {
337
+ accountId: string;
338
+ workspaceId: string;
339
+ operationId: string;
340
+ operationVersion: number;
341
+ packInstallationId: string;
342
+ phase: string;
343
+ errorCode: string;
344
+ },
345
+ ): Promise<void> {
346
+ await withRlsContext(
347
+ db,
348
+ { accountId: input.accountId, workspaceId: input.workspaceId },
349
+ async (scopedDb) =>
350
+ await scopedDb.transaction(async (tx) => {
351
+ const operation = await assertActivePackOperationClaim(tx as unknown as Database, {
352
+ accountId: input.accountId,
353
+ workspaceId: input.workspaceId,
354
+ operationId: input.operationId,
355
+ operationVersion: input.operationVersion,
356
+ packInstallationId: input.packInstallationId,
357
+ });
358
+ await tx
359
+ .update(schema.packInstallations)
360
+ .set({ status: "needs_attention", updatedAt: new Date() })
361
+ .where(
362
+ and(
363
+ eq(schema.packInstallations.workspaceId, input.workspaceId),
364
+ eq(schema.packInstallations.id, input.packInstallationId),
365
+ eq(schema.packInstallations.packId, operation.targetId),
366
+ ),
367
+ );
368
+ const [deferred] = await tx
369
+ .update(schema.capabilityOperations)
370
+ .set({
371
+ status: "pending",
372
+ phase: input.phase.slice(0, 120),
373
+ errorCode: input.errorCode.slice(0, 120),
374
+ version: sql`${schema.capabilityOperations.version} + 1`,
375
+ updatedAt: new Date(),
376
+ })
377
+ .where(
378
+ and(
379
+ eq(schema.capabilityOperations.id, input.operationId),
380
+ eq(schema.capabilityOperations.status, "running"),
381
+ eq(schema.capabilityOperations.version, input.operationVersion),
382
+ ),
383
+ )
384
+ .returning({ id: schema.capabilityOperations.id });
385
+ if (!deferred) throw new PackOperationClaimLostError("Pack operation claim was lost");
386
+ }),
387
+ );
388
+ }
389
+
390
+ export async function touchPackInstallationOperation(
391
+ db: Database,
392
+ input: {
393
+ accountId: string;
394
+ workspaceId: string;
395
+ operationId: string;
396
+ operationVersion: number;
397
+ },
398
+ ): Promise<void> {
399
+ await withRlsContext(
400
+ db,
401
+ { accountId: input.accountId, workspaceId: input.workspaceId },
402
+ async (scopedDb) => {
403
+ const [touched] = await scopedDb
404
+ .update(schema.capabilityOperations)
405
+ .set({ updatedAt: sql`now()` })
406
+ .where(
407
+ and(
408
+ eq(schema.capabilityOperations.workspaceId, input.workspaceId),
409
+ eq(schema.capabilityOperations.id, input.operationId),
410
+ eq(schema.capabilityOperations.status, "running"),
411
+ eq(schema.capabilityOperations.version, input.operationVersion),
412
+ ),
413
+ )
414
+ .returning({ id: schema.capabilityOperations.id });
415
+ if (!touched) throw new PackOperationClaimLostError("Pack operation claim was lost");
416
+ },
417
+ );
418
+ }
419
+
420
+ export async function preparePackUninstallOperation(
421
+ db: Database,
422
+ input: {
423
+ accountId: string;
424
+ workspaceId: string;
425
+ subjectId: string;
426
+ packId: string;
427
+ expectedInstallationVersion: number;
428
+ idempotencyKey: string;
429
+ requestDigest: string;
430
+ },
431
+ ): Promise<PreparedPackInstallation | { replayResult: Record<string, unknown> }> {
432
+ return await withRlsContext(
433
+ db,
434
+ { accountId: input.accountId, workspaceId: input.workspaceId },
435
+ async (scopedDb) =>
436
+ await scopedDb.transaction(async (tx) => {
437
+ await tx.execute(
438
+ sql`select pg_advisory_xact_lock(hashtextextended(${`capability-operation:${input.workspaceId}:${input.idempotencyKey}`}, 0))`,
439
+ );
440
+ await tx.execute(
441
+ sql`select pg_advisory_xact_lock(hashtextextended(${`pack-installation:${input.workspaceId}:${input.packId}`}, 0))`,
442
+ );
443
+ const [operation] = await tx
444
+ .select()
445
+ .from(schema.capabilityOperations)
446
+ .where(
447
+ and(
448
+ eq(schema.capabilityOperations.workspaceId, input.workspaceId),
449
+ eq(schema.capabilityOperations.idempotencyKey, input.idempotencyKey),
450
+ ),
451
+ )
452
+ .for("update")
453
+ .limit(1);
454
+ const [installation] = await tx
455
+ .select()
456
+ .from(schema.packInstallations)
457
+ .where(
458
+ and(
459
+ eq(schema.packInstallations.workspaceId, input.workspaceId),
460
+ eq(schema.packInstallations.packId, input.packId),
461
+ ),
462
+ )
463
+ .for("update")
464
+ .limit(1);
465
+ if (operation) {
466
+ if (operation.requestDigest !== input.requestDigest) {
467
+ throw new PackOperationIdempotencyError("Pack idempotency key was reused");
468
+ }
469
+ if (operation.status === "completed" && operation.result) {
470
+ return { replayResult: operation.result };
471
+ }
472
+ assertPackOperationRetryableStatus(operation);
473
+ const otherOperation = await findOtherPackOperationInProgress(tx as unknown as Database, {
474
+ workspaceId: input.workspaceId,
475
+ packId: input.packId,
476
+ operationId: operation.id,
477
+ });
478
+ if (otherOperation) {
479
+ throw new PackOperationInProgressError("Another Pack operation is already in progress");
480
+ }
481
+ const resumed = await resumePackOperation(tx as unknown as Database, operation);
482
+ if (!installation || installation.status === "disabled") {
483
+ const result = { status: "not_installed" };
484
+ await completePackOperation(
485
+ tx as unknown as Database,
486
+ operation.id,
487
+ resumed.version,
488
+ result,
489
+ );
490
+ return { replayResult: result };
491
+ }
492
+ return {
493
+ installation: mapPackInstallation(installation),
494
+ operationId: operation.id,
495
+ operationVersion: resumed.version,
496
+ replayResult: null,
497
+ };
498
+ }
499
+ if (!installation || installation.status === "disabled") {
500
+ const result = { status: "not_installed" };
501
+ const otherOperation = await findOtherPackOperationInProgress(tx as unknown as Database, {
502
+ workspaceId: input.workspaceId,
503
+ packId: input.packId,
504
+ });
505
+ if (otherOperation?.status === "running") {
506
+ if (otherOperation.kind !== "uninstall") {
507
+ throw new PackOperationInProgressError(
508
+ "Another Pack operation is already in progress",
509
+ );
510
+ }
511
+ await supersedeStaleRunningPackOperation(tx as unknown as Database, otherOperation);
512
+ } else if (otherOperation?.status === "pending") {
513
+ await supersedePendingPackOperation(tx as unknown as Database, otherOperation);
514
+ }
515
+ await tx.insert(schema.capabilityOperations).values({
516
+ accountId: input.accountId,
517
+ workspaceId: input.workspaceId,
518
+ idempotencyKey: input.idempotencyKey,
519
+ requestDigest: input.requestDigest,
520
+ kind: "uninstall",
521
+ targetKind: "pack",
522
+ targetId: input.packId,
523
+ status: "completed",
524
+ phase: "completed",
525
+ result,
526
+ createdBySubjectId: input.subjectId,
527
+ completedAt: new Date(),
528
+ });
529
+ return { replayResult: result };
530
+ }
531
+ if (installation.version !== input.expectedInstallationVersion) {
532
+ throw new PackInstallationVersionConflictError(
533
+ input.expectedInstallationVersion,
534
+ installation.version,
535
+ );
536
+ }
537
+ const otherOperation = await findOtherPackOperationInProgress(tx as unknown as Database, {
538
+ workspaceId: input.workspaceId,
539
+ packId: input.packId,
540
+ });
541
+ if (otherOperation?.status === "running") {
542
+ if (otherOperation.kind !== "uninstall") {
543
+ throw new PackOperationInProgressError("Another Pack operation is already in progress");
544
+ }
545
+ await supersedeStaleRunningPackOperation(tx as unknown as Database, otherOperation);
546
+ } else if (otherOperation?.status === "pending") {
547
+ await supersedePendingPackOperation(tx as unknown as Database, otherOperation);
548
+ }
549
+ const [createdOperation] = await tx
550
+ .insert(schema.capabilityOperations)
551
+ .values({
552
+ accountId: input.accountId,
553
+ workspaceId: input.workspaceId,
554
+ idempotencyKey: input.idempotencyKey,
555
+ requestDigest: input.requestDigest,
556
+ kind: "uninstall",
557
+ targetKind: "pack",
558
+ targetId: input.packId,
559
+ status: "running",
560
+ phase: "admitted",
561
+ createdBySubjectId: input.subjectId,
562
+ })
563
+ .returning();
564
+ if (!createdOperation) throw new Error("Failed to admit Pack uninstall");
565
+ const [prepared] = await tx
566
+ .update(schema.packInstallations)
567
+ .set({
568
+ status: "needs_attention",
569
+ version: installation.version + 1,
570
+ updatedAt: new Date(),
571
+ })
572
+ .where(eq(schema.packInstallations.id, installation.id))
573
+ .returning();
574
+ if (!prepared) throw new Error("Failed to prepare Pack uninstall");
575
+ return {
576
+ installation: mapPackInstallation(prepared),
577
+ operationId: createdOperation.id,
578
+ operationVersion: createdOperation.version,
579
+ replayResult: null,
580
+ };
581
+ }),
582
+ );
583
+ }
584
+
585
+ export async function finalizePackUninstallOperation(
586
+ db: Database,
587
+ input: {
588
+ accountId: string;
589
+ workspaceId: string;
590
+ operationId: string;
591
+ operationVersion: number;
592
+ packInstallationId: string;
593
+ packId: string;
594
+ result: Record<string, unknown>;
595
+ },
596
+ ): Promise<void> {
597
+ await withRlsContext(
598
+ db,
599
+ { accountId: input.accountId, workspaceId: input.workspaceId },
600
+ async (scopedDb) =>
601
+ await scopedDb.transaction(async (tx) => {
602
+ await assertActivePackOperationClaim(tx as unknown as Database, {
603
+ accountId: input.accountId,
604
+ workspaceId: input.workspaceId,
605
+ operationId: input.operationId,
606
+ operationVersion: input.operationVersion,
607
+ packInstallationId: input.packInstallationId,
608
+ });
609
+ const now = new Date();
610
+ await tx
611
+ .update(schema.packInstallations)
612
+ .set({ status: "disabled", updatedAt: now })
613
+ .where(
614
+ and(
615
+ eq(schema.packInstallations.workspaceId, input.workspaceId),
616
+ eq(schema.packInstallations.id, input.packInstallationId),
617
+ eq(schema.packInstallations.packId, input.packId),
618
+ ),
619
+ );
620
+ await tx
621
+ .update(schema.capabilityInstallations)
622
+ .set({ status: "disabled", updatedAt: now })
623
+ .where(
624
+ and(
625
+ eq(schema.capabilityInstallations.workspaceId, input.workspaceId),
626
+ eq(schema.capabilityInstallations.capabilityId, `pack:${input.packId}`),
627
+ ),
628
+ );
629
+ const [completed] = await tx
630
+ .update(schema.capabilityOperations)
631
+ .set({
632
+ status: "completed",
633
+ phase: "completed",
634
+ result: input.result,
635
+ errorCode: null,
636
+ version: sql`${schema.capabilityOperations.version} + 1`,
637
+ completedAt: now,
638
+ updatedAt: now,
639
+ })
640
+ .where(
641
+ and(
642
+ eq(schema.capabilityOperations.id, input.operationId),
643
+ eq(schema.capabilityOperations.status, "running"),
644
+ eq(schema.capabilityOperations.version, input.operationVersion),
645
+ ),
646
+ )
647
+ .returning({ id: schema.capabilityOperations.id });
648
+ if (!completed) throw new PackOperationClaimLostError("Pack operation claim was lost");
649
+ }),
650
+ );
651
+ }
652
+
653
+ async function findOtherPackOperationInProgress(
654
+ db: Database,
655
+ input: { workspaceId: string; packId: string; operationId?: string },
656
+ ): Promise<Pick<
657
+ typeof schema.capabilityOperations.$inferSelect,
658
+ "id" | "kind" | "status" | "version" | "updatedAt"
659
+ > | null> {
660
+ const [operation] = await db
661
+ .select({
662
+ id: schema.capabilityOperations.id,
663
+ kind: schema.capabilityOperations.kind,
664
+ status: schema.capabilityOperations.status,
665
+ version: schema.capabilityOperations.version,
666
+ updatedAt: schema.capabilityOperations.updatedAt,
667
+ })
668
+ .from(schema.capabilityOperations)
669
+ .where(
670
+ and(
671
+ eq(schema.capabilityOperations.workspaceId, input.workspaceId),
672
+ eq(schema.capabilityOperations.targetKind, "pack"),
673
+ eq(schema.capabilityOperations.targetId, input.packId),
674
+ inArray(schema.capabilityOperations.status, ["running", "pending"]),
675
+ input.operationId ? ne(schema.capabilityOperations.id, input.operationId) : sql`true`,
676
+ ),
677
+ )
678
+ .orderBy(schema.capabilityOperations.createdAt)
679
+ .limit(1);
680
+ return operation ?? null;
681
+ }
682
+
683
+ async function resumePackOperation(
684
+ db: Database,
685
+ operation: typeof schema.capabilityOperations.$inferSelect,
686
+ ): Promise<typeof schema.capabilityOperations.$inferSelect> {
687
+ if (operation.status === "completed") {
688
+ if (operation.result) return operation;
689
+ throw new Error("Completed Pack operation has no replay result");
690
+ }
691
+ if (operation.status !== "pending" && operation.status !== "running") {
692
+ throw new PackOperationIdempotencyError("Pack idempotency key is no longer retryable");
693
+ }
694
+ const [resumed] = await db
695
+ .update(schema.capabilityOperations)
696
+ .set({
697
+ status: "running",
698
+ phase: operation.status === "running" ? "recovering_stale" : "resuming",
699
+ errorCode: null,
700
+ version: operation.version + 1,
701
+ updatedAt: sql`now()`,
702
+ })
703
+ .where(
704
+ and(
705
+ eq(schema.capabilityOperations.id, operation.id),
706
+ eq(schema.capabilityOperations.status, operation.status),
707
+ eq(schema.capabilityOperations.version, operation.version),
708
+ operation.status === "running"
709
+ ? sql`${schema.capabilityOperations.updatedAt} <= now() - interval '15 minutes'`
710
+ : sql`true`,
711
+ ),
712
+ )
713
+ .returning();
714
+ if (!resumed) {
715
+ throw new PackOperationInProgressError(
716
+ operation.status === "running"
717
+ ? "This Pack operation is still running"
718
+ : "This Pack operation changed before retry admission",
719
+ );
720
+ }
721
+ return resumed;
722
+ }
723
+
724
+ function assertPackOperationRetryableStatus(
725
+ operation: typeof schema.capabilityOperations.$inferSelect | undefined,
726
+ ): void {
727
+ if (!operation || operation.status === "pending" || operation.status === "running") return;
728
+ if (operation.status === "completed") {
729
+ throw new Error("Completed Pack operation has no replay result");
730
+ }
731
+ throw new PackOperationIdempotencyError("Pack idempotency key is no longer retryable");
732
+ }
733
+
734
+ async function supersedePendingPackOperation(
735
+ db: Database,
736
+ operation: Pick<typeof schema.capabilityOperations.$inferSelect, "id" | "status" | "version">,
737
+ ): Promise<void> {
738
+ const now = new Date();
739
+ const [superseded] = await db
740
+ .update(schema.capabilityOperations)
741
+ .set({
742
+ status: "failed",
743
+ phase: "superseded",
744
+ errorCode: "superseded_by_retry",
745
+ version: sql`${schema.capabilityOperations.version} + 1`,
746
+ completedAt: now,
747
+ updatedAt: now,
748
+ })
749
+ .where(
750
+ and(
751
+ eq(schema.capabilityOperations.id, operation.id),
752
+ eq(schema.capabilityOperations.status, "pending"),
753
+ eq(schema.capabilityOperations.version, operation.version),
754
+ ),
755
+ )
756
+ .returning({ id: schema.capabilityOperations.id });
757
+ if (!superseded) {
758
+ throw new PackOperationInProgressError("Another Pack operation changed before retry admission");
759
+ }
760
+ }
761
+
762
+ async function supersedeStaleRunningPackOperation(
763
+ db: Database,
764
+ operation: Pick<typeof schema.capabilityOperations.$inferSelect, "id" | "status" | "version">,
765
+ ): Promise<void> {
766
+ const now = new Date();
767
+ const [superseded] = await db
768
+ .update(schema.capabilityOperations)
769
+ .set({
770
+ status: "failed",
771
+ phase: "superseded_stale",
772
+ errorCode: "superseded_by_recovery",
773
+ version: operation.version + 1,
774
+ completedAt: now,
775
+ updatedAt: now,
776
+ })
777
+ .where(
778
+ and(
779
+ eq(schema.capabilityOperations.id, operation.id),
780
+ eq(schema.capabilityOperations.status, "running"),
781
+ eq(schema.capabilityOperations.version, operation.version),
782
+ sql`${schema.capabilityOperations.updatedAt} <= now() - interval '15 minutes'`,
783
+ ),
784
+ )
785
+ .returning({ id: schema.capabilityOperations.id });
786
+ if (!superseded) {
787
+ throw new PackOperationInProgressError("Another Pack operation is still running");
788
+ }
789
+ }
790
+
791
+ async function assertActivePackOperationClaim(
792
+ db: Database,
793
+ input: {
794
+ accountId: string;
795
+ workspaceId: string;
796
+ operationId: string;
797
+ operationVersion: number;
798
+ packInstallationId: string;
799
+ },
800
+ ): Promise<typeof schema.capabilityOperations.$inferSelect> {
801
+ const [operation] = await db
802
+ .select()
803
+ .from(schema.capabilityOperations)
804
+ .where(
805
+ and(
806
+ eq(schema.capabilityOperations.id, input.operationId),
807
+ eq(schema.capabilityOperations.accountId, input.accountId),
808
+ eq(schema.capabilityOperations.workspaceId, input.workspaceId),
809
+ eq(schema.capabilityOperations.targetKind, "pack"),
810
+ eq(schema.capabilityOperations.status, "running"),
811
+ eq(schema.capabilityOperations.version, input.operationVersion),
812
+ ),
813
+ )
814
+ .for("update")
815
+ .limit(1);
816
+ if (!operation) throw new PackOperationClaimLostError("Pack operation claim was lost");
817
+ const [installation] = await db
818
+ .select({ id: schema.packInstallations.id })
819
+ .from(schema.packInstallations)
820
+ .where(
821
+ and(
822
+ eq(schema.packInstallations.id, input.packInstallationId),
823
+ eq(schema.packInstallations.accountId, input.accountId),
824
+ eq(schema.packInstallations.workspaceId, input.workspaceId),
825
+ eq(schema.packInstallations.packId, operation.targetId),
826
+ ),
827
+ )
828
+ .limit(1);
829
+ if (!installation) {
830
+ throw new PackOperationClaimLostError("Pack operation no longer matches its installation");
831
+ }
832
+ return operation;
833
+ }
834
+
835
+ function samePackInstallationIntent(
836
+ existing: typeof schema.packInstallations.$inferSelect,
837
+ input: {
838
+ manifestDigest: string;
839
+ selectedRigId: string | null;
840
+ metadata: Record<string, unknown>;
841
+ },
842
+ ): boolean {
843
+ return (
844
+ existing.manifestDigest === input.manifestDigest &&
845
+ existing.selectedRigId === input.selectedRigId &&
846
+ stableJson(existing.metadata) === stableJson(input.metadata)
847
+ );
848
+ }
849
+
850
+ async function assertRegisteredPackManifestCurrent(
851
+ db: Database,
852
+ input: {
853
+ workspaceId: string;
854
+ packId: string;
855
+ expectedDigest: string;
856
+ admittedDigest: string;
857
+ },
858
+ ): Promise<void> {
859
+ const [registration] = await db
860
+ .select({ manifest: schema.workspacePacks.manifest })
861
+ .from(schema.workspacePacks)
862
+ .where(
863
+ and(
864
+ eq(schema.workspacePacks.workspaceId, input.workspaceId),
865
+ eq(schema.workspacePacks.packId, input.packId),
866
+ ),
867
+ )
868
+ .for("update")
869
+ .limit(1);
870
+ const parsed = registration ? CapabilityPackSchema.safeParse(registration.manifest) : null;
871
+ const currentDigest =
872
+ parsed?.success === true
873
+ ? createHash("sha256").update(stableJson(parsed.data)).digest("hex")
874
+ : null;
875
+ if (input.expectedDigest !== input.admittedDigest || currentDigest !== input.expectedDigest) {
876
+ throw new PackManifestChangedError(
877
+ "Pack manifest changed after preview; review the current installation plan",
878
+ );
879
+ }
880
+ }
881
+
882
+ async function completePackOperation(
883
+ db: Database,
884
+ operationId: string,
885
+ operationVersion: number,
886
+ result: Record<string, unknown>,
887
+ ): Promise<void> {
888
+ const now = new Date();
889
+ const [completed] = await db
890
+ .update(schema.capabilityOperations)
891
+ .set({
892
+ status: "completed",
893
+ phase: "completed",
894
+ result,
895
+ errorCode: null,
896
+ version: sql`${schema.capabilityOperations.version} + 1`,
897
+ completedAt: now,
898
+ updatedAt: now,
899
+ })
900
+ .where(
901
+ and(
902
+ eq(schema.capabilityOperations.id, operationId),
903
+ eq(schema.capabilityOperations.status, "running"),
904
+ eq(schema.capabilityOperations.version, operationVersion),
905
+ ),
906
+ )
907
+ .returning({ id: schema.capabilityOperations.id });
908
+ if (!completed) throw new PackOperationClaimLostError("Pack operation claim was lost");
909
+ }
910
+
911
+ function mapPackInstallation(row: typeof schema.packInstallations.$inferSelect): PackInstallation {
912
+ return {
913
+ id: row.id,
914
+ accountId: row.accountId,
915
+ workspaceId: row.workspaceId,
916
+ packId: row.packId,
917
+ status: row.status as PackInstallationStatus,
918
+ version: row.version,
919
+ manifestSnapshot: row.manifestSnapshot
920
+ ? (row.manifestSnapshot as unknown as CapabilityPack)
921
+ : null,
922
+ manifestDigest: row.manifestDigest,
923
+ selectedRigId: row.selectedRigId,
924
+ installedBySubjectId: row.installedBySubjectId,
925
+ metadata: row.metadata,
926
+ enabledAt: row.enabledAt.toISOString(),
927
+ updatedAt: row.updatedAt.toISOString(),
928
+ };
929
+ }