@nodetool-ai/runtime 0.8.1 → 0.8.2

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 (256) hide show
  1. package/dist/.nodetool-build-stamp +1 -1
  2. package/dist/context.d.ts +31 -3
  3. package/dist/context.d.ts.map +1 -1
  4. package/dist/context.js +366 -158
  5. package/dist/context.js.map +1 -1
  6. package/dist/generation-registry.d.ts +8 -0
  7. package/dist/generation-registry.d.ts.map +1 -1
  8. package/dist/generation-registry.js +18 -0
  9. package/dist/generation-registry.js.map +1 -1
  10. package/dist/host-binaries.d.ts.map +1 -1
  11. package/dist/host-binaries.js +18 -10
  12. package/dist/host-binaries.js.map +1 -1
  13. package/dist/image-codec.d.ts +2 -0
  14. package/dist/image-codec.d.ts.map +1 -1
  15. package/dist/image-codec.js +23 -3
  16. package/dist/image-codec.js.map +1 -1
  17. package/dist/image-model-tasks.d.ts +15 -0
  18. package/dist/image-model-tasks.d.ts.map +1 -0
  19. package/dist/image-model-tasks.js +24 -0
  20. package/dist/image-model-tasks.js.map +1 -0
  21. package/dist/index.d.ts +14 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +10 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/media-ref-bytes.d.ts +18 -2
  26. package/dist/media-ref-bytes.d.ts.map +1 -1
  27. package/dist/media-ref-bytes.js +26 -16
  28. package/dist/media-ref-bytes.js.map +1 -1
  29. package/dist/node-executor.d.ts +6 -0
  30. package/dist/node-executor.d.ts.map +1 -1
  31. package/dist/providers/aki-provider.d.ts +2 -0
  32. package/dist/providers/aki-provider.d.ts.map +1 -1
  33. package/dist/providers/aki-provider.js +6 -0
  34. package/dist/providers/aki-provider.js.map +1 -1
  35. package/dist/providers/atlascloud-provider.d.ts +2 -0
  36. package/dist/providers/atlascloud-provider.d.ts.map +1 -1
  37. package/dist/providers/atlascloud-provider.js +22 -0
  38. package/dist/providers/atlascloud-provider.js.map +1 -1
  39. package/dist/providers/base-provider.d.ts +36 -24
  40. package/dist/providers/base-provider.d.ts.map +1 -1
  41. package/dist/providers/base-provider.js +124 -148
  42. package/dist/providers/base-provider.js.map +1 -1
  43. package/dist/providers/claude-agent-provider.d.ts +6 -36
  44. package/dist/providers/claude-agent-provider.d.ts.map +1 -1
  45. package/dist/providers/claude-agent-provider.js +208 -26
  46. package/dist/providers/claude-agent-provider.js.map +1 -1
  47. package/dist/providers/codex-provider.d.ts +2 -0
  48. package/dist/providers/codex-provider.d.ts.map +1 -1
  49. package/dist/providers/codex-provider.js +15 -2
  50. package/dist/providers/codex-provider.js.map +1 -1
  51. package/dist/providers/cohere-provider.d.ts +2 -1
  52. package/dist/providers/cohere-provider.d.ts.map +1 -1
  53. package/dist/providers/cohere-provider.js +5 -0
  54. package/dist/providers/cohere-provider.js.map +1 -1
  55. package/dist/providers/core-tools.d.ts +2 -1
  56. package/dist/providers/core-tools.d.ts.map +1 -1
  57. package/dist/providers/core-tools.js +2 -1
  58. package/dist/providers/core-tools.js.map +1 -1
  59. package/dist/providers/credential-check.d.ts.map +1 -1
  60. package/dist/providers/credential-check.js +5 -0
  61. package/dist/providers/credential-check.js.map +1 -1
  62. package/dist/providers/custom-openai-provider.d.ts +2 -0
  63. package/dist/providers/custom-openai-provider.d.ts.map +1 -1
  64. package/dist/providers/custom-openai-provider.js +8 -0
  65. package/dist/providers/custom-openai-provider.js.map +1 -1
  66. package/dist/providers/dreamina-models.d.ts +5 -0
  67. package/dist/providers/dreamina-models.d.ts.map +1 -0
  68. package/dist/providers/dreamina-models.js +57 -0
  69. package/dist/providers/dreamina-models.js.map +1 -0
  70. package/dist/providers/dreamina-music.d.ts +52 -0
  71. package/dist/providers/dreamina-music.d.ts.map +1 -0
  72. package/dist/providers/dreamina-music.js +115 -0
  73. package/dist/providers/dreamina-music.js.map +1 -0
  74. package/dist/providers/dreamina-provider.d.ts +62 -0
  75. package/dist/providers/dreamina-provider.d.ts.map +1 -0
  76. package/dist/providers/dreamina-provider.js +790 -0
  77. package/dist/providers/dreamina-provider.js.map +1 -0
  78. package/dist/providers/elevenlabs-provider.d.ts +2 -1
  79. package/dist/providers/elevenlabs-provider.d.ts.map +1 -1
  80. package/dist/providers/elevenlabs-provider.js +5 -0
  81. package/dist/providers/elevenlabs-provider.js.map +1 -1
  82. package/dist/providers/evolink-provider.d.ts +2 -0
  83. package/dist/providers/evolink-provider.d.ts.map +1 -1
  84. package/dist/providers/evolink-provider.js +8 -0
  85. package/dist/providers/evolink-provider.js.map +1 -1
  86. package/dist/providers/fake-provider.d.ts +2 -1
  87. package/dist/providers/fake-provider.d.ts.map +1 -1
  88. package/dist/providers/fake-provider.js +10 -0
  89. package/dist/providers/fake-provider.js.map +1 -1
  90. package/dist/providers/fal-provider.d.ts +2 -1
  91. package/dist/providers/fal-provider.d.ts.map +1 -1
  92. package/dist/providers/fal-provider.js +27 -0
  93. package/dist/providers/fal-provider.js.map +1 -1
  94. package/dist/providers/gemini-provider.d.ts +2 -1
  95. package/dist/providers/gemini-provider.d.ts.map +1 -1
  96. package/dist/providers/gemini-provider.js +15 -0
  97. package/dist/providers/gemini-provider.js.map +1 -1
  98. package/dist/providers/higgsfield-provider.d.ts +2 -1
  99. package/dist/providers/higgsfield-provider.d.ts.map +1 -1
  100. package/dist/providers/higgsfield-provider.js +12 -0
  101. package/dist/providers/higgsfield-provider.js.map +1 -1
  102. package/dist/providers/huggingface-provider.d.ts +2 -1
  103. package/dist/providers/huggingface-provider.d.ts.map +1 -1
  104. package/dist/providers/huggingface-provider.js +10 -0
  105. package/dist/providers/huggingface-provider.js.map +1 -1
  106. package/dist/providers/index.d.ts +5 -2
  107. package/dist/providers/index.d.ts.map +1 -1
  108. package/dist/providers/index.js +13 -2
  109. package/dist/providers/index.js.map +1 -1
  110. package/dist/providers/jina-provider.d.ts +2 -1
  111. package/dist/providers/jina-provider.d.ts.map +1 -1
  112. package/dist/providers/jina-provider.js +5 -0
  113. package/dist/providers/jina-provider.js.map +1 -1
  114. package/dist/providers/kie-provider.d.ts +2 -1
  115. package/dist/providers/kie-provider.d.ts.map +1 -1
  116. package/dist/providers/kie-provider.js +14 -0
  117. package/dist/providers/kie-provider.js.map +1 -1
  118. package/dist/providers/llama-provider.d.ts +4 -1
  119. package/dist/providers/llama-provider.d.ts.map +1 -1
  120. package/dist/providers/llama-provider.js +9 -3
  121. package/dist/providers/llama-provider.js.map +1 -1
  122. package/dist/providers/loop-tracing.d.ts +17 -0
  123. package/dist/providers/loop-tracing.d.ts.map +1 -0
  124. package/dist/providers/loop-tracing.js +132 -0
  125. package/dist/providers/loop-tracing.js.map +1 -0
  126. package/dist/providers/meshy-provider.d.ts +2 -1
  127. package/dist/providers/meshy-provider.d.ts.map +1 -1
  128. package/dist/providers/meshy-provider.js +6 -0
  129. package/dist/providers/meshy-provider.js.map +1 -1
  130. package/dist/providers/minimax-provider.d.ts +2 -0
  131. package/dist/providers/minimax-provider.d.ts.map +1 -1
  132. package/dist/providers/minimax-provider.js +11 -0
  133. package/dist/providers/minimax-provider.js.map +1 -1
  134. package/dist/providers/mistral-provider.d.ts +2 -0
  135. package/dist/providers/mistral-provider.d.ts.map +1 -1
  136. package/dist/providers/mistral-provider.js +5 -0
  137. package/dist/providers/mistral-provider.js.map +1 -1
  138. package/dist/providers/node-llama-cpp-provider.d.ts +2 -1
  139. package/dist/providers/node-llama-cpp-provider.d.ts.map +1 -1
  140. package/dist/providers/node-llama-cpp-provider.js +8 -3
  141. package/dist/providers/node-llama-cpp-provider.js.map +1 -1
  142. package/dist/providers/nodetool-provider.d.ts +1 -1
  143. package/dist/providers/nodetool-provider.d.ts.map +1 -1
  144. package/dist/providers/nodetool-provider.js +0 -2
  145. package/dist/providers/nodetool-provider.js.map +1 -1
  146. package/dist/providers/ollama-provider.d.ts +2 -1
  147. package/dist/providers/ollama-provider.d.ts.map +1 -1
  148. package/dist/providers/ollama-provider.js +5 -0
  149. package/dist/providers/ollama-provider.js.map +1 -1
  150. package/dist/providers/openai-provider.d.ts +10 -2
  151. package/dist/providers/openai-provider.d.ts.map +1 -1
  152. package/dist/providers/openai-provider.js +63 -16
  153. package/dist/providers/openai-provider.js.map +1 -1
  154. package/dist/providers/openrouter-provider.d.ts +52 -1
  155. package/dist/providers/openrouter-provider.d.ts.map +1 -1
  156. package/dist/providers/openrouter-provider.js +372 -11
  157. package/dist/providers/openrouter-provider.js.map +1 -1
  158. package/dist/providers/provider-error.d.ts.map +1 -1
  159. package/dist/providers/provider-error.js +7 -1
  160. package/dist/providers/provider-error.js.map +1 -1
  161. package/dist/providers/provider-generations.d.ts +2 -2
  162. package/dist/providers/provider-generations.js +2 -2
  163. package/dist/providers/provider-request-log.d.ts.map +1 -1
  164. package/dist/providers/provider-request-log.js +18 -7
  165. package/dist/providers/provider-request-log.js.map +1 -1
  166. package/dist/providers/python-provider.d.ts +6 -1
  167. package/dist/providers/python-provider.d.ts.map +1 -1
  168. package/dist/providers/python-provider.js +30 -11
  169. package/dist/providers/python-provider.js.map +1 -1
  170. package/dist/providers/replicate-provider.d.ts +2 -1
  171. package/dist/providers/replicate-provider.d.ts.map +1 -1
  172. package/dist/providers/replicate-provider.js +22 -0
  173. package/dist/providers/replicate-provider.js.map +1 -1
  174. package/dist/providers/requesty-provider.d.ts +30 -0
  175. package/dist/providers/requesty-provider.d.ts.map +1 -0
  176. package/dist/providers/requesty-provider.js +87 -0
  177. package/dist/providers/requesty-provider.js.map +1 -0
  178. package/dist/providers/reve-provider.d.ts +2 -1
  179. package/dist/providers/reve-provider.d.ts.map +1 -1
  180. package/dist/providers/reve-provider.js +6 -0
  181. package/dist/providers/reve-provider.js.map +1 -1
  182. package/dist/providers/rodin-provider.d.ts +2 -1
  183. package/dist/providers/rodin-provider.d.ts.map +1 -1
  184. package/dist/providers/rodin-provider.js +6 -0
  185. package/dist/providers/rodin-provider.js.map +1 -1
  186. package/dist/providers/together-operations.d.ts +27 -0
  187. package/dist/providers/together-operations.d.ts.map +1 -0
  188. package/dist/providers/together-operations.js +95 -0
  189. package/dist/providers/together-operations.js.map +1 -0
  190. package/dist/providers/together-provider.d.ts +5 -8
  191. package/dist/providers/together-provider.d.ts.map +1 -1
  192. package/dist/providers/together-provider.js +54 -159
  193. package/dist/providers/together-provider.js.map +1 -1
  194. package/dist/providers/topaz-provider.d.ts +2 -1
  195. package/dist/providers/topaz-provider.d.ts.map +1 -1
  196. package/dist/providers/topaz-provider.js +5 -0
  197. package/dist/providers/topaz-provider.js.map +1 -1
  198. package/dist/providers/types.d.ts +38 -0
  199. package/dist/providers/types.d.ts.map +1 -1
  200. package/dist/providers/types.js.map +1 -1
  201. package/dist/providers/useapi-provider.d.ts +2 -1
  202. package/dist/providers/useapi-provider.d.ts.map +1 -1
  203. package/dist/providers/useapi-provider.js +8 -0
  204. package/dist/providers/useapi-provider.js.map +1 -1
  205. package/dist/providers/vllm-provider.d.ts +2 -4
  206. package/dist/providers/vllm-provider.d.ts.map +1 -1
  207. package/dist/providers/vllm-provider.js +1 -5
  208. package/dist/providers/vllm-provider.js.map +1 -1
  209. package/dist/providers/voyage-provider.d.ts +2 -1
  210. package/dist/providers/voyage-provider.d.ts.map +1 -1
  211. package/dist/providers/voyage-provider.js +5 -0
  212. package/dist/providers/voyage-provider.js.map +1 -1
  213. package/dist/providers/xai-provider.d.ts +2 -0
  214. package/dist/providers/xai-provider.d.ts.map +1 -1
  215. package/dist/providers/xai-provider.js +8 -0
  216. package/dist/providers/xai-provider.js.map +1 -1
  217. package/dist/recommended-models.d.ts.map +1 -1
  218. package/dist/recommended-models.js +20 -0
  219. package/dist/recommended-models.js.map +1 -1
  220. package/dist/run-context.d.ts +20 -0
  221. package/dist/run-context.d.ts.map +1 -0
  222. package/dist/run-context.js +18 -0
  223. package/dist/run-context.js.map +1 -0
  224. package/dist/run-trace-context.d.ts +30 -0
  225. package/dist/run-trace-context.d.ts.map +1 -0
  226. package/dist/run-trace-context.js +80 -0
  227. package/dist/run-trace-context.js.map +1 -0
  228. package/dist/run-trace-processor.d.ts +65 -0
  229. package/dist/run-trace-processor.d.ts.map +1 -0
  230. package/dist/run-trace-processor.js +417 -0
  231. package/dist/run-trace-processor.js.map +1 -0
  232. package/dist/run-trace-serialization.d.ts +5 -0
  233. package/dist/run-trace-serialization.d.ts.map +1 -0
  234. package/dist/run-trace-serialization.js +113 -0
  235. package/dist/run-trace-serialization.js.map +1 -0
  236. package/dist/storage-workspace.d.ts +3 -0
  237. package/dist/storage-workspace.d.ts.map +1 -1
  238. package/dist/storage-workspace.js +32 -17
  239. package/dist/storage-workspace.js.map +1 -1
  240. package/dist/telemetry.d.ts +9 -3
  241. package/dist/telemetry.d.ts.map +1 -1
  242. package/dist/telemetry.js +53 -20
  243. package/dist/telemetry.js.map +1 -1
  244. package/dist/trace-exporters.d.ts +2 -29
  245. package/dist/trace-exporters.d.ts.map +1 -1
  246. package/dist/trace-exporters.js +10 -2
  247. package/dist/trace-exporters.js.map +1 -1
  248. package/dist/tracing-helpers.d.ts +15 -22
  249. package/dist/tracing-helpers.d.ts.map +1 -1
  250. package/dist/tracing-helpers.js +38 -6
  251. package/dist/tracing-helpers.js.map +1 -1
  252. package/dist/trim-video-window.d.ts +8 -0
  253. package/dist/trim-video-window.d.ts.map +1 -0
  254. package/dist/trim-video-window.js +50 -0
  255. package/dist/trim-video-window.js.map +1 -0
  256. package/package.json +19 -5
