@opengeni/db 0.28.9 → 0.36.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 (225) 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 +299 -0
  4. package/dist/browser-downloads.d.ts +51 -0
  5. package/dist/browser-identities.d.ts +137 -0
  6. package/dist/browser-sessions.d.ts +354 -0
  7. package/dist/browser-state-artifacts.d.ts +91 -0
  8. package/dist/canonical-human-identities.d.ts +48 -0
  9. package/dist/canonical-human-identities.js +28 -0
  10. package/dist/canonical-human-identities.js.map +1 -0
  11. package/dist/capability-components.d.ts +37 -0
  12. package/dist/capability-integrations.d.ts +194 -0
  13. package/dist/chunk-5KTIEDX7.js +13200 -0
  14. package/dist/chunk-5KTIEDX7.js.map +1 -0
  15. package/dist/chunk-CQZ4QSP5.js +995 -0
  16. package/dist/chunk-CQZ4QSP5.js.map +1 -0
  17. package/dist/chunk-F32YT7MP.js +117 -0
  18. package/dist/chunk-F32YT7MP.js.map +1 -0
  19. package/dist/chunk-JC5QPLUM.js +666 -0
  20. package/dist/chunk-JC5QPLUM.js.map +1 -0
  21. package/dist/chunk-P3RE4D2B.js +3535 -0
  22. package/dist/chunk-P3RE4D2B.js.map +1 -0
  23. package/dist/chunk-TAUTWOS5.js +259 -0
  24. package/dist/chunk-TAUTWOS5.js.map +1 -0
  25. package/dist/chunk-VKLB2ZGI.js +2389 -0
  26. package/dist/chunk-VKLB2ZGI.js.map +1 -0
  27. package/dist/codemode-operations.d.ts +117 -0
  28. package/dist/codex-token-resolver.d.ts +2 -2
  29. package/dist/company-profile-schema.d.ts +798 -0
  30. package/dist/company-profile.d.ts +268 -0
  31. package/dist/computer-sessions.d.ts +109 -0
  32. package/dist/connection-token-resolver.d.ts +9 -4
  33. package/dist/database.d.ts +57 -3
  34. package/dist/editable-artifact-durable-export.d.ts +153 -0
  35. package/dist/editable-artifact-durable-export.js +788 -0
  36. package/dist/editable-artifact-durable-export.js.map +1 -0
  37. package/dist/editable-artifact-materialization.d.ts +123 -0
  38. package/dist/editable-artifacts-schema.d.ts +4152 -0
  39. package/dist/editable-artifacts.d.ts +610 -0
  40. package/dist/editable-artifacts.js +26 -0
  41. package/dist/editable-artifacts.js.map +1 -0
  42. package/dist/generated-images.d.ts +139 -0
  43. package/dist/index.d.ts +1122 -126
  44. package/dist/index.js +42329 -18407
  45. package/dist/index.js.map +1 -1
  46. package/dist/insights.d.ts +73 -10
  47. package/dist/integration-bindings.d.ts +70 -0
  48. package/dist/integration-facets.d.ts +99 -0
  49. package/dist/interaction-revisions.d.ts +14 -0
  50. package/dist/interaction-schema.d.ts +6036 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  52. package/dist/knowledge-source-sync.d.ts +274 -0
  53. package/dist/memory-governance.d.ts +2 -2
  54. package/dist/memory-slack-delivery.d.ts +101 -0
  55. package/dist/new-session-drafts.d.ts +2 -2
  56. package/dist/pack-components.d.ts +76 -0
  57. package/dist/pack-installations.d.ts +86 -0
  58. package/dist/persistence-errors.d.ts +3 -3
  59. package/dist/plugin-packages.d.ts +126 -0
  60. package/dist/preference-registry.d.ts +1 -1
  61. package/dist/provision-roles.d.ts +9 -1
  62. package/dist/provision-roles.js +1 -1
  63. package/dist/runtime-posture.d.ts +19 -6
  64. package/dist/schema.d.ts +22654 -9359
  65. package/dist/schema.js +209 -3
  66. package/dist/scoped-knowledge.d.ts +97 -1
  67. package/dist/session-control.d.ts +33 -7
  68. package/dist/session-queue-commands.d.ts +19 -11
  69. package/dist/session-realtime-context.d.ts +2 -2
  70. package/dist/session-realtime-ledger.d.ts +4 -4
  71. package/dist/session-realtime-mirror.d.ts +2 -2
  72. package/dist/session-realtime-state.d.ts +1 -1
  73. package/dist/session-realtime-terminal.d.ts +1 -1
  74. package/dist/session-realtime.d.ts +7 -7
  75. package/dist/session-tenancy.d.ts +44 -0
  76. package/dist/session-tenancy.js +16 -0
  77. package/dist/session-tenancy.js.map +1 -0
  78. package/dist/session-tool-call-settlement.d.ts +1 -1
  79. package/dist/slack-task-policy-schema.d.ts +847 -0
  80. package/dist/slack-task-policy.d.ts +65 -0
  81. package/dist/slack-user-link-access.d.ts +54 -0
  82. package/dist/task-notes-schema.d.ts +924 -0
  83. package/dist/task-notes.d.ts +85 -0
  84. package/dist/transcription-recordings.d.ts +2 -2
  85. package/dist/turn-initiator.d.ts +2 -2
  86. package/dist/video-generation.d.ts +225 -0
  87. package/dist/video-generation.js +68 -0
  88. package/dist/video-generation.js.map +1 -0
  89. package/dist/workspace-artifacts.d.ts +1 -1
  90. package/dist/workspace-instruction-policies.d.ts +1 -1
  91. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  92. package/dist/workspace-learning-policy.d.ts +161 -0
  93. package/dist/xai-subscription.d.ts +326 -0
  94. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  95. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  96. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  97. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  98. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  99. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  100. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  101. package/drizzle/0190_timeline_annotations.sql +31 -0
  102. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  103. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  104. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  105. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  106. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  107. package/drizzle/0196_rig_provider_images.sql +152 -0
  108. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  109. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  110. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  111. package/drizzle/0201_company_profile_authority.sql +849 -0
  112. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  113. package/drizzle/0203_durable_video_generation.sql +353 -0
  114. package/drizzle/0204_video_generation_funding.sql +44 -0
  115. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  116. package/drizzle/0206_browser_sessions.sql +646 -0
  117. package/drizzle/0207_browser_identities.sql +379 -0
  118. package/drizzle/0208_attached_browser_devices.sql +165 -0
  119. package/drizzle/0209_computer_sessions.sql +575 -0
  120. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  121. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  122. package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
  123. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  124. package/drizzle/0213_browser_interaction_authority.sql +67 -0
  125. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  126. package/drizzle/0214_browser_download_saves.sql +34 -0
  127. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  128. package/drizzle/0215_browser_controller_host.sql +385 -0
  129. package/drizzle/0215_capabilities_platform.sql +980 -0
  130. package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
  131. package/drizzle/0216_pack_component_ownership.sql +235 -0
  132. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  133. package/drizzle/0217_external_browser_auth_operations.sql +22 -0
  134. package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
  135. package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
  136. package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
  137. package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
  138. package/drizzle/0220_session_channels.sql +54 -0
  139. package/drizzle/0221_sessions_channel_index.sql +5 -0
  140. package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
  141. package/drizzle/0222_sessions_channel_fk.sql +89 -0
  142. package/drizzle/0223_pending_tool_event_output.sql +55 -0
  143. package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
  144. package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
  145. package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
  146. package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
  147. package/drizzle/0227_slack_native_actions.sql +145 -0
  148. package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
  149. package/drizzle/0228_slack_task_policy.sql +400 -0
  150. package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
  151. package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
  152. package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
  153. package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
  154. package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
  155. package/drizzle/0234_xai_subscription_authority.sql +1139 -0
  156. package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
  157. package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
  158. package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
  159. package/drizzle/0237_interaction_transition_reaper.sql +263 -0
  160. package/drizzle/0238_goal_persistence_policy.sql +471 -0
  161. package/drizzle/0238_supergrok_realtime_model.sql +29 -0
  162. package/drizzle/0239_supergrok_video_funding.sql +50 -0
  163. package/drizzle/0239_task_tree_notes.sql +990 -0
  164. package/package.json +26 -5
  165. package/src/attached-browser-devices.ts +371 -0
  166. package/src/attempt-tool-catalogs.ts +131 -0
  167. package/src/browser-auth.ts +4373 -0
  168. package/src/browser-downloads.ts +517 -0
  169. package/src/browser-identities.ts +1385 -0
  170. package/src/browser-sessions.ts +2294 -0
  171. package/src/browser-state-artifacts.ts +449 -0
  172. package/src/canonical-human-identities.ts +317 -0
  173. package/src/capability-components.ts +166 -0
  174. package/src/capability-integrations.ts +1571 -0
  175. package/src/codemode-operations.ts +674 -0
  176. package/src/company-profile-schema.ts +143 -0
  177. package/src/company-profile.ts +968 -0
  178. package/src/computer-sessions.ts +1130 -0
  179. package/src/connection-token-resolver.ts +270 -25
  180. package/src/database.ts +468 -24
  181. package/src/editable-artifact-durable-export.ts +1201 -0
  182. package/src/editable-artifact-materialization.ts +893 -0
  183. package/src/editable-artifacts-schema.ts +879 -0
  184. package/src/editable-artifacts.ts +5404 -0
  185. package/src/generated-images.ts +740 -0
  186. package/src/index.ts +9963 -1465
  187. package/src/insights.ts +251 -23
  188. package/src/integration-bindings.ts +455 -0
  189. package/src/integration-facets.ts +862 -0
  190. package/src/interaction-revisions.ts +70 -0
  191. package/src/interaction-schema.ts +1606 -0
  192. package/src/knowledge-source-sync-schema.ts +192 -0
  193. package/src/knowledge-source-sync.ts +1922 -0
  194. package/src/memory-slack-delivery.ts +801 -0
  195. package/src/pack-components.ts +954 -0
  196. package/src/pack-installations.ts +886 -0
  197. package/src/persistence-errors.ts +4 -7
  198. package/src/plugin-packages.ts +1186 -0
  199. package/src/provision-roles.ts +534 -1
  200. package/src/runtime-posture-cli.ts +9 -0
  201. package/src/runtime-posture.ts +721 -5
  202. package/src/schema.ts +6270 -3159
  203. package/src/scoped-knowledge-schema.ts +1 -1
  204. package/src/scoped-knowledge.ts +655 -3
  205. package/src/session-control.ts +130 -42
  206. package/src/session-queue-commands.ts +265 -15
  207. package/src/session-realtime-context.ts +2 -2
  208. package/src/session-realtime-ledger.ts +3 -3
  209. package/src/session-realtime.ts +7 -7
  210. package/src/session-tenancy.ts +188 -0
  211. package/src/session-tool-call-settlement.ts +18 -3
  212. package/src/slack-task-policy-schema.ts +130 -0
  213. package/src/slack-task-policy.ts +341 -0
  214. package/src/slack-user-link-access.ts +702 -0
  215. package/src/task-notes-schema.ts +135 -0
  216. package/src/task-notes.ts +178 -0
  217. package/src/video-generation.ts +1530 -0
  218. package/src/workspace-artifacts.ts +1 -1
  219. package/src/workspace-learning-policy-schema.ts +139 -0
  220. package/src/workspace-learning-policy.ts +595 -0
  221. package/src/xai-subscription.ts +1887 -0
  222. package/dist/chunk-P6CUHXQZ.js +0 -7248
  223. package/dist/chunk-P6CUHXQZ.js.map +0 -1
  224. package/dist/chunk-VHBFHMPC.js +0 -1219
  225. package/dist/chunk-VHBFHMPC.js.map +0 -1
