@fungi.computer/darkroom 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +315 -2
  3. package/dist/app-media-contracts.d.ts +499 -0
  4. package/dist/app-media-contracts.js +98 -0
  5. package/dist/browser-failure.d.ts +40 -0
  6. package/dist/browser-failure.js +39 -0
  7. package/dist/browser.d.ts +2 -0
  8. package/dist/browser.js +1 -0
  9. package/dist/cloudflare/provider.d.ts +13 -0
  10. package/dist/cloudflare/provider.js +140 -0
  11. package/dist/cloudflare/reference-images.d.ts +42 -0
  12. package/dist/cloudflare/reference-images.js +102 -0
  13. package/dist/cloudflare.d.ts +6 -0
  14. package/dist/cloudflare.js +4 -0
  15. package/dist/cost-estimate.d.ts +22 -0
  16. package/dist/cost-estimate.js +76 -0
  17. package/dist/effect.d.ts +2 -0
  18. package/dist/effect.js +2 -0
  19. package/dist/execution-error.d.ts +54 -0
  20. package/dist/execution-error.js +62 -0
  21. package/dist/failure.d.ts +2 -0
  22. package/dist/failure.js +8 -0
  23. package/dist/gateway-models.d.ts +23 -0
  24. package/dist/gateway-models.js +77 -0
  25. package/dist/gateway-wire.d.ts +16 -0
  26. package/dist/gateway-wire.js +22 -0
  27. package/dist/index.d.ts +361 -0
  28. package/dist/index.js +611 -0
  29. package/dist/media-binding.d.ts +291 -0
  30. package/dist/media-binding.js +146 -0
  31. package/dist/media-generation.d.ts +10 -0
  32. package/dist/media-generation.js +110 -0
  33. package/dist/media-operation-contract.d.ts +104 -0
  34. package/dist/media-operation-contract.js +29 -0
  35. package/dist/media-operation-do.d.ts +55 -0
  36. package/dist/media-operation-do.js +105 -0
  37. package/dist/media-operation-owner.d.ts +301 -0
  38. package/dist/media-operation-owner.js +903 -0
  39. package/dist/media-operation-storage.d.ts +81 -0
  40. package/dist/media-operation-storage.js +1 -0
  41. package/dist/media-operation.d.ts +33 -0
  42. package/dist/media-operation.js +230 -0
  43. package/dist/model-catalog.d.ts +456 -0
  44. package/dist/model-catalog.js +565 -0
  45. package/dist/neurons.d.ts +4 -0
  46. package/dist/neurons.js +4 -0
  47. package/dist/provenance-bytes.d.ts +3 -0
  48. package/dist/provenance-bytes.js +15 -0
  49. package/dist/provenance-export.d.ts +23 -0
  50. package/dist/provenance-export.js +103 -0
  51. package/dist/provenance-jpeg.d.ts +3 -0
  52. package/dist/provenance-jpeg.js +76 -0
  53. package/dist/provenance-png.d.ts +1 -0
  54. package/dist/provenance-png.js +21 -0
  55. package/dist/provenance-xmp.d.ts +2 -0
  56. package/dist/provenance-xmp.js +54 -0
  57. package/dist/provenance.d.ts +20 -0
  58. package/dist/provenance.js +58 -0
  59. package/dist/server.d.ts +158 -0
  60. package/dist/server.js +207 -0
  61. package/dist/video-providers/index.d.ts +14 -0
  62. package/dist/video-providers/index.js +14 -0
  63. package/dist/video-providers/minimax.d.ts +25 -0
  64. package/dist/video-providers/minimax.js +63 -0
  65. package/dist/video-providers/reference-encoding.d.ts +4 -0
  66. package/dist/video-providers/reference-encoding.js +11 -0
  67. package/dist/video-providers/types.d.ts +28 -0
  68. package/dist/video-providers/types.js +1 -0
  69. package/dist/video-providers/unified.d.ts +18 -0
  70. package/dist/video-providers/unified.js +52 -0
  71. package/docs/api/README.md +16 -0
  72. package/docs/api/app-media-contracts.md +507 -0
  73. package/docs/api/browser.md +126 -0
  74. package/docs/api/cloudflare.md +678 -0
  75. package/docs/api/effect.md +39 -0
  76. package/docs/api/index.md +1775 -0
  77. package/docs/api/modules.md +19 -0
  78. package/docs/api/provenance-export.md +114 -0
  79. package/docs/api/server.md +1672 -0
  80. package/package.json +110 -3
