@koda-sl/baker-cli 0.119.0 → 0.120.0-dev.3bcc79f9c

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.
@@ -24,9 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  mod
25
25
  ));
26
26
 
27
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
27
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
28
28
  var require_safe_stable_stringify = __commonJS({
29
- "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
29
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
30
30
  "use strict";
31
31
  var { hasOwnProperty } = Object.prototype;
32
32
  var stringify = configure2();
@@ -159,9 +159,9 @@ var require_safe_stable_stringify = __commonJS({
159
159
  }
160
160
  if (value) {
161
161
  return (value2) => {
162
- let message = `Object can not safely be stringified. Received type ${typeof value2}`;
163
- if (typeof value2 !== "function") message += ` (${value2.toString()})`;
164
- throw new Error(message);
162
+ let message2 = `Object can not safely be stringified. Received type ${typeof value2}`;
163
+ if (typeof value2 !== "function") message2 += ` (${value2.toString()})`;
164
+ throw new Error(message2);
165
165
  };
166
166
  }
167
167
  }
@@ -673,6 +673,9 @@ var HttpClient = class {
673
673
  async postJson(path16, body, signal) {
674
674
  return await this.requestJson("POST", path16, body, signal);
675
675
  }
676
+ async putJson(path16, body, signal) {
677
+ return await this.requestJson("PUT", path16, body, signal);
678
+ }
676
679
  async getJson(path16, signal) {
677
680
  return await this.requestJson("GET", path16, void 0, signal);
678
681
  }
@@ -700,8 +703,8 @@ var HttpClient = class {
700
703
  try {
701
704
  const res = await this.fetchFn(url, {
702
705
  method,
703
- headers: method === "POST" ? { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` } : { Authorization: `Bearer ${this.apiKey}` },
704
- body: method === "POST" ? JSON.stringify(body) : void 0,
706
+ headers: method === "GET" ? { Authorization: `Bearer ${this.apiKey}` } : { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
707
+ body: method === "GET" ? void 0 : JSON.stringify(body),
705
708
  signal: controller.signal
706
709
  });
707
710
  if (res.ok) return { kind: "value", value: await res.json() };
@@ -738,33 +741,33 @@ async function parseErrorBody(res) {
738
741
  const errObj = body.error ?? {};
739
742
  return classifyHttpError(res.status, errObj, errObj.message ?? `HTTP ${res.status}`);
740
743
  }
741
- function classifyHttpError(status, errObj, message) {
744
+ function classifyHttpError(status, errObj, message2) {
742
745
  if (errObj.code === CONTENT_POLICY_CODE) {
743
- return { kind: "content_policy", status, provider: errObj.provider, message };
746
+ return { kind: "content_policy", status, provider: errObj.provider, message: message2 };
744
747
  }
745
748
  if (status === 401 || status === 403) {
746
- return { kind: "unauthorized", status, message };
749
+ return { kind: "unauthorized", status, message: message2 };
747
750
  }
748
751
  if (status === 400 || status === 422) {
749
- return { kind: "validation", status, message, details: errObj.details };
752
+ return { kind: "validation", status, message: message2, details: errObj.details };
750
753
  }
751
754
  if (status === 502 || status === 504) {
752
755
  if (errObj.code === "provider_timeout" || status === 504) {
753
- return { kind: "timeout", provider: errObj.provider, message };
756
+ return { kind: "timeout", provider: errObj.provider, message: message2 };
754
757
  }
755
758
  return {
756
759
  kind: "provider",
757
760
  status,
758
761
  provider: errObj.provider,
759
762
  code: errObj.code ?? "provider_error",
760
- message,
763
+ message: message2,
761
764
  retryable: errObj.retryable ?? true
762
765
  };
763
766
  }
764
767
  if (status >= 500 || status === 429) {
765
- return { kind: "server", status, message };
768
+ return { kind: "server", status, message: message2 };
766
769
  }
767
- return { kind: "validation", status, message, details: errObj.details };
770
+ return { kind: "validation", status, message: message2, details: errObj.details };
768
771
  }
769
772
  function backoffMs(attempt) {
770
773
  return 1e3 * 2 ** attempt;
@@ -835,6 +838,27 @@ var BackendClient = class {
835
838
  signal
836
839
  );
837
840
  }
841
+ /** Remote cache lookup. A miss (404) — or an old backend without the route — returns null. */
842
+ async getCacheEntry(cacheKey, signal) {
843
+ try {
844
+ const res = await this.http.getJson(`/api/canvas/cache/${encodeURIComponent(cacheKey)}`, signal);
845
+ return res.entry;
846
+ } catch (e) {
847
+ if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
848
+ throw e;
849
+ }
850
+ }
851
+ async putCacheEntry(entry, signal) {
852
+ await this.http.putJson(
853
+ `/api/canvas/cache/${encodeURIComponent(entry.cacheKey)}`,
854
+ entry,
855
+ signal
856
+ );
857
+ }
858
+ /** Durable run-history record — POST /api/canvas/runs (idempotent server-side on runId). */
859
+ async recordRun(payload, signal) {
860
+ await this.http.postJson("/api/canvas/runs", payload, signal);
861
+ }
838
862
  getArtifact(kind, name, version, signal) {
839
863
  const path16 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
840
864
  return this.http.getJson(path16, signal);
@@ -858,14 +882,17 @@ function requireCredentialsFromEnv(env = process.env) {
858
882
  }
859
883
  return c;
860
884
  }
885
+ function remoteCacheEnabledFromEnv(env = process.env) {
886
+ return env.BAKER_CANVAS_REMOTE_CACHE !== "off";
887
+ }
861
888
 
862
889
  // src/engine/engine/errors.ts
863
890
  function isBlocking(issue) {
864
891
  return issue.severity !== "warning";
865
892
  }
866
893
  var CanvasError = class extends Error {
867
- constructor(message) {
868
- super(message);
894
+ constructor(message2) {
895
+ super(message2);
869
896
  this.name = "CanvasError";
870
897
  }
871
898
  };
@@ -922,7 +949,7 @@ function describeCause(c) {
922
949
  }
923
950
  }
924
951
 
925
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
952
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
926
953
  var import__ = __toESM(require_safe_stable_stringify(), 1);
927
954
  var configure = import__.default.configure;
928
955
  var wrapper_default = import__.default;
@@ -1583,6 +1610,158 @@ function encodeRandom() {
1583
1610
  return out;
1584
1611
  }
1585
1612
 
1613
+ // src/engine/storage/remote-cache-store.ts
1614
+ var CANVAS_ASSETS_URL_SEGMENT = "/canvas-assets/";
1615
+ function isPersistedAssetRef(ref) {
1616
+ return typeof ref.url === "string" && ref.url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${ref.sha256}`);
1617
+ }
1618
+ function isAssetRefLike(value) {
1619
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.sha256 === "string" && typeof value.mime === "string";
1620
+ }
1621
+ function collectAssetRefLikes(value, out = []) {
1622
+ if (Array.isArray(value)) {
1623
+ for (const item of value) collectAssetRefLikes(item, out);
1624
+ return out;
1625
+ }
1626
+ if (typeof value !== "object" || value === null) return out;
1627
+ if (isAssetRefLike(value)) {
1628
+ out.push(value);
1629
+ }
1630
+ for (const item of Object.values(value)) collectAssetRefLikes(item, out);
1631
+ return out;
1632
+ }
1633
+ function entryFullyPersisted(entry) {
1634
+ return collectAssetRefLikes(entry.outputs).every((ref) => isPersistedAssetRef(ref));
1635
+ }
1636
+ function stripLocalFields(entry) {
1637
+ const clone = JSON.parse(JSON.stringify(entry));
1638
+ for (const ref of collectAssetRefLikes(clone.outputs)) {
1639
+ delete ref.path;
1640
+ delete ref.bytes;
1641
+ }
1642
+ return clone;
1643
+ }
1644
+ var RemoteCacheStore = class {
1645
+ client;
1646
+ log;
1647
+ constructor(client, log) {
1648
+ this.client = client;
1649
+ this.log = log ?? (() => void 0);
1650
+ }
1651
+ async get(cacheKey) {
1652
+ return await this.client.getCacheEntry(cacheKey);
1653
+ }
1654
+ async put(entry) {
1655
+ if (!entryFullyPersisted(entry)) {
1656
+ this.log(`[cache ] ${entry.cacheKey.slice(0, 12)}\u2026 has local-only assets, kept local`);
1657
+ return;
1658
+ }
1659
+ const stripped = stripLocalFields(entry);
1660
+ if (stripped.refs.length > MAX_REMOTE_REFS) {
1661
+ stripped.refs = stripped.refs.slice(0, MAX_REMOTE_REFS);
1662
+ }
1663
+ await this.client.putCacheEntry(stripped);
1664
+ }
1665
+ };
1666
+ var MAX_REMOTE_REFS = 512;
1667
+ var LayeredCacheStore = class {
1668
+ rootDir;
1669
+ local;
1670
+ remote;
1671
+ assets;
1672
+ log;
1673
+ constructor(opts) {
1674
+ this.local = opts.local;
1675
+ this.remote = opts.remote;
1676
+ this.assets = opts.assets;
1677
+ this.rootDir = opts.local.rootDir;
1678
+ this.log = opts.log ?? (() => void 0);
1679
+ }
1680
+ async get(cacheKey) {
1681
+ const localHit = await this.local.get(cacheKey);
1682
+ if (localHit) return localHit;
1683
+ let remoteEntry;
1684
+ try {
1685
+ remoteEntry = await this.remote.get(cacheKey);
1686
+ } catch (e) {
1687
+ this.log(`[cache ] remote lookup failed (${message(e)}) \u2014 treating as miss`);
1688
+ return null;
1689
+ }
1690
+ if (!remoteEntry) return null;
1691
+ let rehydrated;
1692
+ try {
1693
+ rehydrated = await this.rehydrate(remoteEntry);
1694
+ } catch (e) {
1695
+ this.log(`[cache ] ${cacheKey.slice(0, 12)}\u2026 rehydration failed (${message(e)}) \u2014 treating as miss`);
1696
+ return null;
1697
+ }
1698
+ await this.local.put(rehydrated);
1699
+ return rehydrated;
1700
+ }
1701
+ async put(entry) {
1702
+ await this.local.put(entry);
1703
+ try {
1704
+ await this.remote.put(entry);
1705
+ } catch (e) {
1706
+ this.log(`[cache ] remote write failed (${message(e)}) \u2014 entry kept local`);
1707
+ }
1708
+ }
1709
+ /**
1710
+ * Download every referenced asset into the local content-addressed store
1711
+ * (sha-verified) and stamp fresh local paths. Any ref that cannot be
1712
+ * rehydrated fails the WHOLE entry — a partially-hydrated cache hit would
1713
+ * crash materialization later with a far less actionable error.
1714
+ */
1715
+ async rehydrate(entry) {
1716
+ const clone = JSON.parse(JSON.stringify(entry));
1717
+ for (const ref of collectAssetRefLikes(clone.outputs)) {
1718
+ if (!isPersistedAssetRef(ref)) {
1719
+ throw new Error(`ref ${ref.sha256.slice(0, 12)}\u2026 has no persisted url`);
1720
+ }
1721
+ const ingested = await this.assets.ingestRemote({
1722
+ kind: typeof ref.kind === "string" ? ref.kind : "json",
1723
+ url: ref.url,
1724
+ sha256: ref.sha256,
1725
+ mime: ref.mime,
1726
+ metadata: ref.metadata ?? void 0
1727
+ });
1728
+ ref.path = ingested.path;
1729
+ }
1730
+ return clone;
1731
+ }
1732
+ };
1733
+ function message(e) {
1734
+ return e instanceof Error ? e.message : String(e);
1735
+ }
1736
+
1737
+ // src/engine/nodes/remote/upload.ts
1738
+ async function presignAndPut(args) {
1739
+ const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
1740
+ const putRes = await fetch(putUrl, {
1741
+ method: "PUT",
1742
+ body: new Uint8Array(args.bytes),
1743
+ headers: { "Content-Type": args.mime },
1744
+ signal: args.ctx.signal
1745
+ });
1746
+ if (!putRes.ok) {
1747
+ throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
1748
+ }
1749
+ return publicUrl;
1750
+ }
1751
+ async function ensureUploaded(ref, ctx) {
1752
+ if (ref.url) return ref;
1753
+ const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
1754
+ const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
1755
+ return { ...ref, url };
1756
+ }
1757
+ async function persistOutputAssetUrls(outputs, ctx) {
1758
+ for (const ref of collectAssetRefLikes(outputs)) {
1759
+ if (isPersistedAssetRef(ref)) continue;
1760
+ const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
1761
+ ref.url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
1762
+ }
1763
+ }
1764
+
1586
1765
  // src/engine/schema/canvas.ts
1587
1766
  import { z } from "zod";
1588
1767
  var REF_PREFIX = "$ref:";
@@ -1644,11 +1823,7 @@ var VideoMeta = z.object({
1644
1823
  // Advisory: the scene's visual length vs the estimated spoken length, so
1645
1824
  // a reviewer can see a native line that may run past its cut. Not gated.
1646
1825
  scene_s: z.number().optional(),
1647
- est_speech_s: z.number().optional(),
1648
- // Word count of the line est_speech_s was measured for. Together they carry
1649
- // the speaker's OBSERVED pace (from the deconstruct's word timings), so the
1650
- // overrun check budgets re-authored lines at the real rate, not a wps guess.
1651
- speech_words: z.number().optional()
1826
+ est_speech_s: z.number().optional()
1652
1827
  }),
1653
1828
  z.object({ scene: z.number(), lipsync_node: z.string() })
1654
1829
  ])
@@ -2178,9 +2353,7 @@ var STAGE_CODES = {
2178
2353
  SPEECH_OVERRUN: "VIDEO_SPEECH_OVERRUN",
2179
2354
  ASPECT_MISMATCH: "VIDEO_ASPECT_MISMATCH",
2180
2355
  REFERENCE_MISSING: "VIDEO_REFERENCE_MISSING",
2181
- SPAN_EXCEEDS_MODEL: "VIDEO_SPAN_EXCEEDS_MODEL",
2182
- UI_IN_PROMPT: "VIDEO_UI_IN_PROMPT",
2183
- BRANDMARK_IN_PROMPT: "VIDEO_BRANDMARK_IN_PROMPT"
2356
+ SPAN_EXCEEDS_MODEL: "VIDEO_SPAN_EXCEEDS_MODEL"
2184
2357
  };
2185
2358
  var SPAN_MODEL_SLACK_S = 0.25;
2186
2359
  var VIDEO_TIME_SLACK_S = 0.75;
@@ -2559,8 +2732,6 @@ function checkVideoInvariants(ctx) {
2559
2732
  }
2560
2733
  checkSpeechOverrun(ctx, meta.talking_scenes);
2561
2734
  checkAspectConsistency(ctx);
2562
- checkUiInPrompt(ctx);
2563
- checkBrandmarkInPrompt(ctx);
2564
2735
  checkReferenceCompleteness(ctx, meta);
2565
2736
  checkClipSpanFitsModel(ctx, meta);
2566
2737
  }
@@ -2590,30 +2761,18 @@ function keywordTokens(text) {
2590
2761
  if (!text) return [];
2591
2762
  return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3 && !KEYWORD_STOPWORDS.has(t));
2592
2763
  }
2593
- function elementMentionKeywords(el) {
2594
- const typeWords = ELEMENT_TYPE_KEYWORDS[el.type.toLowerCase()] ?? [];
2595
- return [.../* @__PURE__ */ new Set([...typeWords, ...keywordTokens(el.label), ...keywordTokens(el.description)])];
2596
- }
2597
2764
  function keywordsForElement(el) {
2598
- return elementMentionKeywords(el);
2765
+ const type = el.type.toLowerCase();
2766
+ const typeWords = ELEMENT_TYPE_KEYWORDS[type] ?? [];
2767
+ return [.../* @__PURE__ */ new Set([...typeWords, ...keywordTokens(el.label), ...keywordTokens(el.description)])];
2599
2768
  }
2600
2769
  function containsWord(text, word) {
2601
2770
  const esc = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2602
2771
  return new RegExp(`\\b${esc}\\b`, "i").test(text);
2603
2772
  }
2604
- var FRAME_DESCRIPTION_START = "FRAME DESCRIPTION (this frame's editable prompt):";
2605
- var FRAME_DESCRIPTION_END = "Render exactly what the FRAME DESCRIPTION";
2606
- function frameDescriptionOf(prompt) {
2607
- const i = prompt.indexOf(FRAME_DESCRIPTION_START);
2608
- if (i < 0) return prompt;
2609
- const rest = prompt.slice(i + FRAME_DESCRIPTION_START.length);
2610
- const j = rest.indexOf(FRAME_DESCRIPTION_END);
2611
- return j < 0 ? rest : rest.slice(0, j);
2612
- }
2613
2773
  function checkFrameReferences(ctx, node, index, keyworded) {
2614
- const rawPrompt = node.params?.prompt;
2615
- if (typeof rawPrompt !== "string" || rawPrompt.length === 0) return;
2616
- const prompt = frameDescriptionOf(rawPrompt);
2774
+ const prompt = node.params?.prompt;
2775
+ if (typeof prompt !== "string" || prompt.length === 0) return;
2617
2776
  const inputsBlob = JSON.stringify(node.inputs ?? {});
2618
2777
  for (const { el, keywords } of keyworded) {
2619
2778
  if (inputsBlob.includes(el.ref)) continue;
@@ -2663,24 +2822,13 @@ function checkClipSpanFitsModel(ctx, meta) {
2663
2822
  });
2664
2823
  }
2665
2824
  }
2666
- var OBSERVED_WPS_MIN = 1;
2667
- var OBSERVED_WPS_MAX = 6;
2668
- function secondsPerWord(stamped) {
2669
- const est = stamped?.est_speech_s;
2670
- const words = stamped?.speech_words;
2671
- if (est && words && est > 0 && words > 0) {
2672
- const wps = words / est;
2673
- if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return est / words;
2674
- }
2675
- return 1 / SPEECH_WORDS_PER_SECOND;
2676
- }
2677
- function speechOverrunOf(node, secPerWord) {
2825
+ function speechOverrunOf(node) {
2678
2826
  const params = node.params;
2679
2827
  if (params?.generate_audio !== true) return null;
2680
2828
  const line = nativeDialogueOf(params.prompt);
2681
2829
  const duration = typeof params.duration === "number" ? params.duration : void 0;
2682
2830
  if (!line || !duration) return null;
2683
- const estSpeechS = line.split(/\s+/).filter(Boolean).length * secPerWord;
2831
+ const estSpeechS = line.split(/\s+/).filter(Boolean).length / SPEECH_WORDS_PER_SECOND;
2684
2832
  return estSpeechS > duration * SPEECH_OVERRUN_RATIO ? { estSpeechS, duration } : null;
2685
2833
  }
2686
2834
  function checkSpeechOverrun(ctx, talkingScenes) {
@@ -2689,7 +2837,7 @@ function checkSpeechOverrun(ctx, talkingScenes) {
2689
2837
  const nativeClipRe = new RegExp(`^s${entry.scene}(_r\\d+)?_clip$`);
2690
2838
  for (const n of ctx.canvas.nodes) {
2691
2839
  if (!nativeClipRe.test(n.id) || n.type !== "video_generate") continue;
2692
- const overrun = speechOverrunOf(n, secondsPerWord(entry));
2840
+ const overrun = speechOverrunOf(n);
2693
2841
  if (!overrun) continue;
2694
2842
  ctx.issues.push({
2695
2843
  path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
@@ -2699,38 +2847,6 @@ function checkSpeechOverrun(ctx, talkingScenes) {
2699
2847
  }
2700
2848
  }
2701
2849
  }
2702
- var UI_IN_PROMPT_RE = /\bscreen[- ]?(?:recording|capture|grab|share)\b|\bapp (?:interface|screen)\b|\bphone screen overlay\b/i;
2703
- function checkUiInPrompt(ctx) {
2704
- for (const n of ctx.canvas.nodes) {
2705
- if (n.type !== "video_generate") continue;
2706
- const prompt = n.params?.prompt;
2707
- if (typeof prompt !== "string" || !UI_IN_PROMPT_RE.test(prompt)) continue;
2708
- ctx.issues.push({
2709
- path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
2710
- code: STAGE_CODES.UI_IN_PROMPT,
2711
- severity: "warning",
2712
- message: `"${n.id}" asks the video model to render a screen/UI surface \u2014 generative video garbles UI text and chrome. Composite the real screen on the overlay layer (screenshot / brand HTML) and keep this prompt to the background plate`,
2713
- node_id: n.id,
2714
- node_type: "video_generate"
2715
- });
2716
- }
2717
- }
2718
- var BRANDMARK_IN_PROMPT_RE = /\b(?:logo|wordmark) (?:overlay|animation|sting|card|reveal)\b|\b(?:google|facebook|instagram|tiktok|youtube|amazon|apple|microsoft|whatsapp|netflix|spotify|excel|trustpilot) (?:logo|wordmark|branding)\b/i;
2719
- function checkBrandmarkInPrompt(ctx) {
2720
- for (const n of ctx.canvas.nodes) {
2721
- if (n.type !== "video_generate" && n.type !== "image_generate") continue;
2722
- const prompt = n.params?.prompt;
2723
- if (typeof prompt !== "string" || !BRANDMARK_IN_PROMPT_RE.test(prompt)) continue;
2724
- ctx.issues.push({
2725
- path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
2726
- code: STAGE_CODES.BRANDMARK_IN_PROMPT,
2727
- severity: "warning",
2728
- message: `"${n.id}" asks the model to render a brand logo/wordmark \u2014 generation garbles marks and third-party logos carry IP exposure. Source the real mark (baker images logo <domain>) and composite it on the overlay layer`,
2729
- node_id: n.id,
2730
- node_type: n.type
2731
- });
2732
- }
2733
- }
2734
2850
  function checkAspectConsistency(ctx) {
2735
2851
  const clips = ctx.canvas.nodes.filter((n) => n.type === "video_generate");
2736
2852
  if (clips.length < 2) return;
@@ -2862,6 +2978,7 @@ var Engine = class {
2862
2978
  cache;
2863
2979
  outputsDir;
2864
2980
  log;
2981
+ persistAssets;
2865
2982
  constructor(opts) {
2866
2983
  this.registry = opts.registry;
2867
2984
  this.client = opts.client;
@@ -2869,6 +2986,7 @@ var Engine = class {
2869
2986
  this.cache = opts.cache;
2870
2987
  this.outputsDir = opts.outputsDir;
2871
2988
  this.log = opts.log ?? (() => void 0);
2989
+ this.persistAssets = opts.persistAssets ?? false;
2872
2990
  }
2873
2991
  validate(canvas) {
2874
2992
  return validateCanvas(canvas, this.registry);
@@ -2915,7 +3033,7 @@ var Engine = class {
2915
3033
  `[done ] ${stats.cached_nodes}/${stats.total_nodes} cached, ${stats.total_credits} credits, ${stats.duration_ms}ms`
2916
3034
  );
2917
3035
  this.log(`outputs in: ${writer.runDir}`);
2918
- return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir };
3036
+ return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
2919
3037
  }
2920
3038
  async runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns) {
2921
3039
  const layers = topologicalLayers(this.pruneToOutput(canvas, buildGraph(canvas)));
@@ -3012,6 +3130,14 @@ var Engine = class {
3012
3130
  const credits = def.cost ? def.cost({ params: parsedParams }).credits : 0;
3013
3131
  const outputsObj = result;
3014
3132
  outputs[node.id] = outputsObj;
3133
+ if (this.persistAssets) {
3134
+ try {
3135
+ await persistOutputAssetUrls(outputsObj, ctx);
3136
+ } catch (e) {
3137
+ const msg = e instanceof Error ? e.message : String(e);
3138
+ this.log(`[warn ] ${node.id}: asset persistence failed (${msg}) \u2014 outputs stay local-only`);
3139
+ }
3140
+ }
3015
3141
  if (policy === "read_write") {
3016
3142
  await this.cache.put({
3017
3143
  cacheKey: prepared.cacheKey,
@@ -3330,27 +3456,6 @@ var FontRef = BaseAssetRef.extend({
3330
3456
  });
3331
3457
  var AssetRef = z4.discriminatedUnion("kind", [ImageRef, VideoRef, AudioRef, JsonRef, TextRef, FontRef]);
3332
3458
 
3333
- // src/engine/nodes/remote/upload.ts
3334
- async function presignAndPut(args) {
3335
- const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
3336
- const putRes = await fetch(putUrl, {
3337
- method: "PUT",
3338
- body: new Uint8Array(args.bytes),
3339
- headers: { "Content-Type": args.mime },
3340
- signal: args.ctx.signal
3341
- });
3342
- if (!putRes.ok) {
3343
- throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
3344
- }
3345
- return publicUrl;
3346
- }
3347
- async function ensureUploaded(ref, ctx) {
3348
- if (ref.url) return ref;
3349
- const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
3350
- const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
3351
- return { ...ref, url };
3352
- }
3353
-
3354
3459
  // src/engine/nodes/remote/delegate.ts
3355
3460
  function delegated(spec) {
3356
3461
  return {
@@ -3745,10 +3850,10 @@ function inferKindFromMime(mime) {
3745
3850
  if (mime.startsWith("font/")) return "font";
3746
3851
  return null;
3747
3852
  }
3748
- function localExecError(ctx, message) {
3853
+ function localExecError(ctx, message2) {
3749
3854
  return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
3750
3855
  kind: "local",
3751
- cause: new Error(`ingest: ${message}`)
3856
+ cause: new Error(`ingest: ${message2}`)
3752
3857
  });
3753
3858
  }
3754
3859
  async function execLocalFile(params, ctx) {
@@ -4415,7 +4520,7 @@ async function refToUrl(ref) {
4415
4520
  return `data:${ref.mime};base64,${bytes.toString("base64")}`;
4416
4521
  }
4417
4522
  var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
4418
- function isAssetRefLike(value) {
4523
+ function isAssetRefLike2(value) {
4419
4524
  if (!value || typeof value !== "object") return false;
4420
4525
  const v = value;
4421
4526
  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");
@@ -4787,8 +4892,8 @@ var NEVER_BLOCK = [
4787
4892
  /text[_-]?occluded/i
4788
4893
  ];
4789
4894
  var UNAVAILABLE = /unknown command|command not found|not found|Did you mean|Unknown argument|ENOENT/i;
4790
- function isAdvisory(code, message) {
4791
- const hay = `${code} ${message}`;
4895
+ function isAdvisory(code, message2) {
4896
+ const hay = `${code} ${message2}`;
4792
4897
  return NEVER_BLOCK.some((re) => re.test(hay));
4793
4898
  }
4794
4899
  function parseCheckJson(raw) {
@@ -4816,10 +4921,10 @@ function classifyLint(json) {
4816
4921
  for (const f of findings) {
4817
4922
  const rec = f;
4818
4923
  const code = String(rec?.code ?? "");
4819
- const message = String(rec?.message ?? "");
4924
+ const message2 = String(rec?.message ?? "");
4820
4925
  const severity = String(rec?.severity ?? "info");
4821
- const blocking = severity === "error" && !isAdvisory(code, message);
4822
- out.push({ source: "lint", code, message, severity: blocking ? "blocking" : "warning" });
4926
+ const blocking = severity === "error" && !isAdvisory(code, message2);
4927
+ out.push({ source: "lint", code, message: message2, severity: blocking ? "blocking" : "warning" });
4823
4928
  }
4824
4929
  return out;
4825
4930
  }
@@ -4831,9 +4936,9 @@ function classifyInspect(json) {
4831
4936
  for (const iss of issues) {
4832
4937
  const rec = iss;
4833
4938
  const code = String(rec?.code ?? rec?.type ?? "overflow");
4834
- const message = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
4939
+ const message2 = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
4835
4940
  const severity = rec?.severity ? String(rec.severity) : obj?.ok === false ? "error" : "warning";
4836
- out.push({ source: "inspect", code, message, severity: severity === "error" ? "blocking" : "warning" });
4941
+ out.push({ source: "inspect", code, message: message2, severity: severity === "error" ? "blocking" : "warning" });
4837
4942
  }
4838
4943
  return out;
4839
4944
  }
@@ -5278,7 +5383,7 @@ async function buildSubstitutionValues(compositionParams, meta, duration) {
5278
5383
  }
5279
5384
  function coerceImageParam(value) {
5280
5385
  if (typeof value === "string") return Promise.resolve(value);
5281
- if (isAssetRefLike(value)) return refToUrl(value);
5386
+ if (isAssetRefLike2(value)) return refToUrl(value);
5282
5387
  throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
5283
5388
  }
5284
5389
  async function substituteCompositionFiles(tmp, values) {
@@ -5508,7 +5613,7 @@ async function buildSubstitutionValues2(compositionParams, meta) {
5508
5613
  }
5509
5614
  function coerceImageParam2(value) {
5510
5615
  if (typeof value === "string") return Promise.resolve(value);
5511
- if (isAssetRefLike(value)) return refToUrl(value);
5616
+ if (isAssetRefLike2(value)) return refToUrl(value);
5512
5617
  throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
5513
5618
  }
5514
5619
  async function substituteCompositionFiles2(tmp, values) {
@@ -6534,17 +6639,29 @@ function createEngineFromEnv(opts = {}) {
6534
6639
  const cacheDir = opts.cacheDir ?? path15.join(cwd, "canvas", ".cache");
6535
6640
  const outputsDir = opts.outputsDir ?? path15.join(cwd, "canvas");
6536
6641
  const creds = requireCredentialsFromEnv();
6642
+ const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
6643
+ const assets = new LocalAssetStore(path15.join(cacheDir, "assets"));
6644
+ const localCache = new LocalCacheStore(path15.join(cacheDir, "index"));
6645
+ const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
6646
+ const cache = remoteCacheEnabled ? new LayeredCacheStore({
6647
+ local: localCache,
6648
+ remote: new RemoteCacheStore(client, opts.log),
6649
+ assets,
6650
+ log: opts.log
6651
+ }) : localCache;
6537
6652
  return new Engine({
6538
6653
  registry: defaultRegistry(),
6539
- client: new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey }),
6540
- assets: new LocalAssetStore(path15.join(cacheDir, "assets")),
6541
- cache: new LocalCacheStore(path15.join(cacheDir, "index")),
6654
+ client,
6655
+ assets,
6656
+ cache,
6542
6657
  outputsDir,
6543
- log: opts.log
6658
+ log: opts.log,
6659
+ persistAssets: remoteCacheEnabled
6544
6660
  });
6545
6661
  }
6546
6662
 
6547
6663
  export {
6664
+ requireCredentialsFromEnv,
6548
6665
  LayerExecutionError,
6549
6666
  describeFailureReason,
6550
6667
  SEEDANCE_DURATIONS,
@@ -6552,7 +6669,10 @@ export {
6552
6669
  IMAGE_GENERATE_MODELS,
6553
6670
  MODEL_REGISTRY,
6554
6671
  resolveConcurrency,
6555
- elementMentionKeywords,
6672
+ ulid,
6673
+ isPersistedAssetRef,
6674
+ collectAssetRefLikes,
6675
+ sha256Hex,
6556
6676
  BackendClient2 as BackendClient,
6557
6677
  Engine2 as Engine,
6558
6678
  LocalAssetStore2 as LocalAssetStore,
@@ -6563,4 +6683,4 @@ export {
6563
6683
  defaultRegistry,
6564
6684
  createEngineFromEnv
6565
6685
  };
6566
- //# sourceMappingURL=chunk-MF34WJ7M.js.map
6686
+ //# sourceMappingURL=chunk-OCMOQOIJ.js.map