@koda-sl/baker-cli 0.124.0 → 0.128.0-dev.70bf43ce4

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.
@@ -3,9 +3,9 @@ import {
3
3
  __toESM
4
4
  } from "./chunk-5WRI5ZAA.js";
5
5
 
6
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
6
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
7
7
  var require_safe_stable_stringify = __commonJS({
8
- "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
8
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
9
9
  "use strict";
10
10
  var { hasOwnProperty } = Object.prototype;
11
11
  var stringify = configure2();
@@ -138,9 +138,9 @@ var require_safe_stable_stringify = __commonJS({
138
138
  }
139
139
  if (value) {
140
140
  return (value2) => {
141
- let message = `Object can not safely be stringified. Received type ${typeof value2}`;
142
- if (typeof value2 !== "function") message += ` (${value2.toString()})`;
143
- throw new Error(message);
141
+ let message2 = `Object can not safely be stringified. Received type ${typeof value2}`;
142
+ if (typeof value2 !== "function") message2 += ` (${value2.toString()})`;
143
+ throw new Error(message2);
144
144
  };
145
145
  }
146
146
  }
@@ -652,6 +652,9 @@ var HttpClient = class {
652
652
  async postJson(path16, body, signal) {
653
653
  return await this.requestJson("POST", path16, body, signal);
654
654
  }
655
+ async putJson(path16, body, signal) {
656
+ return await this.requestJson("PUT", path16, body, signal);
657
+ }
655
658
  async getJson(path16, signal) {
656
659
  return await this.requestJson("GET", path16, void 0, signal);
657
660
  }
@@ -679,8 +682,8 @@ var HttpClient = class {
679
682
  try {
680
683
  const res = await this.fetchFn(url, {
681
684
  method,
682
- headers: method === "POST" ? { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` } : { Authorization: `Bearer ${this.apiKey}` },
683
- body: method === "POST" ? JSON.stringify(body) : void 0,
685
+ headers: method === "GET" ? { Authorization: `Bearer ${this.apiKey}` } : { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
686
+ body: method === "GET" ? void 0 : JSON.stringify(body),
684
687
  signal: controller.signal
685
688
  });
686
689
  if (res.ok) return { kind: "value", value: await res.json() };
@@ -717,33 +720,33 @@ async function parseErrorBody(res) {
717
720
  const errObj = body.error ?? {};
718
721
  return classifyHttpError(res.status, errObj, errObj.message ?? `HTTP ${res.status}`);
719
722
  }
720
- function classifyHttpError(status, errObj, message) {
723
+ function classifyHttpError(status, errObj, message2) {
721
724
  if (errObj.code === CONTENT_POLICY_CODE) {
722
- return { kind: "content_policy", status, provider: errObj.provider, message };
725
+ return { kind: "content_policy", status, provider: errObj.provider, message: message2 };
723
726
  }
724
727
  if (status === 401 || status === 403) {
725
- return { kind: "unauthorized", status, message };
728
+ return { kind: "unauthorized", status, message: message2 };
726
729
  }
727
730
  if (status === 400 || status === 422) {
728
- return { kind: "validation", status, message, details: errObj.details };
731
+ return { kind: "validation", status, message: message2, details: errObj.details };
729
732
  }
730
733
  if (status === 502 || status === 504) {
731
734
  if (errObj.code === "provider_timeout" || status === 504) {
732
- return { kind: "timeout", provider: errObj.provider, message };
735
+ return { kind: "timeout", provider: errObj.provider, message: message2 };
733
736
  }
734
737
  return {
735
738
  kind: "provider",
736
739
  status,
737
740
  provider: errObj.provider,
738
741
  code: errObj.code ?? "provider_error",
739
- message,
742
+ message: message2,
740
743
  retryable: errObj.retryable ?? true
741
744
  };
742
745
  }
743
746
  if (status >= 500 || status === 429) {
744
- return { kind: "server", status, message };
747
+ return { kind: "server", status, message: message2 };
745
748
  }
746
- return { kind: "validation", status, message, details: errObj.details };
749
+ return { kind: "validation", status, message: message2, details: errObj.details };
747
750
  }
748
751
  function backoffMs(attempt) {
749
752
  return 1e3 * 2 ** attempt;
@@ -777,7 +780,9 @@ function failedJobError(error) {
777
780
  retryable: error.retryable ?? false
778
781
  });
779
782
  }
780
- var JOB_POLL_INTERVAL_MS = 3e3;
783
+ function pollInterval(attempt) {
784
+ return attempt < 15 ? 1e3 : 3e3;
785
+ }
781
786
  var JOB_POLL_MAX_MS = 20 * 60 * 1e3;
782
787
  var BackendClient = class {
783
788
  http;
@@ -794,7 +799,7 @@ var BackendClient = class {
794
799
  async pollJob(jobId, signal) {
795
800
  const deadline = Date.now() + JOB_POLL_MAX_MS;
796
801
  const path16 = `/api/canvas/jobs/${encodeURIComponent(jobId)}`;
797
- while (true) {
802
+ for (let attempt = 0; ; attempt++) {
798
803
  if (signal?.aborted) {
799
804
  throw new BackendHttpError({ kind: "network", cause: signal.reason ?? new Error("aborted") });
800
805
  }
@@ -804,7 +809,7 @@ var BackendClient = class {
804
809
  if (Date.now() > deadline) {
805
810
  throw new BackendHttpError({ kind: "timeout", message: `job ${jobId} did not finish in time` });
806
811
  }
807
- await sleep(JOB_POLL_INTERVAL_MS);
812
+ await sleep(pollInterval(attempt));
808
813
  }
809
814
  }
810
815
  presignAssetUpload(sha256, mime, signal) {
@@ -814,6 +819,35 @@ var BackendClient = class {
814
819
  signal
815
820
  );
816
821
  }
822
+ /** Remote cache lookup. A miss (404) — or an old backend without the route — returns null. */
823
+ async getCacheEntry(cacheKey, signal) {
824
+ try {
825
+ const res = await this.http.getJson(`/api/canvas/cache/${encodeURIComponent(cacheKey)}`, signal);
826
+ return res.entry;
827
+ } catch (e) {
828
+ if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
829
+ throw e;
830
+ }
831
+ }
832
+ async putCacheEntry(entry, signal) {
833
+ await this.http.putJson(
834
+ `/api/canvas/cache/${encodeURIComponent(entry.cacheKey)}`,
835
+ entry,
836
+ signal
837
+ );
838
+ }
839
+ /** Durable run-history record — POST /api/canvas/runs (idempotent server-side on runId). */
840
+ async recordRun(payload, signal) {
841
+ await this.http.postJson("/api/canvas/runs", payload, signal);
842
+ }
843
+ /**
844
+ * Chat-scoped blueprint sync — POST /api/creatives/definition. Lets the
845
+ * dashboard draw a scaffolded creative's workflow graph BEFORE the first run.
846
+ * Additive on the backend (never archives siblings, never sets definitionPath).
847
+ */
848
+ async syncCreativeDefinition(payload, signal) {
849
+ await this.http.postJson("/api/creatives/definition", payload, signal);
850
+ }
817
851
  getArtifact(kind, name, version, signal) {
818
852
  const path16 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
819
853
  return this.http.getJson(path16, signal);
@@ -837,14 +871,17 @@ function requireCredentialsFromEnv(env = process.env) {
837
871
  }
838
872
  return c;
839
873
  }
874
+ function remoteCacheEnabledFromEnv(env = process.env) {
875
+ return env.BAKER_CANVAS_REMOTE_CACHE !== "off";
876
+ }
840
877
 
841
878
  // src/engine/engine/errors.ts
842
879
  function isBlocking(issue) {
843
880
  return issue.severity !== "warning";
844
881
  }
845
882
  var CanvasError = class extends Error {
846
- constructor(message) {
847
- super(message);
883
+ constructor(message2) {
884
+ super(message2);
848
885
  this.name = "CanvasError";
849
886
  }
850
887
  };
@@ -901,7 +938,7 @@ function describeCause(c) {
901
938
  }
902
939
  }
903
940
 
904
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
941
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
905
942
  var import__ = __toESM(require_safe_stable_stringify(), 1);
906
943
  var configure = import__.default.configure;
907
944
  var wrapper_default = import__.default;
@@ -959,10 +996,10 @@ var ELEVENLABS_OUTPUT_FORMATS = [
959
996
  var ELEVENLABS_MAX_TEXT_CHARS = 45454;
960
997
  var ELEVENLABS_MAX_MUSIC_LENGTH_MS = 454545;
961
998
  var OPENROUTER_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
962
- var FAL_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
963
- var FAL_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
999
+ var REPLICATE_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
1000
+ var REPLICATE_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
964
1001
  var DECONSTRUCT_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
965
- var FAL_AUDIO_MIMES = ["audio/wav", "audio/mpeg", "audio/mp3"];
1002
+ var REPLICATE_AUDIO_MIMES = ["audio/wav", "audio/mpeg", "audio/mp3"];
966
1003
  var IMAGE_GENERATE_MODELS = [
967
1004
  "openai/gpt-5.4-image-2",
968
1005
  "google/gemini-3.5-flash",
@@ -1180,20 +1217,23 @@ var MODEL_REGISTRY = {
1180
1217
  },
1181
1218
  video_generate: {
1182
1219
  "bytedance/seedance-2.0": {
1183
- // Routed via fal.ai (not OpenRouter) because OpenRouter's Seedance
1184
- // passthrough rejects photorealistic human reference frames via
1185
- // ByteDance's "real person" safety filter.
1220
+ // Routed via Replicate's official `bytedance/seedance-2.0` model. NOTE:
1221
+ // ByteDance's upstream "real person" likeness filter still blocks photoreal
1222
+ // human reference frames on ANY reseller — the escape is a synthetic/AI
1223
+ // presenter face or routing real faces to Veo, not the provider choice.
1186
1224
  label: "ByteDance Seedance 2.0",
1187
1225
  inputs: [],
1188
- optional_inputs: [{ kind: "image", mimes: FAL_IMAGE_MIMES }],
1226
+ optional_inputs: [{ kind: "image", mimes: REPLICATE_IMAGE_MIMES }],
1189
1227
  required: ["prompt"],
1190
1228
  params: {
1191
- prompt: { kind: "string" },
1229
+ // Replicate's Seedance wrapper hard-caps the prompt at 4000 chars; gate
1230
+ // it here so an over-length prompt fails validate (free) not the billed call.
1231
+ prompt: { kind: "string", maxLength: 4e3 },
1192
1232
  aspect_ratio: {
1193
1233
  kind: "string",
1194
1234
  enum: ["1:1", "3:4", "9:16", "4:3", "16:9", "21:9", "9:21"]
1195
1235
  },
1196
- resolution: { kind: "string", enum: ["480p", "720p", "1080p"] },
1236
+ resolution: { kind: "string", enum: ["480p", "720p", "1080p", "4k"] },
1197
1237
  duration: { kind: "number", enum: SEEDANCE_DURATIONS },
1198
1238
  seed: { kind: "number" },
1199
1239
  generate_audio: { kind: "boolean" }
@@ -1214,7 +1254,10 @@ var MODEL_REGISTRY = {
1214
1254
  duration: { kind: "number", enum: [4, 6, 8] },
1215
1255
  seed: { kind: "number" },
1216
1256
  generate_audio: { kind: "boolean" },
1217
- person_generation: { kind: "string", enum: ["allow_all"] },
1257
+ // Image-to-video and EU/UK/CH/MENA regions cap this at `allow_adult`;
1258
+ // `allow_all` is text-to-video only. Allow both so an image-conditioned
1259
+ // Veo clip (the real-face fallback) validates.
1260
+ person_generation: { kind: "string", enum: ["allow_all", "allow_adult"] },
1218
1261
  enhance_prompt: { kind: "boolean" },
1219
1262
  conditioning_scale: { kind: "number" }
1220
1263
  }
@@ -1265,8 +1308,8 @@ var MODEL_REGISTRY = {
1265
1308
  "fal/veed-lipsync": {
1266
1309
  label: "VEED Lipsync (fal.ai)",
1267
1310
  inputs: [
1268
- { kind: "video", mimes: FAL_VIDEO_MIMES },
1269
- { kind: "audio", mimes: FAL_AUDIO_MIMES }
1311
+ { kind: "video", mimes: REPLICATE_VIDEO_MIMES },
1312
+ { kind: "audio", mimes: REPLICATE_AUDIO_MIMES }
1270
1313
  ],
1271
1314
  required: [],
1272
1315
  params: {}
@@ -1302,7 +1345,7 @@ var MODEL_REGISTRY = {
1302
1345
  // TARGET voice, preserving timing/prosody. Used to normalize a talking-head
1303
1346
  // clip's native (generator-chosen) voice into ONE consistent brand voice.
1304
1347
  label: "ElevenLabs Voice Changer (multilingual STS v2)",
1305
- inputs: [{ kind: "audio", mimes: FAL_AUDIO_MIMES }],
1348
+ inputs: [{ kind: "audio", mimes: REPLICATE_AUDIO_MIMES }],
1306
1349
  required: ["voice"],
1307
1350
  params: {
1308
1351
  voice: { kind: "string" },
@@ -1329,7 +1372,7 @@ var MODEL_REGISTRY = {
1329
1372
  },
1330
1373
  "elevenlabs/video-background-music-v1": {
1331
1374
  label: "ElevenLabs Video Background Music v1",
1332
- inputs: [{ kind: "video", mimes: FAL_VIDEO_MIMES }],
1375
+ inputs: [{ kind: "video", mimes: REPLICATE_VIDEO_MIMES }],
1333
1376
  required: [],
1334
1377
  params: {
1335
1378
  description: { kind: "string" },
@@ -1500,7 +1543,7 @@ function validateValue(key, value, schema, model) {
1500
1543
  }
1501
1544
 
1502
1545
  // src/engine/lib/concurrency.ts
1503
- var DEFAULT_CONCURRENCY = 5;
1546
+ var DEFAULT_CONCURRENCY = 8;
1504
1547
  function resolveConcurrency(...candidates) {
1505
1548
  for (const candidate of candidates) {
1506
1549
  if (candidate === void 0 || candidate === "") continue;
@@ -1562,6 +1605,160 @@ function encodeRandom() {
1562
1605
  return out;
1563
1606
  }
1564
1607
 
1608
+ // src/engine/storage/remote-cache-store.ts
1609
+ var CANVAS_ASSETS_URL_SEGMENT = "/canvas-assets/";
1610
+ function isPersistedAssetRef(ref) {
1611
+ const { url, sha256 } = ref;
1612
+ if (typeof url !== "string" || typeof sha256 !== "string") return false;
1613
+ return url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${sha256.slice(0, 2)}/${sha256}`) || url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${sha256}`);
1614
+ }
1615
+ function isAssetRefLike(value) {
1616
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.sha256 === "string" && typeof value.mime === "string";
1617
+ }
1618
+ function collectAssetRefLikes(value, out = []) {
1619
+ if (Array.isArray(value)) {
1620
+ for (const item of value) collectAssetRefLikes(item, out);
1621
+ return out;
1622
+ }
1623
+ if (typeof value !== "object" || value === null) return out;
1624
+ if (isAssetRefLike(value)) {
1625
+ out.push(value);
1626
+ }
1627
+ for (const item of Object.values(value)) collectAssetRefLikes(item, out);
1628
+ return out;
1629
+ }
1630
+ function entryFullyPersisted(entry) {
1631
+ return collectAssetRefLikes(entry.outputs).every((ref) => isPersistedAssetRef(ref));
1632
+ }
1633
+ function stripLocalFields(entry) {
1634
+ const clone = JSON.parse(JSON.stringify(entry));
1635
+ for (const ref of collectAssetRefLikes(clone.outputs)) {
1636
+ delete ref.path;
1637
+ delete ref.bytes;
1638
+ }
1639
+ return clone;
1640
+ }
1641
+ var RemoteCacheStore = class {
1642
+ client;
1643
+ log;
1644
+ constructor(client, log) {
1645
+ this.client = client;
1646
+ this.log = log ?? (() => void 0);
1647
+ }
1648
+ async get(cacheKey) {
1649
+ return await this.client.getCacheEntry(cacheKey);
1650
+ }
1651
+ async put(entry) {
1652
+ if (!entryFullyPersisted(entry)) {
1653
+ this.log(`[cache ] ${entry.cacheKey.slice(0, 12)}\u2026 has local-only assets, kept local`);
1654
+ return;
1655
+ }
1656
+ const stripped = stripLocalFields(entry);
1657
+ if (stripped.refs.length > MAX_REMOTE_REFS) {
1658
+ stripped.refs = stripped.refs.slice(0, MAX_REMOTE_REFS);
1659
+ }
1660
+ await this.client.putCacheEntry(stripped);
1661
+ }
1662
+ };
1663
+ var MAX_REMOTE_REFS = 512;
1664
+ var LayeredCacheStore = class {
1665
+ rootDir;
1666
+ local;
1667
+ remote;
1668
+ assets;
1669
+ log;
1670
+ constructor(opts) {
1671
+ this.local = opts.local;
1672
+ this.remote = opts.remote;
1673
+ this.assets = opts.assets;
1674
+ this.rootDir = opts.local.rootDir;
1675
+ this.log = opts.log ?? (() => void 0);
1676
+ }
1677
+ async get(cacheKey) {
1678
+ const localHit = await this.local.get(cacheKey);
1679
+ if (localHit) return localHit;
1680
+ let remoteEntry;
1681
+ try {
1682
+ remoteEntry = await this.remote.get(cacheKey);
1683
+ } catch (e) {
1684
+ this.log(`[cache ] remote lookup failed (${message(e)}) \u2014 treating as miss`);
1685
+ return null;
1686
+ }
1687
+ if (!remoteEntry) return null;
1688
+ let rehydrated;
1689
+ try {
1690
+ rehydrated = await this.rehydrate(remoteEntry);
1691
+ } catch (e) {
1692
+ this.log(`[cache ] ${cacheKey.slice(0, 12)}\u2026 rehydration failed (${message(e)}) \u2014 treating as miss`);
1693
+ return null;
1694
+ }
1695
+ await this.local.put(rehydrated);
1696
+ return rehydrated;
1697
+ }
1698
+ async put(entry) {
1699
+ await this.local.put(entry);
1700
+ try {
1701
+ await this.remote.put(entry);
1702
+ } catch (e) {
1703
+ this.log(`[cache ] remote write failed (${message(e)}) \u2014 entry kept local`);
1704
+ }
1705
+ }
1706
+ /**
1707
+ * Download every referenced asset into the local content-addressed store
1708
+ * (sha-verified) and stamp fresh local paths. Any ref that cannot be
1709
+ * rehydrated fails the WHOLE entry — a partially-hydrated cache hit would
1710
+ * crash materialization later with a far less actionable error.
1711
+ */
1712
+ async rehydrate(entry) {
1713
+ const clone = JSON.parse(JSON.stringify(entry));
1714
+ for (const ref of collectAssetRefLikes(clone.outputs)) {
1715
+ if (!isPersistedAssetRef(ref)) {
1716
+ throw new Error(`ref ${ref.sha256.slice(0, 12)}\u2026 has no persisted url`);
1717
+ }
1718
+ const ingested = await this.assets.ingestRemote({
1719
+ kind: typeof ref.kind === "string" ? ref.kind : "json",
1720
+ url: ref.url,
1721
+ sha256: ref.sha256,
1722
+ mime: ref.mime,
1723
+ metadata: ref.metadata ?? void 0
1724
+ });
1725
+ ref.path = ingested.path;
1726
+ }
1727
+ return clone;
1728
+ }
1729
+ };
1730
+ function message(e) {
1731
+ return e instanceof Error ? e.message : String(e);
1732
+ }
1733
+
1734
+ // src/engine/nodes/remote/upload.ts
1735
+ async function presignAndPut(args) {
1736
+ const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
1737
+ const putRes = await fetch(putUrl, {
1738
+ method: "PUT",
1739
+ body: new Uint8Array(args.bytes),
1740
+ headers: { "Content-Type": args.mime },
1741
+ signal: args.ctx.signal
1742
+ });
1743
+ if (!putRes.ok) {
1744
+ throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
1745
+ }
1746
+ return publicUrl;
1747
+ }
1748
+ async function ensureUploaded(ref, ctx) {
1749
+ if (ref.url) return ref;
1750
+ const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
1751
+ const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
1752
+ return { ...ref, url };
1753
+ }
1754
+ async function persistOutputAssetUrls(outputs, ctx) {
1755
+ for (const ref of collectAssetRefLikes(outputs)) {
1756
+ if (isPersistedAssetRef(ref)) continue;
1757
+ const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
1758
+ ref.url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
1759
+ }
1760
+ }
1761
+
1565
1762
  // src/engine/schema/canvas.ts
1566
1763
  import { z } from "zod";
1567
1764
  var REF_PREFIX = "$ref:";
@@ -1591,7 +1788,14 @@ var NodeDecl = z.object({
1591
1788
  version: z.string().min(1).optional(),
1592
1789
  inputs: z.record(z.string(), z.unknown()).optional(),
1593
1790
  params: z.record(z.string(), z.unknown()).optional(),
1594
- when: z.unknown().optional()
1791
+ when: z.unknown().optional(),
1792
+ // Regenerate knob. The engine is content-addressed: identical params + inputs
1793
+ // return the cached render, so re-running an unchanged node NEVER re-bills or
1794
+ // produces a new result. Bump this token (any string/number — a `2`, a `"v3"`,
1795
+ // a note) and re-run to force THIS node to render fresh; because its new output
1796
+ // changes downstream input hashes, everything depending on it regenerates too.
1797
+ // This is the declarative "change a value, re-run, get a new render" affordance.
1798
+ regenerate: z.union([z.string(), z.number()]).optional()
1595
1799
  }).strict();
1596
1800
  var OutputRef = z.object({
1597
1801
  node: z.string(),
@@ -2841,6 +3045,7 @@ var Engine = class {
2841
3045
  cache;
2842
3046
  outputsDir;
2843
3047
  log;
3048
+ persistAssets;
2844
3049
  constructor(opts) {
2845
3050
  this.registry = opts.registry;
2846
3051
  this.client = opts.client;
@@ -2848,6 +3053,7 @@ var Engine = class {
2848
3053
  this.cache = opts.cache;
2849
3054
  this.outputsDir = opts.outputsDir;
2850
3055
  this.log = opts.log ?? (() => void 0);
3056
+ this.persistAssets = opts.persistAssets ?? false;
2851
3057
  }
2852
3058
  validate(canvas) {
2853
3059
  return validateCanvas(canvas, this.registry);
@@ -2871,7 +3077,16 @@ var Engine = class {
2871
3077
  const outputs = {};
2872
3078
  const counters = { cachedNodes: 0, totalCredits: 0 };
2873
3079
  const nodeRuns = [];
2874
- await this.runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns);
3080
+ const graph = this.pruneToOutput(canvas, buildGraph(canvas));
3081
+ const needsBytes = computeNeedsLocalBytes(canvas, graph, this.registry);
3082
+ this.emitProgress(opts, {
3083
+ kind: "plan",
3084
+ nodes: [...graph.entries()].map(([id, deps]) => {
3085
+ const node = canvas.nodes.find((n) => n.id === id);
3086
+ return { node_id: id, node_type: node?.type ?? "unknown", deps: [...deps], params: node?.params };
3087
+ })
3088
+ });
3089
+ await this.runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes);
2875
3090
  const output = pickFinalOutput(canvas, outputs);
2876
3091
  const stats = {
2877
3092
  total_nodes: canvas.nodes.length,
@@ -2894,39 +3109,51 @@ var Engine = class {
2894
3109
  `[done ] ${stats.cached_nodes}/${stats.total_nodes} cached, ${stats.total_credits} credits, ${stats.duration_ms}ms`
2895
3110
  );
2896
3111
  this.log(`outputs in: ${writer.runDir}`);
2897
- return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir };
3112
+ return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
2898
3113
  }
2899
- async runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns) {
2900
- const layers = topologicalLayers(this.pruneToOutput(canvas, buildGraph(canvas)));
3114
+ async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
3115
+ const layers = topologicalLayers(graph);
2901
3116
  const limit = resolveConcurrency(opts.concurrency);
2902
3117
  for (const layer of layers) {
2903
- const settled = await mapWithConcurrency(
2904
- layer,
2905
- limit,
2906
- (nodeId) => this.executeOne(canvas, nodeId, outputs, runId, writer, opts).then((r) => {
3118
+ const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
3119
+ this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
3120
+ return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
2907
3121
  if (r.cached) counters.cachedNodes++;
2908
3122
  counters.totalCredits += r.credits;
2909
3123
  const node = canvas.nodes.find((n) => n.id === nodeId);
2910
3124
  if (node) {
2911
- nodeRuns.push({
3125
+ const run = {
2912
3126
  node_id: nodeId,
2913
3127
  node_type: node.type,
2914
3128
  cached: r.cached,
2915
3129
  duration_ms: r.durationMs,
2916
3130
  credits: r.credits
2917
- });
3131
+ };
3132
+ nodeRuns.push(run);
3133
+ this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
2918
3134
  }
2919
- })
2920
- );
3135
+ });
3136
+ });
2921
3137
  const failures = [];
2922
3138
  settled.forEach((result, i) => {
2923
3139
  const nodeId = layer[i];
2924
- if (result.status === "rejected" && nodeId) failures.push({ nodeId, reason: result.reason });
3140
+ if (result.status === "rejected" && nodeId) {
3141
+ failures.push({ nodeId, reason: result.reason });
3142
+ this.emitProgress(opts, { kind: "node_failed", node_id: nodeId });
3143
+ }
2925
3144
  });
2926
3145
  if (failures.length === 1 && failures[0]) throw failures[0].reason;
2927
3146
  if (failures.length > 1) throw new LayerExecutionError(failures);
2928
3147
  }
2929
3148
  }
3149
+ /** Progress consumers are observers only — an exception there must never fail the run. */
3150
+ emitProgress(opts, event) {
3151
+ if (!opts.onProgress) return;
3152
+ try {
3153
+ opts.onProgress(event);
3154
+ } catch {
3155
+ }
3156
+ }
2930
3157
  /**
2931
3158
  * Dead-node elimination: when the canvas declares an `output`, execute only the
2932
3159
  * nodes that output transitively depends on. Orphaned nodes (left by an edit or
@@ -2957,12 +3184,13 @@ var Engine = class {
2957
3184
  }
2958
3185
  await writer.writeManifest("_final", output);
2959
3186
  }
2960
- async executeOne(canvas, nodeId, outputs, runId, writer, opts) {
3187
+ async executeOne(canvas, nodeId, outputs, runId, writer, opts, downloadOutputs) {
2961
3188
  const node = canvas.nodes.find((n) => n.id === nodeId);
2962
3189
  if (!node) throw new Error(`executor: missing node ${nodeId}`);
2963
3190
  const def = this.registry.get(node.type);
2964
3191
  if (!def) throw new Error(`executor: missing registry entry for type ${node.type}`);
2965
- const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, this.assets);
3192
+ const regenerateToken = resolveRegenerateToken(node, opts.regenerate, runId);
3193
+ const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, regenerateToken, this.assets);
2966
3194
  const policy = opts.cache_policy ?? "read_write";
2967
3195
  if (policy !== "bypass") {
2968
3196
  const cacheT0 = Date.now();
@@ -2980,17 +3208,27 @@ var Engine = class {
2980
3208
  nodeId: node.id,
2981
3209
  nodeType: node.type,
2982
3210
  cacheKey: prepared.cacheKey,
3211
+ downloadOutputs,
2983
3212
  client: this.client,
2984
3213
  assets: this.assets,
2985
3214
  log: this.log,
2986
3215
  signal: opts.signal
2987
3216
  };
2988
- const { parsedInputs, parsedParams } = parseNodeArgs(def, prepared, node.id, node.type);
3217
+ const preparedForExec = def.location === "local" ? { ...prepared, resolvedInputs: await this.materializeLocalInputs(prepared.resolvedInputs) } : prepared;
3218
+ const { parsedInputs, parsedParams } = parseNodeArgs(def, preparedForExec, node.id, node.type);
2989
3219
  const result = await invokeExecute(def, parsedInputs, parsedParams, ctx, node.id, node.type);
2990
3220
  const elapsed = Date.now() - t0;
2991
3221
  const credits = def.cost ? def.cost({ params: parsedParams }).credits : 0;
2992
3222
  const outputsObj = result;
2993
3223
  outputs[node.id] = outputsObj;
3224
+ if (this.persistAssets) {
3225
+ try {
3226
+ await persistOutputAssetUrls(outputsObj, ctx);
3227
+ } catch (e) {
3228
+ const msg = e instanceof Error ? e.message : String(e);
3229
+ this.log(`[warn ] ${node.id}: asset persistence failed (${msg}) \u2014 outputs stay local-only`);
3230
+ }
3231
+ }
2994
3232
  if (policy === "read_write") {
2995
3233
  await this.cache.put({
2996
3234
  cacheKey: prepared.cacheKey,
@@ -3019,8 +3257,42 @@ var Engine = class {
3019
3257
  }
3020
3258
  }
3021
3259
  }
3260
+ /**
3261
+ * Download any URL-only asset ref reachable in a local node's inputs so the
3262
+ * bytes are on disk before the local runner stages them. Returns a copy —
3263
+ * refs are replaced, never mutated in place, so the producer's cached output
3264
+ * (shared object) keeps its URL-only shape.
3265
+ */
3266
+ async materializeLocalInputs(inputs) {
3267
+ const fix = async (value) => {
3268
+ if (Array.isArray(value)) return Promise.all(value.map(fix));
3269
+ if (value && typeof value === "object") {
3270
+ const v = value;
3271
+ if (typeof v.kind === "string" && typeof v.url === "string" && typeof v.sha256 === "string" && typeof v.mime === "string" && typeof v.path !== "string") {
3272
+ this.log(`[warn ] materializing URL-only input on demand (${v.kind}/${v.mime}) \u2014 missed graph edge`);
3273
+ return this.assets.ingestRemote({
3274
+ kind: v.kind,
3275
+ url: v.url,
3276
+ sha256: v.sha256,
3277
+ mime: v.mime,
3278
+ metadata: v.metadata
3279
+ });
3280
+ }
3281
+ const out = {};
3282
+ for (const [k, val] of Object.entries(v)) out[k] = await fix(val);
3283
+ return out;
3284
+ }
3285
+ return value;
3286
+ };
3287
+ return await fix(inputs);
3288
+ }
3022
3289
  };
3023
- async function prepareForExecution(node, outputs, def, cacheSalt, assets) {
3290
+ function resolveRegenerateToken(node, forced, runId) {
3291
+ if (forced?.has(node.id)) return `run:${runId}`;
3292
+ if (node.regenerate !== void 0) return `node:${String(node.regenerate)}`;
3293
+ return void 0;
3294
+ }
3295
+ async function prepareForExecution(node, outputs, def, cacheSalt, regenerateToken, assets) {
3024
3296
  const resolvedInputs = resolveRefs(node.inputs ?? {}, { outputs }) ?? {};
3025
3297
  const resolvedParams = resolveRefs(node.params ?? {}, { outputs }) ?? {};
3026
3298
  const slotValues = await hydrateTextSlots(resolvedInputs, assets, node.id, node.type);
@@ -3033,6 +3305,9 @@ async function prepareForExecution(node, outputs, def, cacheSalt, assets) {
3033
3305
  throw new NodeExecutionError(node.id, node.type, { kind: "local", cause: e });
3034
3306
  }
3035
3307
  }
3308
+ if (regenerateToken !== void 0) {
3309
+ extras = { ...extras ?? {}, __regenerate__: regenerateToken };
3310
+ }
3036
3311
  const cacheKey = computeCacheKey({
3037
3312
  node_id: node.type,
3038
3313
  node_version: def.version,
@@ -3071,6 +3346,16 @@ function pickFinalOutput(canvas, outputs) {
3071
3346
  const lastOut = outputs[last.id];
3072
3347
  return lastOut ? Object.values(lastOut)[0] : void 0;
3073
3348
  }
3349
+ function computeNeedsLocalBytes(canvas, graph, registry) {
3350
+ const typeById = new Map(canvas.nodes.map((n) => [n.id, n.type]));
3351
+ const needs = /* @__PURE__ */ new Set();
3352
+ for (const [consumerId, deps] of graph) {
3353
+ const def = registry.get(typeById.get(consumerId) ?? "");
3354
+ if (def?.location !== "local") continue;
3355
+ for (const dep of deps) needs.add(dep);
3356
+ }
3357
+ return needs;
3358
+ }
3074
3359
  function buildGraph(canvas) {
3075
3360
  const graph = /* @__PURE__ */ new Map();
3076
3361
  for (const n of canvas.nodes) graph.set(n.id, /* @__PURE__ */ new Set());
@@ -3201,7 +3486,16 @@ async function hydrateSlotValue(value, assets, nodeId, nodeType) {
3201
3486
  try {
3202
3487
  bytes = await assets.readBytes(value.sha256, value.mime);
3203
3488
  } catch (e) {
3204
- throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
3489
+ if (value.url) {
3490
+ try {
3491
+ await assets.ingestRemote({ kind: value.kind, url: value.url, sha256: value.sha256, mime: value.mime });
3492
+ bytes = await assets.readBytes(value.sha256, value.mime);
3493
+ } catch (e2) {
3494
+ throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e2 });
3495
+ }
3496
+ } else {
3497
+ throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
3498
+ }
3205
3499
  }
3206
3500
  if (bytes.length > MAX_INLINE_TEXT_BYTES) {
3207
3501
  throw new NodeExecutionError(nodeId, nodeType, {
@@ -3309,27 +3603,6 @@ var FontRef = BaseAssetRef.extend({
3309
3603
  });
3310
3604
  var AssetRef = z4.discriminatedUnion("kind", [ImageRef, VideoRef, AudioRef, JsonRef, TextRef, FontRef]);
3311
3605
 
3312
- // src/engine/nodes/remote/upload.ts
3313
- async function presignAndPut(args) {
3314
- const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
3315
- const putRes = await fetch(putUrl, {
3316
- method: "PUT",
3317
- body: new Uint8Array(args.bytes),
3318
- headers: { "Content-Type": args.mime },
3319
- signal: args.ctx.signal
3320
- });
3321
- if (!putRes.ok) {
3322
- throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
3323
- }
3324
- return publicUrl;
3325
- }
3326
- async function ensureUploaded(ref, ctx) {
3327
- if (ref.url) return ref;
3328
- const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
3329
- const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
3330
- return { ...ref, url };
3331
- }
3332
-
3333
3606
  // src/engine/nodes/remote/delegate.ts
3334
3607
  function delegated(spec) {
3335
3608
  return {
@@ -3357,7 +3630,9 @@ async function callBackendExec(args) {
3357
3630
  nodeVersion: args.nodeVersion,
3358
3631
  params: args.params,
3359
3632
  inputs: serialized,
3360
- idempotency_key: idempotencyKey
3633
+ idempotency_key: idempotencyKey,
3634
+ canvas_run_id: args.ctx.canvasRunId,
3635
+ node_id: args.ctx.nodeId
3361
3636
  },
3362
3637
  args.ctx.signal
3363
3638
  );
@@ -3409,6 +3684,9 @@ async function ingestValue(value, ctx, declaredKind) {
3409
3684
  }
3410
3685
  if (isRawAsset(value)) {
3411
3686
  const kind = value.kind ?? declaredKind ?? "json";
3687
+ if (ctx.downloadOutputs === false) {
3688
+ return buildRef({ kind, sha: value.sha256, mime: value.mime, url: value.url, metadata: value.metadata });
3689
+ }
3412
3690
  return ctx.assets.ingestRemote({
3413
3691
  kind,
3414
3692
  url: value.url,
@@ -3523,7 +3801,7 @@ function safePathname(rawUrl) {
3523
3801
  }
3524
3802
  var ingestNode = defineNode({
3525
3803
  id: "ingest",
3526
- version: "1.1.0",
3804
+ version: "1.2.0",
3527
3805
  category: "io",
3528
3806
  location: "local",
3529
3807
  summary: "Ingest an external URL or a local file into the asset store. Declare the kind you expect (image/video/audio/text/json/font); the node picks the strategy. For source=url: yt-dlp for video/audio (YouTube/TikTok/Vimeo/etc. and direct file URLs), Handinger for HTML/PDF pages \u2192 markdown, direct HTTP fetch for binary URLs (images, fonts) and raw .txt/.md. For source=path: read from the local filesystem and upload to R2.",
@@ -3566,6 +3844,9 @@ function runStrategy(strategy, params, ctx) {
3566
3844
  }
3567
3845
  }
3568
3846
  async function execDirectFetch(params, ctx) {
3847
+ if (params.expect === "image") {
3848
+ return ingestImageUrl(params.url, ctx);
3849
+ }
3569
3850
  const result = await callBackendExec({
3570
3851
  nodeType: "ingest",
3571
3852
  nodeVersion: ingestNode.version,
@@ -3576,6 +3857,37 @@ async function execDirectFetch(params, ctx) {
3576
3857
  });
3577
3858
  return assertAssetOutput(result, params.expect);
3578
3859
  }
3860
+ async function ingestImageUrl(url, ctx) {
3861
+ const res = await fetch(url);
3862
+ if (!res.ok) {
3863
+ throw localExecError(ctx, `fetch ${url} \u2192 ${res.status}`);
3864
+ }
3865
+ const ab = await res.arrayBuffer();
3866
+ if (ab.byteLength > MAX_ASSET_BYTES) {
3867
+ throw localExecError(ctx, `file_too_large: ${url} is ${ab.byteLength} bytes (limit ${MAX_ASSET_BYTES})`);
3868
+ }
3869
+ let normalized;
3870
+ try {
3871
+ normalized = await toModelSafeImage(Buffer.from(ab));
3872
+ } catch (e) {
3873
+ throw localExecError(ctx, `${url}: ${e.message}`);
3874
+ }
3875
+ if (normalized.rasterizedFrom) {
3876
+ ctx.log(`ingest: normalized ${normalized.rasterizedFrom} URL -> PNG (${normalized.bytes.length}B)`);
3877
+ }
3878
+ return uploadAndIngest({
3879
+ bytes: normalized.bytes,
3880
+ kind: "image",
3881
+ mime: normalized.mime,
3882
+ metadata: {
3883
+ source_url: url,
3884
+ strategy: "direct_fetch",
3885
+ ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
3886
+ ...normalized.rasterizedFrom ? { rasterized_from: normalized.rasterizedFrom } : {}
3887
+ },
3888
+ ctx
3889
+ });
3890
+ }
3579
3891
  async function execHandinger(params, ctx) {
3580
3892
  const result = await callBackendExec({
3581
3893
  nodeType: "ingest",
@@ -3612,7 +3924,15 @@ var EXT_TO_MIME = {
3612
3924
  jpeg: "image/jpeg",
3613
3925
  webp: "image/webp",
3614
3926
  gif: "image/gif",
3927
+ // Non-model-safe rasters `toModelSafeImage` transcodes to PNG at ingest — they
3928
+ // must resolve to an image mime here or the kind-check rejects the local file
3929
+ // before normalization ever runs.
3615
3930
  avif: "image/avif",
3931
+ heic: "image/heic",
3932
+ heif: "image/heif",
3933
+ tif: "image/tiff",
3934
+ tiff: "image/tiff",
3935
+ bmp: "image/bmp",
3616
3936
  mp4: "video/mp4",
3617
3937
  webm: "video/webm",
3618
3938
  mov: "video/quicktime",
@@ -3663,15 +3983,45 @@ async function rasterizeSvgToPng(bytes) {
3663
3983
  }
3664
3984
  return await sharp(bytes, { density }).png({ force: true, palette: false }).toBuffer();
3665
3985
  }
3986
+ var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
3987
+ async function toModelSafeImage(bytes) {
3988
+ const safe = sniffImageMime(bytes);
3989
+ if (safe && MODEL_SAFE_IMAGE_MIMES.has(safe)) {
3990
+ return { bytes, mime: safe };
3991
+ }
3992
+ if (sniffSvg(bytes)) {
3993
+ return { bytes: await rasterizeSvgToPng(bytes), mime: "image/png", rasterizedFrom: "svg" };
3994
+ }
3995
+ const { default: sharp } = await import("sharp");
3996
+ try {
3997
+ const img = sharp(bytes);
3998
+ const format = (await img.metadata()).format;
3999
+ const png = await img.png({ force: true }).toBuffer();
4000
+ return { bytes: png, mime: "image/png", rasterizedFrom: format ?? "unknown" };
4001
+ } catch (e) {
4002
+ throw new Error(`bytes are not a decodable image (${e.message})`);
4003
+ }
4004
+ }
4005
+ function hasAscii(buf, offset, sig) {
4006
+ return buf.length >= offset + sig.length && buf.toString("ascii", offset, offset + sig.length) === sig;
4007
+ }
4008
+ var HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1", "heif"]);
4009
+ function sniffIsoBmff(buf) {
4010
+ if (!hasAscii(buf, 4, "ftyp")) return null;
4011
+ const brand = buf.subarray(8, 12).toString("ascii");
4012
+ if (brand === "avif" || brand === "avis") return "image/avif";
4013
+ if (HEIC_BRANDS.has(brand)) return "image/heic";
4014
+ return null;
4015
+ }
3666
4016
  function sniffImageMime(buf) {
3667
4017
  if (buf.length < 4) return null;
3668
- if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
4018
+ if (buf[0] === 137 && hasAscii(buf, 1, "PNG")) return "image/png";
3669
4019
  if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
3670
- if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70) return "image/gif";
3671
- if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
3672
- return "image/webp";
3673
- }
3674
- return null;
4020
+ if (hasAscii(buf, 0, "GIF")) return "image/gif";
4021
+ if (hasAscii(buf, 0, "RIFF") && hasAscii(buf, 8, "WEBP")) return "image/webp";
4022
+ if (hasAscii(buf, 0, "II*\0") || hasAscii(buf, 0, "MM\0*")) return "image/tiff";
4023
+ if (hasAscii(buf, 0, "BM")) return "image/bmp";
4024
+ return sniffIsoBmff(buf);
3675
4025
  }
3676
4026
  function findBoxPayload(buf, start, end, type) {
3677
4027
  let offset = start;
@@ -3724,10 +4074,10 @@ function inferKindFromMime(mime) {
3724
4074
  if (mime.startsWith("font/")) return "font";
3725
4075
  return null;
3726
4076
  }
3727
- function localExecError(ctx, message) {
4077
+ function localExecError(ctx, message2) {
3728
4078
  return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
3729
4079
  kind: "local",
3730
- cause: new Error(`ingest: ${message}`)
4080
+ cause: new Error(`ingest: ${message2}`)
3731
4081
  });
3732
4082
  }
3733
4083
  async function execLocalFile(params, ctx) {
@@ -3774,17 +4124,20 @@ async function execLocalFile(params, ctx) {
3774
4124
  ctx.log(`ingest: local file ${stats.size}B mime=${mime}`);
3775
4125
  let outBytes = bytes;
3776
4126
  let outMime = mime;
3777
- if (mime === SVG_MIME) {
3778
- outBytes = await rasterizeSvgToPng(bytes);
3779
- outMime = "image/png";
3780
- ctx.log(`ingest: rasterized SVG -> PNG (${outBytes.length}B)`);
4127
+ let rasterizedFrom;
4128
+ if (kind === "image") {
4129
+ const normalized = await toModelSafeImage(bytes);
4130
+ outBytes = normalized.bytes;
4131
+ outMime = normalized.mime;
4132
+ rasterizedFrom = normalized.rasterizedFrom;
4133
+ if (rasterizedFrom) ctx.log(`ingest: normalized ${rasterizedFrom} -> PNG (${outBytes.length}B)`);
3781
4134
  }
3782
4135
  const durationMs = probeVideoDurationMs(params.expect, outBytes, ctx);
3783
4136
  const ref = await uploadAndIngest({
3784
4137
  bytes: outBytes,
3785
4138
  kind: params.expect,
3786
4139
  mime: outMime,
3787
- metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs }),
4140
+ metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs, rasterizedFrom }),
3788
4141
  ctx
3789
4142
  });
3790
4143
  return withProbedDuration(ref, durationMs);
@@ -3802,7 +4155,7 @@ function localFileMetadata(args) {
3802
4155
  ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
3803
4156
  file_size: args.fileSize,
3804
4157
  original_filename: path3.basename(args.absPath),
3805
- ...args.mime === SVG_MIME ? { rasterized_from: "svg" } : {},
4158
+ ...args.rasterizedFrom ? { rasterized_from: args.rasterizedFrom } : {},
3806
4159
  ...args.durationMs !== void 0 ? { duration_ms: args.durationMs } : {}
3807
4160
  };
3808
4161
  }
@@ -4394,7 +4747,7 @@ async function refToUrl(ref) {
4394
4747
  return `data:${ref.mime};base64,${bytes.toString("base64")}`;
4395
4748
  }
4396
4749
  var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
4397
- function isAssetRefLike(value) {
4750
+ function isAssetRefLike2(value) {
4398
4751
  if (!value || typeof value !== "object") return false;
4399
4752
  const v = value;
4400
4753
  return typeof v.kind === "string" && ASSET_KINDS.has(v.kind) && typeof v.mime === "string" && typeof v.sha256 === "string" && (typeof v.url === "string" || typeof v.path === "string");
@@ -4766,8 +5119,8 @@ var NEVER_BLOCK = [
4766
5119
  /text[_-]?occluded/i
4767
5120
  ];
4768
5121
  var UNAVAILABLE = /unknown command|command not found|not found|Did you mean|Unknown argument|ENOENT/i;
4769
- function isAdvisory(code, message) {
4770
- const hay = `${code} ${message}`;
5122
+ function isAdvisory(code, message2) {
5123
+ const hay = `${code} ${message2}`;
4771
5124
  return NEVER_BLOCK.some((re) => re.test(hay));
4772
5125
  }
4773
5126
  function parseCheckJson(raw) {
@@ -4795,10 +5148,10 @@ function classifyLint(json) {
4795
5148
  for (const f of findings) {
4796
5149
  const rec = f;
4797
5150
  const code = String(rec?.code ?? "");
4798
- const message = String(rec?.message ?? "");
5151
+ const message2 = String(rec?.message ?? "");
4799
5152
  const severity = String(rec?.severity ?? "info");
4800
- const blocking = severity === "error" && !isAdvisory(code, message);
4801
- out.push({ source: "lint", code, message, severity: blocking ? "blocking" : "warning" });
5153
+ const blocking = severity === "error" && !isAdvisory(code, message2);
5154
+ out.push({ source: "lint", code, message: message2, severity: blocking ? "blocking" : "warning" });
4802
5155
  }
4803
5156
  return out;
4804
5157
  }
@@ -4810,9 +5163,9 @@ function classifyInspect(json) {
4810
5163
  for (const iss of issues) {
4811
5164
  const rec = iss;
4812
5165
  const code = String(rec?.code ?? rec?.type ?? "overflow");
4813
- const message = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
5166
+ const message2 = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
4814
5167
  const severity = rec?.severity ? String(rec.severity) : obj?.ok === false ? "error" : "warning";
4815
- out.push({ source: "inspect", code, message, severity: severity === "error" ? "blocking" : "warning" });
5168
+ out.push({ source: "inspect", code, message: message2, severity: severity === "error" ? "blocking" : "warning" });
4816
5169
  }
4817
5170
  return out;
4818
5171
  }
@@ -5257,7 +5610,7 @@ async function buildSubstitutionValues(compositionParams, meta, duration) {
5257
5610
  }
5258
5611
  function coerceImageParam(value) {
5259
5612
  if (typeof value === "string") return Promise.resolve(value);
5260
- if (isAssetRefLike(value)) return refToUrl(value);
5613
+ if (isAssetRefLike2(value)) return refToUrl(value);
5261
5614
  throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
5262
5615
  }
5263
5616
  async function substituteCompositionFiles(tmp, values) {
@@ -5487,7 +5840,7 @@ async function buildSubstitutionValues2(compositionParams, meta) {
5487
5840
  }
5488
5841
  function coerceImageParam2(value) {
5489
5842
  if (typeof value === "string") return Promise.resolve(value);
5490
- if (isAssetRefLike(value)) return refToUrl(value);
5843
+ if (isAssetRefLike2(value)) return refToUrl(value);
5491
5844
  throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
5492
5845
  }
5493
5846
  async function substituteCompositionFiles2(tmp, values) {
@@ -6513,17 +6866,29 @@ function createEngineFromEnv(opts = {}) {
6513
6866
  const cacheDir = opts.cacheDir ?? path15.join(cwd, "canvas", ".cache");
6514
6867
  const outputsDir = opts.outputsDir ?? path15.join(cwd, "canvas");
6515
6868
  const creds = requireCredentialsFromEnv();
6869
+ const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
6870
+ const assets = new LocalAssetStore(path15.join(cacheDir, "assets"));
6871
+ const localCache = new LocalCacheStore(path15.join(cacheDir, "index"));
6872
+ const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
6873
+ const cache = remoteCacheEnabled ? new LayeredCacheStore({
6874
+ local: localCache,
6875
+ remote: new RemoteCacheStore(client, opts.log),
6876
+ assets,
6877
+ log: opts.log
6878
+ }) : localCache;
6516
6879
  return new Engine({
6517
6880
  registry: defaultRegistry(),
6518
- client: new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey }),
6519
- assets: new LocalAssetStore(path15.join(cacheDir, "assets")),
6520
- cache: new LocalCacheStore(path15.join(cacheDir, "index")),
6881
+ client,
6882
+ assets,
6883
+ cache,
6521
6884
  outputsDir,
6522
- log: opts.log
6885
+ log: opts.log,
6886
+ persistAssets: remoteCacheEnabled
6523
6887
  });
6524
6888
  }
6525
6889
 
6526
6890
  export {
6891
+ requireCredentialsFromEnv,
6527
6892
  LayerExecutionError,
6528
6893
  describeFailureReason,
6529
6894
  SEEDANCE_DURATIONS,
@@ -6531,7 +6896,14 @@ export {
6531
6896
  IMAGE_GENERATE_MODELS,
6532
6897
  MODEL_REGISTRY,
6533
6898
  resolveConcurrency,
6899
+ ulid,
6900
+ isPersistedAssetRef,
6901
+ collectAssetRefLikes,
6902
+ REF_PREFIX,
6903
+ parseRefExpr,
6904
+ sha256Hex,
6534
6905
  elementMentionKeywords,
6906
+ toModelSafeImage,
6535
6907
  BackendClient2 as BackendClient,
6536
6908
  Engine2 as Engine,
6537
6909
  LocalAssetStore2 as LocalAssetStore,
@@ -6542,4 +6914,4 @@ export {
6542
6914
  defaultRegistry,
6543
6915
  createEngineFromEnv
6544
6916
  };
6545
- //# sourceMappingURL=chunk-IWPAXJC3.js.map
6917
+ //# sourceMappingURL=chunk-43KBQLP5.js.map