@opengeni/db 0.28.1 → 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.
- package/dist/attached-browser-devices.d.ts +42 -0
- package/dist/attempt-tool-catalogs.d.ts +16 -0
- package/dist/browser-auth.d.ts +91 -0
- package/dist/browser-identities.d.ts +128 -0
- package/dist/browser-sessions.d.ts +272 -0
- package/dist/browser-state-artifacts.d.ts +17 -0
- package/dist/capability-components.d.ts +37 -0
- package/dist/capability-integrations.d.ts +192 -0
- package/dist/chunk-F5FT7BEZ.js +3535 -0
- package/dist/chunk-F5FT7BEZ.js.map +1 -0
- package/dist/chunk-ISI2TIUG.js +639 -0
- package/dist/chunk-ISI2TIUG.js.map +1 -0
- package/dist/chunk-MWDVXQOL.js +11639 -0
- package/dist/chunk-MWDVXQOL.js.map +1 -0
- package/dist/chunk-NBE6CHRI.js +955 -0
- package/dist/chunk-NBE6CHRI.js.map +1 -0
- package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
- package/dist/chunk-YHZXQ7HP.js.map +1 -0
- package/dist/codemode-operations.d.ts +117 -0
- package/dist/codex-token-resolver.d.ts +2 -2
- package/dist/company-profile-schema.d.ts +798 -0
- package/dist/company-profile.d.ts +268 -0
- package/dist/computer-sessions.d.ts +109 -0
- package/dist/connection-token-resolver.d.ts +9 -4
- package/dist/database.d.ts +52 -3
- package/dist/editable-artifact-durable-export.d.ts +153 -0
- package/dist/editable-artifact-durable-export.js +788 -0
- package/dist/editable-artifact-durable-export.js.map +1 -0
- package/dist/editable-artifact-materialization.d.ts +123 -0
- package/dist/editable-artifacts-schema.d.ts +4152 -0
- package/dist/editable-artifacts.d.ts +610 -0
- package/dist/editable-artifacts.js +26 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/environment-crypto.d.ts +8 -4
- package/dist/generated-images.d.ts +139 -0
- package/dist/index.d.ts +958 -126
- package/dist/index.js +31204 -12172
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +51 -4
- package/dist/integration-bindings.d.ts +70 -0
- package/dist/integration-features.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +3 -0
- package/dist/interaction-schema.d.ts +5372 -0
- package/dist/knowledge-source-sync-schema.d.ts +1679 -0
- package/dist/knowledge-source-sync.d.ts +274 -0
- package/dist/lossless-columns.d.ts +58 -0
- package/dist/lossless-json.d.ts +39 -0
- package/dist/memory-domain.d.ts +3 -6
- package/dist/memory-governance.d.ts +2 -2
- package/dist/memory-slack-delivery.d.ts +101 -0
- package/dist/new-session-drafts.d.ts +2 -2
- package/dist/pack-components.d.ts +76 -0
- package/dist/pack-installations.d.ts +86 -0
- package/dist/persistence-errors.d.ts +7 -14
- package/dist/plugin-packages.d.ts +126 -0
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +9 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +18 -6
- package/dist/schema.d.ts +19122 -8731
- package/dist/schema.js +173 -1
- package/dist/scoped-knowledge.d.ts +97 -1
- package/dist/session-control.d.ts +4 -4
- package/dist/session-queue-commands.d.ts +19 -11
- package/dist/session-realtime-context.d.ts +2 -2
- package/dist/session-realtime-ledger.d.ts +4 -4
- package/dist/session-realtime-mirror.d.ts +2 -2
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +8 -1
- package/dist/session-realtime.d.ts +7 -7
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/slack-user-link-access.d.ts +54 -0
- package/dist/transcription-recordings-schema.d.ts +44 -6
- package/dist/transcription-recordings.d.ts +3 -3
- package/dist/turn-initiator.d.ts +2 -2
- package/dist/video-generation.d.ts +216 -0
- package/dist/video-generation.js +66 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-learning-policy-schema.d.ts +932 -0
- package/dist/workspace-learning-policy.d.ts +161 -0
- package/drizzle/0065_enrollment_credential_generation.sql +10 -0
- package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
- package/drizzle/0176_lossless_canonical_json.sql +975 -0
- package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
- package/drizzle/0178_permissioned_secret_reads.sql +14 -0
- package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
- package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
- package/drizzle/0181_connected_machine_removal.sql +52 -0
- package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
- package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
- package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
- package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
- package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
- package/drizzle/0187_generated_image_artifacts.sql +173 -0
- package/drizzle/0188_image_generation_retention_failure.sql +25 -0
- package/drizzle/0189_retained_session_image_formats.sql +13 -0
- package/drizzle/0190_timeline_annotations.sql +31 -0
- package/drizzle/0191_editable_artifact_engine.sql +3853 -0
- package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
- package/drizzle/0193_editable_artifact_authorization.sql +190 -0
- package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
- package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
- package/drizzle/0196_rig_provider_images.sql +152 -0
- package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
- package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
- package/drizzle/0199_workspace_learning_policy.sql +980 -0
- package/drizzle/0201_company_profile_authority.sql +849 -0
- package/drizzle/0202_document_index_checkpoints.sql +85 -0
- package/drizzle/0203_durable_video_generation.sql +353 -0
- package/drizzle/0204_video_generation_funding.sql +44 -0
- package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
- package/drizzle/0206_browser_sessions.sql +646 -0
- package/drizzle/0207_browser_identities.sql +379 -0
- package/drizzle/0208_attached_browser_devices.sql +165 -0
- package/drizzle/0209_computer_sessions.sql +575 -0
- package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
- package/drizzle/0211_editable_artifact_session_links.sql +58 -0
- package/drizzle/0212_slack_installation_bindings.sql +348 -0
- package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
- package/drizzle/0214_session_activity_commit_gate.sql +262 -0
- package/drizzle/0215_capabilities_platform.sql +980 -0
- package/drizzle/0216_pack_component_ownership.sql +235 -0
- package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
- package/package.json +18 -5
- package/src/attached-browser-devices.ts +371 -0
- package/src/attempt-tool-catalogs.ts +131 -0
- package/src/browser-auth.ts +1896 -0
- package/src/browser-identities.ts +1168 -0
- package/src/browser-sessions.ts +2005 -0
- package/src/browser-state-artifacts.ts +59 -0
- package/src/capability-components.ts +166 -0
- package/src/capability-integrations.ts +1663 -0
- package/src/codemode-operations.ts +674 -0
- package/src/company-profile-schema.ts +143 -0
- package/src/company-profile.ts +968 -0
- package/src/computer-sessions.ts +1130 -0
- package/src/connection-token-resolver.ts +270 -25
- package/src/database.ts +434 -24
- package/src/editable-artifact-durable-export.ts +1201 -0
- package/src/editable-artifact-materialization.ts +893 -0
- package/src/editable-artifacts-schema.ts +879 -0
- package/src/editable-artifacts.ts +5404 -0
- package/src/environment-crypto.ts +22 -9
- package/src/generated-images.ts +740 -0
- package/src/index.ts +10117 -2730
- package/src/insights.ts +160 -13
- package/src/integration-bindings.ts +455 -0
- package/src/integration-features.ts +859 -0
- package/src/interaction-revisions.ts +40 -0
- package/src/interaction-schema.ts +1303 -0
- package/src/knowledge-source-sync-schema.ts +192 -0
- package/src/knowledge-source-sync.ts +1922 -0
- package/src/lossless-columns.ts +35 -0
- package/src/lossless-json.ts +322 -0
- package/src/memory-domain.ts +5 -44
- package/src/memory-slack-delivery.ts +801 -0
- package/src/pack-components.ts +954 -0
- package/src/pack-installations.ts +929 -0
- package/src/persistence-errors.ts +26 -34
- package/src/plugin-packages.ts +1186 -0
- package/src/provision-roles.ts +302 -1
- package/src/runtime-posture-cli.ts +9 -0
- package/src/runtime-posture.ts +514 -5
- package/src/schema.ts +5177 -2957
- package/src/scoped-knowledge-schema.ts +1 -1
- package/src/scoped-knowledge.ts +655 -3
- package/src/session-control.ts +133 -81
- package/src/session-queue-commands.ts +511 -247
- package/src/session-realtime-context.ts +20 -7
- package/src/session-realtime-ledger.ts +32 -10
- package/src/session-realtime-mirror.ts +4 -2
- package/src/session-realtime-terminal.ts +89 -21
- package/src/session-realtime.ts +10 -9
- package/src/session-tool-call-settlement.ts +38 -15
- package/src/slack-user-link-access.ts +702 -0
- package/src/transcription-recordings-schema.ts +5 -2
- package/src/transcription-recordings.ts +70 -40
- package/src/video-generation.ts +1452 -0
- package/src/workspace-learning-policy-schema.ts +139 -0
- package/src/workspace-learning-policy.ts +595 -0
- package/dist/chunk-DBS5HPEW.js.map +0 -1
- package/dist/chunk-HT5T62CC.js +0 -6803
- package/dist/chunk-HT5T62CC.js.map +0 -1
- package/dist/event-payload-sanitizer.d.ts +0 -32
- package/src/event-payload-sanitizer.ts +0 -377
|
@@ -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
|
+
}
|