@@ -0,0 +1,1530 @@
1
+ import {
2
+ GENERATED_VIDEO_MAX_BYTES,
3
+ GeneratedVideoReceipt,
4
+ GROK_IMAGINE_VIDEO_1_5_MODEL_ID,
5
+ SEEDANCE_2_5_MODEL_ID,
6
+ VideoGenerationPolicy,
7
+ VideoGenerationOperationSummary,
8
+ retainedGeneratedVideoReferenceFromFile,
9
+ type GeneratedVideoFacts,
10
+ type MediaGenerationResult,
11
+ type VideoGenerationFundingSource,
12
+ type VideoGenerationPolicy as VideoGenerationPolicyValue,
13
+ } from "@opengeni/contracts";
14
+ import { and, eq, inArray, sql } from "drizzle-orm";
15
+ import type { Database } from "./database";
16
+ import { rawRows, withRlsContext, withWorkspaceRls } from "./database";
17
+ import * as schema from "./schema";
18
+
19
+ export const ACTIVE_VIDEO_GENERATION_STATUSES = [
20
+ "preparing",
21
+ "prepared",
22
+ "accepted",
23
+ "submission_uncertain",
24
+ "provider_started",
25
+ "retaining",
26
+ ] as const;
27
+
28
+ export type VideoGenerationOperation = typeof schema.videoGenerationOperations.$inferSelect;
29
+ export type VideoGenerationReference = typeof schema.videoGenerationReferences.$inferSelect;
30
+ export type GeneratedVideoArtifact = typeof schema.generatedVideoArtifacts.$inferSelect;
31
+
32
+ export type VideoGenerationOperationWithReferences = VideoGenerationOperation & {
33
+ references: VideoGenerationReference[];
34
+ };
35
+
36
+ export class VideoGenerationConflictError extends Error {
37
+ constructor(message: string) {
38
+ super(message);
39
+ this.name = "VideoGenerationConflictError";
40
+ }
41
+ }
42
+
43
+ export class VideoGenerationCapacityError extends Error {
44
+ constructor(message: string) {
45
+ super(message);
46
+ this.name = "VideoGenerationCapacityError";
47
+ }
48
+ }
49
+
50
+ export class VideoGenerationCreditError extends Error {
51
+ constructor(message = "insufficient OpenGeni credits") {
52
+ super(message);
53
+ this.name = "VideoGenerationCreditError";
54
+ }
55
+ }
56
+
57
+ export async function getWorkspaceVideoGenerationPolicy(
58
+ db: Database,
59
+ workspaceId: string,
60
+ ): Promise<VideoGenerationPolicyValue> {
61
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
62
+ const [row] = await scopedDb
63
+ .select()
64
+ .from(schema.workspaceVideoGenerationPolicies)
65
+ .where(eq(schema.workspaceVideoGenerationPolicies.workspaceId, workspaceId))
66
+ .limit(1);
67
+ return policyFromRow(row);
68
+ });
69
+ }
70
+
71
+ export async function updateWorkspaceVideoGenerationPolicy(
72
+ db: Database,
73
+ input: {
74
+ accountId: string;
75
+ workspaceId: string;
76
+ subjectId: string;
77
+ expectedRevision: number;
78
+ fundingSource: VideoGenerationFundingSource;
79
+ enabledModelIds: string[];
80
+ defaultModelId: string | null;
81
+ },
82
+ ): Promise<VideoGenerationPolicyValue> {
83
+ const requested = VideoGenerationPolicy.parse({
84
+ schemaVersion: 1,
85
+ revision: input.expectedRevision + 1,
86
+ fundingSource: input.fundingSource,
87
+ enabledModelIds: input.enabledModelIds,
88
+ defaultModelId: input.defaultModelId,
89
+ });
90
+ assertSupportedModels(requested.enabledModelIds);
91
+ return await withRlsContext(
92
+ db,
93
+ { accountId: input.accountId, workspaceId: input.workspaceId },
94
+ async (scopedDb) =>
95
+ await scopedDb.transaction(async (tx) => {
96
+ const [workspace] = await tx
97
+ .select({ id: schema.workspaces.id })
98
+ .from(schema.workspaces)
99
+ .where(
100
+ and(
101
+ eq(schema.workspaces.id, input.workspaceId),
102
+ eq(schema.workspaces.accountId, input.accountId),
103
+ ),
104
+ )
105
+ .for("update")
106
+ .limit(1);
107
+ if (!workspace) throw new Error("Workspace not found");
108
+ const [current] = await tx
109
+ .select()
110
+ .from(schema.workspaceVideoGenerationPolicies)
111
+ .where(eq(schema.workspaceVideoGenerationPolicies.workspaceId, input.workspaceId))
112
+ .for("update")
113
+ .limit(1);
114
+ const currentRevision = current?.revision ?? 0;
115
+ if (currentRevision !== input.expectedRevision) {
116
+ throw new VideoGenerationConflictError("Video generation policy changed");
117
+ }
118
+ const now = new Date();
119
+ const values = {
120
+ workspaceId: input.workspaceId,
121
+ accountId: input.accountId,
122
+ revision: currentRevision + 1,
123
+ fundingSource: requested.fundingSource,
124
+ enabledModelIds: [...requested.enabledModelIds],
125
+ defaultModelId: requested.defaultModelId,
126
+ updatedBySubjectId: input.subjectId.slice(0, 1_024),
127
+ updatedAt: now,
128
+ };
129
+ const [row] = current
130
+ ? await tx
131
+ .update(schema.workspaceVideoGenerationPolicies)
132
+ .set(values)
133
+ .where(eq(schema.workspaceVideoGenerationPolicies.workspaceId, input.workspaceId))
134
+ .returning()
135
+ : await tx.insert(schema.workspaceVideoGenerationPolicies).values(values).returning();
136
+ if (!row) throw new Error("Video generation policy update was not persisted");
137
+ return policyFromRow(row);
138
+ }),
139
+ );
140
+ }
141
+
142
+ export type AdmitVideoGenerationOperationInput = {
143
+ id: string;
144
+ accountId: string;
145
+ workspaceId: string;
146
+ sessionId: string;
147
+ turnId: string;
148
+ attemptId: string;
149
+ toolCallId: string;
150
+ admissionKey: string;
151
+ requestDigest: string;
152
+ promptDigest: string;
153
+ requestEncrypted: string;
154
+ modelId: string;
155
+ sourceMode: string;
156
+ capabilityRevision: string;
157
+ policyRevision: number;
158
+ fundingSource: VideoGenerationFundingSource;
159
+ pricedCostMicros: number;
160
+ connectionId: string | null;
161
+ credentialVersion: number;
162
+ credentialEncrypted: string;
163
+ providerIdempotencyKey: string;
164
+ expectedArtifactId: string;
165
+ expectedFileId: string;
166
+ reservedBytes?: number;
167
+ workspaceQuotaBytes: number;
168
+ maxConcurrentPerWorkspace: number;
169
+ recoveryDeadlineAt: Date;
170
+ references: Array<{
171
+ ordinal: number;
172
+ role: string;
173
+ contentType: string;
174
+ sizeBytes: number;
175
+ sha256: string;
176
+ stagingObjectKey: string;
177
+ grantExpiresAt?: Date | null;
178
+ cleanupAfter: Date;
179
+ }>;
180
+ };
181
+
182
+ export async function admitVideoGenerationOperation(
183
+ db: Database,
184
+ input: AdmitVideoGenerationOperationInput,
185
+ ): Promise<{ operation: VideoGenerationOperation; created: boolean }> {
186
+ const reservedBytes = input.reservedBytes ?? GENERATED_VIDEO_MAX_BYTES;
187
+ return await withRlsContext(
188
+ db,
189
+ { accountId: input.accountId, workspaceId: input.workspaceId },
190
+ async (scopedDb) =>
191
+ await scopedDb.transaction(async (tx) => {
192
+ await tx.execute(
193
+ sql`select pg_advisory_xact_lock(hashtextextended(${`video-generation:${input.admissionKey}`}, 0))`,
194
+ );
195
+ const [existing] = await tx
196
+ .select()
197
+ .from(schema.videoGenerationOperations)
198
+ .where(
199
+ and(
200
+ eq(schema.videoGenerationOperations.workspaceId, input.workspaceId),
201
+ eq(schema.videoGenerationOperations.admissionKey, input.admissionKey),
202
+ ),
203
+ )
204
+ .limit(1);
205
+ if (existing) {
206
+ assertAdmissionMatches(existing, input);
207
+ return { operation: existing, created: false };
208
+ }
209
+ const [policy] = await tx
210
+ .select()
211
+ .from(schema.workspaceVideoGenerationPolicies)
212
+ .where(eq(schema.workspaceVideoGenerationPolicies.workspaceId, input.workspaceId))
213
+ .for("update")
214
+ .limit(1);
215
+ if (
216
+ !policy ||
217
+ policy.revision !== input.policyRevision ||
218
+ policy.fundingSource !== input.fundingSource ||
219
+ !policy.enabledModelIds.includes(input.modelId)
220
+ ) {
221
+ throw new VideoGenerationConflictError("Video generation capability changed");
222
+ }
223
+ const active = await tx
224
+ .select({ id: schema.videoGenerationOperations.id })
225
+ .from(schema.videoGenerationOperations)
226
+ .where(
227
+ and(
228
+ eq(schema.videoGenerationOperations.workspaceId, input.workspaceId),
229
+ inArray(schema.videoGenerationOperations.status, [
230
+ ...ACTIVE_VIDEO_GENERATION_STATUSES,
231
+ ]),
232
+ ),
233
+ )
234
+ .for("update");
235
+ if (active.length >= input.maxConcurrentPerWorkspace) {
236
+ throw new VideoGenerationCapacityError("Workspace video generation limit reached");
237
+ }
238
+ await tx
239
+ .insert(schema.workspaceVideoGenerationQuotas)
240
+ .values({
241
+ workspaceId: input.workspaceId,
242
+ accountId: input.accountId,
243
+ })
244
+ .onConflictDoNothing({
245
+ target: schema.workspaceVideoGenerationQuotas.workspaceId,
246
+ });
247
+ const [quota] = await tx
248
+ .select()
249
+ .from(schema.workspaceVideoGenerationQuotas)
250
+ .where(eq(schema.workspaceVideoGenerationQuotas.workspaceId, input.workspaceId))
251
+ .for("update")
252
+ .limit(1);
253
+ if (!quota) throw new Error("Video generation quota row disappeared");
254
+ if (quota.reservedBytes + quota.readyBytes + reservedBytes > input.workspaceQuotaBytes) {
255
+ throw new VideoGenerationCapacityError("Workspace video storage quota reached");
256
+ }
257
+ const [operation] = await tx
258
+ .insert(schema.videoGenerationOperations)
259
+ .values({
260
+ id: input.id,
261
+ accountId: input.accountId,
262
+ workspaceId: input.workspaceId,
263
+ sessionId: input.sessionId,
264
+ turnId: input.turnId,
265
+ attemptId: input.attemptId,
266
+ toolCallId: input.toolCallId,
267
+ admissionKey: input.admissionKey,
268
+ requestDigest: input.requestDigest,
269
+ promptDigest: input.promptDigest,
270
+ requestEncrypted: input.requestEncrypted,
271
+ modelId: input.modelId,
272
+ sourceMode: input.sourceMode,
273
+ capabilityRevision: input.capabilityRevision,
274
+ fundingSource: input.fundingSource,
275
+ pricedCostMicros: input.pricedCostMicros,
276
+ creditState: input.pricedCostMicros > 0 ? "debited" : "not_applicable",
277
+ connectionId: input.connectionId,
278
+ credentialVersion: input.credentialVersion,
279
+ credentialEncrypted: input.credentialEncrypted,
280
+ providerIdempotencyKey: input.providerIdempotencyKey,
281
+ expectedArtifactId: input.expectedArtifactId,
282
+ expectedFileId: input.expectedFileId,
283
+ reservedBytes,
284
+ recoveryDeadlineAt: input.recoveryDeadlineAt,
285
+ status: input.references.length === 0 ? "prepared" : "preparing",
286
+ admissionOutputState: "pending",
287
+ terminalUpdateState: "ineligible",
288
+ })
289
+ .returning();
290
+ if (!operation) throw new Error("Video generation operation was not admitted");
291
+ if (input.pricedCostMicros > 0) {
292
+ if (input.fundingSource !== "opengeni_credits" || input.connectionId !== null) {
293
+ throw new Error("Only OpenGeni-funded video operations may debit credits");
294
+ }
295
+ await debitVideoGenerationCredits(tx, operation);
296
+ }
297
+ if (input.references.length > 0) {
298
+ await tx.insert(schema.videoGenerationReferences).values(
299
+ input.references.map((reference) => ({
300
+ operationId: operation.id,
301
+ workspaceId: input.workspaceId,
302
+ accountId: input.accountId,
303
+ grantExpiresAt: reference.grantExpiresAt ?? null,
304
+ ...reference,
305
+ })),
306
+ );
307
+ }
308
+ await tx
309
+ .update(schema.workspaceVideoGenerationQuotas)
310
+ .set({
311
+ reservedBytes: sql`${schema.workspaceVideoGenerationQuotas.reservedBytes} + ${reservedBytes}`,
312
+ updatedAt: new Date(),
313
+ })
314
+ .where(eq(schema.workspaceVideoGenerationQuotas.workspaceId, input.workspaceId));
315
+ return { operation, created: true };
316
+ }),
317
+ );
318
+ }
319
+
320
+ /** Persist a rotated provider token before it is used for another remote call. */
321
+ export async function rotateVideoGenerationCredential(
322
+ db: Database,
323
+ input: {
324
+ accountId: string;
325
+ workspaceId: string;
326
+ operationId: string;
327
+ requestDigest: string;
328
+ expectedCredentialEncrypted: string;
329
+ credentialEncrypted: string;
330
+ },
331
+ ): Promise<VideoGenerationOperation> {
332
+ const [row] = await db
333
+ .update(schema.videoGenerationOperations)
334
+ .set({
335
+ credentialEncrypted: input.credentialEncrypted,
336
+ updatedAt: new Date(),
337
+ })
338
+ .where(
339
+ and(
340
+ eq(schema.videoGenerationOperations.accountId, input.accountId),
341
+ eq(schema.videoGenerationOperations.workspaceId, input.workspaceId),
342
+ eq(schema.videoGenerationOperations.id, input.operationId),
343
+ eq(schema.videoGenerationOperations.requestDigest, input.requestDigest),
344
+ eq(schema.videoGenerationOperations.credentialEncrypted, input.expectedCredentialEncrypted),
345
+ inArray(schema.videoGenerationOperations.status, [
346
+ "accepted",
347
+ "submission_uncertain",
348
+ "provider_started",
349
+ "retaining",
350
+ ]),
351
+ ),
352
+ )
353
+ .returning();
354
+ if (!row) throw new VideoGenerationConflictError("Video provider credential changed");
355
+ return row;
356
+ }
357
+
358
+ /**
359
+ * Seal a reference-bearing operation only after every immutable staging object
360
+ * has been uploaded and independently verified by the worker. The source
361
+ * sandbox path is intentionally not stored, so a failed preparation can only
362
+ * be retried by the same logical tool call while its sandbox still exists.
363
+ */
364
+ export async function markVideoGenerationPrepared(
365
+ db: Database,
366
+ input: OperationMutationInput,
367
+ ): Promise<VideoGenerationOperation> {
368
+ return await mutateOperation(db, input, async (tx, operation) => {
369
+ if (operation.status === "prepared") return operation;
370
+ if (operation.status !== "preparing") throw invalidTransition(operation, "prepared");
371
+ const references = await tx
372
+ .select({
373
+ stagingObjectKey: schema.videoGenerationReferences.stagingObjectKey,
374
+ cleanedAt: schema.videoGenerationReferences.cleanedAt,
375
+ })
376
+ .from(schema.videoGenerationReferences)
377
+ .where(eq(schema.videoGenerationReferences.operationId, operation.id));
378
+ if (
379
+ references.length === 0 ||
380
+ references.some((reference) => !reference.stagingObjectKey || reference.cleanedAt)
381
+ ) {
382
+ throw new VideoGenerationConflictError("Video references are not sealed");
383
+ }
384
+ const [updated] = await tx
385
+ .update(schema.videoGenerationOperations)
386
+ .set({ status: "prepared", updatedAt: new Date() })
387
+ .where(eq(schema.videoGenerationOperations.id, operation.id))
388
+ .returning();
389
+ return requireRow(updated, "seal video generation references");
390
+ });
391
+ }
392
+
393
+ /** Final authorization fence. Call only after the accepted function output is durable. */
394
+ export async function markVideoGenerationAccepted(
395
+ db: Database,
396
+ input: {
397
+ accountId: string;
398
+ workspaceId: string;
399
+ operationId: string;
400
+ requestDigest: string;
401
+ },
402
+ ): Promise<VideoGenerationOperation> {
403
+ return await mutateOperation(
404
+ db,
405
+ input,
406
+ async (tx, operation) =>
407
+ await markVideoGenerationAcceptedInTransaction(tx, {
408
+ workspaceId: input.workspaceId,
409
+ operation,
410
+ }),
411
+ );
412
+ }
413
+
414
+ /**
415
+ * Transactional half of acceptance. The pending function result and this
416
+ * transition must commit together: the provider reconciler never observes an
417
+ * accepted operation whose model-visible `accepted` receipt was lost.
418
+ */
419
+ export async function markVideoGenerationAcceptedInTransaction(
420
+ tx: Database,
421
+ input: {
422
+ workspaceId: string;
423
+ operation: VideoGenerationOperation;
424
+ toolCallId?: string;
425
+ },
426
+ ): Promise<VideoGenerationOperation> {
427
+ const operation = input.operation;
428
+ if (input.toolCallId !== undefined && operation.toolCallId !== input.toolCallId) {
429
+ throw new VideoGenerationConflictError("Video generation tool-call identity changed");
430
+ }
431
+ if (operation.admissionOutputState === "recorded") return operation;
432
+ if (operation.status !== "prepared") {
433
+ throw new VideoGenerationConflictError("Video generation is no longer awaiting acceptance");
434
+ }
435
+ const [policy] = await tx
436
+ .select()
437
+ .from(schema.workspaceVideoGenerationPolicies)
438
+ .where(eq(schema.workspaceVideoGenerationPolicies.workspaceId, input.workspaceId))
439
+ .for("share")
440
+ .limit(1);
441
+ const [connection] = operation.connectionId
442
+ ? await tx
443
+ .select({
444
+ id: schema.connections.id,
445
+ version: schema.connections.version,
446
+ status: schema.connections.status,
447
+ metadata: schema.connections.metadata,
448
+ })
449
+ .from(schema.connections)
450
+ .where(
451
+ and(
452
+ eq(schema.connections.workspaceId, input.workspaceId),
453
+ eq(schema.connections.id, operation.connectionId),
454
+ ),
455
+ )
456
+ .for("share")
457
+ .limit(1)
458
+ : [];
459
+ const workspaceGatewayAuthorized =
460
+ operation.fundingSource === "workspace_gateway" &&
461
+ connection?.status === "active" &&
462
+ connection.version === operation.credentialVersion &&
463
+ connection.metadata.credentialRole === "vercel_ai_gateway";
464
+ const managedAuthorized =
465
+ operation.fundingSource === "opengeni_credits" &&
466
+ operation.connectionId === null &&
467
+ (operation.creditState === "debited" ||
468
+ (operation.creditState === "not_applicable" && operation.pricedCostMicros === 0));
469
+ const supergrokAuthorized =
470
+ operation.fundingSource === "supergrok_subscription" &&
471
+ operation.connectionId === null &&
472
+ operation.credentialEncrypted !== null &&
473
+ operation.pricedCostMicros === 0 &&
474
+ operation.creditState === "not_applicable";
475
+ if (
476
+ !policy ||
477
+ policy.fundingSource !== operation.fundingSource ||
478
+ !policy.enabledModelIds.includes(operation.modelId) ||
479
+ (!workspaceGatewayAuthorized && !managedAuthorized && !supergrokAuthorized)
480
+ ) {
481
+ throw new VideoGenerationConflictError("Video generation authorization changed");
482
+ }
483
+ const now = new Date();
484
+ const [updated] = await tx
485
+ .update(schema.videoGenerationOperations)
486
+ .set({
487
+ status: "accepted",
488
+ admissionOutputState: "recorded",
489
+ terminalUpdateState: "pending",
490
+ nextReconcileAt: now,
491
+ updatedAt: now,
492
+ })
493
+ .where(eq(schema.videoGenerationOperations.id, operation.id))
494
+ .returning();
495
+ return requireRow(updated, "record video generation acceptance");
496
+ }
497
+
498
+ export async function cancelVideoGenerationBeforeSubmit(
499
+ db: Database,
500
+ input: {
501
+ accountId: string;
502
+ workspaceId: string;
503
+ operationId: string;
504
+ requestDigest: string;
505
+ reason: string;
506
+ },
507
+ ): Promise<VideoGenerationOperation> {
508
+ return await mutateOperation(db, input, async (tx, operation) => {
509
+ if (operation.status === "cancelled_before_submit") return operation;
510
+ if (!inPreSubmitState(operation.status) || operation.providerRequestSentAt) {
511
+ throw new VideoGenerationConflictError(
512
+ "Video generation may already have reached the provider",
513
+ );
514
+ }
515
+ const now = new Date();
516
+ await releaseReservedQuota(tx, operation, false);
517
+ const creditState = await refundVideoGenerationCredits(tx, operation, now);
518
+ const [updated] = await tx
519
+ .update(schema.videoGenerationOperations)
520
+ .set({
521
+ status: "cancelled_before_submit",
522
+ creditState,
523
+ quotaState: "released",
524
+ terminalAt: now,
525
+ terminalUpdateState:
526
+ operation.admissionOutputState === "recorded" ? "pending" : "suppressed",
527
+ credentialEncrypted: null,
528
+ requestEncrypted: null,
529
+ boundedPublicReason: boundPublicReason(input.reason),
530
+ updatedAt: now,
531
+ })
532
+ .where(eq(schema.videoGenerationOperations.id, operation.id))
533
+ .returning();
534
+ if (!updated) throw new Error("Video generation cancellation was not recorded");
535
+ return updated;
536
+ });
537
+ }
538
+
539
+ /**
540
+ * Turn-interruption half of pre-submit cancellation. It runs under the same
541
+ * session/turn transaction that closes unresolved tool calls, so an accepted
542
+ * receipt can never race a cancellation of its operation.
543
+ */
544
+ export async function cancelUnacceptedVideoGenerationsForToolCallsInTransaction(
545
+ tx: Database,
546
+ input: {
547
+ workspaceId: string;
548
+ sessionId: string;
549
+ turnId: string;
550
+ toolCallIds: readonly string[];
551
+ reason: string;
552
+ now: Date;
553
+ },
554
+ ): Promise<string[]> {
555
+ const toolCallIds = [...new Set(input.toolCallIds)];
556
+ if (toolCallIds.length === 0) return [];
557
+ const operations = await tx
558
+ .select()
559
+ .from(schema.videoGenerationOperations)
560
+ .where(
561
+ and(
562
+ eq(schema.videoGenerationOperations.workspaceId, input.workspaceId),
563
+ eq(schema.videoGenerationOperations.sessionId, input.sessionId),
564
+ eq(schema.videoGenerationOperations.turnId, input.turnId),
565
+ inArray(schema.videoGenerationOperations.toolCallId, toolCallIds),
566
+ eq(schema.videoGenerationOperations.admissionOutputState, "pending"),
567
+ inArray(schema.videoGenerationOperations.status, ["preparing", "prepared"]),
568
+ sql`${schema.videoGenerationOperations.providerRequestSentAt} is null`,
569
+ ),
570
+ )
571
+ .orderBy(schema.videoGenerationOperations.id)
572
+ .for("update");
573
+ const cancelled: string[] = [];
574
+ for (const operation of operations) {
575
+ await releaseReservedQuota(tx, operation, false);
576
+ const creditState = await refundVideoGenerationCredits(tx, operation, input.now);
577
+ const [updated] = await tx
578
+ .update(schema.videoGenerationOperations)
579
+ .set({
580
+ status: "cancelled_before_submit",
581
+ creditState,
582
+ quotaState: "released",
583
+ terminalAt: input.now,
584
+ terminalUpdateState: "suppressed",
585
+ nextReconcileAt: input.now,
586
+ providerRequestEncrypted: null,
587
+ providerRequestExpiresAt: null,
588
+ credentialEncrypted: null,
589
+ requestEncrypted: null,
590
+ privateDataEraseAfter: input.now,
591
+ boundedPublicReason: boundPublicReason(input.reason),
592
+ reconcileLeaseOwner: null,
593
+ reconcileLeaseExpiresAt: null,
594
+ updatedAt: input.now,
595
+ })
596
+ .where(eq(schema.videoGenerationOperations.id, operation.id))
597
+ .returning({ id: schema.videoGenerationOperations.id });
598
+ if (!updated) throw new Error("Interrupted video generation was not cancelled");
599
+ await tx
600
+ .update(schema.videoGenerationReferences)
601
+ .set({ cleanupAfter: input.now })
602
+ .where(eq(schema.videoGenerationReferences.operationId, operation.id));
603
+ cancelled.push(updated.id);
604
+ }
605
+ return cancelled;
606
+ }
607
+
608
+ export async function claimVideoGenerationOperations(
609
+ db: Database,
610
+ input: { owner: string; leaseSeconds: number; limit: number },
611
+ ): Promise<Array<{ operationId: string; accountId: string; workspaceId: string }>> {
612
+ const rows = await rawRows<{
613
+ operation_id: string;
614
+ account_id: string;
615
+ workspace_id: string;
616
+ }>(
617
+ db,
618
+ sql`select * from opengeni_private.claim_video_generation_operations(
619
+ ${input.owner}, ${input.leaseSeconds}, ${input.limit}
620
+ )`,
621
+ );
622
+ return rows.map((row) => ({
623
+ operationId: row.operation_id,
624
+ accountId: row.account_id,
625
+ workspaceId: row.workspace_id,
626
+ }));
627
+ }
628
+
629
+ export async function getVideoGenerationOperation(
630
+ db: Database,
631
+ workspaceId: string,
632
+ operationId: string,
633
+ ): Promise<VideoGenerationOperationWithReferences | null> {
634
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
635
+ const [operation] = await scopedDb
636
+ .select()
637
+ .from(schema.videoGenerationOperations)
638
+ .where(
639
+ and(
640
+ eq(schema.videoGenerationOperations.workspaceId, workspaceId),
641
+ eq(schema.videoGenerationOperations.id, operationId),
642
+ ),
643
+ )
644
+ .limit(1);
645
+ if (!operation) return null;
646
+ const references = await scopedDb
647
+ .select()
648
+ .from(schema.videoGenerationReferences)
649
+ .where(eq(schema.videoGenerationReferences.operationId, operationId))
650
+ .orderBy(schema.videoGenerationReferences.ordinal);
651
+ return { ...operation, references };
652
+ });
653
+ }
654
+
655
+ export async function markVideoGenerationSubmissionUncertain(
656
+ db: Database,
657
+ input: OperationMutationInput & { nextReconcileAt: Date; error: string },
658
+ ): Promise<VideoGenerationOperation> {
659
+ return await mutateOperation(db, input, async (tx, operation) => {
660
+ if (operation.status !== "submission_uncertain" || !operation.providerRequestEncrypted) {
661
+ throw invalidTransition(operation, "submission_uncertain");
662
+ }
663
+ const now = new Date();
664
+ const [updated] = await tx
665
+ .update(schema.videoGenerationOperations)
666
+ .set({
667
+ status: "submission_uncertain",
668
+ providerRequestSentAt: operation.providerRequestSentAt ?? now,
669
+ nextReconcileAt: input.nextReconcileAt,
670
+ lastError: boundPrivateError(input.error),
671
+ reconcileLeaseOwner: null,
672
+ reconcileLeaseExpiresAt: null,
673
+ updatedAt: now,
674
+ })
675
+ .where(eq(schema.videoGenerationOperations.id, operation.id))
676
+ .returning();
677
+ return requireRow(updated, "record uncertain video submission");
678
+ });
679
+ }
680
+
681
+ /**
682
+ * Freeze the exact provider start body before issuing the request. If the
683
+ * worker disappears after this transaction, recovery replays the same body
684
+ * and idempotency key rather than reminting URLs or changing request bytes.
685
+ */
686
+ export async function markVideoGenerationSubmissionIntent(
687
+ db: Database,
688
+ input: OperationMutationInput & {
689
+ encryptedProviderRequest: string;
690
+ providerRequestExpiresAt: Date;
691
+ nextReconcileAt: Date;
692
+ },
693
+ ): Promise<VideoGenerationOperation> {
694
+ return await mutateOperation(db, input, async (tx, operation) => {
695
+ if (operation.status === "submission_uncertain") {
696
+ if (!operation.providerRequestEncrypted || !operation.providerRequestExpiresAt) {
697
+ throw new VideoGenerationConflictError("Video provider request is incomplete");
698
+ }
699
+ return operation;
700
+ }
701
+ if (operation.status !== "accepted") {
702
+ throw invalidTransition(operation, "submission_uncertain");
703
+ }
704
+ const now = new Date();
705
+ const [updated] = await tx
706
+ .update(schema.videoGenerationOperations)
707
+ .set({
708
+ status: "submission_uncertain",
709
+ providerRequestEncrypted: input.encryptedProviderRequest,
710
+ providerRequestExpiresAt: input.providerRequestExpiresAt,
711
+ nextReconcileAt: input.nextReconcileAt,
712
+ lastError: null,
713
+ reconcileLeaseOwner: null,
714
+ reconcileLeaseExpiresAt: null,
715
+ updatedAt: now,
716
+ })
717
+ .where(eq(schema.videoGenerationOperations.id, operation.id))
718
+ .returning();
719
+ await tx
720
+ .update(schema.videoGenerationReferences)
721
+ .set({
722
+ grantExpiresAt: input.providerRequestExpiresAt,
723
+ cleanupAfter: new Date(
724
+ Math.max(
725
+ operation.recoveryDeadlineAt.getTime(),
726
+ input.providerRequestExpiresAt.getTime(),
727
+ ),
728
+ ),
729
+ })
730
+ .where(eq(schema.videoGenerationReferences.operationId, operation.id));
731
+ return requireRow(updated, "record video provider submission intent");
732
+ });
733
+ }
734
+
735
+ export async function markVideoGenerationProviderStarted(
736
+ db: Database,
737
+ input: OperationMutationInput & {
738
+ providerJobId: string;
739
+ nextReconcileAt: Date;
740
+ },
741
+ ): Promise<VideoGenerationOperation> {
742
+ return await mutateOperation(db, input, async (tx, operation) => {
743
+ if (
744
+ operation.providerJobId === input.providerJobId &&
745
+ operation.status === "provider_started"
746
+ ) {
747
+ return operation;
748
+ }
749
+ if (!inArrayValue(operation.status, ["accepted", "submission_uncertain"])) {
750
+ throw invalidTransition(operation, "provider_started");
751
+ }
752
+ const now = new Date();
753
+ const [updated] = await tx
754
+ .update(schema.videoGenerationOperations)
755
+ .set({
756
+ status: "provider_started",
757
+ providerJobId: input.providerJobId.slice(0, 1_024),
758
+ providerRequestEncrypted: null,
759
+ providerRequestExpiresAt: null,
760
+ providerRequestSentAt: operation.providerRequestSentAt ?? now,
761
+ providerStartedAt: operation.providerStartedAt ?? now,
762
+ nextReconcileAt: input.nextReconcileAt,
763
+ lastError: null,
764
+ reconcileLeaseOwner: null,
765
+ reconcileLeaseExpiresAt: null,
766
+ updatedAt: now,
767
+ })
768
+ .where(eq(schema.videoGenerationOperations.id, operation.id))
769
+ .returning();
770
+ return requireRow(updated, "record video provider job");
771
+ });
772
+ }
773
+
774
+ export async function rescheduleVideoGenerationOperation(
775
+ db: Database,
776
+ input: OperationMutationInput & {
777
+ owner?: string;
778
+ nextReconcileAt: Date;
779
+ error?: string;
780
+ },
781
+ ): Promise<boolean> {
782
+ return await withRlsContext(
783
+ db,
784
+ { accountId: input.accountId, workspaceId: input.workspaceId },
785
+ async (scopedDb) => {
786
+ const [updated] = await scopedDb
787
+ .update(schema.videoGenerationOperations)
788
+ .set({
789
+ nextReconcileAt: input.nextReconcileAt,
790
+ ...(input.error ? { lastError: boundPrivateError(input.error) } : {}),
791
+ reconcileLeaseOwner: null,
792
+ reconcileLeaseExpiresAt: null,
793
+ updatedAt: new Date(),
794
+ })
795
+ .where(
796
+ and(
797
+ eq(schema.videoGenerationOperations.workspaceId, input.workspaceId),
798
+ eq(schema.videoGenerationOperations.id, input.operationId),
799
+ eq(schema.videoGenerationOperations.requestDigest, input.requestDigest),
800
+ ...(input.owner
801
+ ? [eq(schema.videoGenerationOperations.reconcileLeaseOwner, input.owner)]
802
+ : []),
803
+ ),
804
+ )
805
+ .returning({ id: schema.videoGenerationOperations.id });
806
+ return updated !== undefined;
807
+ },
808
+ );
809
+ }
810
+
811
+ export async function markVideoGenerationRetaining(
812
+ db: Database,
813
+ input: OperationMutationInput,
814
+ ): Promise<VideoGenerationOperation> {
815
+ return await mutateOperation(db, input, async (tx, operation) => {
816
+ if (operation.status === "retaining") return operation;
817
+ if (operation.status !== "provider_started") throw invalidTransition(operation, "retaining");
818
+ const [updated] = await tx
819
+ .update(schema.videoGenerationOperations)
820
+ .set({
821
+ status: "retaining",
822
+ nextReconcileAt: new Date(),
823
+ reconcileLeaseOwner: null,
824
+ reconcileLeaseExpiresAt: null,
825
+ updatedAt: new Date(),
826
+ })
827
+ .where(eq(schema.videoGenerationOperations.id, operation.id))
828
+ .returning();
829
+ return requireRow(updated, "begin video retention");
830
+ });
831
+ }
832
+
833
+ export async function settleVideoGenerationFailure(
834
+ db: Database,
835
+ input: OperationMutationInput & {
836
+ status: "provider_failed" | "retention_failed" | "outcome_unknown";
837
+ publicReason: string;
838
+ privateError?: string;
839
+ },
840
+ ): Promise<VideoGenerationOperation> {
841
+ return await mutateOperation(db, input, async (tx, operation) => {
842
+ if (operation.status === input.status) return operation;
843
+ const allowed =
844
+ input.status === "provider_failed"
845
+ ? ["accepted", "submission_uncertain", "provider_started"]
846
+ : input.status === "retention_failed"
847
+ ? ["retaining"]
848
+ : ["submission_uncertain", "provider_started"];
849
+ if (!inArrayValue(operation.status, allowed)) throw invalidTransition(operation, input.status);
850
+ await releaseReservedQuota(tx, operation, false);
851
+ const now = new Date();
852
+ const creditState = await refundVideoGenerationCredits(tx, operation, now);
853
+ const [updated] = await tx
854
+ .update(schema.videoGenerationOperations)
855
+ .set({
856
+ status: input.status,
857
+ creditState,
858
+ quotaState: "released",
859
+ terminalAt: now,
860
+ nextReconcileAt: null,
861
+ terminalUpdateState:
862
+ operation.admissionOutputState === "recorded" ? "pending" : "suppressed",
863
+ providerRequestEncrypted: null,
864
+ providerRequestExpiresAt: null,
865
+ credentialEncrypted: null,
866
+ requestEncrypted: null,
867
+ boundedPublicReason: boundPublicReason(input.publicReason),
868
+ lastError: input.privateError ? boundPrivateError(input.privateError) : null,
869
+ reconcileLeaseOwner: null,
870
+ reconcileLeaseExpiresAt: null,
871
+ privateDataEraseAfter: now,
872
+ updatedAt: now,
873
+ })
874
+ .where(eq(schema.videoGenerationOperations.id, operation.id))
875
+ .returning();
876
+ return requireRow(updated, "settle failed video generation");
877
+ });
878
+ }
879
+
880
+ export type SettleVideoGenerationReadyInput = OperationMutationInput & {
881
+ fileId: string;
882
+ bucket: string;
883
+ objectKey: string;
884
+ sizeBytes: number;
885
+ sha256: string;
886
+ facts: GeneratedVideoFacts;
887
+ };
888
+
889
+ export async function settleVideoGenerationReady(
890
+ db: Database,
891
+ input: SettleVideoGenerationReadyInput,
892
+ ): Promise<{
893
+ operation: VideoGenerationOperation;
894
+ artifact: GeneratedVideoArtifact;
895
+ }> {
896
+ return await withRlsContext(
897
+ db,
898
+ { accountId: input.accountId, workspaceId: input.workspaceId },
899
+ async (scopedDb) =>
900
+ await scopedDb.transaction(async (tx) => {
901
+ const [operation] = await tx
902
+ .select()
903
+ .from(schema.videoGenerationOperations)
904
+ .where(
905
+ and(
906
+ eq(schema.videoGenerationOperations.workspaceId, input.workspaceId),
907
+ eq(schema.videoGenerationOperations.id, input.operationId),
908
+ eq(schema.videoGenerationOperations.requestDigest, input.requestDigest),
909
+ ),
910
+ )
911
+ .for("update")
912
+ .limit(1);
913
+ if (!operation) throw new VideoGenerationConflictError("Video operation not found");
914
+ const [existingArtifact] = await tx
915
+ .select()
916
+ .from(schema.generatedVideoArtifacts)
917
+ .where(eq(schema.generatedVideoArtifacts.operationId, operation.id))
918
+ .limit(1);
919
+ if (operation.status === "completed" && existingArtifact) {
920
+ return { operation, artifact: existingArtifact };
921
+ }
922
+ if (operation.status !== "retaining") throw invalidTransition(operation, "completed");
923
+ if (operation.expectedFileId !== input.fileId) {
924
+ throw new VideoGenerationConflictError("Video File identity changed during settlement");
925
+ }
926
+ if (operation.expectedArtifactId === operation.expectedFileId) {
927
+ throw new Error("Video artifact and File identities must be distinct");
928
+ }
929
+ const now = new Date();
930
+ await tx.insert(schema.files).values({
931
+ id: input.fileId,
932
+ accountId: input.accountId,
933
+ workspaceId: input.workspaceId,
934
+ status: "ready",
935
+ filename: `generated-video-${operation.expectedArtifactId}.mp4`,
936
+ safeFilename: `generated-video-${operation.expectedArtifactId}.mp4`,
937
+ contentType: "video/mp4",
938
+ sizeBytes: input.sizeBytes,
939
+ sha256: input.sha256,
940
+ bucket: input.bucket,
941
+ objectKey: input.objectKey,
942
+ updatedAt: now,
943
+ });
944
+ const [artifact] = await tx
945
+ .insert(schema.generatedVideoArtifacts)
946
+ .values({
947
+ id: operation.expectedArtifactId,
948
+ accountId: input.accountId,
949
+ workspaceId: input.workspaceId,
950
+ primaryFileId: input.fileId,
951
+ operationId: operation.id,
952
+ sessionId: operation.sessionId,
953
+ turnId: operation.turnId,
954
+ attemptId: operation.attemptId,
955
+ modelId: operation.modelId,
956
+ sourceMode: operation.sourceMode,
957
+ promptDigest: operation.promptDigest,
958
+ contentType: "video/mp4",
959
+ sizeBytes: input.sizeBytes,
960
+ sha256: input.sha256,
961
+ durationMillis: Math.round(input.facts.durationSeconds * 1_000),
962
+ width: input.facts.width,
963
+ height: input.facts.height,
964
+ fpsMilli: Math.round(input.facts.fps * 1_000),
965
+ videoCodec: input.facts.videoCodec,
966
+ audioCodec: input.facts.audioCodec,
967
+ hasAudio: input.facts.hasAudio,
968
+ sandboxFilename: `generated-video-${operation.expectedArtifactId}.mp4`,
969
+ readyAt: now,
970
+ })
971
+ .returning();
972
+ if (!artifact) throw new Error("Generated video artifact was not persisted");
973
+ await tx
974
+ .update(schema.workspaceVideoGenerationQuotas)
975
+ .set({
976
+ reservedBytes: sql`${schema.workspaceVideoGenerationQuotas.reservedBytes} - ${operation.reservedBytes}`,
977
+ readyBytes: sql`${schema.workspaceVideoGenerationQuotas.readyBytes} + ${input.sizeBytes}`,
978
+ updatedAt: now,
979
+ })
980
+ .where(eq(schema.workspaceVideoGenerationQuotas.workspaceId, input.workspaceId));
981
+ const [updated] = await tx
982
+ .update(schema.videoGenerationOperations)
983
+ .set({
984
+ status: "completed",
985
+ quotaState: "ready",
986
+ terminalAt: now,
987
+ nextReconcileAt: null,
988
+ terminalUpdateState:
989
+ operation.admissionOutputState === "recorded" ? "pending" : "suppressed",
990
+ providerRequestEncrypted: null,
991
+ providerRequestExpiresAt: null,
992
+ credentialEncrypted: null,
993
+ requestEncrypted: null,
994
+ boundedPublicReason: null,
995
+ lastError: null,
996
+ privateDataEraseAfter: now,
997
+ reconcileLeaseOwner: null,
998
+ reconcileLeaseExpiresAt: null,
999
+ updatedAt: now,
1000
+ })
1001
+ .where(eq(schema.videoGenerationOperations.id, operation.id))
1002
+ .returning();
1003
+ return {
1004
+ operation: requireRow(updated, "settle ready video generation"),
1005
+ artifact,
1006
+ };
1007
+ }),
1008
+ );
1009
+ }
1010
+
1011
+ export async function getGeneratedVideoArtifact(
1012
+ db: Database,
1013
+ workspaceId: string,
1014
+ artifactId: string,
1015
+ ): Promise<{
1016
+ artifact: GeneratedVideoArtifact;
1017
+ file: typeof schema.files.$inferSelect;
1018
+ } | null> {
1019
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
1020
+ const [row] = await scopedDb
1021
+ .select({ artifact: schema.generatedVideoArtifacts, file: schema.files })
1022
+ .from(schema.generatedVideoArtifacts)
1023
+ .innerJoin(schema.files, eq(schema.files.id, schema.generatedVideoArtifacts.primaryFileId))
1024
+ .where(
1025
+ and(
1026
+ eq(schema.generatedVideoArtifacts.workspaceId, workspaceId),
1027
+ eq(schema.generatedVideoArtifacts.id, artifactId),
1028
+ ),
1029
+ )
1030
+ .limit(1);
1031
+ return row ?? null;
1032
+ });
1033
+ }
1034
+
1035
+ export async function listSessionGeneratedVideoArtifacts(
1036
+ db: Database,
1037
+ workspaceId: string,
1038
+ sessionId: string,
1039
+ ): Promise<
1040
+ Array<{
1041
+ artifact: GeneratedVideoArtifact;
1042
+ file: typeof schema.files.$inferSelect;
1043
+ }>
1044
+ > {
1045
+ return await withWorkspaceRls(
1046
+ db,
1047
+ workspaceId,
1048
+ async (scopedDb) =>
1049
+ await scopedDb
1050
+ .select({
1051
+ artifact: schema.generatedVideoArtifacts,
1052
+ file: schema.files,
1053
+ })
1054
+ .from(schema.generatedVideoArtifacts)
1055
+ .innerJoin(schema.files, eq(schema.files.id, schema.generatedVideoArtifacts.primaryFileId))
1056
+ .where(
1057
+ and(
1058
+ eq(schema.generatedVideoArtifacts.workspaceId, workspaceId),
1059
+ eq(schema.generatedVideoArtifacts.sessionId, sessionId),
1060
+ sql`${schema.generatedVideoArtifacts.deletedAt} is null`,
1061
+ ),
1062
+ )
1063
+ .orderBy(schema.generatedVideoArtifacts.createdAt, schema.generatedVideoArtifacts.id),
1064
+ );
1065
+ }
1066
+
1067
+ /**
1068
+ * Record one staging object as erased only after the object-store delete has
1069
+ * succeeded. Deleting first is safe because object deletion is idempotent; a
1070
+ * crash before this CAS simply repeats the same delete on recovery.
1071
+ */
1072
+ export async function markVideoGenerationReferenceCleaned(
1073
+ db: Database,
1074
+ input: {
1075
+ accountId: string;
1076
+ workspaceId: string;
1077
+ operationId: string;
1078
+ ordinal: number;
1079
+ stagingObjectKey: string;
1080
+ },
1081
+ ): Promise<boolean> {
1082
+ return await withRlsContext(
1083
+ db,
1084
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1085
+ async (scopedDb) => {
1086
+ const [updated] = await scopedDb
1087
+ .update(schema.videoGenerationReferences)
1088
+ .set({
1089
+ stagingObjectKey: null,
1090
+ grantExpiresAt: null,
1091
+ cleanupAfter: null,
1092
+ cleanedAt: new Date(),
1093
+ })
1094
+ .where(
1095
+ and(
1096
+ eq(schema.videoGenerationReferences.workspaceId, input.workspaceId),
1097
+ eq(schema.videoGenerationReferences.operationId, input.operationId),
1098
+ eq(schema.videoGenerationReferences.ordinal, input.ordinal),
1099
+ eq(schema.videoGenerationReferences.stagingObjectKey, input.stagingObjectKey),
1100
+ ),
1101
+ )
1102
+ .returning({
1103
+ operationId: schema.videoGenerationReferences.operationId,
1104
+ });
1105
+ if (updated) return true;
1106
+ const [existing] = await scopedDb
1107
+ .select({ cleanedAt: schema.videoGenerationReferences.cleanedAt })
1108
+ .from(schema.videoGenerationReferences)
1109
+ .where(
1110
+ and(
1111
+ eq(schema.videoGenerationReferences.workspaceId, input.workspaceId),
1112
+ eq(schema.videoGenerationReferences.operationId, input.operationId),
1113
+ eq(schema.videoGenerationReferences.ordinal, input.ordinal),
1114
+ ),
1115
+ )
1116
+ .limit(1);
1117
+ return existing?.cleanedAt !== null && existing?.cleanedAt !== undefined;
1118
+ },
1119
+ );
1120
+ }
1121
+
1122
+ export async function markVideoGenerationTerminalUpdate(
1123
+ db: Database,
1124
+ input: {
1125
+ accountId: string;
1126
+ workspaceId: string;
1127
+ operationId: string;
1128
+ state: "leased" | "delivered" | "suppressed";
1129
+ updateId?: string;
1130
+ },
1131
+ ): Promise<boolean> {
1132
+ return await withRlsContext(
1133
+ db,
1134
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1135
+ async (scopedDb) => {
1136
+ return await markVideoGenerationTerminalUpdateInTransaction(scopedDb, input);
1137
+ },
1138
+ );
1139
+ }
1140
+
1141
+ export async function markVideoGenerationTerminalUpdateInTransaction(
1142
+ tx: Database,
1143
+ input: {
1144
+ workspaceId: string;
1145
+ operationId: string;
1146
+ state: "leased" | "delivered" | "suppressed";
1147
+ updateId?: string;
1148
+ },
1149
+ ): Promise<boolean> {
1150
+ const allowed =
1151
+ input.state === "leased"
1152
+ ? ["pending", "leased"]
1153
+ : input.state === "delivered"
1154
+ ? ["leased"]
1155
+ : ["pending", "leased"];
1156
+ const [updated] = await tx
1157
+ .update(schema.videoGenerationOperations)
1158
+ .set({
1159
+ terminalUpdateState: input.state,
1160
+ terminalUpdateId: input.state === "suppressed" ? null : (input.updateId ?? null),
1161
+ updatedAt: new Date(),
1162
+ })
1163
+ .where(
1164
+ and(
1165
+ eq(schema.videoGenerationOperations.workspaceId, input.workspaceId),
1166
+ eq(schema.videoGenerationOperations.id, input.operationId),
1167
+ inArray(schema.videoGenerationOperations.terminalUpdateState, allowed),
1168
+ ...(input.state === "delivered" && input.updateId
1169
+ ? [eq(schema.videoGenerationOperations.terminalUpdateId, input.updateId)]
1170
+ : []),
1171
+ ),
1172
+ )
1173
+ .returning({ id: schema.videoGenerationOperations.id });
1174
+ return updated !== undefined;
1175
+ }
1176
+
1177
+ export function mediaGenerationResultForOperation(input: {
1178
+ operation: VideoGenerationOperation;
1179
+ readyReceipt?: Extract<MediaGenerationResult, { status: "ready" }>["receipt"];
1180
+ }): MediaGenerationResult {
1181
+ if (input.operation.status === "completed" && input.readyReceipt) {
1182
+ return {
1183
+ type: "media_generation_result",
1184
+ schemaVersion: 1,
1185
+ status: "ready",
1186
+ operationId: input.operation.id,
1187
+ receipt: input.readyReceipt,
1188
+ };
1189
+ }
1190
+ const failureStatus = input.operation.status;
1191
+ if (
1192
+ !inArrayValue(failureStatus, [
1193
+ "provider_failed",
1194
+ "retention_failed",
1195
+ "cancelled_before_submit",
1196
+ "outcome_unknown",
1197
+ ])
1198
+ ) {
1199
+ throw new Error("Video generation operation is not terminal");
1200
+ }
1201
+ return {
1202
+ type: "media_generation_result",
1203
+ schemaVersion: 1,
1204
+ status: failureStatus,
1205
+ operationId: input.operation.id,
1206
+ boundedPublicReason:
1207
+ input.operation.boundedPublicReason ?? "Video generation did not complete.",
1208
+ } as MediaGenerationResult;
1209
+ }
1210
+
1211
+ /** Build the compact public/model terminal envelope from durable canonical rows. */
1212
+ export async function mediaGenerationResultForStoredOperation(
1213
+ db: Database,
1214
+ operation: VideoGenerationOperation,
1215
+ ): Promise<MediaGenerationResult> {
1216
+ if (operation.status !== "completed") {
1217
+ return mediaGenerationResultForOperation({ operation });
1218
+ }
1219
+ const retained = await getGeneratedVideoArtifact(
1220
+ db,
1221
+ operation.workspaceId,
1222
+ operation.expectedArtifactId,
1223
+ );
1224
+ if (!retained || retained.artifact.deletedAt) {
1225
+ throw new Error("Completed video generation lost its retained artifact");
1226
+ }
1227
+ const artifact = retainedGeneratedVideoReferenceFromFile({
1228
+ id: retained.artifact.id,
1229
+ workspaceId: retained.artifact.workspaceId,
1230
+ status: retained.file.status,
1231
+ contentType: retained.file.contentType,
1232
+ sizeBytes: retained.file.sizeBytes,
1233
+ sha256: retained.file.sha256,
1234
+ updatedAt: retained.artifact.readyAt.toISOString(),
1235
+ width: retained.artifact.width,
1236
+ height: retained.artifact.height,
1237
+ });
1238
+ if (!artifact) throw new Error("Generated video receipt could not be constructed");
1239
+ const receipt = GeneratedVideoReceipt.parse({
1240
+ type: "generated_video",
1241
+ schemaVersion: 1,
1242
+ operationId: operation.id,
1243
+ artifact,
1244
+ video: {
1245
+ durationSeconds: retained.artifact.durationMillis / 1_000,
1246
+ width: retained.artifact.width,
1247
+ height: retained.artifact.height,
1248
+ fps: retained.artifact.fpsMilli / 1_000,
1249
+ hasAudio: retained.artifact.hasAudio,
1250
+ videoCodec: retained.artifact.videoCodec,
1251
+ audioCodec: retained.artifact.audioCodec,
1252
+ },
1253
+ sandboxPath: `/workspace/generated-videos/${retained.artifact.sandboxFilename}`,
1254
+ });
1255
+ return mediaGenerationResultForOperation({
1256
+ operation,
1257
+ readyReceipt: receipt,
1258
+ });
1259
+ }
1260
+
1261
+ export async function getVideoGenerationOperationSummary(
1262
+ db: Database,
1263
+ workspaceId: string,
1264
+ operationId: string,
1265
+ ): Promise<import("@opengeni/contracts").VideoGenerationOperationSummary | null> {
1266
+ const operation = await getVideoGenerationOperation(db, workspaceId, operationId);
1267
+ if (!operation) return null;
1268
+ const terminal = inArrayValue(operation.status, [
1269
+ "completed",
1270
+ "provider_failed",
1271
+ "cancelled_before_submit",
1272
+ "outcome_unknown",
1273
+ "retention_failed",
1274
+ ])
1275
+ ? await mediaGenerationResultForStoredOperation(db, operation)
1276
+ : null;
1277
+ return VideoGenerationOperationSummary.parse({
1278
+ schemaVersion: 1,
1279
+ operationId: operation.id,
1280
+ modelId: operation.modelId,
1281
+ status: operation.status === "submission_uncertain" ? "accepted" : operation.status,
1282
+ createdAt: operation.createdAt.toISOString(),
1283
+ updatedAt: operation.updatedAt.toISOString(),
1284
+ terminal,
1285
+ });
1286
+ }
1287
+
1288
+ type OperationMutationInput = {
1289
+ accountId: string;
1290
+ workspaceId: string;
1291
+ operationId: string;
1292
+ requestDigest: string;
1293
+ };
1294
+
1295
+ async function mutateOperation<T>(
1296
+ db: Database,
1297
+ input: OperationMutationInput,
1298
+ mutate: (tx: Database, operation: VideoGenerationOperation) => Promise<T>,
1299
+ ): Promise<T> {
1300
+ return await withRlsContext(
1301
+ db,
1302
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1303
+ async (scopedDb) =>
1304
+ await scopedDb.transaction(async (txRaw) => {
1305
+ const tx = txRaw as unknown as Database;
1306
+ const [operation] = await tx
1307
+ .select()
1308
+ .from(schema.videoGenerationOperations)
1309
+ .where(
1310
+ and(
1311
+ eq(schema.videoGenerationOperations.workspaceId, input.workspaceId),
1312
+ eq(schema.videoGenerationOperations.id, input.operationId),
1313
+ eq(schema.videoGenerationOperations.requestDigest, input.requestDigest),
1314
+ ),
1315
+ )
1316
+ .for("update")
1317
+ .limit(1);
1318
+ if (!operation) throw new VideoGenerationConflictError("Video operation not found");
1319
+ return await mutate(tx, operation);
1320
+ }),
1321
+ );
1322
+ }
1323
+
1324
+ async function releaseReservedQuota(
1325
+ tx: Database,
1326
+ operation: VideoGenerationOperation,
1327
+ ready: boolean,
1328
+ ): Promise<void> {
1329
+ if (operation.quotaState !== "reserved") return;
1330
+ await tx
1331
+ .update(schema.workspaceVideoGenerationQuotas)
1332
+ .set({
1333
+ reservedBytes: sql`${schema.workspaceVideoGenerationQuotas.reservedBytes} - ${operation.reservedBytes}`,
1334
+ ...(ready
1335
+ ? {
1336
+ readyBytes: sql`${schema.workspaceVideoGenerationQuotas.readyBytes} + ${operation.reservedBytes}`,
1337
+ }
1338
+ : {}),
1339
+ updatedAt: new Date(),
1340
+ })
1341
+ .where(eq(schema.workspaceVideoGenerationQuotas.workspaceId, operation.workspaceId));
1342
+ }
1343
+
1344
+ async function debitVideoGenerationCredits(
1345
+ tx: Database,
1346
+ operation: VideoGenerationOperation,
1347
+ ): Promise<void> {
1348
+ if (operation.pricedCostMicros <= 0 || operation.fundingSource !== "opengeni_credits") {
1349
+ throw new Error("Managed video credit debit has an invalid funding binding");
1350
+ }
1351
+ await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${operation.accountId}))`);
1352
+ const [balanceRow] = await tx
1353
+ .select({
1354
+ balanceMicros: sql<number>`coalesce(sum(${schema.creditLedgerEntries.amountMicros}), 0)`,
1355
+ })
1356
+ .from(schema.creditLedgerEntries)
1357
+ .where(eq(schema.creditLedgerEntries.accountId, operation.accountId));
1358
+ const balanceMicros = Number(balanceRow?.balanceMicros ?? 0);
1359
+ if (!Number.isSafeInteger(balanceMicros) || balanceMicros < operation.pricedCostMicros) {
1360
+ throw new VideoGenerationCreditError();
1361
+ }
1362
+ const now = new Date();
1363
+ const inserted = await tx
1364
+ .insert(schema.creditLedgerEntries)
1365
+ .values({
1366
+ accountId: operation.accountId,
1367
+ workspaceId: operation.workspaceId,
1368
+ type: "video_generation_debit",
1369
+ amountMicros: -operation.pricedCostMicros,
1370
+ sourceType: "video_generation_operation",
1371
+ sourceId: operation.id,
1372
+ idempotencyKey: `credit:video_generation_debit:${operation.id}`,
1373
+ metadata: {
1374
+ modelId: operation.modelId,
1375
+ sourceMode: operation.sourceMode,
1376
+ pricedCostMicros: operation.pricedCostMicros,
1377
+ },
1378
+ occurredAt: now,
1379
+ })
1380
+ .onConflictDoNothing({ target: schema.creditLedgerEntries.idempotencyKey })
1381
+ .returning({ id: schema.creditLedgerEntries.id });
1382
+ if (inserted.length !== 1) {
1383
+ throw new VideoGenerationConflictError("Video generation credit debit already exists");
1384
+ }
1385
+ await tx
1386
+ .insert(schema.usageEvents)
1387
+ .values({
1388
+ accountId: operation.accountId,
1389
+ workspaceId: operation.workspaceId,
1390
+ eventType: "video_generation.cost",
1391
+ quantity: operation.pricedCostMicros,
1392
+ unit: "usd_micros",
1393
+ sourceResourceType: "video_generation_operation",
1394
+ sourceResourceId: operation.id,
1395
+ sessionId: operation.sessionId,
1396
+ turnId: operation.turnId,
1397
+ turnAttemptId: operation.attemptId,
1398
+ idempotencyKey: `usage:video_generation.cost:${operation.id}`,
1399
+ occurredAt: now,
1400
+ })
1401
+ .onConflictDoNothing({ target: schema.usageEvents.idempotencyKey });
1402
+ }
1403
+
1404
+ async function refundVideoGenerationCredits(
1405
+ tx: Database,
1406
+ operation: VideoGenerationOperation,
1407
+ now: Date,
1408
+ ): Promise<VideoGenerationOperation["creditState"]> {
1409
+ if (operation.creditState !== "debited") return operation.creditState;
1410
+ if (operation.fundingSource !== "opengeni_credits" || operation.pricedCostMicros <= 0) {
1411
+ throw new Error("Video generation credit refund has an invalid funding binding");
1412
+ }
1413
+ await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${operation.accountId}))`);
1414
+ const inserted = await tx
1415
+ .insert(schema.creditLedgerEntries)
1416
+ .values({
1417
+ accountId: operation.accountId,
1418
+ workspaceId: operation.workspaceId,
1419
+ type: "video_generation_refund",
1420
+ amountMicros: operation.pricedCostMicros,
1421
+ sourceType: "video_generation_operation",
1422
+ sourceId: operation.id,
1423
+ idempotencyKey: `credit:video_generation_refund:${operation.id}`,
1424
+ metadata: {
1425
+ modelId: operation.modelId,
1426
+ sourceMode: operation.sourceMode,
1427
+ pricedCostMicros: operation.pricedCostMicros,
1428
+ },
1429
+ occurredAt: now,
1430
+ })
1431
+ .onConflictDoNothing({ target: schema.creditLedgerEntries.idempotencyKey })
1432
+ .returning({ id: schema.creditLedgerEntries.id });
1433
+ if (inserted.length !== 1) {
1434
+ throw new VideoGenerationConflictError("Video generation credit refund already exists");
1435
+ }
1436
+ await tx
1437
+ .insert(schema.usageEvents)
1438
+ .values({
1439
+ accountId: operation.accountId,
1440
+ workspaceId: operation.workspaceId,
1441
+ eventType: "video_generation.refund",
1442
+ quantity: operation.pricedCostMicros,
1443
+ unit: "usd_micros",
1444
+ sourceResourceType: "video_generation_operation",
1445
+ sourceResourceId: operation.id,
1446
+ sessionId: operation.sessionId,
1447
+ turnId: operation.turnId,
1448
+ turnAttemptId: operation.attemptId,
1449
+ idempotencyKey: `usage:video_generation.refund:${operation.id}`,
1450
+ occurredAt: now,
1451
+ })
1452
+ .onConflictDoNothing({ target: schema.usageEvents.idempotencyKey });
1453
+ return "refunded";
1454
+ }
1455
+
1456
+ function policyFromRow(
1457
+ row: typeof schema.workspaceVideoGenerationPolicies.$inferSelect | undefined,
1458
+ ): VideoGenerationPolicyValue {
1459
+ return VideoGenerationPolicy.parse(
1460
+ row
1461
+ ? {
1462
+ schemaVersion: 1,
1463
+ revision: row.revision,
1464
+ fundingSource: row.fundingSource,
1465
+ enabledModelIds: row.enabledModelIds,
1466
+ defaultModelId: row.defaultModelId,
1467
+ }
1468
+ : {
1469
+ schemaVersion: 1,
1470
+ revision: 0,
1471
+ fundingSource: "workspace_gateway",
1472
+ enabledModelIds: [],
1473
+ defaultModelId: null,
1474
+ },
1475
+ );
1476
+ }
1477
+
1478
+ function assertSupportedModels(modelIds: readonly string[]): void {
1479
+ for (const modelId of modelIds) {
1480
+ if (modelId !== SEEDANCE_2_5_MODEL_ID && modelId !== GROK_IMAGINE_VIDEO_1_5_MODEL_ID) {
1481
+ throw new Error(`Unknown video generation model: ${modelId}`);
1482
+ }
1483
+ }
1484
+ }
1485
+
1486
+ function assertAdmissionMatches(
1487
+ operation: VideoGenerationOperation,
1488
+ input: AdmitVideoGenerationOperationInput,
1489
+ ): void {
1490
+ if (
1491
+ operation.requestDigest !== input.requestDigest ||
1492
+ operation.toolCallId !== input.toolCallId ||
1493
+ operation.sessionId !== input.sessionId ||
1494
+ operation.turnId !== input.turnId ||
1495
+ operation.fundingSource !== input.fundingSource ||
1496
+ operation.pricedCostMicros !== input.pricedCostMicros
1497
+ ) {
1498
+ throw new VideoGenerationConflictError(
1499
+ "Video generation admission key was reused with different arguments",
1500
+ );
1501
+ }
1502
+ }
1503
+
1504
+ function inPreSubmitState(status: string): boolean {
1505
+ return inArrayValue(status, ["preparing", "prepared", "accepted"]);
1506
+ }
1507
+
1508
+ function inArrayValue<T extends string>(value: string, candidates: readonly T[]): value is T {
1509
+ return (candidates as readonly string[]).includes(value);
1510
+ }
1511
+
1512
+ function invalidTransition(operation: VideoGenerationOperation, target: string): Error {
1513
+ return new VideoGenerationConflictError(
1514
+ `Invalid video generation transition ${operation.status} -> ${target}`,
1515
+ );
1516
+ }
1517
+
1518
+ function requireRow<T>(value: T | undefined, action: string): T {
1519
+ if (!value) throw new Error(`Failed to ${action}`);
1520
+ return value;
1521
+ }
1522
+
1523
+ function boundPublicReason(value: string): string {
1524
+ const normalized = value.replace(/\s+/gu, " ").trim();
1525
+ return (normalized || "Video generation did not complete.").slice(0, 1_000);
1526
+ }
1527
+
1528
+ function boundPrivateError(value: string): string {
1529
+ return value.replace(/\s+/gu, " ").trim().slice(0, 4_096);
1530
+ }