@fungi.computer/darkroom 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -156,9 +156,18 @@ Agar's public `pendingSchema`. `appMediaModelsOutcomeSchema` and
156
156
  closed admission codes, the Media attachment-budget refusal, the existing
157
157
  `agentAppMediaLibraryFailureSchema`, and `appMediaBrokerFailureSchema`. The
158
158
  latter preserves the broker projection: HTTP `code` becomes `error`, HTTP
159
- `error` becomes `message`, and `stage` and `retryable` stay unchanged. HTTP
160
- failures and transport envelopes are separate representations and do not decode
161
- as these outcomes.
159
+ `error` becomes `message`, and `stage`, `retryable` and optional `provider`
160
+ facts stay unchanged. HTTP failures and transport envelopes are separate
161
+ representations and do not decode as these outcomes.
162
+
163
+ Provider facts use Agar's canonical Media grammar: an allowlisted error category
164
+ and bounded numeric code, or `result_download` and a native HTTP status. The
165
+ durable owner retains those facts through interruption and replay, including
166
+ safety refusals, without retaining the original error message or cause in the
167
+ failure record. A terminal provider failure never redispatches the paid attempt.
168
+ The fresh pre-release schema replaces the safety-only refusal table; old preview
169
+ stores require an explicit host reset, not an automatic migration or dual
170
+ reader.
162
171
 
163
172
  See the [generated API](docs/api/README.md) for signatures and host ports. The
164
173
  Cloudflare entry needs the native Workers type declarations in a TypeScript
@@ -221,6 +221,13 @@ export declare const appMediaBrokerFailureSchema: z.ZodObject<{
221
221
  readonly "destination-write": "destination-write";
222
222
  }>;
223
223
  retryable: z.ZodBoolean;
224
+ provider: z.ZodOptional<z.ZodPipe<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodTransform<Readonly<{
225
+ category: "InferenceUpstreamError" | "AiInternalError" | "TypeError" | "AbortError" | "TimeoutError" | "Error";
226
+ code: number | null;
227
+ }> | Readonly<{
228
+ category: "result_download";
229
+ status: number;
230
+ }>, unknown>>>;
224
231
  }, z.core.$strict>;
225
232
  /** Refusal payloads emitted by the App media broker, separate from transport errors. */
226
233
  export declare const appMediaRefusalSchema: z.ZodUnion<readonly [z.ZodObject<{
@@ -268,6 +275,13 @@ export declare const appMediaRefusalSchema: z.ZodUnion<readonly [z.ZodObject<{
268
275
  readonly "destination-write": "destination-write";
269
276
  }>;
270
277
  retryable: z.ZodBoolean;
278
+ provider: z.ZodOptional<z.ZodPipe<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodTransform<Readonly<{
279
+ category: "InferenceUpstreamError" | "AiInternalError" | "TypeError" | "AbortError" | "TimeoutError" | "Error";
280
+ code: number | null;
281
+ }> | Readonly<{
282
+ category: "result_download";
283
+ status: number;
284
+ }>, unknown>>>;
271
285
  }, z.core.$strict>]>;
272
286
  /** Model-list payload or an existing broker refusal. */
273
287
  export declare const appMediaModelsOutcomeSchema: z.ZodUnion<readonly [z.ZodLazy<z.ZodArray<z.ZodReadonly<z.ZodObject<{
@@ -429,6 +443,13 @@ export declare const appMediaModelsOutcomeSchema: z.ZodUnion<readonly [z.ZodLazy
429
443
  readonly "destination-write": "destination-write";
430
444
  }>;
431
445
  retryable: z.ZodBoolean;
446
+ provider: z.ZodOptional<z.ZodPipe<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodTransform<Readonly<{
447
+ category: "InferenceUpstreamError" | "AiInternalError" | "TypeError" | "AbortError" | "TimeoutError" | "Error";
448
+ code: number | null;
449
+ }> | Readonly<{
450
+ category: "result_download";
451
+ status: number;
452
+ }>, unknown>>>;
432
453
  }, z.core.$strict>]>]>;
433
454
  /** Completed generation, Agar polling result, or an existing broker refusal. */
434
455
  export declare const appMediaGenerationOutcomeSchema: z.ZodUnion<readonly [z.ZodObject<{
@@ -496,4 +517,11 @@ export declare const appMediaGenerationOutcomeSchema: z.ZodUnion<readonly [z.Zod
496
517
  readonly "destination-write": "destination-write";
497
518
  }>;
498
519
  retryable: z.ZodBoolean;
520
+ provider: z.ZodOptional<z.ZodPipe<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodTransform<Readonly<{
521
+ category: "InferenceUpstreamError" | "AiInternalError" | "TypeError" | "AbortError" | "TimeoutError" | "Error";
522
+ code: number | null;
523
+ }> | Readonly<{
524
+ category: "result_download";
525
+ status: number;
526
+ }>, unknown>>>;
499
527
  }, z.core.$strict>]>]>;