package/dist/context.js CHANGED
@@ -1,22 +1,13 @@
1
- /**
2
- * ProcessingContext – runtime context for node execution.
3
- *
4
- * Port of src/nodetool/workflows/processing_context.py.
5
- *
6
- * Provides:
7
- * - Message queue for emitting ProcessingMessages.
8
- * - Cache get/set interface.
9
- * - Output normalization (sanitize memory URIs, etc.).
10
- * - Asset handling with pluggable storage adapters.
11
- */
12
- import { buildAssetGenerationMetadata, isShortResourceId, packageAssetHttpPath, parsePackageAssetUri } from "@nodetool-ai/protocol";
1
+ import { getRunTraceScope } from "./run-trace-context.js";
2
+ import { withSpan } from "./tracing-helpers.js";
3
+ import { assetFileNameCandidates, buildAssetGenerationMetadata, isShortResourceId, packageAssetHttpPath, parsePackageAssetUri } from "@nodetool-ai/protocol";
13
4
  import { AgentMemory } from "./agent-memory.js";
14
5
  import { recordInvocationAsset, recordInvocationCost } from "./invocation-account.js";
15
6
  import { VariableChannel } from "./variable-channel.js";
16
7
  import { loadMediaRefBytes } from "./media-ref-bytes.js";
