@field/cascade 0.11.0 → 0.12.0

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 (216) hide show
  1. package/AGENTS.md +7 -4
  2. package/ARCHITECTURE.md +5 -4
  3. package/CHANGELOG.md +195 -0
  4. package/README.md +1 -1
  5. package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
  6. package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
  7. package/dist/assets/{CodeEditor-4O7FQ__R.js → CodeEditor-C-xYG-Wn.js} +120 -120
  8. package/dist/assets/CodePanel-CtXd50m6.js +2 -0
  9. package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
  10. package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
  11. package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
  12. package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
  13. package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
  14. package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
  15. package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
  16. package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
  17. package/dist/assets/{SettingsDialog-Bz8JxISH.js → SettingsDialog-CBX3Wa87.js} +1 -1
  18. package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
  19. package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
  20. package/dist/assets/{VersionHistoryDialog-D5Z0nJ5-.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
  21. package/dist/assets/assert-BO2_erPD.js +1 -0
  22. package/dist/assets/assert-hvykGCnP.js +1 -0
  23. package/dist/assets/builder-BmBKcDix.js +1 -0
  24. package/dist/assets/camera-DFF7YA3F.js +1 -0
  25. package/dist/assets/checks-DAvyO9Gf.js +1 -0
  26. package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
  27. package/dist/assets/{core-Yqi0Ni61.js → core-DB-MkGjj.js} +3 -4
  28. package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
  29. package/dist/assets/{cssMode-CxUzc4vp.js → cssMode-DPVckSOc.js} +1 -1
  30. package/dist/assets/definitions-B227e7f_.js +1 -0
  31. package/dist/assets/definitions-D0WOUJd7.js +1 -0
  32. package/dist/assets/{editor.api-BNFb25CX.js → editor.api-Dkutg_Z1.js} +1 -1
  33. package/dist/assets/expressions-X-Z-GrgW.js +5 -0
  34. package/dist/assets/file-DAL6IfjV.js +1 -0
  35. package/dist/assets/filters-D3k1LRMa.js +1 -0
  36. package/dist/assets/{freemarker2-0ZDnWxeM.js → freemarker2-CLiLKk3B.js} +1 -1
  37. package/dist/assets/generators-BhDxRAFC.js +1 -0
  38. package/dist/assets/geo-BsMImJIJ.js +1 -0
  39. package/dist/assets/geo-C4YngDob.js +2 -0
  40. package/dist/assets/{handlebars-Cy18MoaD.js → handlebars-CcD3aMeL.js} +1 -1
  41. package/dist/assets/{html-B-2Dx7nq.js → html-DvZWGfqb.js} +1 -1
  42. package/dist/assets/{htmlMode-DUAc9B_d.js → htmlMode-DUD3VaoN.js} +1 -1
  43. package/dist/assets/image-0SfH-snh.js +1 -0
  44. package/dist/assets/image-Ck-RdGU4.js +2 -0
  45. package/dist/assets/index-BWnrmIB9.js +137 -0
  46. package/dist/assets/index-DIaxEONI.css +1 -0
  47. package/dist/assets/io-BGmt2g0D.js +1 -0
  48. package/dist/assets/{javascript-DeQsDqP5.js → javascript-Cap2ahgh.js} +1 -1
  49. package/dist/assets/{jsonMode-BiFwJVSk.js → jsonMode-B0uLjp9C.js} +1 -1
  50. package/dist/assets/{liquid-BLoHH68a.js → liquid-CnxwhfLn.js} +1 -1
  51. package/dist/assets/{lspLanguageFeatures-CNFgm1Kv.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
  52. package/dist/assets/math-GeULS-sk.js +1 -0
  53. package/dist/assets/{mdx-CFXQVNfs.js → mdx-BV3jbrVa.js} +1 -1
  54. package/dist/assets/pop-KMF_qloi.js +1 -0
  55. package/dist/assets/preload-helper-Czpn1I53.js +1 -0
  56. package/dist/assets/{python-Cf7S7p2E.js → python-CGyFk4XK.js} +1 -1
  57. package/dist/assets/{razor-CMHBcG-g.js → razor-CQfzX3-N.js} +1 -1
  58. package/dist/assets/remap-5SjXQU-q.js +1 -0
  59. package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
  60. package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
  61. package/dist/assets/text-ZpX7qNkx.js +2 -0
  62. package/dist/assets/time-P_OiBhcf.js +1 -0
  63. package/dist/assets/{tsMode-BEih0GDj.js → tsMode-BOeXzO9H.js} +1 -1
  64. package/dist/assets/{typescript-COEKYD_a.js → typescript-J1SnE_Tu.js} +1 -1
  65. package/dist/assets/{workers-BNhAhMNn.js → workers-CYwwTiVF.js} +1 -1
  66. package/dist/assets/{xml-CPiMpnPl.js → xml-DnPRNzK9.js} +1 -1
  67. package/dist/assets/{yaml-Bgp1ITLR.js → yaml-BmmYBNa9.js} +1 -1
  68. package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
  69. package/dist/cli/{Graph-ZJJTRQTM.js → Graph-OB6EWSGL.js} +12 -12
  70. package/dist/cli/assert-GROGQNCQ.js +25 -0
  71. package/dist/cli/checks-3WB44XBL.js +189 -0
  72. package/dist/cli/{chunk-RQZ4SX5R.js → chunk-2JEU3MGB.js} +2 -2
  73. package/dist/cli/{chunk-WKURDVYP.js → chunk-3RXL5GHB.js} +28 -331
  74. package/dist/cli/chunk-53LGFPWX.js +138 -0
  75. package/dist/cli/chunk-5ORUQ6ES.js +267 -0
  76. package/dist/cli/chunk-73S4JZXK.js +336 -0
  77. package/dist/cli/{chunk-YAT5Y25U.js → chunk-7QE2ZNSN.js} +3 -3
  78. package/dist/cli/{chunk-SGJAET2L.js → chunk-BPODRH52.js} +8 -4
  79. package/dist/cli/{chunk-XWDSKG7N.js → chunk-BZ63XQ3I.js} +2 -2
  80. package/dist/cli/{chunk-FCDXJ6EK.js → chunk-CTPLRSWF.js} +2 -13
  81. package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
  82. package/dist/cli/{chunk-WIH4TEIS.js → chunk-EZRWJ4G2.js} +1 -1
  83. package/dist/cli/{chunk-BIJO5OFS.js → chunk-FWMSGKEH.js} +1 -1
  84. package/dist/cli/{chunk-POMMNKZZ.js → chunk-G23A2BZZ.js} +38 -5
  85. package/dist/cli/{chunk-35D343DC.js → chunk-GRIN2DDP.js} +19 -1
  86. package/dist/cli/chunk-JVOCK5W4.js +83 -0
  87. package/dist/cli/chunk-KBGUO27Q.js +15 -0
  88. package/dist/cli/{chunk-GFS6W3C5.js → chunk-L6B6FUQP.js} +54 -32
  89. package/dist/cli/chunk-LTXWBXOM.js +438 -0
  90. package/dist/cli/chunk-MI5JLQYA.js +220 -0
  91. package/dist/cli/{chunk-EUIAC3U3.js → chunk-MP653A5O.js} +884 -124
  92. package/dist/cli/{chunk-A4OAMOZH.js → chunk-NJFMDXHR.js} +13 -3
  93. package/dist/cli/{chunk-3JTJHF7V.js → chunk-OM6BEME6.js} +4 -4
  94. package/dist/cli/{chunk-DUIUFVU2.js → chunk-ST6LROUG.js} +49 -23
  95. package/dist/cli/{chunk-RLCAJBVR.js → chunk-TJW67QSI.js} +8 -7
  96. package/dist/cli/chunk-WGKPJZDY.js +364 -0
  97. package/dist/cli/{chunk-D6BUOHUG.js → chunk-X4WKAOQP.js} +4 -4
  98. package/dist/cli/{chunk-VVGSLVSF.js → chunk-X566FWBV.js} +6 -1
  99. package/dist/cli/chunk-YH637PDL.js +317 -0
  100. package/dist/cli/chunk-ZSCUZE6I.js +112 -0
  101. package/dist/cli/{cliCommands-UQSFGEHF.js → cliCommands-JJN5Z3ZC.js} +23 -17
  102. package/dist/cli/{commands-Q23M26QM.js → commands-PHYFYA3F.js} +129 -47
  103. package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
  104. package/dist/cli/{definitions-HF2FTHYU.js → definitions-BPOPW52L.js} +7 -7
  105. package/dist/cli/{doctorCommand-B7BPQMN6.js → doctorCommand-ORWQRNYP.js} +36 -6
  106. package/dist/cli/{edit-JHCPTQFT.js → edit-S5KEQ5RA.js} +3 -3
  107. package/dist/cli/editorCommands-NFR32JOY.js +41 -0
  108. package/dist/cli/{encode-HAUZTYPT.js → encode-ICPEPMH5.js} +2 -2
  109. package/dist/cli/file-3HTQHPYQ.js +41 -0
  110. package/dist/cli/filters-6ODFWYK4.js +379 -0
  111. package/dist/cli/generators-YU7GB4MD.js +210 -0
  112. package/dist/cli/{geo-4RNK3QVN.js → geo-6ZPGKZRU.js} +7 -7
  113. package/dist/cli/{graphCommands-6BMAKGQE.js → graphCommands-EOUOBOIJ.js} +4 -4
  114. package/dist/cli/{graphFile-VPSQ6OF7.js → graphFile-YTGJB5F5.js} +25 -19
  115. package/dist/cli/{headlessCanvas-5XLDZT7M.js → headlessCanvas-ENRHC7GS.js} +3 -2
  116. package/dist/cli/{helpCommand-3VRE3KVO.js → helpCommand-PULH6ZYB.js} +9 -7
  117. package/dist/cli/image-TXELZZK5.js +55 -0
  118. package/dist/cli/index.js +14 -14
  119. package/dist/cli/{math-ZRMLKU2D.js → math-RSYRQ3C6.js} +5 -5
  120. package/dist/cli/{mcp-ZLEEMYGF.js → mcp-JVZFQOEX.js} +4 -4
  121. package/dist/cli/{parity-KINARCP2.js → parity-X6DEH2DV.js} +42 -36
  122. package/dist/cli/{pop-USDFXB7H.js → pop-B7SIIYKN.js} +5 -5
  123. package/dist/cli/project-VYGIA7JX.js +12 -0
  124. package/dist/cli/{projectDefinition-DMTZN2NE.js → projectDefinition-F3D7UIHJ.js} +8 -8
  125. package/dist/cli/{projectRuntime-Z5PJOH47.js → projectRuntime-2Q4JKEH3.js} +19 -12
  126. package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
  127. package/dist/cli/{render-YURAPPUI.js → render-PT4MWI6U.js} +24 -22
  128. package/dist/cli/{runCommand-PI764LEG.js → runCommand-HNPQVZMS.js} +3 -3
  129. package/dist/cli/runner-5XPXVW7J.js +43 -0
  130. package/dist/cli/runtime/image.ts +27 -2
  131. package/dist/cli/runtime/io.ts +78 -0
  132. package/dist/cli/share-MHF6TNRK.js +166 -0
  133. package/dist/cli/stageCache-TY2BFZJR.js +15 -0
  134. package/dist/cli/{stageRunner-KUNIMQKJ.js → stageRunner-KY2EJP7X.js} +2 -2
  135. package/dist/cli/text-MB3EKR4H.js +126 -0
  136. package/dist/cli/{verbs-RCE3K4VQ.js → verbs-PI2JH5FI.js} +2 -2
  137. package/dist/cli/webBuild-WPPMGGW7.js +44 -0
  138. package/dist/contracts/capabilities.d.ts +18 -1
  139. package/dist/contracts/color.d.ts +8 -0
  140. package/dist/contracts/color.js +23 -0
  141. package/dist/contracts/definition.d.ts +10 -0
  142. package/dist/contracts/schema.d.ts +1 -1
  143. package/dist/contracts/schema.js +1 -0
  144. package/dist/contracts/validate.js +11 -1
  145. package/dist/contracts/values.d.ts +24 -0
  146. package/dist/image.d.ts +15 -0
  147. package/dist/image.js +36 -2
  148. package/dist/index.html +8 -2
  149. package/dist/io.d.ts +29 -0
  150. package/dist/io.js +40 -0
  151. package/dist/player/app.js +3 -2
  152. package/dist/player/editor.js +3 -2
  153. package/dist/player/editorEngine.js +3 -2
  154. package/dist/runtime/builtins/assert/checks.d.ts +18 -0
  155. package/dist/runtime/builtins/assert/checks.js +207 -0
  156. package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
  157. package/dist/runtime/builtins/assert/definitions.js +54 -0
  158. package/dist/runtime/builtins/assert/index.d.ts +152 -0
  159. package/dist/runtime/builtins/assert/index.js +36 -0
  160. package/dist/runtime/builtins/image/contactSheet.d.ts +3 -165
  161. package/dist/runtime/builtins/image/contactSheet.js +0 -49
  162. package/dist/runtime/builtins/image/definitions.d.ts +950 -0
  163. package/dist/runtime/builtins/image/definitions.js +343 -0
  164. package/dist/runtime/builtins/image/file.d.ts +8 -0
  165. package/dist/runtime/builtins/image/file.js +37 -0
  166. package/dist/runtime/builtins/image/filters.d.ts +8 -0
  167. package/dist/runtime/builtins/image/filters.js +394 -0
  168. package/dist/runtime/builtins/image/generators.d.ts +7 -0
  169. package/dist/runtime/builtins/image/generators.js +144 -0
  170. package/dist/runtime/builtins/image/index.d.ts +947 -4
  171. package/dist/runtime/builtins/image/index.js +41 -6
  172. package/dist/runtime/builtins/image/io.d.ts +100 -0
  173. package/dist/runtime/builtins/image/io.js +142 -0
  174. package/dist/runtime/builtins/image/planar.d.ts +55 -0
  175. package/dist/runtime/builtins/image/planar.js +274 -0
  176. package/dist/runtime/builtins/image/simplex.d.ts +22 -0
  177. package/dist/runtime/builtins/image/simplex.js +92 -0
  178. package/dist/runtime/builtins/image/text.d.ts +4 -0
  179. package/dist/runtime/builtins/image/text.js +141 -0
  180. package/dist/runtime/builtins/index.d.ts +3 -0
  181. package/dist/runtime/builtins/index.js +7 -0
  182. package/dist/runtime/gpu.d.ts +25 -0
  183. package/dist/runtime/gpu.js +40 -0
  184. package/dist/runtime/runtime.d.ts +2 -0
  185. package/dist/runtime/runtime.js +268 -44
  186. package/dist/runtime/types.d.ts +30 -0
  187. package/doc/CLI_REFERENCE.md +21 -3
  188. package/doc/EDITOR_EXPORT.md +26 -0
  189. package/doc/MCP.md +2 -1
  190. package/doc/NODE_REFERENCE.md +286 -14
  191. package/doc/PROJECT_AUTHORING.md +98 -3
  192. package/package.json +4 -1
  193. package/dist/assets/CodePanel-BU-autmq.js +0 -2
  194. package/dist/assets/CustomNodeDialog-BRpDfOV5.js +0 -1
  195. package/dist/assets/OpenGraphDialog-BwbRlC0X.js +0 -1
  196. package/dist/assets/ProjectSettings-DLOXLxcA.js +0 -2
  197. package/dist/assets/definitions-GreEMXLV.js +0 -1
  198. package/dist/assets/geo-u0n_jOsD.js +0 -1
  199. package/dist/assets/image-cAa-Nu9O.js +0 -2062
  200. package/dist/assets/index-5ZtDAuy2.js +0 -220
  201. package/dist/assets/index-7vae6YFC.css +0 -1
  202. package/dist/assets/math-DioIQOPJ.js +0 -1
  203. package/dist/assets/pop-DC_TQG9t.js +0 -1
  204. package/dist/cli/chunk-36P5K3SD.js +0 -631
  205. package/dist/cli/chunk-4UDFYF5G.js +0 -132
  206. package/dist/cli/chunk-53NMBJAO.js +0 -35
  207. package/dist/cli/chunk-MIIEFUGG.js +0 -623
  208. package/dist/cli/chunk-QBBKRNGY.js +0 -90
  209. package/dist/cli/chunk-WPUDGEFY.js +0 -20
  210. package/dist/cli/editorCommands-UIF5YLUZ.js +0 -33
  211. package/dist/cli/project-K3QIPGBB.js +0 -12
  212. package/dist/cli/registerStandardNodes-S2VLMF6J.js +0 -3081
  213. package/dist/cli/runner-RA6UKEPD.js +0 -37
  214. package/dist/cli/webBuild-GT4PZT5D.js +0 -37
  215. package/dist/runtime/builtins/headless.d.ts +0 -19
  216. package/dist/runtime/builtins/headless.js +0 -25
@@ -0,0 +1,317 @@
1
+ // server/src/pythonWorker.ts
2
+ import { spawn } from "child_process";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import readline from "readline";
6
+ var REQUEST_TIMEOUT_MS = 12e4;
7
+ var CANCEL_GRACE_MS = 3e3;
8
+ var StageCancelledError = class extends Error {
9
+ constructor(stage) {
10
+ super(`stage "${stage}" was cancelled`);
11
+ this.cancelled = true;
12
+ this.name = "StageCancelledError";
13
+ }
14
+ };
15
+ var liveWorkers = /* @__PURE__ */ new Set();
16
+ function pythonWorkerStatuses() {
17
+ return [...liveWorkers].map((worker) => worker.status);
18
+ }
19
+ function shutdownAllPythonWorkers() {
20
+ for (const worker of [...liveWorkers]) worker.shutdown();
21
+ }
22
+ var PythonWorker = class {
23
+ constructor(project, entrypoint, python = "python3", env = process.env, options = {}) {
24
+ this.proc = null;
25
+ this.nextId = 1;
26
+ this.queue = [];
27
+ /** The request the worker is running, or one it was told to abandon and has
28
+ * not yet answered. Nothing else is written until it answers or exits. */
29
+ this.inFlight = null;
30
+ /** Set once the in-flight request has been cancelled: its answer is
31
+ * discarded whatever it says. */
32
+ this.inFlightCancelled = false;
33
+ this.killTimer = null;
34
+ /** The code hash the running process started with. */
35
+ this.spawnedCodeHash = null;
36
+ /** Set by a restart, cleared by the answer that follows it. */
37
+ this.restartPending = false;
38
+ /** How many times the worker restarted because its code changed. */
39
+ this.restarts = 0;
40
+ this.project = project;
41
+ this.entrypoint = entrypoint;
42
+ this.python = python;
43
+ this.env = env;
44
+ this.cancelGraceMs = options.cancelGraceMs ?? CANCEL_GRACE_MS;
45
+ this.requestTimeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
46
+ this.sources = options.sources ?? null;
47
+ this.onStderr = options.onStderr ?? ((text) => console.error("[python-worker]", text));
48
+ liveWorkers.add(this);
49
+ }
50
+ get status() {
51
+ const pid = this.pid;
52
+ return {
53
+ entrypoint: this.entrypoint,
54
+ pid,
55
+ state: pid === null ? "stopped" : this.inFlight ? "busy" : "idle",
56
+ queued: this.queue.length,
57
+ codeHash: this.spawnedCodeHash,
58
+ restarts: this.restarts
59
+ };
60
+ }
61
+ /** Requests sent or waiting: what a pool balances on. */
62
+ get pending() {
63
+ return (this.inFlight ? 1 : 0) + this.queue.length;
64
+ }
65
+ /** The code hash the running worker started with; null before the first
66
+ * request or when sources are not watched. */
67
+ get codeHash() {
68
+ return this.spawnedCodeHash;
69
+ }
70
+ /** The worker's process id, or null when none is running. For tests and
71
+ * diagnostics: a cancelled stage must not leave an orphan behind. */
72
+ get pid() {
73
+ return this.proc?.pid ?? null;
74
+ }
75
+ ensureStarted() {
76
+ if (this.proc && this.proc.exitCode === null && this.proc.signalCode === null) return this.proc;
77
+ this.spawnedCodeHash = this.sources?.codeHash() ?? null;
78
+ const scriptPath = this.project.resolve(this.entrypoint);
79
+ const proc = spawn(this.python, [scriptPath], { cwd: this.project.root, env: this.spawnEnv() });
80
+ readline.createInterface({ input: proc.stdout }).on("line", (line) => this.handleLine(proc, line));
81
+ proc.stderr.on("data", (chunk) => this.onStderr(chunk.toString().trimEnd()));
82
+ proc.stdin.on("error", () => {
83
+ });
84
+ proc.on("error", (error) => {
85
+ if (this.proc !== proc) return;
86
+ this.proc = null;
87
+ const current = this.inFlight;
88
+ this.inFlight = null;
89
+ this.inFlightCancelled = false;
90
+ if (current) this.settle(current, new Error(`python worker could not start (${this.python}): ${error.message}`));
91
+ else this.onStderr(`python worker could not start (${this.python}): ${error.message}`);
92
+ });
93
+ proc.on("exit", (code, signal) => {
94
+ if (this.proc !== proc) return;
95
+ this.proc = null;
96
+ this.clearKillTimer();
97
+ const current = this.inFlight;
98
+ const cancelled = this.inFlightCancelled;
99
+ this.inFlight = null;
100
+ this.inFlightCancelled = false;
101
+ if (current && !cancelled) {
102
+ console.warn(`[python-worker] exited (${signal ?? `code ${code}`}) \u2014 will respawn on next call`);
103
+ this.settle(current, new Error(`python worker exited (${signal ?? `code ${code}`}) before responding`));
104
+ }
105
+ this.pump();
106
+ });
107
+ this.proc = proc;
108
+ return proc;
109
+ }
110
+ /**
111
+ * The environment for a new process. With sources watched, Python's bytecode
112
+ * cache is moved to a folder named by the code hash.
113
+ *
114
+ * Without that a restart is not enough: Python validates a `.pyc` by the
115
+ * source's size and its mtime in whole seconds, so an edit that keeps the
116
+ * length (`VALUE = 1` to `VALUE = 2`) within the same second is served from
117
+ * the old bytecode by the fresh process. Found by the respawn test.
118
+ */
119
+ spawnEnv() {
120
+ if (!this.spawnedCodeHash) return this.env;
121
+ const prefix = path.join(os.tmpdir(), "cascade-pycache", this.spawnedCodeHash);
122
+ return { ...this.env, PYTHONPYCACHEPREFIX: prefix };
123
+ }
124
+ handleLine(proc, line) {
125
+ if (this.proc !== proc) return;
126
+ let msg;
127
+ try {
128
+ msg = JSON.parse(line);
129
+ } catch {
130
+ console.error("[python-worker] non-JSON line from worker:", line);
131
+ return;
132
+ }
133
+ const current = this.inFlight;
134
+ if (!current || msg.id !== current.id) return;
135
+ const cancelled = this.inFlightCancelled;
136
+ this.inFlight = null;
137
+ this.inFlightCancelled = false;
138
+ this.clearKillTimer();
139
+ if (!cancelled) {
140
+ const restarted = this.restartPending;
141
+ this.restartPending = false;
142
+ if (msg.ok) {
143
+ this.settle(current, null, {
144
+ result: msg.result,
145
+ codeHash: this.spawnedCodeHash,
146
+ restarted,
147
+ ...typeof msg.cache === "string" ? { projectCache: msg.cache } : {}
148
+ });
149
+ } else this.settle(current, new Error(msg.error || "python worker reported an error"));
150
+ }
151
+ this.pump();
152
+ }
153
+ settle(request, error, answer) {
154
+ if (request.timer) clearTimeout(request.timer);
155
+ request.timer = null;
156
+ if (error) request.reject(error);
157
+ else request.resolve(answer);
158
+ }
159
+ /**
160
+ * Stop a running worker whose code changed since it started. Only ever
161
+ * called with nothing in flight, so no request is lost: the exit handler
162
+ * finds no current request, and the next `ensureStarted` spawns afresh.
163
+ */
164
+ restartIfCodeChanged() {
165
+ if (!this.sources || !this.proc) return;
166
+ const now = this.sources.codeHash();
167
+ if (now === this.spawnedCodeHash) return;
168
+ console.warn(`[python-worker] stage code changed (${this.spawnedCodeHash} -> ${now}), restarting`);
169
+ const stale = this.proc;
170
+ this.proc = null;
171
+ stale.kill();
172
+ this.restarts += 1;
173
+ this.restartPending = true;
174
+ }
175
+ clearKillTimer() {
176
+ if (this.killTimer) clearTimeout(this.killTimer);
177
+ this.killTimer = null;
178
+ }
179
+ /** Send the next queued request if the worker is free. */
180
+ pump() {
181
+ if (this.inFlight) return;
182
+ const next = this.queue.shift();
183
+ if (!next) return;
184
+ this.restartIfCodeChanged();
185
+ const proc = this.ensureStarted();
186
+ this.inFlight = next;
187
+ this.inFlightCancelled = false;
188
+ proc.stdin.write(next.line);
189
+ }
190
+ /**
191
+ * Start the process now rather than on the first call, so its interpreter
192
+ * start and imports (4.7 s for cloud-plots) overlap other work. A render
193
+ * worker warms while the probe frames run. A no-op when one is running.
194
+ */
195
+ warm() {
196
+ if (this.inFlight || this.queue.length) return;
197
+ this.restartIfCodeChanged();
198
+ this.ensureStarted();
199
+ }
200
+ /** Run a stage and return its result. */
201
+ async call(stage, args, options = {}) {
202
+ return (await this.callWithMeta(stage, args, options)).result;
203
+ }
204
+ /** Run a stage and return its result with the code hash that produced it. */
205
+ callWithMeta(stage, args, options = {}) {
206
+ const { signal, cascade } = options;
207
+ if (signal?.aborted) return Promise.reject(new StageCancelledError(stage));
208
+ const id = this.nextId++;
209
+ return new Promise((resolve, reject) => {
210
+ const onAbort = () => this.cancel(id);
211
+ const request = {
212
+ id,
213
+ stage,
214
+ line: JSON.stringify(cascade ? { id, stage, args, cascade } : { id, stage, args }) + "\n",
215
+ resolve: (r) => {
216
+ signal?.removeEventListener("abort", onAbort);
217
+ resolve(r);
218
+ },
219
+ reject: (e) => {
220
+ signal?.removeEventListener("abort", onAbort);
221
+ reject(e);
222
+ },
223
+ timer: null
224
+ };
225
+ request.timer = setTimeout(() => {
226
+ this.cancel(id, new Error(`python worker timed out after ${this.requestTimeoutMs}ms on stage "${stage}"`));
227
+ }, this.requestTimeoutMs);
228
+ signal?.addEventListener("abort", onAbort, { once: true });
229
+ this.queue.push(request);
230
+ this.pump();
231
+ });
232
+ }
233
+ /**
234
+ * Withdraw a request. A queued one is dropped without ever reaching Python;
235
+ * the one in flight is interrupted, and its answer — whenever it comes — is
236
+ * discarded, so a superseded result is never delivered.
237
+ */
238
+ cancel(id, reason) {
239
+ const queued = this.queue.findIndex((request) => request.id === id);
240
+ if (queued >= 0) {
241
+ const [request] = this.queue.splice(queued, 1);
242
+ this.settle(request, reason ?? new StageCancelledError(request.stage));
243
+ return;
244
+ }
245
+ const current = this.inFlight;
246
+ if (!current || current.id !== id || this.inFlightCancelled) return;
247
+ this.inFlightCancelled = true;
248
+ this.settle(current, reason ?? new StageCancelledError(current.stage));
249
+ const proc = this.proc;
250
+ if (!proc) return;
251
+ proc.kill("SIGINT");
252
+ this.clearKillTimer();
253
+ this.killTimer = setTimeout(() => {
254
+ this.killTimer = null;
255
+ if (this.proc === proc && this.inFlight === current) proc.kill("SIGKILL");
256
+ }, this.cancelGraceMs);
257
+ }
258
+ shutdown() {
259
+ liveWorkers.delete(this);
260
+ this.clearKillTimer();
261
+ for (const request of this.queue.splice(0)) this.settle(request, new Error("python worker shut down"));
262
+ const current = this.inFlight;
263
+ this.inFlight = null;
264
+ this.inFlightCancelled = false;
265
+ if (current) this.settle(current, new Error("python worker shut down"));
266
+ const proc = this.proc;
267
+ this.proc = null;
268
+ proc?.kill();
269
+ }
270
+ };
271
+ function defaultStageWorkerCount(cores = os.availableParallelism?.() ?? os.cpus().length, memoryBytes = os.totalmem()) {
272
+ const byCores = Math.floor(cores / 4);
273
+ const byMemory = Math.floor(memoryBytes / (8 * 1024 ** 3));
274
+ return Math.max(2, Math.min(4, byCores, byMemory));
275
+ }
276
+ var PythonWorkerPool = class {
277
+ constructor(size, create) {
278
+ this.size = size;
279
+ this.create = create;
280
+ this.workers = [];
281
+ }
282
+ /** The workers started so far. */
283
+ get members() {
284
+ return this.workers;
285
+ }
286
+ /** Start the first worker now (see `PythonWorker.warm`). */
287
+ warm() {
288
+ if (!this.workers.length) this.workers.push(this.create());
289
+ this.workers[0].warm();
290
+ }
291
+ callWithMeta(stage, args, options = {}) {
292
+ return this.pick().callWithMeta(stage, args, options);
293
+ }
294
+ pick() {
295
+ const idle = this.workers.find((worker) => worker.pending === 0);
296
+ if (idle) return idle;
297
+ if (this.workers.length < Math.max(1, this.size)) {
298
+ const worker = this.create();
299
+ this.workers.push(worker);
300
+ return worker;
301
+ }
302
+ return this.workers.reduce((best, worker) => worker.pending < best.pending ? worker : best);
303
+ }
304
+ shutdown() {
305
+ for (const worker of this.workers.splice(0)) worker.shutdown();
306
+ }
307
+ };
308
+
309
+ export {
310
+ StageCancelledError,
311
+ pythonWorkerStatuses,
312
+ shutdownAllPythonWorkers,
313
+ PythonWorker,
314
+ defaultStageWorkerCount,
315
+ PythonWorkerPool
316
+ };
317
+ //# sourceMappingURL=chunk-YH637PDL.js.map
@@ -0,0 +1,112 @@
1
+ // src/nodes/definition/studioImages.ts
2
+ var STUDIO_IMAGE_ROOT = ".cascade-studio/";
3
+ var PER_NAME = 3;
4
+ var BUDGET_BYTES = 512 * 1024 * 1024;
5
+ var kept = /* @__PURE__ */ new Map();
6
+ var bytes = 0;
7
+ var sequence = 0;
8
+ function clean(path) {
9
+ return path.replace(/^\.\//, "");
10
+ }
11
+ function drop(path) {
12
+ const entry = kept.get(path);
13
+ if (!entry) return;
14
+ kept.delete(path);
15
+ bytes -= entry.pixels.data.byteLength;
16
+ }
17
+ function keepStudioImage(pixels, name = "image.png") {
18
+ const safe = name.replace(/[^A-Za-z0-9._-]/g, "_") || "image.png";
19
+ const path = `${STUDIO_IMAGE_ROOT}${++sequence}-${safe}`;
20
+ kept.set(path, { pixels, name: safe });
21
+ bytes += pixels.data.byteLength;
22
+ const siblings = [...kept].filter(([, entry]) => entry.name === safe).map(([key]) => key);
23
+ for (const old of siblings.slice(0, Math.max(0, siblings.length - PER_NAME))) drop(old);
24
+ for (const old of kept.keys()) {
25
+ if (bytes <= BUDGET_BYTES || old === path) break;
26
+ drop(old);
27
+ }
28
+ return path;
29
+ }
30
+ function isStudioImage(path) {
31
+ return kept.has(clean(path));
32
+ }
33
+ function studioImagePixels(path) {
34
+ return kept.get(clean(path))?.pixels;
35
+ }
36
+ function draw(pixels) {
37
+ const globals = globalThis;
38
+ if (!globals.ImageData) throw new Error("Studio cannot draw a kept image: no ImageData here");
39
+ let canvas;
40
+ if (globals.document) {
41
+ canvas = globals.document.createElement("canvas");
42
+ canvas.width = pixels.width;
43
+ canvas.height = pixels.height;
44
+ } else if (globals.OffscreenCanvas) {
45
+ canvas = new globals.OffscreenCanvas(pixels.width, pixels.height);
46
+ } else {
47
+ throw new Error("Studio cannot draw a kept image: no canvas here");
48
+ }
49
+ canvas.getContext("2d").putImageData(new globals.ImageData(pixels.data, pixels.width, pixels.height), 0, 0);
50
+ return canvas;
51
+ }
52
+ async function studioImageBytes(path) {
53
+ const entry = kept.get(clean(path));
54
+ if (!entry) return void 0;
55
+ const canvas = draw(entry.pixels);
56
+ const blob = canvas.convertToBlob ? await canvas.convertToBlob({ type: "image/png" }) : await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
57
+ if (!blob) throw new Error(`Studio could not encode ${path}`);
58
+ return new Uint8Array(await blob.arrayBuffer());
59
+ }
60
+ function studioImageUrl(path) {
61
+ const entry = kept.get(clean(path));
62
+ if (!entry) return void 0;
63
+ entry.url ??= draw(entry.pixels).toDataURL("image/png");
64
+ return entry.url;
65
+ }
66
+
67
+ // src/nodes/definition/browserCapabilities.ts
68
+ function assetUrl(path) {
69
+ const clean2 = path.replace(/^\.\//, "");
70
+ return `/api/media/${clean2.split("/").map(encodeURIComponent).join("/")}?raw=1`;
71
+ }
72
+ var browserAssetCapability = {
73
+ async read(ref, { signal }) {
74
+ const kept2 = await studioImageBytes(ref.path);
75
+ if (kept2) return kept2;
76
+ const response = await fetch(assetUrl(ref.path), { signal });
77
+ if (!response.ok) throw new Error(`Could not read asset ${ref.path} (${response.status})`);
78
+ return new Uint8Array(await response.arrayBuffer());
79
+ },
80
+ async write(data, metadata, { signal }) {
81
+ const name = metadata.suggestedName?.trim() || "asset.bin";
82
+ const form = new FormData();
83
+ form.append("file", new Blob([new Uint8Array(data)], { type: metadata.mediaType }), name);
84
+ const response = await fetch("/api/assets", {
85
+ method: "POST",
86
+ body: form,
87
+ signal
88
+ });
89
+ if (!response.ok) throw new Error(`Could not write asset ${name} (${response.status})`);
90
+ const result = await response.json();
91
+ if (!result.path) throw new Error(`Asset upload returned no path for ${name}`);
92
+ return { path: result.path, ...metadata.mediaType ? { mediaType: metadata.mediaType } : {} };
93
+ },
94
+ imagePixels(ref) {
95
+ return studioImagePixels(ref.path);
96
+ },
97
+ async resolveUrl(ref) {
98
+ return { value: isStudioImage(ref.path) ? studioImageUrl(ref.path) : assetUrl(ref.path), release: () => {
99
+ } };
100
+ }
101
+ };
102
+ var studioImageAssetCapability = {
103
+ ...browserAssetCapability,
104
+ async keepImage(pixels, metadata) {
105
+ return { path: keepStudioImage(pixels, metadata.suggestedName), mediaType: "image/png" };
106
+ }
107
+ };
108
+
109
+ export {
110
+ browserAssetCapability
111
+ };
112
+ //# sourceMappingURL=chunk-ZSCUZE6I.js.map
@@ -2,26 +2,28 @@ import {
2
2
  formatCompactJSON
3
3
  } from "./chunk-OWWBYLHC.js";
4
4
  import {
5
- PythonWorker,
6
- StageCancelledError,
7
- callStage,
8
5
  compileEmbedded,
9
6
  compileProjectModule,
10
- compileProjectPanel,
11
- formatStageTrace,
12
- pythonWorkerStatuses,
13
- shutdownAllPythonWorkers,
14
- stageSources
15
- } from "./chunk-MIIEFUGG.js";
7
+ compileProjectPanel
8
+ } from "./chunk-EU4ZFZ7Y.js";
16
9
  import {
17
- writeFileAtomic,
18
- writeFileAtomicSync
19
- } from "./chunk-WPUDGEFY.js";
10
+ PythonWorker,
11
+ StageCancelledError,
12
+ pythonWorkerStatuses,
13
+ shutdownAllPythonWorkers
14
+ } from "./chunk-YH637PDL.js";
20
15
  import {
21
16
  readStageConfig,
22
17
  runProjectStage
23
- } from "./chunk-VVGSLVSF.js";
18
+ } from "./chunk-X566FWBV.js";
24
19
  import "./chunk-WD62JIP6.js";
20
+ import {
21
+ callStage,
22
+ formatStageTrace,
23
+ stageSources,
24
+ writeFileAtomic,
25
+ writeFileAtomicSync
26
+ } from "./chunk-5ORUQ6ES.js";
25
27
  import {
26
28
  CredentialStore,
27
29
  PathSafetyError,
@@ -32,10 +34,10 @@ import {
32
34
  resolveWithinRoot,
33
35
  secretValue,
34
36
  writeProjectManifest
35
- } from "./chunk-RQZ4SX5R.js";
36
- import "./chunk-A4OAMOZH.js";
37
+ } from "./chunk-2JEU3MGB.js";
38
+ import "./chunk-NJFMDXHR.js";
37
39
  import "./chunk-J7VK7MNF.js";
38
- import "./chunk-BIJO5OFS.js";
40
+ import "./chunk-FWMSGKEH.js";
39
41
 
40
42
  // server/src/cliCommands.ts
41
43
  import fs13 from "node:fs";
@@ -2306,6 +2308,10 @@ Read \`node_modules/cascade/AGENTS.md\` for the framework itself. What follows i
2306
2308
  Do not start another Cascade process on an occupied port. Remote access requires
2307
2309
  both the explicit bind address and the exact trusted browser hostname.
2308
2310
 
2311
+ ## Choosing a variant
2312
+
2313
+ \`npx --no-install cascade ladder index.cascade <node> --name <name> --seeds 1,2,3\` cooks the variants side by side; \`cascade pick <ladder id> <n>\` locks the approved one. To let Marcus pick on his phone in the Cairn web chat, add \`--cairn <Substrate vault>/<space>/_attachments/ladders/<ladder id> --json\`, post \`data.cairnChoice\` (a \`cairn:choice\` fence, at most 8 pictures) in the reply as is, and when "Picked: <label> (pick:<id>:<n>)" comes back run \`cascade pick <id> <n>\` and reply with that locked image. See \`node_modules/cascade/doc/CLI_REFERENCE.md\`, "Picking a ladder variant in Cairn".
2314
+
2309
2315
  ## The \`.cascade\` document format
2310
2316
 
2311
2317
  Hand-editing is supported. Preserve metadata, source fields, IDs, and valid connection endpoints, then run the static checks. This example uses the node below; save it at \`nodes/Multiply/index.ts\` before running the graph.
@@ -3461,4 +3467,4 @@ export {
3461
3467
  resolveProjectTarget,
3462
3468
  runStudioCli
3463
3469
  };
3464
- //# sourceMappingURL=cliCommands-UQSFGEHF.js.map
3470
+ //# sourceMappingURL=cliCommands-JJN5Z3ZC.js.map