@@ -0,0 +1,903 @@
1
+ import { mediaRequestedImageFormatSchema } from "./model-catalog.js";
2
+ import { defineIdFamily } from "@fungi.computer/cairn/id";
3
+ import { mediaGenerationSettingsSchema, validateMediaGenerationSettings, } from "./index.js";
4
+ import { z } from "zod";
5
+ import { reportCatchFailure } from "./failure.js";
6
+ import { MediaSafetyFilterRefusal } from "./execution-error.js";
7
+ const providerRefusalSchema = z.literal("safety_filter_refused");
8
+ const ids = defineIdFamily({
9
+ family: "hub-media-operation",
10
+ version: 1,
11
+ kinds: { lease: { prefix: "media_lease" } },
12
+ });
13
+ const text = z.string().min(1).max(256);
14
+ const path = z.string().min(1).max(1_024);
15
+ const referenceMetadataSchema = z.object({
16
+ format: z.enum(["png", "jpeg", "gif", "webp"]),
17
+ width: z.coerce.number().int().positive(),
18
+ height: z.coerce.number().int().positive(),
19
+ });
20
+ const referenceKey = (operationId, index) => `media-reference/v1/${operationId}/${String(index)}`;
21
+ /** Fixed generation destination captured before provider dispatch. */
22
+ export const mediaOperationDestinationSchema = z.strictObject({
23
+ /** Host-admitted destination or reference identity. */
24
+ computerId: text,
25
+ /** Fixed relative path in the admitted destination. */
26
+ path,
27
+ /** Optional verified image encoding pin. */
28
+ outputFormat: mediaRequestedImageFormatSchema.optional(),
29
+ });
30
+ /** Decode host namespace and owner identity. */
31
+ export const mediaOperationCoordinatesSchema = z.strictObject({
32
+ /** Host isolation namespace. */
33
+ namespace: text,
34
+ /** Host owner within the namespace. */
35
+ owner: text,
36
+ });
37
+ /** Decode durable generation, save and settlement states. */
38
+ export const mediaOperationStateSchema = z.enum({
39
+ /** Host identity and input captured. */
40
+ admitted: "admitted",
41
+ /** Exclusive input preparation lease. */
42
+ estimating: "estimating",
43
+ /** Provider dispatch admitted; output not yet retained. */
44
+ generating: "generating",
45
+ /** Generated output retained; destination save in progress. */
46
+ writing: "writing",
47
+ /** Destination committed; idempotent host settlement pending. */
48
+ charging: "charging",
49
+ /** Unmetered generation and destination write completed with a quote. */
50
+ written: "written",
51
+ /** Generation and settlement completed. */
52
+ succeeded: "succeeded",
53
+ /** Provider execution ended without a reusable output. */
54
+ interrupted: "interrupted",
55
+ });
56
+ /** Decode retained or saved output size and encoding. */
57
+ export const mediaImageSchema = z.strictObject({
58
+ /** Output byte length. */
59
+ bytes: z.number().int().positive(),
60
+ /** Actual output encoding. */
61
+ format: text,
62
+ });
63
+ const retainedFormatSchema = z
64
+ .enum(["image/png", "image/jpeg", "image/gif", "image/webp", "video/mp4"])
65
+ .transform((mime) => ({
66
+ "image/png": "png",
67
+ "image/jpeg": "jpeg",
68
+ "image/gif": "gif",
69
+ "image/webp": "webp",
70
+ "video/mp4": "mp4",
71
+ })[mime]);
72
+ const outputSchema = z.strictObject({
73
+ key: text,
74
+ version: text,
75
+ bytes: z.number().int().positive(),
76
+ format: text,
77
+ });
78
+ /** Decode the settled receipt, including the host-admitted customer price. */
79
+ export const mediaOperationResultSchema = z.strictObject({
80
+ /** Durable operation identifier. */
81
+ generationId: text,
82
+ /** Output byte length. */
83
+ bytes: z.number().int().positive(),
84
+ /** Actual output encoding. */
85
+ format: text,
86
+ /** Admitted price in milliunits, where 1 cent is 1,000 milliunits. */
87
+ costMilliunits: z.number().int().nonnegative(),
88
+ });
89
+ class MediaOperationError extends Error {
90
+ code;
91
+ constructor(
92
+ /** Bounded refusal or failure reason. */
93
+ code, options) {
94
+ super(`Media operation ${code}`, options);
95
+ this.code = code;
96
+ this.name = "MediaOperationError";
97
+ }
98
+ }
99
+ /** Stored media type for an output format. */
100
+ const outputMime = (format) => format === "mp4" ? "video/mp4" : `image/${format}`;
101
+ const operationRowSchema = z.object({
102
+ operation_id: z.string(),
103
+ request_id: z.string(),
104
+ actor_user_id: z.string(),
105
+ team_id: z.string(),
106
+ installation_id: z.string(),
107
+ fingerprint: z.string(),
108
+ input: z.string(),
109
+ state: mediaOperationStateSchema,
110
+ lease_id: z.string().nullable(),
111
+ lease_expires_at: z.number().nullable(),
112
+ estimated_cost_milliunits: z.number().nullable(),
113
+ image: z.string().nullable(),
114
+ output: z.string().nullable(),
115
+ write_receipt: z.string().nullable(),
116
+ result: z.string().nullable(),
117
+ created_at: z.number(),
118
+ updated_at: z.number(),
119
+ background: z.number(),
120
+ dispatched_at: z.number().nullable(),
121
+ });
122
+ const schemaStatements = [
123
+ `CREATE TABLE hub_media_operation (
124
+ singleton INTEGER PRIMARY KEY CHECK(singleton=1),
125
+ operation_id TEXT NOT NULL UNIQUE,
126
+ request_id TEXT NOT NULL UNIQUE,
127
+ actor_user_id TEXT NOT NULL,
128
+ team_id TEXT NOT NULL,
129
+ installation_id TEXT NOT NULL,
130
+ fingerprint TEXT NOT NULL,
131
+ input TEXT NOT NULL,
132
+ state TEXT NOT NULL CHECK(state IN ('admitted','estimating','generating','writing','charging','written','succeeded','interrupted')),
133
+ lease_id TEXT,
134
+ lease_expires_at INTEGER,
135
+ estimated_cost_milliunits INTEGER,
136
+ image TEXT,
137
+ output TEXT,
138
+ write_receipt TEXT,
139
+ result TEXT,
140
+ created_at INTEGER NOT NULL,
141
+ updated_at INTEGER NOT NULL,
142
+ background INTEGER NOT NULL DEFAULT 0,
143
+ dispatched_at INTEGER
144
+ ) STRICT`,
145
+ "CREATE TABLE hub_media_spent_failure (singleton INTEGER PRIMARY KEY CHECK(singleton=1),code TEXT NOT NULL CHECK(code='output_format_mismatch')) STRICT",
146
+ "CREATE TABLE hub_media_provider_refusal (singleton INTEGER PRIMARY KEY CHECK(singleton=1),code TEXT NOT NULL CHECK(code='safety_filter_refused')) STRICT",
147
+ ];
148
+ function fingerprint(input) {
149
+ return JSON.stringify(input);
150
+ }
151
+ const objectSchema = z.record(z.string(), z.unknown());
152
+ function decodeStoredRequest(serialized) {
153
+ let raw;
154
+ try {
155
+ raw = JSON.parse(serialized);
156
+ }
157
+ catch (cause) {
158
+ throw new MediaOperationError("partial_init", { cause });
159
+ }
160
+ const object = objectSchema.safeParse(raw);
161
+ if (!object.success)
162
+ throw new MediaOperationError("partial_init");
163
+ const destination = mediaOperationDestinationSchema.safeParse({
164
+ computerId: object.data.computerId,
165
+ path: object.data.path,
166
+ outputFormat: object.data.outputFormat,
167
+ });
168
+ if (!destination.success)
169
+ throw new MediaOperationError("partial_init");
170
+ const generation = { ...object.data };
171
+ delete generation.computerId;
172
+ delete generation.path;
173
+ delete generation.outputFormat;
174
+ const parsedGeneration = mediaGenerationSettingsSchema.safeParse(generation);
175
+ if (!parsedGeneration.success)
176
+ throw new MediaOperationError("partial_init");
177
+ let settings;
178
+ try {
179
+ settings = validateMediaGenerationSettings(parsedGeneration.data);
180
+ }
181
+ catch (cause) {
182
+ throw new MediaOperationError("partial_init", { cause });
183
+ }
184
+ return Object.freeze({
185
+ ...settings,
186
+ computerId: destination.data.computerId,
187
+ path: destination.data.path,
188
+ outputFormat: destination.data.outputFormat,
189
+ });
190
+ }
191
+ /** Own exact replay, dispatch leases, retained output and save recovery in SQL and object storage. */
192
+ export class MediaOperationOwner {
193
+ ctx;
194
+ env;
195
+ provider;
196
+ hooks;
197
+ erased = false;
198
+ leaseMs = 5 * 60 * 1_000;
199
+ /**
200
+ * A background (video) generation holds its lease past the 15-minute alarm
201
+ * wall-clock limit, so a live alarm is never mistaken for a lost one.
202
+ */
203
+ backgroundLeaseMs = 20 * 60 * 1_000;
204
+ initialized = false;
205
+ /**
206
+ * The host supplies the provider for background alarm generations.
207
+ */
208
+ mediaProvider() {
209
+ return this.provider?.();
210
+ }
211
+ constructor(ctx, env, provider, hooks = {}) {
212
+ this.ctx = ctx;
213
+ this.env = env;
214
+ this.provider = provider;
215
+ this.hooks = hooks;
216
+ void ctx.blockConcurrencyWhile(async () => {
217
+ if (await ctx.storage.isErased()) {
218
+ this.erased = true;
219
+ return;
220
+ }
221
+ this.ensureSchema();
222
+ });
223
+ }
224
+ ensureSchema() {
225
+ if (this.erased)
226
+ throw new MediaOperationError("not_found");
227
+ if (this.initialized)
228
+ return;
229
+ const existing = this.ctx.storage.sql
230
+ .exec("SELECT name FROM sqlite_master WHERE type='table' AND name='hub_media_operation'")
231
+ .toArray()[0];
232
+ if (existing === undefined) {
233
+ for (const statement of schemaStatements)
234
+ this.ctx.storage.sql.exec(statement);
235
+ }
236
+ else {
237
+ // Pre-launch stores use the current fresh schema or get reset by the host.
238
+ // Missing refusal facts are not healed or incrementally migrated here.
239
+ this.ctx.storage.sql.exec("SELECT code FROM hub_media_provider_refusal LIMIT 0");
240
+ }
241
+ this.initialized = true;
242
+ }
243
+ decodeJson(value, schema) {
244
+ let parsedValue;
245
+ try {
246
+ parsedValue = JSON.parse(value);
247
+ }
248
+ catch (cause) {
249
+ throw new MediaOperationError("partial_init", { cause });
250
+ }
251
+ const parsed = schema.safeParse(parsedValue);
252
+ if (!parsed.success)
253
+ throw new MediaOperationError("partial_init");
254
+ return parsed.data;
255
+ }
256
+ decode(row) {
257
+ const operationId = text.safeParse(row.operation_id);
258
+ const requestId = text.safeParse(row.request_id);
259
+ const actor = text.safeParse(row.actor_user_id);
260
+ const coordinates = mediaOperationCoordinatesSchema.safeParse({
261
+ namespace: row.team_id,
262
+ owner: row.installation_id,
263
+ });
264
+ if (!operationId.success || !requestId.success || !actor.success)
265
+ throw new MediaOperationError("partial_init");
266
+ if (!coordinates.success ||
267
+ !mediaOperationStateSchema.safeParse(row.state).success)
268
+ throw new MediaOperationError("partial_init");
269
+ return Object.freeze({
270
+ operationId: operationId.data,
271
+ requestId: requestId.data,
272
+ actorUserId: actor.data,
273
+ namespace: coordinates.data.namespace,
274
+ owner: coordinates.data.owner,
275
+ fingerprint: row.fingerprint,
276
+ input: decodeStoredRequest(row.input),
277
+ state: row.state,
278
+ leaseId: row.lease_id,
279
+ leaseExpiresAt: row.lease_expires_at,
280
+ estimatedCostMilliunits: row.estimated_cost_milliunits,
281
+ image: row.image === null
282
+ ? null
283
+ : this.decodeJson(row.image, mediaImageSchema),
284
+ output: row.output === null ? null : this.decodeJson(row.output, outputSchema),
285
+ writeReceipt: row.write_receipt,
286
+ result: row.result === null
287
+ ? null
288
+ : this.decodeJson(row.result, mediaOperationResultSchema),
289
+ createdAt: row.created_at,
290
+ updatedAt: row.updated_at,
291
+ });
292
+ }
293
+ project(record) {
294
+ const failure = this.ctx.storage.sql
295
+ .exec("SELECT code FROM hub_media_spent_failure WHERE singleton=1")
296
+ .toArray()[0];
297
+ const spentFailure = failure === undefined
298
+ ? null
299
+ : z
300
+ .strictObject({
301
+ code: z.literal("output_format_mismatch"),
302
+ })
303
+ .parse(failure).code;
304
+ const refusal = this.ctx.storage.sql
305
+ .exec("SELECT code FROM hub_media_provider_refusal WHERE singleton=1")
306
+ .toArray()[0];
307
+ const providerRefusal = refusal === undefined
308
+ ? null
309
+ : z.strictObject({ code: providerRefusalSchema }).parse(refusal).code;
310
+ return Object.freeze({
311
+ operationId: record.operationId,
312
+ requestId: record.requestId,
313
+ namespace: record.namespace,
314
+ owner: record.owner,
315
+ state: record.state,
316
+ spentFailure,
317
+ providerRefusal,
318
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
319
+ image: record.image,
320
+ result: record.result,
321
+ createdAt: record.createdAt,
322
+ updatedAt: record.updatedAt,
323
+ });
324
+ }
325
+ readRecord() {
326
+ if (this.erased)
327
+ return null;
328
+ this.ensureSchema();
329
+ const row = this.ctx.storage.sql
330
+ .exec("SELECT * FROM hub_media_operation WHERE singleton=1")
331
+ .toArray()[0];
332
+ if (row === undefined)
333
+ return null;
334
+ const parsed = operationRowSchema.safeParse(row);
335
+ if (!parsed.success)
336
+ throw new MediaOperationError("partial_init", { cause: parsed.error });
337
+ return this.decode(parsed.data);
338
+ }
339
+ /** Read the safe persisted operation projection, or null before admission or after erasure. */
340
+ read() {
341
+ const record = this.readRecord();
342
+ return record === null ? null : this.project(record);
343
+ }
344
+ /** Check host namespace, request identity and inactivity before erasure. */
345
+ preflightErasure(namespace, requestId) {
346
+ if (this.ctx.id.name !== `media:${requestId}`)
347
+ throw new Error("Media erasure address mismatch");
348
+ if (this.erased)
349
+ return { namespace, requestId, erased: true };
350
+ const record = this.readRecord();
351
+ const ownerNamespace = record?.namespace ?? this.hooks.namespace?.() ?? null;
352
+ if ((ownerNamespace !== null && ownerNamespace !== namespace) ||
353
+ (record !== null && record.requestId !== requestId))
354
+ throw new Error("Media erasure scope mismatch");
355
+ if (record !== null &&
356
+ ["estimating", "generating", "writing", "charging"].includes(record.state))
357
+ throw new Error("Media operation is still active");
358
+ return { namespace, requestId, erased: false };
359
+ }
360
+ /** Erase inactive output and local storage, then retain the durable erasure marker. */
361
+ async erase(namespace, requestId) {
362
+ if (this.erased)
363
+ return this.readErasure();
364
+ this.preflightErasure(namespace, requestId);
365
+ // The operation's immutable output key is owned by this DO identity.
366
+ await this.env.APP_ARTIFACTS.delete(this.erasureOutputKey());
367
+ if (await this.env.APP_ARTIFACTS.head(this.erasureOutputKey()))
368
+ throw new Error("Media output erasure readback incomplete");
369
+ await this.ctx.storage.deleteAlarm();
370
+ await this.ctx.storage.deleteAll();
371
+ await this.ctx.storage.markErased();
372
+ this.erased = true;
373
+ return this.readErasure();
374
+ }
375
+ /** Read back empty SQL, alarm and retained output with the durable erasure marker. */
376
+ async readErasure() {
377
+ const keys = await this.ctx.storage.listKeys();
378
+ const tables = this.ctx.storage.sql
379
+ .exec("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
380
+ .toArray();
381
+ return {
382
+ clear: keys.length === 1 &&
383
+ keys[0] === "botanical:erased" &&
384
+ (await this.ctx.storage.isErased()) &&
385
+ tables.length === 0 &&
386
+ (await this.ctx.storage.getAlarm()) === null &&
387
+ (await this.env.APP_ARTIFACTS.head(this.erasureOutputKey())) === null,
388
+ };
389
+ }
390
+ erasureOutputKey() {
391
+ const name = this.ctx.id.name;
392
+ if (name === undefined)
393
+ throw new Error("Media operation identity unavailable");
394
+ return `media-output/v1/${name}`;
395
+ }
396
+ requireRecord() {
397
+ const record = this.readRecord();
398
+ if (record === null)
399
+ throw new MediaOperationError("not_found");
400
+ return record;
401
+ }
402
+ update(input) {
403
+ const timestamp = Date.now();
404
+ this.ctx.storage.sql.exec(`UPDATE hub_media_operation SET state=?,lease_id=?,lease_expires_at=?,estimated_cost_milliunits=?,image=?,output=?,write_receipt=?,result=?,updated_at=? WHERE singleton=1 AND state=? AND lease_id IS ? AND (lease_id IS NULL OR ?=1 OR lease_expires_at>?)`, input.state, input.leaseId, input.leaseExpiresAt, input.estimatedCostMilliunits, input.image === null ? null : JSON.stringify(input.image), input.output === null ? null : JSON.stringify(input.output), input.writeReceipt, input.result === null ? null : JSON.stringify(input.result), timestamp, input.expectedState, input.expectedLeaseId, input.allowExpired === true ? 1 : 0, timestamp);
405
+ const changed = z
406
+ .object({ changes: z.number().int().nonnegative() })
407
+ .parse(this.ctx.storage.sql.exec("SELECT changes() AS changes").toArray()[0]).changes;
408
+ if (changed !== 1)
409
+ throw new MediaOperationError("lease_lost");
410
+ const updated = this.readRecord();
411
+ if (updated === null)
412
+ throw new MediaOperationError("partial_init");
413
+ return updated;
414
+ }
415
+ transition(leaseId, expectedState, state, values) {
416
+ return this.ctx.storage.transactionSync(() => {
417
+ const record = this.requireRecord();
418
+ if (record.state !== expectedState || record.leaseId !== leaseId)
419
+ throw new MediaOperationError("lease_lost");
420
+ return this.project(this.update({
421
+ expectedState,
422
+ expectedLeaseId: leaseId,
423
+ state,
424
+ leaseId,
425
+ leaseExpiresAt: Date.now() + this.leaseMs,
426
+ ...values,
427
+ }));
428
+ });
429
+ }
430
+ /** Capture immutable host identity and input, replaying exact requests and refusing changed input. */
431
+ admit(actorUserId, coordinatesInput, requestId, input) {
432
+ const actor = text.safeParse(actorUserId);
433
+ const coordinates = mediaOperationCoordinatesSchema.safeParse(coordinatesInput);
434
+ const requestIdentifier = text.safeParse(requestId);
435
+ if (!actor.success || !coordinates.success)
436
+ throw new MediaOperationError("invalid_input");
437
+ if (!requestIdentifier.success)
438
+ throw new MediaOperationError("invalid_input");
439
+ const requestFingerprint = fingerprint(input);
440
+ return this.ctx.storage.transactionSync(() => {
441
+ this.ensureSchema();
442
+ const prior = this.readRecord();
443
+ if (prior !== null) {
444
+ if (prior.requestId !== requestIdentifier.data ||
445
+ prior.actorUserId !== actor.data ||
446
+ prior.namespace !== coordinates.data.namespace ||
447
+ prior.owner !== coordinates.data.owner ||
448
+ prior.fingerprint !== requestFingerprint)
449
+ return {
450
+ kind: "conflict",
451
+ operation: this.project(prior),
452
+ };
453
+ return {
454
+ kind: "replayed",
455
+ operation: this.project(prior),
456
+ };
457
+ }
458
+ const namespace = this.hooks.namespace?.() ?? null;
459
+ if (namespace !== null && namespace !== coordinates.data.namespace)
460
+ throw new MediaOperationError("conflict");
461
+ const operationId = text.parse(this.ctx.id.name);
462
+ const timestamp = Date.now();
463
+ this.ctx.storage.sql.exec(`INSERT INTO hub_media_operation (singleton,operation_id,request_id,actor_user_id,team_id,installation_id,fingerprint,input,state,lease_id,lease_expires_at,estimated_cost_milliunits,image,output,write_receipt,result,created_at,updated_at) VALUES (1,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, operationId, requestIdentifier.data, actor.data, coordinates.data.namespace, coordinates.data.owner, requestFingerprint, JSON.stringify(input), "admitted", null, null, null, null, null, null, null, timestamp, timestamp);
464
+ const admitted = this.requireRecord();
465
+ return {
466
+ kind: "admitted",
467
+ operation: this.project(admitted),
468
+ };
469
+ });
470
+ }
471
+ /** Claim a safe dispatch or save lease, recovering retained output without redispatching inference. */
472
+ async claim() {
473
+ const observed = this.requireRecord();
474
+ // R2 may have committed the exact output before SQL recorded its receipt.
475
+ // Inspect it only after generation's lease expires; never redispatch generation.
476
+ const retained = observed.state === "generating" &&
477
+ observed.leaseExpiresAt !== null &&
478
+ observed.leaseExpiresAt <= Date.now()
479
+ ? await this.env.APP_ARTIFACTS.head(`media-output/v1/${observed.operationId}`)
480
+ : undefined;
481
+ return this.ctx.storage.transactionSync(() => {
482
+ const record = this.requireRecord();
483
+ if (record.state === "succeeded" || record.state === "interrupted")
484
+ return {
485
+ kind: record.state,
486
+ operation: this.project(record),
487
+ };
488
+ if (record.state === "written")
489
+ return {
490
+ kind: "succeeded",
491
+ operation: this.project(record),
492
+ };
493
+ // Another claimant or the original worker may have committed while R2 was read.
494
+ if (record.state !== observed.state ||
495
+ record.leaseId !== observed.leaseId)
496
+ return {
497
+ kind: "pending",
498
+ operation: this.project(record),
499
+ };
500
+ const timestamp = Date.now();
501
+ if (record.leaseId !== null &&
502
+ (record.leaseExpiresAt === null || record.leaseExpiresAt > timestamp))
503
+ return {
504
+ kind: "pending",
505
+ operation: this.project(record),
506
+ };
507
+ let output = record.output;
508
+ if (record.state === "generating" && retained != null) {
509
+ output = outputSchema.parse({
510
+ key: retained.key,
511
+ version: retained.version,
512
+ bytes: retained.size,
513
+ format: retainedFormatSchema.parse(retained.httpMetadata?.contentType),
514
+ });
515
+ }
516
+ if ((record.state === "writing" || record.state === "generating") &&
517
+ output !== null) {
518
+ const leaseId = ids.lease.generate();
519
+ const claimed = this.update({
520
+ expectedState: record.state,
521
+ expectedLeaseId: record.leaseId,
522
+ allowExpired: true,
523
+ state: "writing",
524
+ leaseId,
525
+ leaseExpiresAt: timestamp + this.leaseMs,
526
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
527
+ image: {
528
+ bytes: output.bytes,
529
+ format: output.format,
530
+ },
531
+ output,
532
+ writeReceipt: null,
533
+ result: null,
534
+ });
535
+ return {
536
+ kind: "claimed",
537
+ operation: this.project(claimed),
538
+ leaseId,
539
+ };
540
+ }
541
+ if (record.state === "charging") {
542
+ const leaseId = ids.lease.generate();
543
+ const claimed = this.update({
544
+ expectedState: record.state,
545
+ expectedLeaseId: record.leaseId,
546
+ allowExpired: true,
547
+ state: "charging",
548
+ leaseId,
549
+ leaseExpiresAt: timestamp + this.leaseMs,
550
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
551
+ image: record.image,
552
+ output: record.output,
553
+ writeReceipt: record.writeReceipt,
554
+ result: null,
555
+ });
556
+ return {
557
+ kind: "claimed",
558
+ operation: this.project(claimed),
559
+ leaseId,
560
+ };
561
+ }
562
+ if (record.state === "admitted" || record.state === "estimating") {
563
+ const leaseId = ids.lease.generate();
564
+ const claimed = this.update({
565
+ expectedState: record.state,
566
+ expectedLeaseId: record.leaseId,
567
+ allowExpired: true,
568
+ state: "estimating",
569
+ leaseId,
570
+ leaseExpiresAt: timestamp + this.leaseMs,
571
+ estimatedCostMilliunits: null,
572
+ image: null,
573
+ output: null,
574
+ writeReceipt: null,
575
+ result: null,
576
+ });
577
+ return {
578
+ kind: "claimed",
579
+ operation: this.project(claimed),
580
+ leaseId,
581
+ };
582
+ }
583
+ const interrupted = this.update({
584
+ expectedState: record.state,
585
+ expectedLeaseId: record.leaseId,
586
+ allowExpired: true,
587
+ state: "interrupted",
588
+ leaseId: null,
589
+ leaseExpiresAt: null,
590
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
591
+ image: record.image,
592
+ output: record.output,
593
+ writeReceipt: record.writeReceipt,
594
+ result: null,
595
+ });
596
+ return {
597
+ kind: "interrupted",
598
+ operation: this.project(interrupted),
599
+ };
600
+ });
601
+ }
602
+ /** Pin the admitted price and begin the generation lease. */
603
+ recordEstimate(leaseId, costMilliunits) {
604
+ if (!Number.isSafeInteger(costMilliunits) || costMilliunits < 0)
605
+ throw new MediaOperationError("invalid_input");
606
+ return this.transition(leaseId, "estimating", "generating", {
607
+ estimatedCostMilliunits: costMilliunits,
608
+ image: null,
609
+ output: null,
610
+ writeReceipt: null,
611
+ result: null,
612
+ });
613
+ }
614
+ /**
615
+ * Hand a long (video) generation to this owner's alarm. The caller's request
616
+ * returns at once; the alarm dispatches the provider exactly once, retains
617
+ * the output, and releases the lease so a later retry saves and charges it.
618
+ */
619
+ async dispatchInBackground(leaseId, references) {
620
+ const record = this.requireRecord();
621
+ if (record.state !== "generating" || record.leaseId !== leaseId)
622
+ throw new MediaOperationError("lease_lost");
623
+ for (const [index, reference] of references.entries()) {
624
+ await this.env.APP_ARTIFACTS.put(referenceKey(record.operationId, index), reference.bytes, {
625
+ customMetadata: {
626
+ format: reference.format,
627
+ width: String(reference.width),
628
+ height: String(reference.height),
629
+ },
630
+ });
631
+ }
632
+ const timestamp = Date.now();
633
+ this.ctx.storage.sql.exec("UPDATE hub_media_operation SET background=1, lease_expires_at=?, updated_at=? WHERE singleton=1 AND state='generating' AND lease_id=?", timestamp + this.backgroundLeaseMs, timestamp, leaseId);
634
+ await this.ctx.storage.setAlarm(timestamp);
635
+ return this.project(this.requireRecord());
636
+ }
637
+ /** Run a background generation once; a retried alarm never redispatches. */
638
+ async alarm() {
639
+ if (this.erased)
640
+ return;
641
+ this.ensureSchema();
642
+ const raw = this.ctx.storage.sql
643
+ .exec("SELECT * FROM hub_media_operation WHERE singleton=1")
644
+ .toArray()[0];
645
+ if (raw === undefined)
646
+ return;
647
+ const parsed = operationRowSchema.safeParse(raw);
648
+ if (!parsed.success)
649
+ throw new MediaOperationError("partial_init", { cause: parsed.error });
650
+ const row = parsed.data;
651
+ if (row.state !== "generating" || row.background !== 1)
652
+ return;
653
+ const record = this.decode(row);
654
+ const leaseId = record.leaseId;
655
+ if (leaseId === null)
656
+ return;
657
+ if (row.dispatched_at !== null) {
658
+ // A previous alarm dispatched and then died: never call the provider
659
+ // again. Recovery (claim) retains any committed output after the lease.
660
+ return;
661
+ }
662
+ this.ctx.storage.sql.exec("UPDATE hub_media_operation SET dispatched_at=? WHERE singleton=1", Date.now());
663
+ const provider = this.mediaProvider();
664
+ let output;
665
+ try {
666
+ if (provider === undefined)
667
+ throw new Error("Media host has no provider");
668
+ const references = await Promise.all((record.input.references ?? []).map(async (_reference, index) => {
669
+ const object = await this.env.APP_ARTIFACTS.get(referenceKey(record.operationId, index));
670
+ if (object === null)
671
+ throw new Error("Media reference is unavailable");
672
+ const metadata = referenceMetadataSchema.parse(object.customMetadata);
673
+ return {
674
+ ...metadata,
675
+ bytes: new Uint8Array(await object.arrayBuffer()),
676
+ };
677
+ }));
678
+ // The captured request includes destination authority; providers receive settings only.
679
+ const settings = validateMediaGenerationSettings(mediaGenerationSettingsSchema.strip().parse(record.input));
680
+ output = await provider(settings, references, undefined, record.input.outputFormat);
681
+ }
682
+ catch (cause) {
683
+ reportCatchFailure("media-operation-provider", cause);
684
+ // Nothing was delivered; the next retry sees interrupted and releases
685
+ // the billing hold at zero units.
686
+ try {
687
+ this.interruptGeneration(leaseId, cause instanceof MediaSafetyFilterRefusal
688
+ ? "safety_filter_refused"
689
+ : undefined);
690
+ }
691
+ catch (cause) {
692
+ reportCatchFailure("media-operation-interrupt", cause);
693
+ /* A recovery claim already ended this lease. */
694
+ }
695
+ return;
696
+ }
697
+ await this.recordGenerated(leaseId, output.bytes, output.format);
698
+ await Promise.all((record.input.references ?? []).map((_reference, index) => this.env.APP_ARTIFACTS.delete(referenceKey(record.operationId, index)))).catch((cause) => {
699
+ reportCatchFailure("media-reference-cleanup", cause);
700
+ });
701
+ // Release the lease: the next retry of this request claims the save.
702
+ this.ctx.storage.sql.exec("UPDATE hub_media_operation SET lease_id=NULL, lease_expires_at=NULL WHERE singleton=1 AND state='writing' AND lease_id=?", leaseId);
703
+ }
704
+ /** An aborted in-flight provider call is terminal for this request ID. */
705
+ interruptGeneration(leaseId, refusalInput) {
706
+ const refusal = providerRefusalSchema.optional().parse(refusalInput);
707
+ const record = this.requireRecord();
708
+ if (record.state !== "generating" || record.leaseId !== leaseId)
709
+ throw new MediaOperationError("lease_lost");
710
+ return this.ctx.storage.transactionSync(() => {
711
+ const interrupted = this.update({
712
+ expectedState: "generating",
713
+ expectedLeaseId: leaseId,
714
+ state: "interrupted",
715
+ leaseId: null,
716
+ leaseExpiresAt: null,
717
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
718
+ image: null,
719
+ output: null,
720
+ writeReceipt: null,
721
+ result: null,
722
+ });
723
+ if (refusal !== undefined)
724
+ this.ctx.storage.sql.exec("INSERT INTO hub_media_provider_refusal(singleton,code) VALUES(1,?)", refusal);
725
+ return this.project(interrupted);
726
+ });
727
+ }
728
+ /** Retain immutable provider output before making the save state visible. */
729
+ async recordGenerated(leaseId, bytes, formatInput) {
730
+ const image = mediaImageSchema.safeParse({
731
+ bytes: bytes.byteLength,
732
+ format: formatInput,
733
+ });
734
+ if (!image.success)
735
+ throw new MediaOperationError("invalid_input");
736
+ const record = this.requireRecord();
737
+ if (record.state !== "generating" || record.leaseId !== leaseId)
738
+ throw new MediaOperationError("lease_lost");
739
+ if (record.input.outputFormat !== undefined &&
740
+ image.data.format !== record.input.outputFormat) {
741
+ this.ctx.storage.transactionSync(() => {
742
+ this.update({
743
+ expectedState: "generating",
744
+ expectedLeaseId: leaseId,
745
+ state: "interrupted",
746
+ leaseId: null,
747
+ leaseExpiresAt: null,
748
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
749
+ image: null,
750
+ output: null,
751
+ writeReceipt: null,
752
+ result: null,
753
+ });
754
+ this.ctx.storage.sql.exec("INSERT INTO hub_media_spent_failure(singleton,code) VALUES(1,'output_format_mismatch')");
755
+ });
756
+ return {
757
+ kind: "refused",
758
+ code: "output_format_mismatch",
759
+ };
760
+ }
761
+ const key = `media-output/v1/${record.operationId}`;
762
+ const stored = await this.env.APP_ARTIFACTS.put(key, bytes, {
763
+ onlyIf: { etagDoesNotMatch: "*" },
764
+ httpMetadata: { contentType: outputMime(image.data.format) },
765
+ });
766
+ if (stored === null)
767
+ throw new MediaOperationError("conflict");
768
+ return this.transition(leaseId, "generating", "writing", {
769
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
770
+ image: image.data,
771
+ output: {
772
+ key,
773
+ version: stored.version,
774
+ bytes: image.data.bytes,
775
+ format: image.data.format,
776
+ },
777
+ writeReceipt: null,
778
+ result: null,
779
+ });
780
+ }
781
+ /** Read exact retained bytes under the current save lease. */
782
+ async readGenerated(leaseId) {
783
+ const record = this.requireRecord();
784
+ if (record.state !== "writing" ||
785
+ record.leaseId !== leaseId ||
786
+ record.output === null)
787
+ throw new MediaOperationError("lease_lost");
788
+ const object = await this.env.APP_ARTIFACTS.get(record.output.key);
789
+ if (object?.version !== record.output.version ||
790
+ object.size !== record.output.bytes)
791
+ throw new MediaOperationError("partial_init");
792
+ return new Uint8Array(await object.arrayBuffer());
793
+ }
794
+ /** A fixed-path exact-byte save is safe to retry without repeating generation. */
795
+ releaseWrite(leaseId) {
796
+ const record = this.requireRecord();
797
+ if (record.state !== "writing" ||
798
+ record.leaseId !== leaseId ||
799
+ record.output === null)
800
+ throw new MediaOperationError("lease_lost");
801
+ return this.ctx.storage.transactionSync(() => this.project(this.update({
802
+ expectedState: "writing",
803
+ expectedLeaseId: leaseId,
804
+ state: "writing",
805
+ leaseId: null,
806
+ leaseExpiresAt: null,
807
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
808
+ image: record.image,
809
+ output: record.output,
810
+ writeReceipt: null,
811
+ result: null,
812
+ })));
813
+ }
814
+ // The saved file may contain embedded provenance. Persist its actual size at
815
+ // write acknowledgement; output keeps the original provider bytes for retry.
816
+ /** Record a destination write and move to idempotent settlement. */
817
+ recordWritten(leaseId, receipt, written) {
818
+ const image = mediaImageSchema.safeParse(written);
819
+ if (!image.success || !text.safeParse(receipt).success)
820
+ throw new MediaOperationError("invalid_input");
821
+ const record = this.requireRecord();
822
+ if (record.state !== "writing" || record.leaseId !== leaseId)
823
+ throw new MediaOperationError("lease_lost");
824
+ if (image.data.format !== record.image?.format)
825
+ throw new MediaOperationError("conflict");
826
+ return this.transition(leaseId, "writing", "charging", {
827
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
828
+ image: image.data,
829
+ output: record.output,
830
+ writeReceipt: receipt,
831
+ result: null,
832
+ });
833
+ }
834
+ /**
835
+ * Persist a completed Computer write without claiming billing success.
836
+ * A lost receipt may replay the exact retained bytes to the captured path;
837
+ * it never repeats provider generation.
838
+ */
839
+ recordWrittenWithoutSettlement(leaseId, receipt, written) {
840
+ const image = mediaImageSchema.safeParse(written);
841
+ if (!image.success || !text.safeParse(receipt).success)
842
+ throw new MediaOperationError("invalid_input");
843
+ const record = this.requireRecord();
844
+ if (record.state !== "writing" || record.leaseId !== leaseId)
845
+ throw new MediaOperationError("lease_lost");
846
+ if (image.data.format !== record.image?.format)
847
+ throw new MediaOperationError("conflict");
848
+ return this.ctx.storage.transactionSync(() => this.project(this.update({
849
+ expectedState: "writing",
850
+ expectedLeaseId: leaseId,
851
+ state: "written",
852
+ leaseId: null,
853
+ leaseExpiresAt: null,
854
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
855
+ image: image.data,
856
+ output: record.output,
857
+ writeReceipt: receipt,
858
+ result: null,
859
+ })));
860
+ }
861
+ /** Commit the settled receipt after verifying the saved output and admitted price. */
862
+ recordSucceeded(leaseId, resultInput) {
863
+ const result = mediaOperationResultSchema.safeParse(resultInput);
864
+ if (!result.success)
865
+ throw new MediaOperationError("invalid_input");
866
+ const record = this.requireRecord();
867
+ if (record.state !== "charging" ||
868
+ record.leaseId !== leaseId ||
869
+ record.estimatedCostMilliunits === null ||
870
+ record.image === null ||
871
+ record.writeReceipt === null ||
872
+ result.data.generationId !== record.operationId ||
873
+ result.data.bytes !== record.image.bytes ||
874
+ result.data.format !== record.image.format ||
875
+ result.data.costMilliunits !== record.estimatedCostMilliunits)
876
+ throw new MediaOperationError("conflict");
877
+ return this.transition(leaseId, "charging", "succeeded", {
878
+ estimatedCostMilliunits: record.estimatedCostMilliunits,
879
+ image: record.image,
880
+ output: record.output,
881
+ writeReceipt: record.writeReceipt,
882
+ result: result.data,
883
+ });
884
+ }
885
+ /** Release preparation or refused admission so the same request can retry. */
886
+ releaseEstimate(leaseId) {
887
+ const record = this.requireRecord();
888
+ if (record.state !== "estimating" || record.leaseId !== leaseId)
889
+ throw new MediaOperationError("lease_lost");
890
+ return this.ctx.storage.transactionSync(() => this.project(this.update({
891
+ expectedState: "estimating",
892
+ expectedLeaseId: leaseId,
893
+ state: "admitted",
894
+ leaseId: null,
895
+ leaseExpiresAt: null,
896
+ estimatedCostMilliunits: null,
897
+ image: null,
898
+ output: null,
899
+ writeReceipt: null,
900
+ result: null,
901
+ })));
902
+ }
903
+ }