17
8
  import { objectTrackingRequestSchema, parseObjectTrackingResult } from "./providers/object-tracking.js";
18
9
  import { requestVideoToAudio, VIDEO_TO_AUDIO_TASK, VideoToAudioRequest } from "./providers/video-to-audio.js";
19
- import { encodeRawImageRef } from "./image-codec.js";
10
+ import { encodeRawImageRef, validateImageEvidence } from "./image-codec.js";
20
11
  import { extForImageMime, sniffImageMime } from "./providers/image-mime.js";
21
12
  import { GenerationScopeError, isAuthoritativeProviderTerminalError, runWithGenerationReceipt } from "./generation-receipt.js";
22
13
  import { generationRegistry } from "./generation-registry.js";
@@ -28,11 +19,6 @@ import { getNodeBuiltinSync } from "@nodetool-ai/config";
28
19
  // lazily so this module loads in browser / Edge runtimes. `resolveWorkspacePath`
29
20
  // and the `randomUUID` fallback both degrade gracefully when these are
30
21
  // unavailable.
31
- /**
32
- * Secret values shorter than this are not recorded for masking: redacting a
33
- * two-character value would blank unrelated text wherever it happens to occur.
34
- */
35
- const MIN_MASKABLE_SECRET_LENGTH = 8;
36
22
  /**
37
23
  * ProcessingContext variable key carrying the provider/model the currently
38
24
  * running agent loop is itself talking to. Every loop that executes tools
@@ -481,6 +467,11 @@ export class ProcessingContext {
481
467
  _secretResolver = null;
482
468
  /** Every secret value the resolver handed out this run (see {@link ProcessingContext.getResolvedSecretValues}). */
483
469
  _resolvedSecrets = new Set();