@@ -75,6 +75,8 @@ export const appMediaBrokerFailureSchema = z.strictObject({
75
75
  stage: mediaFailureSchema.shape.stage,
76
76
  /** Original retry truth. */
77
77
  retryable: mediaFailureSchema.shape.retryable,
78
+ /** Original bounded provider facts, when available. */
79
+ provider: mediaFailureSchema.shape.provider,
78
80
  });
79
81
  /** Refusal payloads emitted by the App media broker, separate from transport errors. */
80
82
  export const appMediaRefusalSchema = z.union([
@@ -33,6 +33,13 @@ export declare const mediaFailureSchema: z.ZodObject<{
33
33
  readonly SAFETY_FILTER_REFUSED: "SAFETY_FILTER_REFUSED";
34
34
  }>;
35
35
  retryable: z.ZodBoolean;
36
+ provider: z.ZodOptional<z.ZodPipe<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodTransform<Readonly<{
37
+ category: "InferenceUpstreamError" | "AiInternalError" | "TypeError" | "AbortError" | "TimeoutError" | "Error";
38
+ code: number | null;
39
+ }> | Readonly<{
40
+ category: "result_download";
41
+ status: number;
42
+ }>, unknown>>>;
36
43
  }, z.core.$strict>;
37
44
  /** Sanitized Media failure carried from the authenticated host to Agar. */
38
45
  export type MediaFailure = z.output<typeof mediaFailureSchema>;
@@ -2,6 +2,7 @@ import * as z from "zod";
2
2
  import { mediaFailureStageSchema } from "./execution-error.js";
3
3
  import { Option, Schema } from "effect";
4
4
  import { reportCatchFailure } from "./failure.js";
5
+ import { mediaProviderDiagnosticWireSchema } from "@fungi.computer/agar/errors";
5
6
  /** Sanitized HTTP refusal emitted by a Media host and decoded at the browser boundary. */
6
7
  export const mediaFailureSchema = z.strictObject({
7
8
  /** Host-supplied presentation copy. */
@@ -23,6 +24,8 @@ export const mediaFailureSchema = z.strictObject({
23
24
  }),
24
25
  /** Whether repeating this exact attempt may recover. */
25
26
  retryable: z.boolean(),
27
+ /** Allowlisted provider facts only; the original cause never crosses HTTP. */
28
+ provider: mediaProviderDiagnosticWireSchema.optional(),
26
29
  });
27
30
  const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
28
31
  function parseMediaFailureJson(json) {
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import * as Redacted from "effect/Redacted";
3
3
  import { buildProviderInput, getMediaModel, estimateImageMilliunits, normalizeMediaProviderOutput, mediaProviderWireSchema, } from "../index.js";
4
4
  import { MediaExecutionError, MediaSafetyFilterRefusal, } from "../execution-error.js";
5
+ import { decodeMediaProviderErrorCategory, } from "@fungi.computer/agar/errors";
5
6
  import { estimateGatewayMilliunits, gatewayInput, isGatewayModel, } from "../gateway-models.js";
6
7
  import { GATEWAY_VIDEO_MAX_BYTES, gatewayOutputSchema, isMp4, } from "../gateway-wire.js";
7
8
  import { videoProviderAdapters } from "../video-providers/index.js";
@@ -31,16 +32,6 @@ const providerFailureCode = z.lazy(() => z
31
32
  .transform((message) => Number(message.split(":", 1)[0]))
32
33
  .nullable()
33
34
  .catch(null));
34
- const providerFailureCategory = z.lazy(() => z
35
- .enum([
36
- "InferenceUpstreamError",
37
- "AiInternalError",
38
- "TypeError",
39
- "AbortError",
40
- "TimeoutError",
41
- "Error",
42
- ])
43
- .catch("Error"));
44
35
  // Recognize the refusal, not the generic Workers AI error number or prompt text.
45
36
  const safetyFilterMessage = z.lazy(() => z.union([
46
37
  z
@@ -64,17 +55,21 @@ function workersAiInput(payload) {
64
55
  }
65
56
  /** Retain the original provider failure alongside its bounded code and category. */
66
57
  function providerFailure(model, cause) {
58
+ const provider = {
59
+ category: decodeMediaProviderErrorCategory(cause instanceof Error ? cause.name : undefined),
60
+ code: providerFailureCode.parse(cause instanceof Error ? cause.message : undefined),
61
+ };
67
62
  // Workers correlates this structured event with the request trace. Never log
68
63
  // the provider message/body: either can contain customer prompt content.
69
64
  console.error("Media provider request failed", {
70
65
  model,
71
- providerCode: providerFailureCode.parse(cause instanceof Error ? cause.message : undefined),
72
- category: providerFailureCategory.parse(cause instanceof Error ? cause.name : undefined),
66
+ providerCode: provider.code,
67
+ category: provider.category,
73
68
  }, Redacted.make(cause));
74
69
  return cause instanceof Error &&
75
70
  safetyFilterMessage.safeParse(cause.message).success
76
- ? new MediaSafetyFilterRefusal({ cause })
77
- : new MediaExecutionError("provider", { cause });
71
+ ? new MediaSafetyFilterRefusal({ cause, provider })
72
+ : new MediaExecutionError("provider", { cause, provider });
78
73
  }
79
74
  /** Run a gateway model and fetch its result URL within the byte bound. */
80
75
  async function generateThroughGateway(ai, settings, references, signal, options, fetchResult, outputFormat) {
@@ -95,7 +90,9 @@ async function generateThroughGateway(ai, settings, references, signal, options,
95
90
  const url = videoProviderAdapters[model.video.provider].decodeResultUrl(providerValue);
96
91
  const response = await fetchResult(url, { signal: signal ?? null });
97
92
  if (!response.ok)
98
- throw new Error("gateway video result unavailable");
93
+ throw new MediaExecutionError("normalization", {
94
+ provider: { category: "result_download", status: response.status },
95
+ });
99
96
  const bytes = await readResponseBytes(response, GATEWAY_VIDEO_MAX_BYTES);
100
97
  if (!isMp4(bytes))
101
98
  throw new Error("gateway video is not an MP4");
@@ -107,6 +104,8 @@ async function generateThroughGateway(ai, settings, references, signal, options,
107
104
  }), { fetchUrl: (url) => fetchResult(url, { signal: signal ?? null }) });
108
105
  }
109
106
  catch (cause) {
107
+ if (cause instanceof MediaExecutionError)
108
+ throw cause;
110
109
  throw new MediaExecutionError("normalization", { cause });
111
110
  }
112
111
  }
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod";
2
+ import { type MediaProviderDiagnostic } from "@fungi.computer/agar/errors";
2
3
  /** Public diagnostic labels only; never provider messages or request data. */
3
4
  export declare const mediaFailureStageSchema: z.ZodEnum<{
4
5
  /** Host admission before operation creation. */
@@ -26,17 +27,24 @@ export declare const mediaFailureStageSchema: z.ZodEnum<{
26
27
  }>;
27
28
  /** Execution stage at which a generation failed. */
28
29
  export type MediaFailureStage = Exclude<z.output<typeof mediaFailureStageSchema>, "admission" | "settings">;
30
+ /** Private cause plus an independently decoded, safe provider diagnostic. */
31
+ export type MediaExecutionErrorOptions = ErrorOptions & {
32
+ /** Safe provider facts, when the failure came from provider transport. */
33
+ readonly provider?: MediaProviderDiagnostic | undefined;
34
+ };
29
35
  /** A bounded stage marker for failures that cross the Media HTTP boundary. */
30
36
  export declare class MediaExecutionError extends Error {
31
37
  /** Execution stage where the failure occurred. */
32
38
  readonly stage: MediaFailureStage;
39
+ /** Safe provider facts only; the original cause remains private. */
40
+ readonly provider: MediaProviderDiagnostic | undefined;
33
41
  constructor(
34
42
  /** Execution stage where the failure occurred. */
35
- stage: MediaFailureStage, options?: ErrorOptions);
43
+ stage: MediaFailureStage, options?: MediaExecutionErrorOptions);
36
44
  }
37
45
  /** A provider safety filter refused generation; no output was delivered. */
38
46
  export declare class MediaSafetyFilterRefusal extends MediaExecutionError {
39
- constructor(options?: ErrorOptions);
47
+ constructor(options?: MediaExecutionErrorOptions);
40
48
  }
41
49
  /** Why the host's billing owner refused to admit a new generation. */
42
50
  export type MediaBillingRefusalCode = "insufficient_credits" | "daily_limit_reached" | "denied";
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod";
2
+ import { decodeMediaProviderDiagnostic, } from "@fungi.computer/agar/errors";
2
3
  /** Public diagnostic labels only; never provider messages or request data. */
3
4
  export const mediaFailureStageSchema = z.enum({
4
5
  /** Host admission before operation creation. */
@@ -27,12 +28,18 @@ export const mediaFailureStageSchema = z.enum({
27
28
  /** A bounded stage marker for failures that cross the Media HTTP boundary. */
28
29
  export class MediaExecutionError extends Error {
29
30
  stage;
31
+ /** Safe provider facts only; the original cause remains private. */
32
+ provider;
30
33
  constructor(
31
34
  /** Execution stage where the failure occurred. */
32
35
  stage, options) {
33
36
  super(`Media ${stage} failed`, options);
34
37
  this.stage = stage;
35
38
  this.name = "MediaExecutionError";
39
+ this.provider =
40
+ options?.provider === undefined
41
+ ? undefined
42
+ : decodeMediaProviderDiagnostic(options.provider);
36
43
  }
37
44
  }
38
45
  /** A provider safety filter refused generation; no output was delivered. */
@@ -1,5 +1,4 @@
1
1
  import * as z from "zod";
2
- import * as Schema from "effect/Schema";
3
2
  import { mediaRequestedImageFormatSchema, type MediaGenerationSettings } from "./index.js";
4
3
  export { mediaResultSchema } from "./media-operation-contract.js";
5
4
  /** Image settings and path references crossing a scoped MEDIA binding. */
@@ -272,17 +271,24 @@ export declare const mediaPendingSchema: z.ZodLazy<z.ZodObject<{
272
271
  retryAfterSeconds: z.ZodNumber;
273
272
  }, z.core.$strict>>;
274
273
  /** Parsing and model refusal stay shared; authorization remains host-owned. */
275
- export declare function parseMediaBindingCall(raw: Schema.Json): Readonly<{
274
+ export declare function parseMediaBindingCall(raw: z.core.util.JSONType): Readonly<{
275
+ /** The binding input passed its shared grammar. */
276
276
  kind: "parsed";
277
+ /** Validated command for host admission. */
277
278
  call: MediaBindingCall;
278
279
  }> | Readonly<{
280
+ /** Input size, grammar or model selection was refused. */
279
281
  kind: "refused";
282
+ /** Safe refusal returned without provider dispatch. */
280
283
  result: z.core.util.JSONType;
281
284
  }>;
282
285
  /** Relative service input after the host attaches its trusted Computer to references. */
283
286
  export type MediaBindingRequest = Readonly<{
287
+ /** Host-selected output filename within the App's library folder. */
284
288
  path: string;
289
+ /** Validated selection with the host's Computer attached to references. */
285
290
  settings: MediaGenerationSettings;
291
+ /** Optional image encoding; video output remains MP4. */
286
292
  outputFormat?: z.output<typeof mediaRequestedImageFormatSchema>;
287
293
  }>;
288
294
  /** Choose the same fixed filename on both hosts without a recursive App lookup. */
@@ -13,12 +13,17 @@ const mediaBindingLibraryPathSchema = z
13
13
  .refine(Schema.is(mediaLibraryPathSchema), "Select an absolute library folder");
14
14
  /** Image settings and path references crossing a scoped MEDIA binding. */
15
15
  export const mediaImageBindingInputSchema = z.strictObject({
16
+ /** Select image generation rather than video. */
16
17
  kind: z.literal("image"),
18
+ /** Attempt identity retained through polling and replay. */
17
19
  requestId: mediaRequestIdSchema,
20
+ /** App-owned absolute folder; the host supplies Computer authority. */
18
21
  libraryPath: mediaBindingLibraryPathSchema,
22
+ /** Image model selection, references and inference settings. */
19
23
  settings: mediaGenerationSelectionSchema
20
24
  .omit({ durationSeconds: true })
21
25
  .extend({
26
+ /** Optional inference-step count for models that support it. */
22
27
  steps: z
23
28
  .number()
24
29
  .int()
@@ -27,13 +32,18 @@ export const mediaImageBindingInputSchema = z.strictObject({
27
32
  .optional(),
28
33
  })
29
34
  .refine((settings) => getMediaModel(settings.model)?.kind === "image", "Select an image model from listModels"),
35
+ /** Requested image encoding, validated against the selected model. */
30
36
  outputFormat: mediaRequestedImageFormatSchema.optional(),
31
37
  });
32
38
  /** Video settings and path references crossing the same scoped binding. */
33
39
  export const mediaVideoBindingInputSchema = z.strictObject({
40
+ /** Select video generation rather than an image. */
34
41
  kind: z.literal("video"),
42
+ /** Attempt identity retained through polling and replay. */
35
43
  requestId: mediaRequestIdSchema,
44
+ /** App-owned absolute folder; the host supplies Computer authority. */
36
45
  libraryPath: mediaBindingLibraryPathSchema,
46
+ /** Catalog-backed video model, size, duration and references. */
37
47
  settings: mediaVideoSelectionSchema,
38
48
  });
39
49
  /** One generation identity, including the App-owned folder captured for replay. */
@@ -44,17 +54,23 @@ export const mediaBindingGenerationSchema = z.discriminatedUnion("kind", [
44
54
  /** Maintained MEDIA method grammar; guest input carries no principal or Computer. */
45
55
  export const mediaBindingCallSchema = z.discriminatedUnion("operation", [
46
56
  z.strictObject({
57
+ /** Read the model catalog without dispatching generation. */
47
58
  operation: z.literal("models"),
59
+ /** Catalog reads carry no guest authority or generation settings. */
48
60
  input: z.strictObject({}),
49
61
  }),
50
62
  z.strictObject({
63
+ /** Execute or poll one admitted generation identity. */
51
64
  operation: z.literal("generate"),
65
+ /** The App's captured generation selection and library folder. */
52
66
  input: mediaBindingGenerationSchema,
53
67
  }),
54
68
  ]);
55
69
  /** Background-generation reply emitted by the durable service. */
56
70
  export const mediaPendingSchema = z.lazy(() => z.strictObject({
71
+ /** Background work has been accepted but is not yet delivered. */
57
72
  status: z.literal("pending"),
73
+ /** Host-supplied delay before polling the same attempt again. */
58
74
  retryAfterSeconds: z.number().int().positive(),
59
75
  }));
60
76
  /** Parsing and model refusal stay shared; authorization remains host-owned. */
@@ -122,12 +138,15 @@ export async function readMediaBindingReply(input, computerId, response) {
122
138
  const failure = mediaFailureSchema.safeParse(value);
123
139
  if (!failure.success)
124
140
  return { error: "unavailable" };
125
- return z.json().parse(appMediaBrokerFailureSchema.parse({
141
+ const projected = {
126
142
  error: failure.data.code,
127
143
  message: failure.data.error,
128
144
  stage: failure.data.stage,
129
145
  retryable: failure.data.retryable,
130
- }));
146
+ };
147
+ if (failure.data.provider !== undefined)
148
+ projected.provider = failure.data.provider;
149
+ return z.json().parse(appMediaBrokerFailureSchema.parse(projected));
131
150
  }
132
151
  if (input.operation === "models")
133
152
  return z.json().parse(mediaModelsSchema.parse(value));
@@ -2,6 +2,8 @@ import { type MediaGenerationSettings } from "./index.js";
2
2
  import { z } from "zod";
3
3
  import type { MediaOperationContext, MediaOperationBucket } from "./media-operation-storage.js";
4
4
  import type { MediaProvider, MediaReferenceBytes } from "./index.js";
5
+ import { type MediaFailureStage } from "./execution-error.js";
6
+ import { type MediaProviderDiagnostic } from "@fungi.computer/agar/errors";
5
7
  /** Fixed generation destination captured before provider dispatch. */
6
8
  export declare const mediaOperationDestinationSchema: z.ZodLazy<z.ZodObject<{
7
9
  computerId: z.ZodString;
@@ -72,8 +74,15 @@ export type MediaOperationProjection = Readonly<{
72
74
  state: MediaOperationState;
73
75
  /** Terminal format mismatch after provider execution. */
74
76
  spentFailure: "output_format_mismatch" | null;
75
- /** Definitive provider refusal with no delivered or billable output. */
76
- providerRefusal: "safety_filter_refused" | null;
77
+ /** Safe terminal provider failure; no original cause or provider message. */
78
+ providerFailure: Readonly<{
79
+ /** Provider dispatch or output normalization, not a save failure. */
80
+ stage: Extract<MediaFailureStage, "provider" | "normalization">;
81
+ /** Preserve the model's safety refusal classification on replay. */
82
+ safetyRefused: boolean;
83
+ /** Allowlisted facts only; never the original error message or cause. */
84
+ provider?: MediaProviderDiagnostic | undefined;
85
+ }> | null;
77
86
  /** Pinned host price or unmetered quote. */
78
87
  estimatedCostMilliunits: number | null;
79
88
  /** Retained or saved image metadata. */
@@ -182,8 +191,15 @@ export declare class MediaOperationOwner {
182
191
  state: MediaOperationState;
183
192
  /** Terminal format mismatch after provider execution. */
184
193
  spentFailure: "output_format_mismatch" | null;
185
- /** Definitive provider refusal with no delivered or billable output. */
186
- providerRefusal: "safety_filter_refused" | null;
194
+ /** Safe terminal provider failure; no original cause or provider message. */
195
+ providerFailure: Readonly<{
196
+ /** Provider dispatch or output normalization, not a save failure. */
197
+ stage: Extract<MediaFailureStage, "provider" | "normalization">;
198
+ /** Preserve the model's safety refusal classification on replay. */
199
+ safetyRefused: boolean;
200
+ /** Allowlisted facts only; never the original error message or cause. */
201
+ provider?: MediaProviderDiagnostic | undefined;
202
+ }> | null;
187
203
  /** Pinned host price or unmetered quote. */
188
204
  estimatedCostMilliunits: number | null;
189
205
  /** Retained or saved image metadata. */
@@ -204,7 +220,7 @@ export declare class MediaOperationOwner {
204
220
  /** Run a background generation once; a retried alarm never redispatches. */
205
221
  alarm(): Promise<void>;
206
222
  /** An aborted in-flight provider call is terminal for this request ID. */
207
- interruptGeneration(leaseId: string, refusalInput?: NonNullable<MediaOperationProjection["providerRefusal"]>): MediaOperationProjection;
223
+ interruptGeneration(leaseId: string, failureInput?: NonNullable<MediaOperationProjection["providerFailure"]>): MediaOperationProjection;
208
224
  /** Retain immutable provider output before making the save state visible. */
209
225
  recordGenerated(leaseId: string, bytes: Uint8Array, formatInput: string): Promise<MediaRetentionResult>;
210
226
  /** Read exact retained bytes under the current save lease. */
@@ -225,8 +241,15 @@ export declare class MediaOperationOwner {
225
241
  state: MediaOperationState;
226
242
  /** Terminal format mismatch after provider execution. */
227
243
  spentFailure: "output_format_mismatch" | null;
228
- /** Definitive provider refusal with no delivered or billable output. */
229
- providerRefusal: "safety_filter_refused" | null;
244
+ /** Safe terminal provider failure; no original cause or provider message. */
245
+ providerFailure: Readonly<{
246
+ /** Provider dispatch or output normalization, not a save failure. */
247
+ stage: Extract<MediaFailureStage, "provider" | "normalization">;
248
+ /** Preserve the model's safety refusal classification on replay. */
249
+ safetyRefused: boolean;
250
+ /** Allowlisted facts only; never the original error message or cause. */
251
+ provider?: MediaProviderDiagnostic | undefined;
252
+ }> | null;
230
253
  /** Pinned host price or unmetered quote. */
231
254
  estimatedCostMilliunits: number | null;
232
255
  /** Retained or saved image metadata. */
@@ -256,8 +279,15 @@ export declare class MediaOperationOwner {
256
279
  state: MediaOperationState;
257
280
  /** Terminal format mismatch after provider execution. */
258
281
  spentFailure: "output_format_mismatch" | null;
259
- /** Definitive provider refusal with no delivered or billable output. */
260
- providerRefusal: "safety_filter_refused" | null;
282
+ /** Safe terminal provider failure; no original cause or provider message. */
283
+ providerFailure: Readonly<{
284
+ /** Provider dispatch or output normalization, not a save failure. */
285
+ stage: Extract<MediaFailureStage, "provider" | "normalization">;
286
+ /** Preserve the model's safety refusal classification on replay. */
287
+ safetyRefused: boolean;
288
+ /** Allowlisted facts only; never the original error message or cause. */
289
+ provider?: MediaProviderDiagnostic | undefined;
290
+ }> | null;
261
291
  /** Pinned host price or unmetered quote. */
262
292
  estimatedCostMilliunits: number | null;
263
293
  /** Retained or saved image metadata. */
@@ -283,8 +313,15 @@ export declare class MediaOperationOwner {
283
313
  state: MediaOperationState;
284
314
  /** Terminal format mismatch after provider execution. */
285
315
  spentFailure: "output_format_mismatch" | null;
286
- /** Definitive provider refusal with no delivered or billable output. */
287
- providerRefusal: "safety_filter_refused" | null;
316
+ /** Safe terminal provider failure; no original cause or provider message. */
317
+ providerFailure: Readonly<{
318
+ /** Provider dispatch or output normalization, not a save failure. */
319
+ stage: Extract<MediaFailureStage, "provider" | "normalization">;
320
+ /** Preserve the model's safety refusal classification on replay. */
321
+ safetyRefused: boolean;
322
+ /** Allowlisted facts only; never the original error message or cause. */
323
+ provider?: MediaProviderDiagnostic | undefined;
324
+ }> | null;
288
325
  /** Pinned host price or unmetered quote. */
289
326
  estimatedCostMilliunits: number | null;
290
327
  /** Retained or saved image metadata. */
@@ -3,8 +3,13 @@ import { defineIdFamily } from "@fungi.computer/cairn/id";
3
3
  import { mediaGenerationSettingsSchema, validateMediaGenerationSettings, } from "./index.js";
4
4
  import { z } from "zod";
5
5
  import { reportCatchFailure } from "./failure.js";
6
- import { MediaSafetyFilterRefusal } from "./execution-error.js";
7
- const providerRefusalSchema = z.literal("safety_filter_refused");
6
+ import { MediaExecutionError, MediaSafetyFilterRefusal, mediaFailureStageSchema, } from "./execution-error.js";
7
+ import { mediaProviderDiagnosticWireSchema, } from "@fungi.computer/agar/errors";
8
+ const providerFailureSchema = z.strictObject({
9
+ stage: mediaFailureStageSchema.extract(["provider", "normalization"]),
10
+ safetyRefused: z.boolean(),
11
+ provider: mediaProviderDiagnosticWireSchema.optional(),
12
+ });
8
13
  const ids = defineIdFamily({
9
14
  family: "hub-media-operation",
10
15
  version: 1,
@@ -143,7 +148,7 @@ const schemaStatements = [
143
148
  dispatched_at INTEGER
144
149
  ) STRICT`,
145
150
  "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",
151
+ "CREATE TABLE hub_media_provider_failure (singleton INTEGER PRIMARY KEY CHECK(singleton=1),failure_json TEXT NOT NULL) STRICT",
147
152
  ];
148
153
  function fingerprint(input) {
149
154
  return JSON.stringify(input);
@@ -235,8 +240,8 @@ export class MediaOperationOwner {
235
240
  }
236
241
  else {
237
242
  // 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");
243
+ // Missing failure facts are not healed or incrementally migrated here.
244
+ this.ctx.storage.sql.exec("SELECT failure_json FROM hub_media_provider_failure LIMIT 0");
240
245
  }
241
246
  this.initialized = true;
242
247
  }
@@ -301,12 +306,14 @@ export class MediaOperationOwner {
301
306
  code: z.literal("output_format_mismatch"),
302
307
  })
303
308
  .parse(failure).code;
304
- const refusal = this.ctx.storage.sql
305
- .exec("SELECT code FROM hub_media_provider_refusal WHERE singleton=1")
309
+ const failureRow = this.ctx.storage.sql
310
+ .exec("SELECT failure_json FROM hub_media_provider_failure WHERE singleton=1")
306
311
  .toArray()[0];
307
- const providerRefusal = refusal === undefined
312
+ const providerFailure = failureRow === undefined
308
313
  ? null
309
- : z.strictObject({ code: providerRefusalSchema }).parse(refusal).code;
314
+ : this.decodeJson(z
315
+ .strictObject({ failure_json: z.string().max(1024) })
316
+ .parse(failureRow).failure_json, providerFailureSchema);
310
317
  return Object.freeze({
311
318
  operationId: record.operationId,
312
319
  requestId: record.requestId,
@@ -314,7 +321,7 @@ export class MediaOperationOwner {
314
321
  owner: record.owner,
315
322
  state: record.state,
316
323
  spentFailure,
317
- providerRefusal,
324
+ providerFailure,
318
325
  estimatedCostMilliunits: record.estimatedCostMilliunits,
319
326
  image: record.image,
320
327
  result: record.result,
@@ -684,8 +691,13 @@ export class MediaOperationOwner {
684
691
  // Nothing was delivered; the next retry sees interrupted and releases
685
692
  // the billing hold at zero units.
686
693
  try {
687
- this.interruptGeneration(leaseId, cause instanceof MediaSafetyFilterRefusal
688
- ? "safety_filter_refused"
694
+ this.interruptGeneration(leaseId, cause instanceof MediaExecutionError &&
695
+ (cause.stage === "provider" || cause.stage === "normalization")
696
+ ? {
697
+ stage: cause.stage,
698
+ safetyRefused: cause instanceof MediaSafetyFilterRefusal,
699
+ provider: cause.provider,
700
+ }
689
701
  : undefined);
690
702
  }
691
703
  catch (cause) {
@@ -702,8 +714,8 @@ export class MediaOperationOwner {
702
714
  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
715
  }
704
716
  /** An aborted in-flight provider call is terminal for this request ID. */
705
- interruptGeneration(leaseId, refusalInput) {
706
- const refusal = providerRefusalSchema.optional().parse(refusalInput);
717
+ interruptGeneration(leaseId, failureInput) {
718
+ const failure = providerFailureSchema.optional().parse(failureInput);
707
719
  const record = this.requireRecord();
708
720
  if (record.state !== "generating" || record.leaseId !== leaseId)
709
721
  throw new MediaOperationError("lease_lost");
@@ -720,8 +732,8 @@ export class MediaOperationOwner {
720
732
  writeReceipt: null,
721
733
  result: null,
722
734
  });
723
- if (refusal !== undefined)
724
- this.ctx.storage.sql.exec("INSERT INTO hub_media_provider_refusal(singleton,code) VALUES(1,?)", refusal);
735
+ if (failure !== undefined)
736
+ this.ctx.storage.sql.exec("INSERT INTO hub_media_provider_failure(singleton,failure_json) VALUES(1,?)", JSON.stringify(failure));
725
737
  return this.project(interrupted);
726
738
  });
727
739
  }
@@ -72,8 +72,13 @@ export function executeMediaOperation(input) {
72
72
  }
73
73
  if (claim.kind === "interrupted") {
74
74
  yield* settleNothing();
75
- if (claim.operation.providerRefusal === "safety_filter_refused")
76
- return yield* Effect.fail(new MediaSafetyFilterRefusal());
75
+ const failure = claim.operation.providerFailure;
76
+ if (failure !== null)
77
+ return yield* Effect.fail(failure.safetyRefused
78
+ ? new MediaSafetyFilterRefusal({ provider: failure.provider })
79
+ : new MediaExecutionError(failure.stage, {
80
+ provider: failure.provider,
81
+ }));
77
82
  }
78
83
  if (claim.kind !== "claimed" || claim.leaseId === undefined)
79
84
  return {
@@ -149,11 +154,16 @@ export function executeMediaOperation(input) {
149
154
  }, "provider").pipe(
150
155
  // A provider failure delivered nothing and wrote no output: end the
151
156
  // request now and release its hold rather than leave it unresolved.
152
- Effect.tapError((error) => error.stage === "provider"
153
- ? call(() => input.operation.interruptGeneration(leaseId, error instanceof MediaSafetyFilterRefusal
154
- ? "safety_filter_refused"
155
- : undefined), "operation").pipe(Effect.andThen(settleNothing()), Effect.ignore)
156
- : Effect.void));
157
+ Effect.tapError((error) => {
158
+ const stage = error.stage;
159
+ return stage === "provider" || stage === "normalization"
160
+ ? call(() => input.operation.interruptGeneration(leaseId, {
161
+ stage,
162
+ safetyRefused: error instanceof MediaSafetyFilterRefusal,
163
+ provider: error.provider,
164
+ }), "operation").pipe(Effect.andThen(settleNothing()), Effect.ignore)
165
+ : Effect.void;
166
+ }));
157
167
  if (generation.kind === "interrupted") {
158
168
  yield* settleNothing();
159
169
  return { kind: "interrupted" };
package/dist/server.d.ts CHANGED
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  import { validateMediaGenerationSettings, type MediaProvider, type MediaReference, type MediaReferenceBytes } from "./index.js";
5
5
  import { type MediaExecution, type MediaMetering } from "./media-operation-contract.js";
6
6
  import { MediaExecutionError, type MediaBillingRefusalCode } from "./execution-error.js";
7
- export { MediaBillingRefusal, MediaExecutionError, type MediaBillingRefusalCode, type MediaFailureStage, } from "./execution-error.js";
7
+ export { MediaBillingRefusal, MediaExecutionError, type MediaBillingRefusalCode, type MediaFailureStage, type MediaExecutionErrorOptions, } from "./execution-error.js";
8
8
  export { mediaUnits, type MediaUnits } from "./gateway-models.js";
9
9
  export type { MediaMetering, MediaOperation, } from "./media-operation-contract.js";
10
10
  export { MediaOperationOwner, mediaOperationDestinationSchema, mediaOperationCoordinatesSchema, mediaOperationStateSchema, mediaImageSchema, mediaOperationResultSchema, } from "./media-operation-owner.js";
package/dist/server.js CHANGED
@@ -51,6 +51,7 @@ function failure(error, refusalMessages) {
51
51
  stage: "provider",
52
52
  code: "SAFETY_FILTER_REFUSED",
53
53
  retryable: false,
54
+ provider: error.provider,
54
55
  }, { status: 400, headers });
55
56
  if (error instanceof MediaExecutionError &&
56
57
  error.cause instanceof MediaOutputFormatMismatch)
@@ -105,6 +106,7 @@ function failure(error, refusalMessages) {
105
106
  stage: error.stage,
106
107
  code: "HOST_FAILURE",
107
108
  retryable: error.stage !== "normalization" && error.stage !== "provider",
109
+ provider: error instanceof MediaExecutionError ? error.provider : undefined,
108
110
  }, {
109
111
  status: 503,
110
112
  headers,