470
+ /** Identity shared by all work beneath an app operation. */
471
+ appRunContext;
472
+ runTraceContext;
473
+ appRunCostAccount;
474
+ _appRunDocuments = new Map();
484
475
  /** Fetch function used by HTTP helpers. */
485
476
  _fetch;
486
477
  /** Optional temporary URL resolver for stored assets. */
@@ -526,6 +517,12 @@ export class ProcessingContext {
526
517
  this.assetOutputMode = opts.assetOutputMode ?? "native";
527
518
  this.persistOutputAssets = opts.persistOutputAssets ?? true;
528
519
  this._generationLifecycle = opts.generationLifecycle ?? null;
520
+ this.appRunContext = opts.appRunContext ? Object.freeze({ ...opts.appRunContext }) : null;
521
+ this.runTraceContext = opts.runTraceContext ?? getRunTraceScope() ?? null;
522
+ if (this.runTraceContext?.secretValues instanceof Set) {
523
+ this._resolvedSecrets = this.runTraceContext.secretValues;
524
+ }
525
+ this.appRunCostAccount = opts.appRunCostAccount ?? (this.appRunContext ? { llmCostUsd: 0, unpriced: false } : null);
529
526
  this.cache = opts.cache ?? new MemoryCache();
530
527
  this.storage = opts.storage ?? null;
531
528
  this.assetStorage = opts.assetStorage ?? null;
@@ -575,12 +572,12 @@ export class ProcessingContext {
575
572
  */
576
573
  copy(opts) {
577
574
  const next = new ProcessingContext({
578
- jobId: this.jobId,
575
+ jobId: opts?.jobId ?? this.jobId,
579
576
  projectId: this.projectId,
580
- workflowId: this.workflowId,
577
+ workflowId: opts?.workflowId !== undefined ? opts.workflowId : this.workflowId,
581
578
  threadId: this.threadId,
582
579
  userId: this.userId,
583
- workspaceDir: this.workspaceDir,
580
+ workspace: opts?.workspace !== undefined ? opts.workspace : this.workspace,
584
581
  assetOutputMode: this.assetOutputMode,
585
582
  persistOutputAssets: this.persistOutputAssets,
586
583
  cache: this.cache,
@@ -596,7 +593,11 @@ export class ProcessingContext {
596
593
  tempUrlResolver: this._tempUrlResolver ?? undefined,
597
594
  modelInterfaces: this._modelInterfaces ?? undefined,
598
595
  retainMessageQueue: this._retainMessageQueue,
599
- triggerEvent: this.triggerEvent
596
+ triggerEvent: this.triggerEvent,
597
+ generationLifecycle: opts?.generationLifecycle ?? this._generationLifecycle ?? undefined,
598
+ appRunContext: opts?.appRunContext ?? this.appRunContext ?? undefined,
599
+ runTraceContext: opts?.runTraceContext ?? this.runTraceContext ?? undefined,
600
+ appRunCostAccount: !opts?.appRunContext || opts.appRunContext.appRunId === this.appRunContext?.appRunId ? this.appRunCostAccount ?? undefined : undefined
600
601
  });
601
602
  if (opts?.inheritMessageListeners !== false) {
602
603
  for (const listener of this._messageListeners) {
@@ -606,6 +607,12 @@ export class ProcessingContext {
606
607
  if (opts?.shareMemory) {
607
608
  next.memory = this.memory;
608
609
  }
610
+ if (!opts?.runTraceContext) {
611
+ next._resolvedSecrets = this._resolvedSecrets;
612
+ }
613
+ if (next.appRunContext?.appRunId === this.appRunContext?.appRunId) {
614
+ next._appRunDocuments = this._appRunDocuments;
615
+ }
609
616
  next.signal = this.signal;
610
617
  next._providerResolver = this._providerResolver;
611
618
  next._modelInterfaces = this._modelInterfaces;
@@ -813,7 +820,7 @@ export class ProcessingContext {
813
820
  if (!this._secretResolver)
814
821
  return null;
815
822
  const value = await this._secretResolver(key, this.userId);
816
- if (value != null && value.length >= MIN_MASKABLE_SECRET_LENGTH) {
823
+ if (value != null && value.length > 0) {
817
824
  this._resolvedSecrets.add(value);
818
825
  }
819
826
  return value ?? null;
@@ -1041,6 +1048,26 @@ export class ProcessingContext {
1041
1048
  getTotalCost() {
1042
1049
  return this._totalCost;
1043
1050
  }
1051
+ /** Null means at least one LLM call could not be priced. */
1052
+ getAppRunLlmCost() {
1053
+ return this.appRunCostAccount && !this.appRunCostAccount.unpriced
1054
+ ? this.appRunCostAccount.llmCostUsd
1055
+ : null;
1056
+ }
1057
+ getAppRunDocuments() {
1058
+ return [...this._appRunDocuments.values()].map((reference) => ({
1059
+ ...reference
1060
+ }));
1061
+ }
1062
+ recordAppRunDocument(kind, document) {
1063
+ if (this.appRunContext && document) {
1064
+ this._appRunDocuments.set(`${kind}:${document.id}`, {
1065
+ kind,
1066
+ id: document.id
1067
+ });
1068
+ }
1069
+ return document;
1070
+ }
1044
1071
  getOperationCosts() {
1045
1072
  return this._operationCosts;
1046
1073
  }
@@ -1199,7 +1226,8 @@ export class ProcessingContext {
1199
1226
  /** Create a persisted sketch (image document) owned by the current user. */
1200
1227
  async createImageDocument(args) {
1201
1228
  const fn = this.requireModelInterface("createImageDocument");
1202
- return fn({ userId: this.userId, ...args });
1229
+ const document = await fn({ userId: this.userId, ...args });
1230
+ return this.recordAppRunDocument("sketch", document);
1203
1231
  }
1204
1232
  /** Load a persisted timeline sequence owned by the current user. */
1205
1233
  async getTimelineSequence(id) {
@@ -1209,12 +1237,14 @@ export class ProcessingContext {
1209
1237
  /** Create a persisted timeline sequence owned by the current user. */
1210
1238
  async createTimelineSequence(sequence) {
1211
1239
  const fn = this.requireModelInterface("createTimelineSequence");
1212
- return fn({ userId: this.userId, sequence });
1240
+ const document = await fn({ userId: this.userId, sequence });
1241
+ return this.recordAppRunDocument("timeline", document);
1213
1242
  }
1214
1243
  /** Replace a persisted timeline sequence's document. */
1215
1244
  async updateTimelineSequence(id, sequence) {
1216
1245
  const fn = this.requireModelInterface("updateTimelineSequence");
1217
- return fn({ userId: this.userId, id, sequence });
1246
+ const document = await fn({ userId: this.userId, id, sequence });
1247
+ return this.recordAppRunDocument("timeline", document);
1218
1248
  }
1219
1249
  /** Load a persisted script owned by the current user. */
1220
1250
  async getScript(id) {
@@ -1224,12 +1254,14 @@ export class ProcessingContext {
1224
1254
  /** Create a persisted script owned by the current user. */
1225
1255
  async createScript(args) {
1226
1256
  const fn = this.requireModelInterface("createScript");
1227
- return fn({ userId: this.userId, ...args });
1257
+ const document = await fn({ userId: this.userId, ...args });
1258
+ return this.recordAppRunDocument("script", document);
1228
1259
  }
1229
1260
  /** Replace a persisted script's document (and optional timeline link). */
1230
1261
  async updateScript(id, args) {
1231
1262
  const fn = this.requireModelInterface("updateScript");
1232
- return fn({ userId: this.userId, id, ...args });
1263
+ const document = await fn({ userId: this.userId, id, ...args });
1264
+ return this.recordAppRunDocument("script", document);
1233
1265
  }
1234
1266
  /** Load a persisted storyboard owned by the current user. */
1235
1267
  async getStoryboard(id) {
@@ -1249,12 +1281,14 @@ export class ProcessingContext {
1249
1281
  /** Create a persisted storyboard owned by the current user. */
1250
1282
  async createStoryboard(args) {
1251
1283
  const fn = this.requireModelInterface("createStoryboard");
1252
- return fn({ userId: this.userId, ...args });
1284
+ const document = await fn({ userId: this.userId, ...args });
1285
+ return this.recordAppRunDocument("storyboard", document);
1253
1286
  }
1254
1287
  /** Replace a persisted storyboard's document (and optional timeline link). */
1255
1288
  async updateStoryboard(id, args) {
1256
1289
  const fn = this.requireModelInterface("updateStoryboard");
1257
- return fn({ userId: this.userId, id, ...args });
1290
+ const document = await fn({ userId: this.userId, id, ...args });
1291
+ return this.recordAppRunDocument("storyboard", document);
1258
1292
  }
1259
1293
  /** The current user's entity library, narrowed by the given filters. */
1260
1294
  async listEntities(args = {}) {
@@ -1289,7 +1323,7 @@ export class ProcessingContext {
1289
1323
  * `null` when that interface is not configured.
1290
1324
  */
1291
1325
  async getAssetInfo(assetId) {
1292
- const fn = this._modelInterfaces?.getAssetInfo;
1326
+ const fn = this.modelInterfaces()?.getAssetInfo;
1293
1327
  if (!fn) {
1294
1328
  return null;
1295
1329
  }
@@ -1507,8 +1541,8 @@ export class ProcessingContext {
1507
1541
  * from the server's `/api/storage` route. Returns the bytes plus an `attempts`
1508
1542
  * trail for callers that build detailed errors; `null` bytes means unresolved.
1509
1543
  */
1510
- async resolveAssetBytes(assetId) {
1511
- const idCandidates = this.parseAssetIdCandidates(assetId);
1544
+ async resolveAssetBytes(assetId, options) {
1545
+ let idCandidates = this.parseAssetIdCandidates(assetId);
1512
1546
  const trimmed = assetId.trim();
1513
1547
  const attempts = [];
1514
1548
  const adapters = this.assetAdapters();
@@ -1595,6 +1629,33 @@ export class ProcessingContext {
1595
1629
  }
1596
1630
  }
1597
1631
  }
1632
+ // An asset row names the stored file exactly (`<owner>/<id>.<ext>`), so a
1633
+ // reference that resolves to one is read by key and never by listing.
1634
+ let assetRowFound = false;
1635
+ if (this.hasModelInterface("getAssetInfo")) {
1636
+ let asset = null;
1637
+ const ownerPrefix = `${this.userId}/`;
1638
+ const rowIdCandidates = new Set(idCandidates);
1639
+ for (const candidate of idCandidates) {
1640
+ if (candidate.startsWith(ownerPrefix)) {
1641
+ rowIdCandidates.add(candidate.slice(ownerPrefix.length));
1642
+ }
1643
+ }
1644
+ for (const candidate of rowIdCandidates) {
1645
+ asset = await this.getAssetInfo(candidate);
1646
+ if (asset) {
1647
+ break;
1648
+ }
1649
+ }
1650
+ if (asset) {
1651
+ assetRowFound = true;
1652
+ idCandidates = assetFileNameCandidates(asset.id, asset.content_type);
1653
+ }
1654
+ else if (options?.requireOwnedAsset) {
1655
+ attempts.push(`asset unavailable or not owned: ${trimmed}`);
1656
+ return { bytes: null, attempts };
1657
+ }
1658
+ }
1598
1659
  for (const adapter of adapters) {
1599
1660
  for (const candidate of idCandidates) {
1600
1661
  for (const key of this.assetKeysFor(candidate)) {
@@ -1606,9 +1667,10 @@ export class ProcessingContext {
1606
1667
  }
1607
1668
  // Extension-tolerant fallback: locate the stored file whose name starts
1608
1669
  // with the id, since the URN extension may differ from the one on disk
1609
- // (e.g. jpeg vs jpg). Only reached when the exact-key lookups all miss.
1670
+ // (e.g. jpeg vs jpg). Only reached when the exact-key lookups all miss
1671
+ // and no asset row named the file.
1610
1672
  const bareId = idCandidates[idCandidates.length - 1];
1611
- if (bareId) {
1673
+ if (bareId && !assetRowFound) {
1612
1674
  for (const prefix of this.assetListingPrefixes(bareId)) {
1613
1675
  try {
1614
1676
  const listing = await adapter.list(prefix);
@@ -1876,8 +1938,9 @@ export class ProcessingContext {
1876
1938
  // Strict, anchored sanitizer for the URIs that may flow into the asset /
1877
1939
  // storage retrieval path. Accept asset references, the browser-facing
1878
1940
  // storage route, and URIs from configured storage adapters. External URLs
1879
- // and unrecognized schemes stay out of the storage adapter.
1880
- const RESOLVABLE_URI_RE = /^(?:asset:\/\/|\/?api\/storage\/|(?:memory|file|s3|supabase):\/\/)[A-Za-z0-9._~/%-]+$/;
1941
+ // and unrecognized schemes stay out of the storage adapter. `:` is allowed
1942
+ // for named owner ids such as `projects/personal:<user_id>/assets/…`.
1943
+ const RESOLVABLE_URI_RE = /^(?:asset:\/\/|\/?api\/storage\/|(?:memory|file|s3|supabase):\/\/)[A-Za-z0-9._~/%:-]+$/;
1881
1944
  const isResolvableUri = (uri) => RESOLVABLE_URI_RE.test(uri);
1882
1945
  // An `asset://` reference that cannot be dereferenced must not reach the
1883
1946
  // provider: providers `fetch` a non-data URI, and the SSRF guard then
@@ -2003,12 +2066,82 @@ export class ProcessingContext {
2003
2066
  }
2004
2067
  return resolved;
2005
2068
  }
2069
+ async resolveRequiredImageEvidence(messages, required = []) {
2070
+ const evidence = new Map();
2071
+ const resolve = async (ref) => {
2072
+ const identity = ref.uri?.startsWith("data:")
2073
+ ? "inline image"
2074
+ : ref.uri ?? ref.asset_id ?? "inline image";
2075
+ const cached = ref.uri ? evidence.get(ref.uri) : undefined;
2076
+ if (cached) {
2077
+ return cached;
2078
+ }
2079
+ try {
2080
+ const bytes = await loadMediaRefBytes(ref, this);
2081
+ if (!bytes?.length) {
2082
+ throw new Error("no image bytes");
2083
+ }
2084
+ const mimeType = sniffImageMime(bytes);
2085
+ if (!mimeType ||
2086
+ !["image/png", "image/jpeg", "image/webp", "image/gif"].includes(mimeType)) {
2087
+ throw new Error("unsupported image format");
2088
+ }
2089
+ await validateImageEvidence(bytes);
2090
+ const block = {
2091
+ type: "image_url",
2092
+ image: {
2093
+ uri: `data:${mimeType};base64,${Buffer.from(bytes).toString("base64")}`,
2094
+ mimeType
2095
+ }
2096
+ };
2097
+ if (ref.uri) {
2098
+ evidence.set(ref.uri, block);
2099
+ }
2100
+ return block;
2101
+ }
2102
+ catch (error) {
2103
+ throw new Error(`Required image evidence unavailable: ${identity} (${error instanceof Error ? error.message : String(error)})`);
2104
+ }
2105
+ };
2106
+ for (const uri of required) {
2107
+ await resolve({ type: "image", uri });
2108
+ }
2109
+ const result = [];
2110
+ for (const message of messages) {
2111
+ if (!Array.isArray(message.content)) {
2112
+ result.push(message);
2113
+ continue;
2114
+ }
2115
+ const content = [];
2116
+ let imageIndex = 0;
2117
+ for (const part of message.content) {
2118
+ if (part.type === "image_url") {
2119
+ imageIndex++;
2120
+ content.push(await resolve(part.image), {
2121
+ type: "text",
2122
+ text: `[required image ${part.image.uri && !part.image.uri.startsWith("data:")
2123
+ ? part.image.uri
2124
+ : `inline image ${imageIndex}`}]`
2125
+ });
2126
+ }
2127
+ else {
2128
+ content.push(part);
2129
+ }
2130
+ }
2131
+ result.push({ ...message, content });
2132
+ }
2133
+ return result;
2134
+ }
2006
2135
  async dispatchCapability(provider, req) {
2007
2136
  const params = req.params ?? {};
2008
2137
  switch (req.capability) {
2009
2138
  case "generate_message":
2010
2139
  return provider.generateMessageTraced({
2011
- messages: await this.resolveMessageMediaUris(params.messages ?? []),
2140
+ messages: params.strict_image_evidence === true
2141
+ ? await this.resolveRequiredImageEvidence(params.messages ?? [], Array.isArray(params.required_images)
2142
+ ? params.required_images.filter(isString)
2143
+ : [])
2144
+ : await this.resolveMessageMediaUris(params.messages ?? []),
2012
2145
  model: req.model,
2013
2146
  tools: params.tools,
2014
2147
  toolChoice: params.tool_choice,
@@ -2163,6 +2296,22 @@ export class ProcessingContext {
2163
2296
  return provider.vectorizeImage(params.image, {
2164
2297
  model: { id: req.model, name: req.model, provider: req.provider }
2165
2298
  });
2299
+ case "estimate_depth":
2300
+ return provider.estimateDepth(params.image, {
2301
+ model: { id: req.model, name: req.model, provider: req.provider },
2302
+ signal: isAbortSignal(params.signal) ? params.signal : this.signal
2303
+ });
2304
+ case "rerank":
2305
+ return provider.rerank({
2306
+ model: req.model,
2307
+ query: isString(params.query) ? params.query : "",
2308
+ // Every entry stays, so result indices point into the caller's list.
2309
+ documents: Array.isArray(params.documents)
2310
+ ? params.documents.map((doc) => (isString(doc) ? doc : String(doc)))
2311
+ : [],
2312
+ topK: isNumber(params.top_k) ? params.top_k : undefined,
2313
+ signal: isAbortSignal(params.signal) ? params.signal : this.signal
2314
+ });
2166
2315
  case "track_object": {
2167
2316
  const request = objectTrackingRequestSchema.parse(params);
2168
2317
  if (!provider.getCapabilities().includes("track_object")) {
@@ -2175,12 +2324,18 @@ export class ProcessingContext {
2175
2324
  throw new Error("The tracking source video could not be loaded.");
2176
2325
  }
2177
2326
  signal.throwIfAborted();
2178
- const output = await provider.trackObject(video, { ...request, model: req.model, signal });
2327
+ const output = await provider.trackObject(video, {
2328
+ ...request,
2329
+ model: req.model,
2330
+ signal
2331
+ });
2179
2332
  signal.throwIfAborted();
2180
2333
  return parseObjectTrackingResult(output, request);
2181
2334
  }
2182
2335
  case "video_to_video": {
2183
- const signal = isAbortSignal(params.signal) ? params.signal : this.signal;
2336
+ const signal = isAbortSignal(params.signal)
2337
+ ? params.signal
2338
+ : this.signal;
2184
2339
  const referenceImages = coerceByteList(params.reference_images ?? params.referenceImages);
2185
2340
  const referenceAssetIds = coerceStringList(params.reference_asset_ids ?? params.referenceAssetIds);
2186
2341
  signal.throwIfAborted();
@@ -2378,137 +2533,174 @@ export class ProcessingContext {
2378
2533
  */
2379
2534
  async runGenerationWith(req, call, opts) {
2380
2535
  const id = req.id ?? randomUUID();
2381
- const startedAt = Date.now();
2382
- const origin = this.generationOrigin(req, req.origin);
2383
- const controller = new AbortController();
2384
- const signals = [controller.signal, this.signal];
2385
- if (req.signal)
2386
- signals.push(req.signal);
2387
- const signal = AbortSignal.any(signals);
2388
- const accept = opts?.onGenerationAccepted ??
2389
- this._generationLifecycle?.onGenerationAccepted;
2390
- const terminal = opts?.onGenerationTerminal ??
2391
- this._generationLifecycle?.onGenerationTerminal;
2392
- let durableAccepted = false;
2393
- let terminalAttempted = false;
2394
- try {
2395
- const accepted = await accept?.({ generationId: id, request: req });
2396
- if (accepted?.skipProvider) {
2397
- throw new GenerationAlreadyAcceptedError(accepted.existingGenerationId ?? id);
2398
- }
2399
- durableAccepted = accepted?.durable === true;
2400
- generationRegistry.register(id, {
2401
- userId: this.userId,
2402
- abort: () => controller.abort(new Error("Generation cancelled"))
2403
- });
2404
- this.emitPrediction("running", req, id, null, undefined, startedAt, {
2405
- origin
2406
- });
2407
- const provider = opts?.withoutProvider
2408
- ? null
2409
- : await this.getProvider(req.provider);
2410
- const { value: output, receipt: recorded, providerResult } = await runWithGenerationReceipt(() => call(provider, signal), {
2411
- providerRequestOptions: accepted ?? undefined,
2412
- onProviderRequestAccepted: opts?.onProviderRequestAccepted ??
2413
- this._generationLifecycle?.onProviderRequestAccepted,
2414
- onProviderRequestBound: opts?.onProviderRequestBound ??
2415
- this._generationLifecycle?.onProviderRequestBound
2416
- });
2417
- const encoded = encodedAudioOf(output);
2418
- const persistedAssets = req.persist
2419
- ? await this.persistGenerationBytes(id, req, encoded ? [encoded.data] : mediaBuffers(output), encoded?.mimeType)
2420
- : [];
2421
- const assets = persistedAssets.filter((asset) => asset !== null);
2422
- const late = opts?.receiptAfterPersist?.() ?? null;
2423
- const receipt = late || recorded ? { ...(recorded ?? {}), ...(late ?? {}) } : null;
2424
- const assetIds = assets
2425
- .map((asset) => asset.asset_id)
2426
- .filter((assetId) => typeof assetId === "string");
2427
- const durableAssetIds = persistedAssets.map((asset) => asset?.asset_id ?? null);
2536
+ return withSpan("generation", { "generation.id": id, "generation.provider": req.provider, "generation.kind": req.capability }, async () => {
2537
+ const startedAt = Date.now();
2538
+ const origin = this.generationOrigin(req, req.origin);
2539
+ const controller = new AbortController();
2540
+ const signals = [controller.signal, this.signal];
2541
+ if (req.signal)
2542
+ signals.push(req.signal);
2543
+ const signal = AbortSignal.any(signals);
2544
+ const accept = opts?.onGenerationAccepted ??
2545
+ this._generationLifecycle?.onGenerationAccepted;
2546
+ const terminal = opts?.onGenerationTerminal ??
2547
+ this._generationLifecycle?.onGenerationTerminal;
2548
+ let durableAccepted = false;
2549
+ let terminalAttempted = false;
2428
2550
  try {
2429
- terminalAttempted = true;
2430
- await terminal?.({
2431
- generationId: id,
2432
- request: req,
2433
- status: "completed",
2434
- output: providerResult ?? output,
2435
- receipt,
2436
- assetIds: durableAssetIds
2551
+ const accepted = await accept?.({ generationId: id, request: req });
2552
+ if (accepted?.skipProvider) {
2553
+ throw new GenerationAlreadyAcceptedError(accepted.existingGenerationId ?? id);
2554
+ }
2555
+ durableAccepted = accepted?.durable === true;
2556
+ generationRegistry.register(id, {
2557
+ userId: this.userId,
2558
+ abort: () => controller.abort(new Error("Generation cancelled"))
2437
2559
  });
2438
- }
2439
- catch (terminalError) {
2440
- this.emit({
2441
- type: "log_update",
2442
- node_id: req.nodeId ?? "",
2443
- node_name: req.nodeId ?? "",
2444
- workflow_id: req.workflowId ?? this.workflowId ?? null,
2445
- severity: "warning",
2446
- content: `Generation ${id} completed but durable finalization will be recovered: ${terminalError instanceof Error ? terminalError.message : String(terminalError)}`
2560
+ this.emitPrediction("running", req, id, null, undefined, startedAt, {
2561
+ origin
2447
2562
  });
2448
- }
2449
- this.emitPrediction("completed", req, id, generationResultData(req.capability, output), undefined, startedAt, { origin, asset_ids: assetIds, receipt });
2450
- generationRegistry.settle(id, {
2451
- status: "completed",
2452
- asset_ids: assetIds,
2453
- receipt
2454
- });
2455
- return {
2456
- id,
2457
- output,
2458
- assets,
2459
- receipt,
2460
- duration_ms: Date.now() - startedAt
2461
- };
2462
- }
2463
- catch (error) {
2464
- const cause = error instanceof GenerationScopeError ? error.cause : error;
2465
- const receipt = error instanceof GenerationScopeError ? error.receipt : null;
2466
- const providerResult = error instanceof GenerationScopeError
2467
- ? error.providerResult
2468
- : undefined;
2469
- const message = cause instanceof Error ? cause.message : String(cause);
2470
- if (cause instanceof GenerationAlreadyAcceptedError) {
2471
- throw cause;
2472
- }
2473
- const status = signal.aborted
2474
- ? durableAccepted
2475
- ? "running"
2476
- : "cancelled"
2477
- : durableAccepted && !isAuthoritativeProviderTerminalError(cause)
2478
- ? "running"
2479
- : "failed";
2480
- if (durableAccepted && !terminalAttempted) {
2563
+ const provider = opts?.withoutProvider
2564
+ ? null
2565
+ : await this.getProvider(req.provider);
2566
+ const { value: output, receipt: recorded, providerResult } = await runWithGenerationReceipt(() => call(provider, signal), {
2567
+ providerRequestOptions: accepted ?? undefined,
2568
+ onProviderRequestAccepted: opts?.onProviderRequestAccepted ??
2569
+ this._generationLifecycle?.onProviderRequestAccepted,
2570
+ onProviderRequestBound: opts?.onProviderRequestBound ??
2571
+ this._generationLifecycle?.onProviderRequestBound
2572
+ });
2573
+ const encoded = encodedAudioOf(output);
2574
+ const persistedAssets = req.persist
2575
+ ? await this.persistGenerationBytes(id, req, encoded ? [encoded.data] : mediaBuffers(output), encoded?.mimeType)
2576
+ : [];
2577
+ const assets = persistedAssets.filter((asset) => asset !== null);
2578
+ let delivery;
2579
+ if (opts?.finalizeOutput) {
2580
+ try {
2581
+ delivery = await opts.finalizeOutput(output, assets);
2582
+ }
2583
+ catch (error) {
2584
+ delivery = {
2585
+ status: "failed",
2586
+ error: error instanceof Error ? error.message : String(error)
2587
+ };
2588
+ }
2589
+ }
2590
+ const deliveryFields = {};
2591
+ if (delivery) {
2592
+ deliveryFields.delivery = delivery;
2593
+ }
2594
+ const late = opts?.receiptAfterPersist?.() ?? null;
2595
+ const receipt = late || recorded ? { ...(recorded ?? {}), ...(late ?? {}) } : null;
2596
+ const assetIds = assets
2597
+ .map((asset) => asset.asset_id)
2598
+ .filter((assetId) => typeof assetId === "string");
2599
+ const durableAssetIds = persistedAssets.map((asset) => asset?.asset_id ?? null);
2481
2600
  try {
2482
2601
  terminalAttempted = true;
2483
2602
  await terminal?.({
2484
2603
  generationId: id,
2485
2604
  request: req,
2486
- status: isAuthoritativeProviderTerminalError(cause)
2487
- ? "failed"
2488
- : "recovering",
2489
- output: providerResult,
2490
- error: message,
2605
+ status: "completed",
2606
+ output: providerResult ?? output,
2491
2607
  receipt,
2492
- assetIds: []
2608
+ assetIds: durableAssetIds,
2609
+ ...deliveryFields
2493
2610
  });
2494
2611
  }
2495
- catch {
2496
- // The provider observation remains authoritative. The durable row
2497
- // can be finalized by the recovery worker.
2612
+ catch (terminalError) {
2613
+ this.emit({
2614
+ type: "log_update",
2615
+ node_id: req.nodeId ?? "",
2616
+ node_name: req.nodeId ?? "",
2617
+ workflow_id: req.workflowId ?? this.workflowId ?? null,
2618
+ severity: "warning",
2619
+ content: `Generation ${id} completed but durable finalization will be recovered: ${terminalError instanceof Error ? terminalError.message : String(terminalError)}`
2620
+ });
2498
2621
  }
2622
+ this.emitPrediction("completed", req, id, delivery ? { delivery } : generationResultData(req.capability, output), undefined, startedAt, { origin, asset_ids: assetIds, receipt });
2623
+ generationRegistry.settle(id, {
2624
+ status: "completed",
2625
+ asset_ids: assetIds,
2626
+ receipt,
2627
+ ...deliveryFields
2628
+ });
2629
+ return {
2630
+ id,
2631
+ output,
2632
+ assets,
2633
+ receipt,
2634
+ duration_ms: Date.now() - startedAt,
2635
+ ...deliveryFields
2636
+ };
2499
2637
  }
2500
- this.emitPrediction(status, req, id, null, message, startedAt, {
2501
- origin,
2502
- receipt
2503
- });
2504
- generationRegistry.settle(id, {
2505
- status,
2506
- error: message,
2507
- asset_ids: [],
2508
- receipt
2509
- });
2510
- throw cause;
2511
- }
2638
+ catch (error) {
2639
+ const cause = error instanceof GenerationScopeError ? error.cause : error;
2640
+ const receipt = error instanceof GenerationScopeError ? error.receipt : null;
2641
+ const providerResult = error instanceof GenerationScopeError
2642
+ ? error.providerResult
2643
+ : undefined;
2644
+ const message = cause instanceof Error ? cause.message : String(cause);
2645
+ if (cause instanceof GenerationAlreadyAcceptedError) {
2646
+ throw cause;
2647
+ }
2648
+ const status = signal.aborted
2649
+ ? durableAccepted
2650
+ ? "running"
2651
+ : "cancelled"
2652
+ : durableAccepted && !isAuthoritativeProviderTerminalError(cause)
2653
+ ? "running"
2654
+ : "failed";
2655
+ if (durableAccepted && !terminalAttempted) {
2656
+ try {
2657
+ terminalAttempted = true;
2658
+ await terminal?.({
2659
+ generationId: id,
2660
+ request: req,
2661
+ status: isAuthoritativeProviderTerminalError(cause)
2662
+ ? "failed"
2663
+ : "recovering",
2664
+ output: providerResult,
2665
+ error: message,
2666
+ receipt,
2667
+ assetIds: []
2668
+ });
2669
+ }
2670
+ catch {
2671
+ // The provider observation remains authoritative. The durable row
2672
+ // can be finalized by the recovery worker.
2673
+ }
2674
+ }
2675
+ if (this.appRunContext && !durableAccepted && !terminalAttempted) {
2676
+ try {
2677
+ terminalAttempted = true;
2678
+ await terminal?.({
2679
+ generationId: id,
2680
+ request: req,
2681
+ status: signal.aborted ? "cancelled" : "failed",
2682
+ error: message,
2683
+ receipt,
2684
+ assetIds: []
2685
+ });
2686
+ }
2687
+ catch {
2688
+ // Preserve the provider failure; the live ledger can retry its write.
2689
+ }
2690
+ }
2691
+ this.emitPrediction(status, req, id, null, message, startedAt, {
2692
+ origin,
2693
+ receipt
2694
+ });
2695
+ generationRegistry.settle(id, {
2696
+ status,
2697
+ error: message,
2698
+ asset_ids: [],
2699
+ receipt
2700
+ });
2701
+ throw cause;
2702
+ }
2703
+ });
2512
2704
  }
2513
2705
  /**
2514
2706
  * Save a generation's media output as assets, stamping the generation id
@@ -2763,6 +2955,22 @@ export class ProcessingContext {
2763
2955
  // retried by the recovery worker.
2764
2956
  }
2765
2957
  }
2958
+ if (this.appRunContext && !durableAccepted && !terminalAttempted) {
2959
+ try {
2960
+ terminalAttempted = true;
2961
+ await terminal?.({
2962
+ generationId: id,
2963
+ request: req,
2964
+ status: signal.aborted ? "cancelled" : "failed",
2965
+ error: message,
2966
+ receipt,
2967
+ assetIds: []
2968
+ });
2969
+ }
2970
+ catch {
2971
+ // Preserve the provider failure; the live ledger can retry its write.
2972
+ }
2973
+ }
2766
2974
  this.emitPrediction(status, req, id, null, message, startedAt, {
2767
2975
  origin,
2768
2976
  receipt