videobook 0.0.0-stage → 0.2.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 (565) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +195 -2
  4. package/bin/videobook.js +7 -0
  5. package/dist/acestep/client.js +203 -0
  6. package/dist/acestep/config.js +62 -0
  7. package/dist/acestep/errors.js +8 -0
  8. package/dist/acestep/process.js +175 -0
  9. package/dist/alive-captions/analyze.js +117 -0
  10. package/dist/alive-captions/audio.js +179 -0
  11. package/dist/alive-captions/frames.js +59 -0
  12. package/dist/alive-captions/model.js +37 -0
  13. package/dist/alive-captions/schema.js +49 -0
  14. package/dist/alive-captions/track.js +9 -0
  15. package/dist/api-key.js +33 -0
  16. package/dist/app-state.js +56 -0
  17. package/dist/app.js +257 -0
  18. package/dist/archive-b2.js +332 -0
  19. package/dist/archive-store.js +169 -0
  20. package/dist/artifact-label.js +36 -0
  21. package/dist/artifact-refs.js +49 -0
  22. package/dist/asset-tag-extraction.js +169 -0
  23. package/dist/asset-tags.js +228 -0
  24. package/dist/asset-usage.js +23 -0
  25. package/dist/audio-cue.js +71 -0
  26. package/dist/audio-encoding.js +32 -0
  27. package/dist/audio-sync-request.js +1 -0
  28. package/dist/audio-sync.js +223 -0
  29. package/dist/audio-trim-range.js +31 -0
  30. package/dist/book-context.js +31 -0
  31. package/dist/book-manager.js +374 -0
  32. package/dist/catalog-gc.js +26 -0
  33. package/dist/chain-executor.js +815 -0
  34. package/dist/chain-generation.js +115 -0
  35. package/dist/chain-plan.js +746 -0
  36. package/dist/chain-step-review.js +60 -0
  37. package/dist/character-name.js +25 -0
  38. package/dist/chat-generation-routing.js +73 -0
  39. package/dist/chat-generation-settings.js +78 -0
  40. package/dist/chat-models.js +44 -0
  41. package/dist/chat-prompt-rewrite.js +67 -0
  42. package/dist/chat-router.js +628 -0
  43. package/dist/chat-view-actions.js +49 -0
  44. package/dist/cli/args.js +242 -0
  45. package/dist/cli/config-command.js +103 -0
  46. package/dist/cli/doctor-command.js +289 -0
  47. package/dist/cli/entry.js +2 -0
  48. package/dist/cli/key-report.js +16 -0
  49. package/dist/cli/main.js +104 -0
  50. package/dist/cli/mcp-command.js +88 -0
  51. package/dist/cli/mcp-relay.js +51 -0
  52. package/dist/cli/open-browser.js +14 -0
  53. package/dist/cli/run-command.js +231 -0
  54. package/dist/cli/serve-command.js +62 -0
  55. package/dist/cli/setup-command.js +60 -0
  56. package/dist/cli/stdio-guard.js +8 -0
  57. package/dist/cold-open-model.js +7 -0
  58. package/dist/cold-open.js +79 -0
  59. package/dist/command-reference.js +95 -0
  60. package/dist/content-store.js +74 -0
  61. package/dist/cost.js +8 -0
  62. package/dist/crop-geometry.js +23 -0
  63. package/dist/dancing-workflow.js +447 -0
  64. package/dist/data-home.js +44 -0
  65. package/dist/diagnostics.js +109 -0
  66. package/dist/dialog-quality.js +34 -0
  67. package/dist/downloads-dest.js +56 -0
  68. package/dist/editing-moments.js +194 -0
  69. package/dist/elevenlabs-audio-extension.js +66 -0
  70. package/dist/engine.js +54 -0
  71. package/dist/env-file.js +91 -0
  72. package/dist/env-secrets.js +148 -0
  73. package/dist/ffmpeg-binary.js +12 -0
  74. package/dist/ffmpeg.js +425 -0
  75. package/dist/ffprobe-binary.js +77 -0
  76. package/dist/forget-asset.js +216 -0
  77. package/dist/frame-utils.js +27 -0
  78. package/dist/generation-credit-error.js +11 -0
  79. package/dist/generation-input-mentions.js +117 -0
  80. package/dist/generation-input-policy.js +68 -0
  81. package/dist/generation-reference-mentions.js +97 -0
  82. package/dist/goal-characters.js +49 -0
  83. package/dist/goal-plan-v3.js +273 -0
  84. package/dist/goal-plan.js +220 -0
  85. package/dist/goal-planner-prompt.js +28 -0
  86. package/dist/goal-prompt-budget.js +4 -0
  87. package/dist/goal-reference-images.js +25 -0
  88. package/dist/goal-state.js +261 -0
  89. package/dist/goal-stills.js +153 -0
  90. package/dist/goal-store.js +144 -0
  91. package/dist/goal-styles.js +144 -0
  92. package/dist/goal-v3-runtime.js +157 -0
  93. package/dist/guides/character_schema_guide.md +70 -0
  94. package/dist/guides/gpt_image_2_5_prompting_guide.md +21 -0
  95. package/dist/guides/grok_imagine_prompting_guide.md +72 -0
  96. package/dist/guides/image_reproduction_guide.md +84 -0
  97. package/dist/guides/kling_3_0_prompting_guide.md +136 -0
  98. package/dist/guides/nano_banana_pro_prompting_guide.md +28 -0
  99. package/dist/guides/seedance_2_prompting_guide.md +172 -0
  100. package/dist/http-access.js +49 -0
  101. package/dist/http-byte-range.js +31 -0
  102. package/dist/hydration.js +56 -0
  103. package/dist/image-compress.js +77 -0
  104. package/dist/image-edit-default.js +1 -0
  105. package/dist/image-edit-models.js +41 -0
  106. package/dist/image-edit-settings.js +11 -0
  107. package/dist/image-region-edit.js +32 -0
  108. package/dist/image-subject-map.js +523 -0
  109. package/dist/image.js +46 -0
  110. package/dist/index.js +12 -0
  111. package/dist/job-deadline.js +60 -0
  112. package/dist/jobs/_alibaba-upload.js +30 -0
  113. package/dist/jobs/_audio-chunks.js +31 -0
  114. package/dist/jobs/_download-into-asset.js +15 -0
  115. package/dist/jobs/_fal-shared.js +22 -0
  116. package/dist/jobs/_fal-upload.js +35 -0
  117. package/dist/jobs/_image-edit-adapter.js +213 -0
  118. package/dist/jobs/_kie-poll.js +119 -0
  119. package/dist/jobs/_kie-upload.js +32 -0
  120. package/dist/jobs/_lease.js +163 -0
  121. package/dist/jobs/_persist-output.js +19 -0
  122. package/dist/jobs/_storage-retry.js +21 -0
  123. package/dist/jobs/_transcribe.js +245 -0
  124. package/dist/jobs/_translate-transcript.js +270 -0
  125. package/dist/jobs/analyze-alive-captions.js +10 -0
  126. package/dist/jobs/analyze-editing-moments.js +104 -0
  127. package/dist/jobs/analyze-jokes.js +143 -0
  128. package/dist/jobs/analyze-keyword-placement.js +155 -0
  129. package/dist/jobs/analyze-music.js +14 -0
  130. package/dist/jobs/analyze-video.js +282 -0
  131. package/dist/jobs/apply-cuts.js +83 -0
  132. package/dist/jobs/apply-sfx.js +107 -0
  133. package/dist/jobs/archive-clip.js +458 -0
  134. package/dist/jobs/assemble-goal.js +98 -0
  135. package/dist/jobs/audio-boost.js +71 -0
  136. package/dist/jobs/audio-mix.js +161 -0
  137. package/dist/jobs/backfill-asset-tags.js +144 -0
  138. package/dist/jobs/burn-watermark.js +316 -0
  139. package/dist/jobs/change-video-speed.js +176 -0
  140. package/dist/jobs/clean-video-audio.js +207 -0
  141. package/dist/jobs/cold-open.js +166 -0
  142. package/dist/jobs/combine-goal.js +108 -0
  143. package/dist/jobs/complete-provider-task.js +185 -0
  144. package/dist/jobs/copy-library-asset.js +33 -0
  145. package/dist/jobs/cost-estimate-assessment.js +113 -0
  146. package/dist/jobs/crop-image.js +47 -0
  147. package/dist/jobs/crop-video.js +71 -0
  148. package/dist/jobs/describe-image.js +181 -0
  149. package/dist/jobs/detect-scenes.js +33 -0
  150. package/dist/jobs/doctor-fix.js +100 -0
  151. package/dist/jobs/download-video.js +427 -0
  152. package/dist/jobs/enqueue-search-index.js +24 -0
  153. package/dist/jobs/even-split.js +122 -0
  154. package/dist/jobs/export-sequence.js +365 -0
  155. package/dist/jobs/extend-audio.js +229 -0
  156. package/dist/jobs/extract-audio.js +72 -0
  157. package/dist/jobs/extract-frame.js +58 -0
  158. package/dist/jobs/extract-notebook-audio.js +82 -0
  159. package/dist/jobs/extract-notebook-frame.js +93 -0
  160. package/dist/jobs/extract-video-clip.js +8 -0
  161. package/dist/jobs/extract-video-poster.js +55 -0
  162. package/dist/jobs/extract-waveform.js +25 -0
  163. package/dist/jobs/finish-goal.js +255 -0
  164. package/dist/jobs/forget-asset.js +11 -0
  165. package/dist/jobs/generate-acestep-cover.js +219 -0
  166. package/dist/jobs/generate-media.js +70 -0
  167. package/dist/jobs/image-subjects.js +1059 -0
  168. package/dist/jobs/index.js +18 -0
  169. package/dist/jobs/inject-first-frame.js +175 -0
  170. package/dist/jobs/isolate-vocals.js +69 -0
  171. package/dist/jobs/kie-paygate.js +177 -0
  172. package/dist/jobs/label-music-sections.js +5 -0
  173. package/dist/jobs/osg-placement.js +135 -0
  174. package/dist/jobs/pad-video.js +72 -0
  175. package/dist/jobs/plan-goal.js +97 -0
  176. package/dist/jobs/post-record.js +21 -0
  177. package/dist/jobs/post-to-x.js +21 -0
  178. package/dist/jobs/prepare-search-reference.js +190 -0
  179. package/dist/jobs/prepare-temporal-search.js +16 -0
  180. package/dist/jobs/process-audio.js +67 -0
  181. package/dist/jobs/process-image.js +58 -0
  182. package/dist/jobs/process-upload.js +166 -0
  183. package/dist/jobs/process-video.js +438 -0
  184. package/dist/jobs/register-all.js +161 -0
  185. package/dist/jobs/relayout-notebook.js +78 -0
  186. package/dist/jobs/remove-sound.js +299 -0
  187. package/dist/jobs/render-project.js +380 -0
  188. package/dist/jobs/render-qr-outro.js +138 -0
  189. package/dist/jobs/render-timeline.js +283 -0
  190. package/dist/jobs/repair-search-indexes.js +42 -0
  191. package/dist/jobs/replace-video-audio.js +64 -0
  192. package/dist/jobs/restore-archive-clip.js +53 -0
  193. package/dist/jobs/reverse-video.js +53 -0
  194. package/dist/jobs/rewrite-script.js +173 -0
  195. package/dist/jobs/run-notebook.js +3335 -0
  196. package/dist/jobs/semantic-index.js +797 -0
  197. package/dist/jobs/smooth-scene-concat.js +257 -0
  198. package/dist/jobs/splice-video.js +280 -0
  199. package/dist/jobs/split-video.js +127 -0
  200. package/dist/jobs/suggest-cuts.js +149 -0
  201. package/dist/jobs/suggest-sfx.js +135 -0
  202. package/dist/jobs/sync-video-to-audio.js +329 -0
  203. package/dist/jobs/tag-asset.js +266 -0
  204. package/dist/jobs/task-failure.js +16 -0
  205. package/dist/jobs/timewarp-video.js +152 -0
  206. package/dist/jobs/transcribe-video-audio.js +117 -0
  207. package/dist/jobs/transcribe.js +35 -0
  208. package/dist/jobs/translate-transcript.js +55 -0
  209. package/dist/jobs/trim-audio.js +133 -0
  210. package/dist/jobs/trim-video.js +291 -0
  211. package/dist/jobs/xai-paygate.js +26 -0
  212. package/dist/keyword-placement-cache.js +74 -0
  213. package/dist/lazy-render.js +142 -0
  214. package/dist/library-browse.js +78 -0
  215. package/dist/lightning-defaults.js +206 -0
  216. package/dist/load-env.js +12 -0
  217. package/dist/local-search-models.js +206 -0
  218. package/dist/local-upload.js +318 -0
  219. package/dist/logger.js +69 -0
  220. package/dist/media/adapters/alibaba.js +20 -0
  221. package/dist/media/adapters/elevenlabs.js +58 -0
  222. package/dist/media/adapters/fal.js +45 -0
  223. package/dist/media/adapters/index.js +26 -0
  224. package/dist/media/adapters/kie.js +120 -0
  225. package/dist/media/adapters/pending.js +21 -0
  226. package/dist/media/adapters/types.js +5 -0
  227. package/dist/media/adapters/xai.js +18 -0
  228. package/dist/media/catalog.js +236 -0
  229. package/dist/media/complete.js +191 -0
  230. package/dist/media/cost.js +45 -0
  231. package/dist/media/descriptor.js +157 -0
  232. package/dist/media/endpoints.js +11 -0
  233. package/dist/media/frame-options.js +18 -0
  234. package/dist/media/insert-video-timing.js +17 -0
  235. package/dist/media/json-schema.js +75 -0
  236. package/dist/media/legacy-task-types.js +129 -0
  237. package/dist/media/listing.js +24 -0
  238. package/dist/media/model-id.js +14 -0
  239. package/dist/media/preview.js +86 -0
  240. package/dist/media/prompt-dialects.js +65 -0
  241. package/dist/media/ref-fields.js +106 -0
  242. package/dist/media/refs.js +131 -0
  243. package/dist/media/request.js +188 -0
  244. package/dist/media/types.js +6 -0
  245. package/dist/media/wire.js +161 -0
  246. package/dist/media-library.js +535 -0
  247. package/dist/media-output-path.js +33 -0
  248. package/dist/multyculty-workflow.js +757 -0
  249. package/dist/notebook-action-catalog.js +608 -0
  250. package/dist/notebook-cell-outputs.js +726 -0
  251. package/dist/notebook-credit-retry.js +45 -0
  252. package/dist/notebook-fixtures.js +8 -0
  253. package/dist/notebook-generation-contract.js +77 -0
  254. package/dist/notebook-generation-controls.js +145 -0
  255. package/dist/notebook-goal-layout.js +40 -0
  256. package/dist/notebook-graph.js +37 -0
  257. package/dist/notebook-image-edit.js +84 -0
  258. package/dist/notebook-initial-generation-prompt.js +222 -0
  259. package/dist/notebook-live-analysis.js +941 -0
  260. package/dist/notebook-media-ops.js +579 -0
  261. package/dist/notebook-mutations.js +20 -0
  262. package/dist/notebook-placement.js +41 -0
  263. package/dist/notebook-provider-failure.js +87 -0
  264. package/dist/notebook-published-post.js +26 -0
  265. package/dist/notebook-reference-resolver.js +227 -0
  266. package/dist/notebook-relayout.js +270 -0
  267. package/dist/notebook-run-plan.js +519 -0
  268. package/dist/notebook-shots.js +162 -0
  269. package/dist/notebook-workflow.js +856 -0
  270. package/dist/one-password.js +200 -0
  271. package/dist/openai-intelligence.js +99 -0
  272. package/dist/original-media-file.js +12 -0
  273. package/dist/osg-assets.js +36 -0
  274. package/dist/osg-placement-cache.js +64 -0
  275. package/dist/osg-settings.js +27 -0
  276. package/dist/paid-tools.js +22 -0
  277. package/dist/platform-guidance.js +26 -0
  278. package/dist/posts/dropbox-auth.js +91 -0
  279. package/dist/posts/dropbox-upload.js +132 -0
  280. package/dist/posts/icloud-drive.js +134 -0
  281. package/dist/posts/icloud-status.js +64 -0
  282. package/dist/posts/post-contract.js +2 -0
  283. package/dist/posts/post-copy-format.js +43 -0
  284. package/dist/posts/post-copy.js +193 -0
  285. package/dist/posts/post-folder.js +18 -0
  286. package/dist/posts/post-metrics.js +119 -0
  287. package/dist/posts/post-store.js +340 -0
  288. package/dist/posts/x-post-target.js +43 -0
  289. package/dist/preview-payload.js +90 -0
  290. package/dist/prompt-surfaces.js +22 -0
  291. package/dist/provider-prompt-transform.js +77 -0
  292. package/dist/provider-status-link.js +21 -0
  293. package/dist/provider-task-record.js +78 -0
  294. package/dist/provider-task-status.js +47 -0
  295. package/dist/provider-usage.js +294 -0
  296. package/dist/queue.js +810 -0
  297. package/dist/reference-video-duration.js +17 -0
  298. package/dist/remotion/BlurFillVideo.js +51 -0
  299. package/dist/remotion/KeywordGraphicsOverlay.js +68 -0
  300. package/dist/remotion/OsgOverlay.js +30 -0
  301. package/dist/remotion/QrOutroComposition.js +47 -0
  302. package/dist/remotion/RenderComposition.js +11 -0
  303. package/dist/remotion/browser.js +59 -0
  304. package/dist/remotion/constants.js +35 -0
  305. package/dist/remotion/index.js +66 -0
  306. package/dist/remotion/keyword-graphics.js +151 -0
  307. package/dist/remotion/keyword-placement.js +182 -0
  308. package/dist/remotion/osg-spec.js +198 -0
  309. package/dist/remotion/public/qr-outro-impact.wav +0 -0
  310. package/dist/remotion/qr-outro-audio.js +1 -0
  311. package/dist/remotion/qr-outro-spec.js +211 -0
  312. package/dist/remotion/shared-bundle.js +37 -0
  313. package/dist/remotion/subtitles/AliveCaptions.js +83 -0
  314. package/dist/remotion/subtitles/Highlight.js +58 -0
  315. package/dist/remotion/subtitles/Impact.js +67 -0
  316. package/dist/remotion/subtitles/SubtitleOverlay.js +9 -0
  317. package/dist/remotion/subtitles/alive-layout.js +321 -0
  318. package/dist/remotion/subtitles/alive-types.js +17 -0
  319. package/dist/remotion/subtitles/chunker.js +68 -0
  320. package/dist/remotion/subtitles/segments.js +42 -0
  321. package/dist/remotion/subtitles/styles.js +4 -0
  322. package/dist/remotion/subtitles/types.js +1 -0
  323. package/dist/remotion/util.js +18 -0
  324. package/dist/remotion-cancellation.js +15 -0
  325. package/dist/rename-helpers.js +204 -0
  326. package/dist/rhythm/analyze.js +128 -0
  327. package/dist/rhythm/contract.js +14 -0
  328. package/dist/rhythm/draft-model.js +19 -0
  329. package/dist/rhythm/energy.js +133 -0
  330. package/dist/rhythm/fft.js +44 -0
  331. package/dist/rhythm/model.js +71 -0
  332. package/dist/rhythm/music-analysis.js +124 -0
  333. package/dist/rhythm/music-draft.js +261 -0
  334. package/dist/rhythm/onset.js +101 -0
  335. package/dist/rhythm/run-worker.js +51 -0
  336. package/dist/rhythm/score.js +47 -0
  337. package/dist/rhythm/section-labels.js +168 -0
  338. package/dist/rhythm/sequence-rhythm.js +67 -0
  339. package/dist/rhythm/synthesize.js +103 -0
  340. package/dist/rhythm/timing.js +98 -0
  341. package/dist/rhythm/track.js +320 -0
  342. package/dist/rhythm/visual-fit.js +4 -0
  343. package/dist/rhythm/worker.js +25 -0
  344. package/dist/scene-detect.js +200 -0
  345. package/dist/scene-transition.js +12 -0
  346. package/dist/scheduled-posts.js +200 -0
  347. package/dist/search-index-coverage.js +13 -0
  348. package/dist/search-index-media.js +71 -0
  349. package/dist/search-reference-store.js +60 -0
  350. package/dist/search-scoring.js +17 -0
  351. package/dist/serve.js +125 -0
  352. package/dist/server-lock.js +143 -0
  353. package/dist/server.js +42 -0
  354. package/dist/srt.js +16 -0
  355. package/dist/tag-backfill-state.js +121 -0
  356. package/dist/tag-settings.js +30 -0
  357. package/dist/tag-vocabulary.js +237 -0
  358. package/dist/test-queue-cleanup.js +1 -0
  359. package/dist/time-parse.js +50 -0
  360. package/dist/timeline-helpers.js +554 -0
  361. package/dist/timewarp.js +87 -0
  362. package/dist/tools/_image-inputs.js +9 -0
  363. package/dist/tools/_media-refs.js +84 -0
  364. package/dist/tools/_shared.js +59 -0
  365. package/dist/tools/_suggest-shared.js +279 -0
  366. package/dist/tools/add-timeline-audio.js +73 -0
  367. package/dist/tools/alive-captions.js +157 -0
  368. package/dist/tools/analyze-jokes.js +56 -0
  369. package/dist/tools/analyze-video.js +66 -0
  370. package/dist/tools/animate-keywords.js +217 -0
  371. package/dist/tools/api-status.js +42 -0
  372. package/dist/tools/application-commands.js +34 -0
  373. package/dist/tools/apply-cuts.js +112 -0
  374. package/dist/tools/apply-sfx.js +152 -0
  375. package/dist/tools/apply-watermark.js +670 -0
  376. package/dist/tools/archive-clip.js +74 -0
  377. package/dist/tools/asset-history.js +61 -0
  378. package/dist/tools/asset-manifest.js +24 -0
  379. package/dist/tools/asset-tags.js +151 -0
  380. package/dist/tools/assign-slot.js +97 -0
  381. package/dist/tools/audio-boost.js +107 -0
  382. package/dist/tools/audio-mix.js +121 -0
  383. package/dist/tools/book-usage.js +16 -0
  384. package/dist/tools/chain.js +136 -0
  385. package/dist/tools/change-video-speed.js +105 -0
  386. package/dist/tools/chat-history.js +5 -0
  387. package/dist/tools/chat.js +848 -0
  388. package/dist/tools/clean-video-audio.js +97 -0
  389. package/dist/tools/clear-failed-tasks.js +66 -0
  390. package/dist/tools/cold-open.js +114 -0
  391. package/dist/tools/command-plan.js +1680 -0
  392. package/dist/tools/commands.js +1666 -0
  393. package/dist/tools/concat-images.js +236 -0
  394. package/dist/tools/connect-service.js +55 -0
  395. package/dist/tools/copy-file-to-clipboard.js +58 -0
  396. package/dist/tools/create-character.js +114 -0
  397. package/dist/tools/create-qart.js +253 -0
  398. package/dist/tools/create-qr-outro.js +149 -0
  399. package/dist/tools/create-qrbtf.js +215 -0
  400. package/dist/tools/create-watermark.js +97 -0
  401. package/dist/tools/crop-asset.js +18 -0
  402. package/dist/tools/crop-image.js +120 -0
  403. package/dist/tools/crop-video.js +97 -0
  404. package/dist/tools/delete-asset.js +104 -0
  405. package/dist/tools/describe-image.js +69 -0
  406. package/dist/tools/detect-scenes.js +44 -0
  407. package/dist/tools/doctor.js +126 -0
  408. package/dist/tools/download-video.js +114 -0
  409. package/dist/tools/duplicate-asset.js +184 -0
  410. package/dist/tools/edit-timeline.js +59 -0
  411. package/dist/tools/editing-moments.js +105 -0
  412. package/dist/tools/estimate-cost.js +171 -0
  413. package/dist/tools/even-split.js +295 -0
  414. package/dist/tools/export-asset-file.js +22 -0
  415. package/dist/tools/export-asset.js +140 -0
  416. package/dist/tools/export-sequence.js +250 -0
  417. package/dist/tools/export-to-downloads.js +54 -0
  418. package/dist/tools/export-transcript-srt.js +79 -0
  419. package/dist/tools/extend-audio.js +100 -0
  420. package/dist/tools/extract-audio.js +174 -0
  421. package/dist/tools/extract-frame.js +108 -0
  422. package/dist/tools/extract-notebook-audio.js +173 -0
  423. package/dist/tools/extract-notebook-frame.js +160 -0
  424. package/dist/tools/extract-video-clip.js +101 -0
  425. package/dist/tools/generate-media.js +157 -0
  426. package/dist/tools/generation-payload.js +48 -0
  427. package/dist/tools/get-timeline.js +12 -0
  428. package/dist/tools/get-watermark-config.js +38 -0
  429. package/dist/tools/goal.js +644 -0
  430. package/dist/tools/image-subjects.js +711 -0
  431. package/dist/tools/index.js +323 -0
  432. package/dist/tools/inject-first-frame.js +154 -0
  433. package/dist/tools/invert-image.js +70 -0
  434. package/dist/tools/isolate-vocals.js +66 -0
  435. package/dist/tools/list-assets.js +98 -0
  436. package/dist/tools/list-cell-generations.js +225 -0
  437. package/dist/tools/list-elevenlabs-voices.js +163 -0
  438. package/dist/tools/log-wrapper.js +29 -0
  439. package/dist/tools/make-cover-song.js +95 -0
  440. package/dist/tools/mark-asset-seen.js +23 -0
  441. package/dist/tools/media-library.js +245 -0
  442. package/dist/tools/move-timeline-audio.js +35 -0
  443. package/dist/tools/music-analysis.js +60 -0
  444. package/dist/tools/music-draft.js +29 -0
  445. package/dist/tools/pad-video.js +87 -0
  446. package/dist/tools/post-record.js +83 -0
  447. package/dist/tools/post.js +168 -0
  448. package/dist/tools/prompt-history.js +164 -0
  449. package/dist/tools/provider-key-status.js +248 -0
  450. package/dist/tools/qrbtf-render.js +849 -0
  451. package/dist/tools/read-asset-metadata.js +26 -0
  452. package/dist/tools/read-file-at-commit.js +29 -0
  453. package/dist/tools/read-transcript.js +41 -0
  454. package/dist/tools/reconcile-project-work.js +1452 -0
  455. package/dist/tools/recover-asset.js +28 -0
  456. package/dist/tools/remove-sound.js +72 -0
  457. package/dist/tools/remove-timeline-audio.js +34 -0
  458. package/dist/tools/remove-timeline-slot.js +40 -0
  459. package/dist/tools/rename-all.js +434 -0
  460. package/dist/tools/rename-asset.js +34 -0
  461. package/dist/tools/render-settings.js +47 -0
  462. package/dist/tools/render-timeline.js +120 -0
  463. package/dist/tools/render.js +384 -0
  464. package/dist/tools/reorder-character-images.js +45 -0
  465. package/dist/tools/replace-video-audio.js +133 -0
  466. package/dist/tools/restore-archive-clips.js +209 -0
  467. package/dist/tools/restore-asset-version.js +37 -0
  468. package/dist/tools/retry-generation.js +54 -0
  469. package/dist/tools/reverse-video.js +74 -0
  470. package/dist/tools/rewrite-prompt.js +388 -0
  471. package/dist/tools/rewrite-script.js +67 -0
  472. package/dist/tools/schema-from-apicity.js +66 -0
  473. package/dist/tools/search-archive.js +150 -0
  474. package/dist/tools/semantic-search.js +1214 -0
  475. package/dist/tools/sequence-rhythm.js +43 -0
  476. package/dist/tools/set-character-metadata.js +84 -0
  477. package/dist/tools/set-env-secret.js +40 -0
  478. package/dist/tools/set-osg.js +143 -0
  479. package/dist/tools/show-folder.js +37 -0
  480. package/dist/tools/smooth-scene-concat.js +122 -0
  481. package/dist/tools/splice-video.js +149 -0
  482. package/dist/tools/split-video.js +202 -0
  483. package/dist/tools/suggest-cuts.js +45 -0
  484. package/dist/tools/suggest-sfx.js +43 -0
  485. package/dist/tools/sync-video-to-audio.js +115 -0
  486. package/dist/tools/tag-backfill.js +177 -0
  487. package/dist/tools/timewarp-video.js +85 -0
  488. package/dist/tools/tool-def.js +1 -0
  489. package/dist/tools/tool-json-schema.js +116 -0
  490. package/dist/tools/transcribe.js +90 -0
  491. package/dist/tools/translate-subtitles.js +59 -0
  492. package/dist/tools/trim-audio.js +139 -0
  493. package/dist/tools/trim-video.js +168 -0
  494. package/dist/tools/update-script.js +25 -0
  495. package/dist/tools/update-timeline-audio.js +40 -0
  496. package/dist/tools/upload-file.js +41 -0
  497. package/dist/tools/verify-x-mentions.js +141 -0
  498. package/dist/tools/video-editing-command-contract.js +291 -0
  499. package/dist/tools/video-source-reference.js +30 -0
  500. package/dist/tools/video-time-input.js +18 -0
  501. package/dist/tools/videobook-domain.js +2021 -0
  502. package/dist/tools/wait-for-asset-ready.js +143 -0
  503. package/dist/tools/workspace-actions.js +54 -0
  504. package/dist/transcript-projection.js +202 -0
  505. package/dist/transcript-srt.js +33 -0
  506. package/dist/upload.js +76 -0
  507. package/dist/usage-providers.js +25 -0
  508. package/dist/video-analysis-projection.js +46 -0
  509. package/dist/video-poster.js +33 -0
  510. package/dist/video-proxy.js +99 -0
  511. package/dist/vision-regions.js +111 -0
  512. package/dist/voice-actions.js +90 -0
  513. package/dist/voice-audio.js +55 -0
  514. package/dist/voice-config.js +122 -0
  515. package/dist/voice-continuation.js +54 -0
  516. package/dist/voice-contract.js +1 -0
  517. package/dist/voice-http.js +145 -0
  518. package/dist/voice-reply-progress.js +55 -0
  519. package/dist/voice-session.js +992 -0
  520. package/dist/watermark-badge.js +259 -0
  521. package/dist/watermark-config.js +111 -0
  522. package/dist/watermark-handle-position.js +55 -0
  523. package/dist/watermark-platform.js +15 -0
  524. package/dist/watermark-timing.js +16 -0
  525. package/dist/workspace-action-catalog.js +315 -0
  526. package/dist/workspace-action-contract.js +21 -0
  527. package/dist/workspace-action-groups.js +98 -0
  528. package/dist/workspace-action-surfaces.js +7 -0
  529. package/dist/x/account.js +88 -0
  530. package/dist/x/authenticated-handle.js +30 -0
  531. package/dist/x/oauth-flow.js +191 -0
  532. package/dist/x/post-contract.js +1 -0
  533. package/dist/x/post-metadata.js +44 -0
  534. package/dist/x/post-to-x.js +456 -0
  535. package/dist/x/published-post-cell.js +90 -0
  536. package/dist/x/x-tokens.js +69 -0
  537. package/dist/ytdlp-binary.js +65 -0
  538. package/dist/zod-slots.js +334 -0
  539. package/package.json +84 -4
  540. package/ui/assets/alive-captions-BWb5nLKj.css +1 -0
  541. package/ui/assets/alive-captions-DPEr1nKM.js +1 -0
  542. package/ui/assets/confirm-bodies-D8ouHXKy.js +1 -0
  543. package/ui/assets/confirm-bodies-DrMhaXC6.css +1 -0
  544. package/ui/assets/fullscreen-B7nw4YbE.js +1 -0
  545. package/ui/assets/goal-ui-DK-xTCK0.js +2 -0
  546. package/ui/assets/goal-ui-DXdUM2KF.css +1 -0
  547. package/ui/assets/index-DzPAnCUj.css +1 -0
  548. package/ui/assets/index-kS1905s5.js +37 -0
  549. package/ui/assets/inspector-MlzQbTVo.js +1 -0
  550. package/ui/assets/jobs-ui-CoinOWx0.js +1 -0
  551. package/ui/assets/music-video-Cbe80kmQ.js +1 -0
  552. package/ui/assets/music-video-DPaiKKEq.css +1 -0
  553. package/ui/assets/ui-Oirr9VOf.css +1 -0
  554. package/ui/assets/ui-xo_Y1yQg.js +2 -0
  555. package/ui/assets/vendor-D2rfiZ5j.css +1 -0
  556. package/ui/assets/vendor-DfTCWgKm.js +19 -0
  557. package/ui/assets/vendor-flow-BXbeWxW7.js +7 -0
  558. package/ui/assets/vendor-flow-BnuhLJ6X.css +1 -0
  559. package/ui/assets/vendor-icons-CGqhCIXV.js +1 -0
  560. package/ui/assets/vendor-mcp-BTlEJcHV.js +67 -0
  561. package/ui/assets/vendor-react-Dj7D1fD4.js +9 -0
  562. package/ui/assets/vendor-remotion-CH1HXrlc.js +95 -0
  563. package/ui/assets/videobook-Qlp9HQFu.js +20 -0
  564. package/ui/favicon.svg +8 -0
  565. package/ui/index.html +126 -0
@@ -0,0 +1,3335 @@
1
+ import { createHash } from "node:crypto";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { LIGHTNING_VIDEO_MODEL, payloadPrompt, submittedGenerationPrompt, } from "../lightning-defaults.js";
4
+ import { v5 as uuidv5, v7 as uuidv7 } from "uuid";
5
+ import { z } from "zod";
6
+ import { MVP_CONTRACT_VERSION, resolveNotebookCellArtifactId, } from "videobook-engine";
7
+ import { notebookGridTag } from "videobook-engine/notebook-grid";
8
+ import { logger } from "../logger.js";
9
+ import { mutateNotebook } from "../notebook-mutations.js";
10
+ import { removeCreditFailedOutput } from "../notebook-credit-retry.js";
11
+ import { allocateGenerateAudioResult, allocateGenerateImageResult, allocateGenerateVideoResult, ensureGenerationMediaOutputs, ensureGenerationMediaOutput, findGenerationMediaOutput, findGenerationResultForRun, isConcurrentGenerationProducer, isImageVideoGenerationResult, migrateStackedGenerationResults, normalizeLegacyExtractCells, } from "../notebook-cell-outputs.js";
12
+ import { queue, registerJobHandler, waitForJob } from "../queue.js";
13
+ import { smoothSceneConcatDef } from "../tools/smooth-scene-concat.js";
14
+ import { spliceVideoDef } from "../tools/splice-video.js";
15
+ import { tryResolveModel } from "../media/descriptor.js";
16
+ import { previewGeneration } from "../media/preview.js";
17
+ import { checkInsertVideoTiming, normalizeRefs } from "../media/refs.js";
18
+ import { REF_ROLES } from "../media/types.js";
19
+ import { captureFrame, concatSourceRanges, exportSequenceMaster, extractSourceAudio, MINIMUM_SPLIT_PIECE_SECONDS, normalizeSceneVideo, splitSourceRange, splitSourceRanges, } from "../notebook-media-ops.js";
20
+ import { analyzeNotebookSource } from "../notebook-live-analysis.js";
21
+ import { analysisFixtureForCell } from "../notebook-fixtures.js";
22
+ import { applyAnalysisResults } from "../notebook-workflow.js";
23
+ import { normalizeLegacyAnalyzeCell, normalizeLegacyAnalyzeDocument, } from "../notebook-action-catalog.js";
24
+ import { ANALYSIS_REASONING_EFFORT, INTELLIGENCE_MODEL, OPENAI_ANALYSIS_TOOL, } from "../openai-intelligence.js";
25
+ import { notebookGenerationInputContract, notebookGenerationInputError, notebookGenerationInputsAreCompatible, } from "../notebook-generation-contract.js";
26
+ import { notebookProviderFailureMessage } from "../notebook-provider-failure.js";
27
+ import { resolveGenerationInputReferences, } from "../notebook-reference-resolver.js";
28
+ import { resolvedInitialGenerationPrompt } from "../notebook-initial-generation-prompt.js";
29
+ import { sortByKindSlot } from "../generation-input-mentions.js";
30
+ import { generationPromptSurface } from "../prompt-surfaces.js";
31
+ import { recoverNotebookProviderTask } from "../provider-task-record.js";
32
+ import { prepareNotebookImageEdit } from "../notebook-image-edit.js";
33
+ function generationPlanStage(plan, cellId) {
34
+ const stages = Array.isArray(plan.stages) ? plan.stages : [];
35
+ const stage = stages.find((candidate) => {
36
+ if (!candidate
37
+ || typeof candidate !== "object"
38
+ || Array.isArray(candidate)
39
+ || candidate.kind !== "queued") {
40
+ return false;
41
+ }
42
+ if (!cellId)
43
+ return true;
44
+ const input = candidate.input;
45
+ return input
46
+ && typeof input === "object"
47
+ && !Array.isArray(input)
48
+ && input.cell_id === cellId;
49
+ }) ?? stages.find((candidate) => candidate
50
+ && typeof candidate === "object"
51
+ && !Array.isArray(candidate)
52
+ && candidate.kind === "queued");
53
+ return stage && typeof stage === "object" && !Array.isArray(stage)
54
+ ? stage
55
+ : null;
56
+ }
57
+ export async function updateGenerationPlanState(engine, notebookId, planId, status, details = {}) {
58
+ const updatedAt = new Date().toISOString();
59
+ const result = await mutateNotebook(engine, notebookId, "update_generation_plan", (notebook) => {
60
+ const plans = notebook.generationPlans ?? [];
61
+ return {
62
+ ...notebook,
63
+ generationPlans: plans.map((record) => record.planId === planId
64
+ && (details.runId === undefined
65
+ || generationPlanClaimRunId(record.plan) === details.runId)
66
+ ? {
67
+ ...record,
68
+ status,
69
+ updatedAt,
70
+ ...(details.outputArtifactId
71
+ ? { outputArtifactId: details.outputArtifactId }
72
+ : {}),
73
+ ...(details.error ? { error: details.error } : {}),
74
+ }
75
+ : record),
76
+ };
77
+ }, {
78
+ planId,
79
+ status,
80
+ outputArtifactId: details.outputArtifactId,
81
+ error: details.error,
82
+ runId: details.runId,
83
+ });
84
+ if (!result.ok)
85
+ throw new Error(result.error.message);
86
+ }
87
+ export async function writeGenerationPlanProvenance(engine, payload, cell, outputArtifactId) {
88
+ const plan = payload.generationPlan;
89
+ if (!plan || typeof plan.planId !== "string")
90
+ return;
91
+ const stage = generationPlanStage(plan, cell.id);
92
+ const written = await engine.metadata.artifacts.write(outputArtifactId, "command-plan", {
93
+ planId: plan.planId,
94
+ parentActionId: typeof plan.parentActionId === "string"
95
+ ? plan.parentActionId
96
+ : null,
97
+ notebookId: payload.notebookId,
98
+ cellId: cell.id,
99
+ actor: payload.actor ?? "videobook-user",
100
+ sourceSurface: plan.sourceSurface,
101
+ provider: stage?.provider,
102
+ model: stage?.model,
103
+ tool: stage?.tool,
104
+ input: stage?.input,
105
+ estimatedCostUsd: stage?.estimatedCostUsd,
106
+ estimateUnavailable: stage?.estimateUnavailable,
107
+ jobId: payload.jobId,
108
+ outputArtifactId,
109
+ createdAt: new Date().toISOString(),
110
+ });
111
+ if (!written.ok)
112
+ throw new Error(written.error.message);
113
+ }
114
+ export function notebookCellMediaType(cell) {
115
+ if (cell.type === "image"
116
+ || cell.type === "generate_image")
117
+ return "image";
118
+ if (cell.type === "audio"
119
+ || cell.type === "extract_audio"
120
+ || cell.type === "generate_audio")
121
+ return "audio";
122
+ if (cell.type === "video"
123
+ || cell.type === "split_video"
124
+ || cell.type === "generate_video"
125
+ || cell.type === "concat"
126
+ || cell.type === "splice")
127
+ return "video";
128
+ return null;
129
+ }
130
+ export function notebookCellIsPaid(cell) {
131
+ return cell.operation === "rewrite_prompt"
132
+ || (cell.tool === "edit_image_subjects" && cell.provider !== "local")
133
+ || notebookCellModel(cell) !== null;
134
+ }
135
+ export function notebookCellIsRunnable(cell) {
136
+ if (cell.inputs?.mediaRole === "extract-audio-output")
137
+ return true;
138
+ return (cell.type !== "timeline"
139
+ && cell.type !== "audio"
140
+ && cell.type !== "image"
141
+ && cell.type !== "video"
142
+ && cell.type !== "character"
143
+ && cell.type !== "analysis"
144
+ && Boolean(cell.operation ?? cell.tool ?? cell.model));
145
+ }
146
+ function textResult(result) {
147
+ const text = result.content.find((item) => item.type === "text")?.text;
148
+ if (!text)
149
+ return null;
150
+ try {
151
+ return JSON.parse(text);
152
+ }
153
+ catch {
154
+ return text;
155
+ }
156
+ }
157
+ function artifactReference(value) {
158
+ if (typeof value === "object" && value !== null) {
159
+ for (const [key, nested] of Object.entries(value)) {
160
+ if ((key.endsWith("asset_id") || key.endsWith("AssetId")) && typeof nested === "string") {
161
+ return nested;
162
+ }
163
+ const found = artifactReference(nested);
164
+ if (found)
165
+ return found;
166
+ }
167
+ }
168
+ if (Array.isArray(value)) {
169
+ for (const nested of value) {
170
+ const found = artifactReference(nested);
171
+ if (found)
172
+ return found;
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+ async function executeSpliceCell(engine, bookId, document, cell) {
178
+ const inputs = upstreamValues(document, cell);
179
+ const first = inputs.filter((entry) => entry.targetInput === "first");
180
+ const second = inputs.filter((entry) => entry.targetInput === "second");
181
+ if (inputs.length !== 2
182
+ || first.length !== 1
183
+ || second.length !== 1
184
+ || !first[0].output
185
+ || !second[0].output
186
+ || notebookCellMediaType(first[0].cell) !== "video"
187
+ || notebookCellMediaType(second[0].cell) !== "video") {
188
+ throw new Error(`Splice cell ${cell.id} requires exactly one completed video on first and second`);
189
+ }
190
+ const firstArtifact = engine.artifacts.get(first[0].output);
191
+ const secondArtifact = engine.artifacts.get(second[0].output);
192
+ if (!firstArtifact.ok)
193
+ throw new Error(firstArtifact.error.message);
194
+ if (!secondArtifact.ok)
195
+ throw new Error(secondArtifact.error.message);
196
+ const smooth = cell.operation === "smooth_scene_concat";
197
+ const overlap = Number(cell.inputs?.overlap ?? 0);
198
+ if (!smooth && (!Number.isFinite(overlap) || overlap < 0)) {
199
+ throw new Error("Splice overlap must be a non-negative number");
200
+ }
201
+ const transition = cell.inputs?.transitionSeconds;
202
+ if (smooth
203
+ && transition !== undefined
204
+ && (typeof transition !== "number" || !Number.isFinite(transition))) {
205
+ throw new Error("Splice transitionSeconds must be a finite number");
206
+ }
207
+ const definition = smooth
208
+ ? smoothSceneConcatDef(engine)
209
+ : spliceVideoDef(engine);
210
+ const response = await definition.handler({
211
+ first_asset_id: firstArtifact.value.artifactId,
212
+ second_asset_id: secondArtifact.value.artifactId,
213
+ book_id: bookId,
214
+ ...(smooth
215
+ ? (transition === undefined ? {} : { transition_seconds: transition })
216
+ : { overlap }),
217
+ });
218
+ if (response.isError) {
219
+ throw new Error(response.content[0]?.text ?? `${cell.operation} failed`);
220
+ }
221
+ const result = textResult(response);
222
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
223
+ throw new Error(`${cell.operation} returned an invalid result`);
224
+ }
225
+ const record = result;
226
+ const jobId = typeof record.job_id === "string" ? record.job_id : null;
227
+ const output = typeof record.asset_id === "string" ? record.asset_id : null;
228
+ if (!jobId || !output) {
229
+ throw new Error(`${cell.operation} did not return a queued output`);
230
+ }
231
+ await waitForJob(jobId, 1_800_000);
232
+ const artifact = engine.artifacts.get(output);
233
+ if (!artifact.ok)
234
+ throw new Error(artifact.error.message);
235
+ return {
236
+ artifactId: artifact.value.artifactId,
237
+ tool: smooth ? "smooth_scene_concat" : "splice_video",
238
+ };
239
+ }
240
+ function executionState(document) {
241
+ return (document.execution ?? {});
242
+ }
243
+ function withExecution(document, cellId, state, outputArtifactId) {
244
+ const execution = { ...executionState(document), [cellId]: state };
245
+ return {
246
+ ...document,
247
+ execution,
248
+ cells: document.cells.map((cell) => (cell.id === cellId
249
+ ? {
250
+ ...cell,
251
+ ...(outputArtifactId ? { outputArtifactId } : {}),
252
+ inputs: {
253
+ ...(cell.inputs ?? {}),
254
+ state: state.status,
255
+ },
256
+ }
257
+ : cell)),
258
+ };
259
+ }
260
+ export async function markNotebookCellQueued(engine, notebookId, cellId, runId) {
261
+ const notebook = engine.notebooks.read(notebookId);
262
+ if (!notebook.ok)
263
+ throw new Error(notebook.error.message);
264
+ const cell = notebook.value.cells.find((candidate) => candidate.id === cellId);
265
+ if (!cell)
266
+ throw new Error(`Notebook cell not found: ${cellId}`);
267
+ const updatedAt = new Date().toISOString();
268
+ const state = {
269
+ fingerprint: notebookCellFingerprint(notebook.value, cell),
270
+ status: "queued",
271
+ ...(cell.outputArtifactId ? { outputArtifactId: cell.outputArtifactId } : {}),
272
+ runId,
273
+ startedAt: updatedAt,
274
+ updatedAt,
275
+ tool: cell.tool || cell.model || cell.operation,
276
+ };
277
+ const result = await mutateNotebook(engine, notebookId, "queue_notebook_cell", (current) => {
278
+ const existing = executionState(current)[cellId];
279
+ if (existing?.runId === runId && existing.status !== "queued")
280
+ return current;
281
+ return withExecution(current, cellId, state);
282
+ }, { cellId, runId });
283
+ if (!result.ok)
284
+ throw new Error(result.error.message);
285
+ }
286
+ function upstreamValues(document, cell) {
287
+ const cells = new Map(document.cells.map((item) => [item.id, item]));
288
+ return document.edges
289
+ .filter((edge) => edge.target === cell.id)
290
+ .flatMap((edge) => {
291
+ const source = cells.get(edge.source);
292
+ if (!source)
293
+ return [];
294
+ return [{
295
+ targetInput: edge.targetInput,
296
+ cell: source,
297
+ output: resolveNotebookCellArtifactId(document, source),
298
+ }];
299
+ });
300
+ }
301
+ function stableCellInputs(inputs) {
302
+ const volatile = new Set([
303
+ "state",
304
+ "resolvedPrompt",
305
+ "dispatchedPayload",
306
+ "rawRewriteResult",
307
+ "providerArtifactId",
308
+ "normalizedFromArtifactId",
309
+ "normalizedDurationSeconds",
310
+ "sequenceRevision",
311
+ "sequenceId",
312
+ "clipArtifactIds",
313
+ "audioArtifactId",
314
+ "assemblyDurationFrames",
315
+ "shotCount",
316
+ "outputVersionArtifactIds",
317
+ "atSeconds",
318
+ ]);
319
+ return Object.fromEntries(Object.entries(inputs ?? {}).filter(([key]) => !volatile.has(key)));
320
+ }
321
+ export function notebookCellFingerprint(document, cell) {
322
+ const payload = {
323
+ type: cell.type,
324
+ id: cell.id,
325
+ prompt: cell.prompt ?? "",
326
+ provider: cell.provider ?? "",
327
+ model: cell.model ?? "",
328
+ operation: cell.operation ?? "",
329
+ tool: cell.tool ?? "",
330
+ inputs: stableCellInputs(cell.inputs),
331
+ upstream: upstreamValues(document, cell).map(({ targetInput, cell: source, output }) => ({
332
+ targetInput,
333
+ sourceId: source.id,
334
+ sourceType: source.type,
335
+ prompt: source.prompt ?? "",
336
+ provider: source.provider ?? "",
337
+ model: source.model ?? "",
338
+ operation: source.operation ?? "",
339
+ tool: source.tool ?? "",
340
+ output: output ?? null,
341
+ outputHash: typeof source.inputs?.objectHash === "string"
342
+ ? source.inputs.objectHash
343
+ : null,
344
+ sequenceRevision: typeof source.inputs?.sequenceRevision === "string"
345
+ ? source.inputs.sequenceRevision
346
+ : null,
347
+ range: {
348
+ startSeconds: source.inputs?.startSeconds ?? null,
349
+ endSeconds: source.inputs?.endSeconds ?? null,
350
+ },
351
+ })),
352
+ };
353
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
354
+ }
355
+ export function notebookCellDefinitionFingerprint(document, cell) {
356
+ const payload = {
357
+ type: cell.type,
358
+ id: cell.id,
359
+ prompt: cell.prompt ?? "",
360
+ provider: cell.provider ?? "",
361
+ model: cell.model ?? "",
362
+ operation: cell.operation ?? "",
363
+ tool: cell.tool ?? "",
364
+ inputs: stableCellInputs(cell.inputs),
365
+ upstream: upstreamValues(document, cell).map(({ targetInput, cell: source }) => ({
366
+ targetInput,
367
+ sourceId: source.id,
368
+ sourceType: source.type,
369
+ prompt: source.prompt ?? "",
370
+ provider: source.provider ?? "",
371
+ model: source.model ?? "",
372
+ operation: source.operation ?? "",
373
+ tool: source.tool ?? "",
374
+ inputs: stableCellInputs(source.inputs),
375
+ })),
376
+ };
377
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
378
+ }
379
+ function firstUpstreamArtifact(document, cell, targetInput) {
380
+ const upstream = upstreamValues(document, cell)
381
+ .filter((entry) => (targetInput ? entry.targetInput === targetInput : true))
382
+ .map((entry) => entry.output)
383
+ .find((value) => typeof value === "string" && value.length > 0);
384
+ return upstream ?? null;
385
+ }
386
+ function requiredVideoInputArtifact(document, cell) {
387
+ const input = upstreamValues(document, cell)
388
+ .find((entry) => entry.targetInput === "video" || entry.targetInput === "segment");
389
+ if (!input) {
390
+ throw new Error(`${cell.type} cell ${cell.id} requires a video input`);
391
+ }
392
+ if (notebookCellMediaType(input.cell) !== "video"
393
+ || input.cell.type === "split_video") {
394
+ throw new Error(`${cell.type} cell ${cell.id} input must be video`);
395
+ }
396
+ if (!input.output) {
397
+ throw new Error(`${cell.type} cell ${cell.id} video input is not completed`);
398
+ }
399
+ return input.output;
400
+ }
401
+ function requiredMediaOutputCellIds(document, cell, type) {
402
+ const connectedIds = new Set(document.edges
403
+ .filter((edge) => edge.source === cell.id
404
+ && edge.targetInput === "media")
405
+ .map((edge) => edge.target));
406
+ const outputIds = new Set(document.cells
407
+ .filter((candidate) => connectedIds.has(candidate.id)
408
+ && candidate.type === type)
409
+ .map((candidate) => candidate.id));
410
+ if (outputIds.size === 0) {
411
+ throw new Error(`${cell.type} cell ${cell.id} requires a downstream ${type} media cell`);
412
+ }
413
+ return outputIds;
414
+ }
415
+ function requiredTwoWaySplitVideoOutputCellIds(document, cell) {
416
+ const videoCellIds = requiredMediaOutputCellIds(document, cell, "video");
417
+ const first = new Set();
418
+ const remaining = new Set();
419
+ for (const candidate of document.cells) {
420
+ if (!videoCellIds.has(candidate.id))
421
+ continue;
422
+ if (candidate.inputs?.splitRole === "first")
423
+ first.add(candidate.id);
424
+ if (candidate.inputs?.splitRole === "remaining") {
425
+ remaining.add(candidate.id);
426
+ }
427
+ }
428
+ if (videoCellIds.size !== 2
429
+ || first.size !== 1
430
+ || remaining.size !== 1) {
431
+ throw new Error(`split_video cell ${cell.id} requires first and remaining video outputs`);
432
+ }
433
+ return { first, remaining };
434
+ }
435
+ function parseSplitOutputGroups(value) {
436
+ if (!Array.isArray(value) || value.length === 0) {
437
+ throw new Error("outputsSeconds must be a non-empty list of range groups");
438
+ }
439
+ return value.map((group) => {
440
+ if (!Array.isArray(group) || group.length === 0) {
441
+ throw new Error("Each split output group must contain at least one range");
442
+ }
443
+ return group.map((entry) => {
444
+ const record = entry && typeof entry === "object"
445
+ ? entry
446
+ : {};
447
+ const startSeconds = Number(record.startSeconds);
448
+ const endSeconds = Number(record.endSeconds);
449
+ if (!Number.isFinite(startSeconds)
450
+ || !Number.isFinite(endSeconds)
451
+ || !(endSeconds > startSeconds)) {
452
+ throw new Error("Split output ranges must use finite start and end seconds");
453
+ }
454
+ return { startSeconds, endSeconds };
455
+ });
456
+ });
457
+ }
458
+ function requiredIndexedSplitVideoOutputCellIds(document, cell, chunkCount) {
459
+ const videoCellIds = requiredMediaOutputCellIds(document, cell, "video");
460
+ const outputIds = new Array(chunkCount);
461
+ for (const candidate of document.cells) {
462
+ if (!videoCellIds.has(candidate.id))
463
+ continue;
464
+ const chunkIndex = candidate.inputs?.chunkIndex;
465
+ const candidateChunkCount = candidate.inputs?.chunkCount;
466
+ if (!Number.isSafeInteger(chunkIndex)
467
+ || Number(candidateChunkCount) !== chunkCount
468
+ || Number(chunkIndex) < 0
469
+ || Number(chunkIndex) >= chunkCount
470
+ || outputIds[Number(chunkIndex)]) {
471
+ throw new Error(`split_video cell ${cell.id} has invalid indexed video outputs`);
472
+ }
473
+ outputIds[Number(chunkIndex)] = candidate.id;
474
+ }
475
+ if (videoCellIds.size !== chunkCount
476
+ || outputIds.filter(Boolean).length !== chunkCount) {
477
+ throw new Error(`split_video cell ${cell.id} requires ${chunkCount} indexed video outputs`);
478
+ }
479
+ return Array.from({ length: chunkCount }, (_, index) => new Set([outputIds[index]]));
480
+ }
481
+ function splitFrameOutputCellIds(document, videoCellIds) {
482
+ const connectedIds = new Set(document.edges
483
+ .filter((edge) => videoCellIds.has(edge.source)
484
+ && edge.targetInput === "segment")
485
+ .map((edge) => edge.target));
486
+ const first = new Set();
487
+ const last = new Set();
488
+ for (const candidate of document.cells) {
489
+ if (!connectedIds.has(candidate.id) || candidate.type !== "image")
490
+ continue;
491
+ if (candidate.inputs?.frameRole === "first")
492
+ first.add(candidate.id);
493
+ if (candidate.inputs?.frameRole === "last")
494
+ last.add(candidate.id);
495
+ }
496
+ return { first, last };
497
+ }
498
+ function withOutputVersionIds(cell, artifactId) {
499
+ const persisted = Array.isArray(cell.inputs?.outputVersionArtifactIds)
500
+ ? cell.inputs.outputVersionArtifactIds.filter((value) => typeof value === "string" && value.length > 0)
501
+ : [];
502
+ return [...new Set([
503
+ ...persisted,
504
+ ...(cell.outputArtifactId ? [cell.outputArtifactId] : []),
505
+ artifactId,
506
+ ])];
507
+ }
508
+ function withMediaOutput(document, outputCellIds, artifactId, inputs) {
509
+ return {
510
+ ...document,
511
+ cells: document.cells.map((candidate) => {
512
+ if (!outputCellIds.has(candidate.id))
513
+ return candidate;
514
+ if (isImageVideoGenerationResult(candidate)) {
515
+ const state = typeof inputs.state === "string"
516
+ ? inputs.state
517
+ : "completed";
518
+ const nextInputs = {
519
+ ...(candidate.inputs ?? {}),
520
+ ...inputs,
521
+ state,
522
+ };
523
+ delete nextInputs.outputVersionArtifactIds;
524
+ return {
525
+ ...candidate,
526
+ outputArtifactId: artifactId,
527
+ inputs: nextInputs,
528
+ };
529
+ }
530
+ const persisted = Array.isArray(candidate.inputs?.outputVersionArtifactIds)
531
+ ? candidate.inputs.outputVersionArtifactIds.filter((value) => typeof value === "string" && value.length > 0)
532
+ : [];
533
+ const outputVersionArtifactIds = [...new Set([
534
+ ...persisted,
535
+ ...(candidate.outputArtifactId ? [candidate.outputArtifactId] : []),
536
+ artifactId,
537
+ ])];
538
+ return {
539
+ ...candidate,
540
+ outputArtifactId: artifactId,
541
+ inputs: {
542
+ ...(candidate.inputs ?? {}),
543
+ ...inputs,
544
+ outputVersionArtifactIds,
545
+ state: "completed",
546
+ },
547
+ };
548
+ }),
549
+ };
550
+ }
551
+ function rollbackGenerationMediaOutput(document, cell, staleArtifactId, outputCellIdHint) {
552
+ const mediaOutput = findGenerationResultForRun(document, cell, {
553
+ outputCellId: outputCellIdHint,
554
+ });
555
+ if (!mediaOutput)
556
+ return document;
557
+ const outputCellId = mediaOutput.outputCell.id;
558
+ const remainingExecution = { ...executionState(document) };
559
+ delete remainingExecution[outputCellId];
560
+ return {
561
+ ...document,
562
+ cells: document.cells.map((candidate) => {
563
+ if (candidate.id !== outputCellId)
564
+ return candidate;
565
+ const versions = Array.isArray(candidate.inputs?.outputVersionArtifactIds)
566
+ ? candidate.inputs.outputVersionArtifactIds.filter((value) => typeof value === "string"
567
+ && value.length > 0
568
+ && value !== staleArtifactId)
569
+ : [];
570
+ const restored = versions.at(-1);
571
+ const inputs = { ...(candidate.inputs ?? {}) };
572
+ delete inputs.state;
573
+ return {
574
+ ...candidate,
575
+ outputArtifactId: restored,
576
+ inputs: {
577
+ ...inputs,
578
+ outputVersionArtifactIds: versions,
579
+ ...(restored ? { state: "completed" } : {}),
580
+ },
581
+ };
582
+ }),
583
+ execution: remainingExecution,
584
+ };
585
+ }
586
+ // A crash, an abort, or a failure predating the rollback in
587
+ // failAwaitingNotebookCell can leave the paired output cell holding an
588
+ // awaiting-output stamp and a placeholder artifact that will never land.
589
+ // Heal that residue when the producer starts a fresh run so the pair always
590
+ // begins clean and a later synchronous failure cannot expose stale state.
591
+ export function healGenerationMediaOutputResidue(document, cell, outputCellId) {
592
+ if (isConcurrentGenerationProducer(cell) && !outputCellId) {
593
+ return document;
594
+ }
595
+ const mediaOutput = findGenerationResultForRun(document, cell, { outputCellId });
596
+ if (!mediaOutput)
597
+ return document;
598
+ if (isConcurrentGenerationProducer(cell)
599
+ && outputCellId
600
+ && mediaOutput.outputCell.id !== outputCellId) {
601
+ return document;
602
+ }
603
+ const outputCell = mediaOutput.outputCell;
604
+ const awaitingInputs = outputCell.inputs?.state === "awaiting-output";
605
+ const awaitingExecution = executionState(document)[outputCell.id]?.status === "awaiting-output";
606
+ if (!awaitingInputs && !awaitingExecution)
607
+ return document;
608
+ return rollbackGenerationMediaOutput(document, cell, outputCell.outputArtifactId, outputCell.id);
609
+ }
610
+ function withSplitOutputs(document, targets, outputs) {
611
+ return {
612
+ ...document,
613
+ cells: document.cells.map((candidate) => {
614
+ const outputArtifactId = targets.video.has(candidate.id)
615
+ ? outputs.videoArtifactId
616
+ : targets.first.has(candidate.id)
617
+ ? outputs.firstFrameArtifactId
618
+ : targets.last.has(candidate.id)
619
+ ? outputs.lastFrameArtifactId
620
+ : null;
621
+ if (!outputArtifactId)
622
+ return candidate;
623
+ return {
624
+ ...candidate,
625
+ outputArtifactId,
626
+ inputs: {
627
+ ...(candidate.inputs ?? {}),
628
+ ...(targets.video.has(candidate.id)
629
+ ? { durationSeconds: outputs.durationSeconds }
630
+ : {
631
+ atSeconds: targets.first.has(candidate.id)
632
+ ? outputs.firstAtSeconds
633
+ : outputs.lastAtSeconds,
634
+ }),
635
+ state: "completed",
636
+ },
637
+ };
638
+ }),
639
+ };
640
+ }
641
+ function promptFor(document, cell) {
642
+ const upstream = upstreamValues(document, cell)
643
+ .filter(({ cell: source }) => source.type === "prompt" || source.operation === "rewrite_prompt")
644
+ .map(({ cell: source }) => source.operation === "rewrite_prompt" && typeof source.inputs?.resolvedPrompt === "string"
645
+ ? source.inputs.resolvedPrompt
646
+ : source.prompt ?? "")
647
+ .filter(Boolean);
648
+ const own = cell.operation === "rewrite_prompt"
649
+ ? [
650
+ typeof cell.inputs?.featureBrief === "string"
651
+ ? cell.inputs.featureBrief
652
+ : "",
653
+ cell.prompt ?? "",
654
+ cell.inputs?.shotContext
655
+ ? `Shot context: ${JSON.stringify(cell.inputs.shotContext)}`
656
+ : "",
657
+ typeof cell.inputs?.audioContext === "string"
658
+ ? cell.inputs.audioContext
659
+ : "",
660
+ ]
661
+ : [
662
+ cell.type === "generate_image" || cell.type === "generate_video"
663
+ ? !cell.prompt?.trim() && upstream.length > 0
664
+ ? ""
665
+ : resolvedInitialGenerationPrompt(document, cell)
666
+ : cell.prompt ?? "",
667
+ ];
668
+ return [...new Set([...own, ...upstream].filter(Boolean))]
669
+ .join("\n\n")
670
+ .trim();
671
+ }
672
+ function isImageLikeCell(source) {
673
+ return notebookCellMediaType(source) === "image";
674
+ }
675
+ function cellGenerationSettings(cell) {
676
+ const settings = cell.inputs?.generationSettings;
677
+ return settings && typeof settings === "object" && !Array.isArray(settings)
678
+ ? settings
679
+ : {};
680
+ }
681
+ function outputsOf(entries) {
682
+ return entries.flatMap(({ output }) => typeof output === "string" ? [output] : []);
683
+ }
684
+ function upstreamOfKind(upstream, kind) {
685
+ return upstream.filter(({ cell: source, output }) => typeof output === "string" && notebookCellMediaType(source) === kind);
686
+ }
687
+ function toolArgsWithMentions(definition, document, cell, bookId, engine) {
688
+ const mentions = resolveGenerationInputReferences({ engine, document, cell, prompt: promptFor(document, cell) });
689
+ const args = {
690
+ ...(cell.inputs ?? {}),
691
+ ...cellGenerationSettings(cell),
692
+ book_id: bookId,
693
+ prompt: mentions.prompt,
694
+ };
695
+ delete args.generationMode;
696
+ delete args.generationSettings;
697
+ if (cell.operation === "rewrite_prompt" && "model" in definition.schema) {
698
+ args.model = typeof cell.inputs?.targetModel === "string" ? cell.inputs.targetModel : "seedance25";
699
+ }
700
+ const images = sortByKindSlot(upstreamOfKind(upstreamValues(document, cell), "image")
701
+ .filter(({ targetInput }) => targetInput !== "first_frame" && targetInput !== "last_frame"), "image");
702
+ if ("image_refs" in definition.schema && images.length > 0)
703
+ args.image_refs = outputsOf(images);
704
+ return { input: args, mentions };
705
+ }
706
+ function generationModeFor(model, document, cell, upstream) {
707
+ const contract = notebookGenerationInputContract(model);
708
+ const configured = typeof cell.inputs?.generationMode === "string" ? cell.inputs.generationMode : undefined;
709
+ if (contract.modes.some((mode) => mode.id === configured))
710
+ return configured;
711
+ const compatible = contract.modes.filter((mode) => notebookGenerationInputsAreCompatible(contract, document, cell, mode.id));
712
+ const hasFrameInput = upstream.some((entry) => entry.targetInput === "first_frame" || entry.targetInput === "last_frame");
713
+ return compatible.find((mode) => mode.id === (hasFrameInput ? "i2v" : "r2v"))?.id
714
+ ?? compatible[0]?.id
715
+ ?? configured;
716
+ }
717
+ function frameEntry(upstream, target, mode) {
718
+ return mode === "r2v"
719
+ ? undefined
720
+ : upstream.find((entry) => entry.targetInput === target && typeof entry.output === "string" && isImageLikeCell(entry.cell));
721
+ }
722
+ function mediaRefsFor(model, upstream, mode) {
723
+ const roles = new Set(model.slots.map((slot) => slot.role));
724
+ const modeRoles = new Set(model.modes.find((candidate) => candidate.id === mode)?.roles ?? [...roles]);
725
+ let firstFrame = frameEntry(upstream, "first_frame", mode);
726
+ let lastFrame = frameEntry(upstream, "last_frame", mode);
727
+ const images = sortByKindSlot(upstreamOfKind(upstream, "image").filter(({ targetInput }) => mode === "r2v" || (targetInput !== "first_frame" && targetInput !== "last_frame")), "image");
728
+ const frameMode = mode === "i2v" || ((mode === undefined || mode === "default") && Boolean(firstFrame || lastFrame));
729
+ if (frameMode && !firstFrame && modeRoles.has("first_frame") && !modeRoles.has("image"))
730
+ firstFrame = images.shift();
731
+ if (frameMode && !lastFrame && modeRoles.has("last_frame") && !modeRoles.has("image"))
732
+ lastFrame = images.shift();
733
+ const videos = sortByKindSlot(upstreamOfKind(upstream, "video"), "video");
734
+ const audios = sortByKindSlot(upstreamOfKind(upstream, "audio").filter(({ targetInput }) => targetInput !== "replace_audio"), "audio");
735
+ const candidates = {
736
+ first_frame: outputsOf(firstFrame ? [firstFrame] : []),
737
+ last_frame: outputsOf(lastFrame ? [lastFrame] : []),
738
+ image: outputsOf(images),
739
+ video: outputsOf(videos),
740
+ audio: outputsOf(audios),
741
+ };
742
+ const refs = {};
743
+ for (const role of REF_ROLES) {
744
+ const list = candidates[role] ?? [];
745
+ if (list.length > 0 && modeRoles.has(role))
746
+ refs[role] = list;
747
+ }
748
+ return refs;
749
+ }
750
+ function mediaOptions(model, cell) {
751
+ const settings = cellGenerationSettings(cell);
752
+ const names = new Set(model.options.map((option) => option.name));
753
+ return Object.fromEntries(Object.entries(settings).filter(([name, value]) => names.has(name) && value !== undefined && value !== ""));
754
+ }
755
+ function mediaGenerationArgs(model, document, cell, bookId, engine) {
756
+ const upstream = upstreamValues(document, cell);
757
+ const mode = generationModeFor(model, document, cell, upstream);
758
+ const generationCell = { ...cell, inputs: { ...cell.inputs, generationMode: mode } };
759
+ const mentions = resolveGenerationInputReferences({
760
+ engine,
761
+ document,
762
+ cell: generationCell,
763
+ prompt: promptFor(document, generationCell),
764
+ });
765
+ const replacementAudio = upstream.find((entry) => entry.targetInput === "replace_audio" && typeof entry.output === "string");
766
+ const input = {
767
+ book_id: bookId,
768
+ model: model.id,
769
+ ...(mentions.prompt ? { prompt: mentions.prompt } : {}),
770
+ refs: mediaRefsFor(model, upstream, mode),
771
+ options: mediaOptions(model, cell),
772
+ ...(mode && mode !== "default" ? { mode } : {}),
773
+ ...(replacementAudio?.output && model.media === "video" ? { replace_audio_with: replacementAudio.output } : {}),
774
+ };
775
+ return { input, mentions };
776
+ }
777
+ export function notebookCellModel(cell) {
778
+ if (cell.type !== "generate_image" && cell.type !== "generate_video" && cell.type !== "generate_audio")
779
+ return null;
780
+ if (cell.tool === "edit_image_subjects")
781
+ return null;
782
+ return cell.model ? tryResolveModel(cell.model.trim()) : null;
783
+ }
784
+ export function resolveNotebookGenerationTool(cell) {
785
+ const model = notebookCellModel(cell);
786
+ if (model)
787
+ return `generate_${model.media}`;
788
+ return cell.tool?.trim() || null;
789
+ }
790
+ function formatNotebookGenerationSchemaError(error) {
791
+ return error.issues.map((issue) => {
792
+ const label = issue.path
793
+ .map(String)
794
+ .join(".")
795
+ .replaceAll("_", " ")
796
+ .replace(/^./u, (character) => character.toUpperCase());
797
+ const input = "input" in issue ? issue.input : undefined;
798
+ const missing = issue.code === "invalid_type" && (input === undefined
799
+ || input === null
800
+ || (typeof input === "number" && Number.isNaN(input)));
801
+ const message = missing ? "Required" : issue.message;
802
+ return label ? `${label}: ${message}` : message;
803
+ }).join("; ");
804
+ }
805
+ function referenceIssueError(cell, mentions) {
806
+ const issues = [...mentions.unresolved, ...mentions.unconnected];
807
+ return issues.length > 0
808
+ ? `Cannot prepare ${notebookGridTag(cell.slot)}: ${issues.map((reference) => reference.message).join("; ")}`
809
+ : null;
810
+ }
811
+ async function prepareMediaGeneration(model, document, cell, bookId, engine, options) {
812
+ const expected = cell.type.replace(/^generate_/u, "");
813
+ if (model.media !== expected) {
814
+ throw new Error(`${model.id} generates ${model.media}, not ${cell.type}`);
815
+ }
816
+ const contract = notebookGenerationInputContract(model);
817
+ const configuredMode = typeof cell.inputs?.generationMode === "string" ? cell.inputs.generationMode : undefined;
818
+ if (!notebookGenerationInputsAreCompatible(contract, document, cell, configuredMode)) {
819
+ const inputError = notebookGenerationInputError(contract, document, cell, configuredMode);
820
+ throw new Error(`${model.title}${configuredMode ? ` mode ${configuredMode}` : ""} does not accept the current media inputs for ${notebookGridTag(cell.slot)}${inputError ? `. ${inputError}` : ""}`);
821
+ }
822
+ const prepared = mediaGenerationArgs(model, document, cell, bookId, engine);
823
+ const referenceError = referenceIssueError(cell, prepared.mentions);
824
+ if (!options.allowReferenceIssues && referenceError)
825
+ throw new Error(referenceError);
826
+ if (!referenceError) {
827
+ const preview = previewGeneration(prepared.input);
828
+ if (!preview.ok)
829
+ throw new Error(preview.error);
830
+ const timingError = await checkInsertVideoTiming(engine, model, normalizeRefs(prepared.input.refs), preview.preview.payload.input ?? {});
831
+ if (timingError)
832
+ throw new Error(timingError);
833
+ }
834
+ return {
835
+ tool: `generate_${model.media}`,
836
+ provider: model.provider,
837
+ model: model.id,
838
+ input: prepared.input,
839
+ mentions: prepared.mentions,
840
+ };
841
+ }
842
+ async function prepareImageEditGeneration(engine, document, cell, options) {
843
+ const mentions = resolveGenerationInputReferences({ engine, document, cell, prompt: cell.prompt ?? "" });
844
+ const issues = [...mentions.unresolved, ...mentions.unconnected];
845
+ if (!options.allowReferenceIssues && issues.length > 0) {
846
+ throw new Error(issues.map((issue) => issue.message).join("; "));
847
+ }
848
+ const imageEdit = await prepareNotebookImageEdit(engine, document, cell, mentions.prompt);
849
+ return { tool: "edit_image_subjects", provider: imageEdit.provider, model: imageEdit.model,
850
+ input: imageEdit.input, mentions, imageEdit };
851
+ }
852
+ async function prepareToolGeneration(engine, document, cell, bookId, tool, options) {
853
+ const { getAllToolDefs } = await import("../tools/index.js");
854
+ const definition = getAllToolDefs(engine).find((candidate) => candidate.name === tool);
855
+ if (!definition)
856
+ throw new Error(`Unknown notebook generation tool: ${tool}`);
857
+ const prepared = toolArgsWithMentions(definition, document, cell, bookId, engine);
858
+ const referenceError = referenceIssueError(cell, prepared.mentions);
859
+ if (!options.allowReferenceIssues && referenceError)
860
+ throw new Error(referenceError);
861
+ const parsed = z.object(definition.schema).safeParse(prepared.input);
862
+ if (!parsed.success)
863
+ throw new Error(formatNotebookGenerationSchemaError(parsed.error));
864
+ return {
865
+ tool,
866
+ provider: cell.provider ?? "unknown",
867
+ model: cell.model ?? tool,
868
+ input: parsed.data,
869
+ mentions: prepared.mentions,
870
+ };
871
+ }
872
+ export async function prepareNotebookGeneration(engine, document, cell, bookId, options = {}) {
873
+ if (cell.tool === "edit_image_subjects")
874
+ return prepareImageEditGeneration(engine, document, cell, options);
875
+ const model = notebookCellModel(cell);
876
+ if (model)
877
+ return prepareMediaGeneration(model, document, cell, bookId, engine, options);
878
+ if (cell.type === "generate_image" || cell.type === "generate_video" || cell.type === "generate_audio") {
879
+ throw new Error(`${notebookGridTag(cell.slot)} uses ${cell.model || cell.tool || "no model"}, which is not a generation model; pick one from list_generation_models`);
880
+ }
881
+ const tool = resolveNotebookGenerationTool(cell);
882
+ if (!tool)
883
+ throw new Error(`No generation tool for ${cell.type} cell ${cell.id}`);
884
+ return prepareToolGeneration(engine, document, cell, bookId, tool, options);
885
+ }
886
+ const GENERATION_ERROR_MAX_LENGTH = 500;
887
+ function truncateGenerationError(message) {
888
+ return message.length > GENERATION_ERROR_MAX_LENGTH
889
+ ? message.slice(0, GENERATION_ERROR_MAX_LENGTH)
890
+ : message;
891
+ }
892
+ async function recordGenerationDispatch(engine, args) {
893
+ try {
894
+ const result = await engine.generations.record(args);
895
+ if (!result.ok) {
896
+ logger.warn("generation-record-failed", { cellId: args.cellId, error: result.error });
897
+ return null;
898
+ }
899
+ return result.value.generationId;
900
+ }
901
+ catch (cause) {
902
+ logger.warn("generation-record-failed", { cellId: args.cellId, error: cause });
903
+ return null;
904
+ }
905
+ }
906
+ async function recordGenerationPrompt(engine, cell, context) {
907
+ const surface = generationPromptSurface(cell.type);
908
+ if (!surface || !cell.prompt?.trim())
909
+ return;
910
+ try {
911
+ const result = await engine.prompts.record({ surface, prompt: cell.prompt, context });
912
+ if (!result.ok) {
913
+ logger.warn("prompt-record-failed", { cellId: cell.id, error: result.error });
914
+ }
915
+ }
916
+ catch (cause) {
917
+ logger.warn("prompt-record-failed", { cellId: cell.id, error: cause });
918
+ }
919
+ }
920
+ async function patchGeneration(engine, generationId, patch) {
921
+ if (!generationId)
922
+ return;
923
+ try {
924
+ const result = await engine.generations.update(generationId, patch);
925
+ if (!result.ok) {
926
+ logger.warn("generation-update-failed", { generationId, error: result.error });
927
+ }
928
+ }
929
+ catch (cause) {
930
+ logger.warn("generation-update-failed", { generationId, error: cause });
931
+ }
932
+ }
933
+ function activeGenerationIdForRun(engine, notebookId, cellId, runId, providerArtifactId) {
934
+ try {
935
+ const listed = engine.generations.listForCell(notebookId, cellId);
936
+ if (!listed.ok) {
937
+ logger.warn("generation-list-failed", { cellId, error: listed.error });
938
+ return null;
939
+ }
940
+ const match = listed.value.find((generation) => (generation.status === "dispatched"
941
+ || generation.status === "awaiting_provider")
942
+ && ((runId !== undefined && generation.runId === runId)
943
+ || (providerArtifactId !== undefined
944
+ && generation.providerArtifactId === providerArtifactId)));
945
+ return match?.generationId ?? null;
946
+ }
947
+ catch (cause) {
948
+ logger.warn("generation-list-failed", { cellId, error: cause });
949
+ return null;
950
+ }
951
+ }
952
+ async function generationTool(engine, document, cell, bookId, options = {}) {
953
+ const prepared = await prepareNotebookGeneration(engine, document, cell, bookId);
954
+ const { getAllToolDefs } = await import("../tools/index.js");
955
+ const definition = getAllToolDefs(engine).find((candidate) => candidate.name === prepared.tool);
956
+ if (!definition) {
957
+ throw new Error(`Unknown notebook generation model: ${prepared.tool}`);
958
+ }
959
+ const resolvedPrompt = typeof prepared.input.prompt === "string"
960
+ ? prepared.input.prompt
961
+ : "";
962
+ const outputCellId = options.outputCellId
963
+ ?? findGenerationResultForRun(document, cell, {
964
+ runId: options.runId,
965
+ outputCellId: options.outputCellId,
966
+ })?.outputCell.id;
967
+ const generationId = await recordGenerationDispatch(engine, {
968
+ notebookId: document.id,
969
+ cellId: cell.id,
970
+ tool: prepared.tool,
971
+ status: "dispatched",
972
+ provider: prepared.provider,
973
+ model: prepared.model,
974
+ ...(cell.prompt ? { prompt: cell.prompt } : {}),
975
+ ...(resolvedPrompt ? { resolvedPrompt } : {}),
976
+ ...(options.runId ? { runId: options.runId } : {}),
977
+ ...(outputCellId ? { outputCellId } : {}),
978
+ });
979
+ await recordGenerationPrompt(engine, cell, {
980
+ tool: prepared.tool,
981
+ provider: prepared.provider,
982
+ model: prepared.model,
983
+ notebook_id: document.id,
984
+ cell_id: cell.id,
985
+ });
986
+ const fail = async (message) => {
987
+ await patchGeneration(engine, generationId, {
988
+ status: "failed",
989
+ error: truncateGenerationError(message),
990
+ });
991
+ throw new Error(message);
992
+ };
993
+ let response;
994
+ try {
995
+ if (prepared.imageEdit) {
996
+ if (!outputCellId)
997
+ throw new Error("Image edit has no notebook output cell");
998
+ const output = await engine.artifacts.create({ kind: "image", label: cell.label ?? "Edited image" });
999
+ if (!output.ok)
1000
+ throw new Error(output.error.message);
1001
+ const imageAssetId = output.value.artifactId;
1002
+ const jobId = `notebook-image-edit-${imageAssetId}`;
1003
+ await queue.enqueue(jobId, {
1004
+ ...prepared.imageEdit.jobInput,
1005
+ type: "generate_image_subject_edit", imageAssetId, bookId,
1006
+ notebookId: document.id, producerCellId: cell.id, outputCellId, jobId,
1007
+ managedByNotebook: true,
1008
+ ...(generationId ? { generationId } : {}),
1009
+ });
1010
+ response = { content: [{ type: "text", text: JSON.stringify({ image_asset_id: imageAssetId }) }] };
1011
+ }
1012
+ else {
1013
+ response = await definition.handler(prepared.input);
1014
+ }
1015
+ }
1016
+ catch (cause) {
1017
+ const message = cause instanceof Error ? cause.message : String(cause);
1018
+ return fail(message);
1019
+ }
1020
+ if (response.isError) {
1021
+ return fail(response.content[0]?.text ?? `${prepared.tool} failed`);
1022
+ }
1023
+ const reference = artifactReference(textResult(response));
1024
+ if (!reference) {
1025
+ return fail(`${prepared.tool} did not return an output artifact`);
1026
+ }
1027
+ const artifact = engine.artifacts.get(reference);
1028
+ if (!artifact.ok)
1029
+ return fail(artifact.error.message);
1030
+ await patchGeneration(engine, generationId, {
1031
+ status: "awaiting_provider",
1032
+ providerArtifactId: artifact.value.artifactId,
1033
+ });
1034
+ return {
1035
+ artifactId: artifact.value.artifactId,
1036
+ tool: prepared.tool,
1037
+ provider: prepared.provider,
1038
+ model: prepared.model,
1039
+ resolvedPrompt,
1040
+ };
1041
+ }
1042
+ async function rewritePromptCell(engine, document, cell, bookId) {
1043
+ const prepared = await prepareNotebookGeneration(engine, document, cell, bookId);
1044
+ if (prepared.tool !== "rewrite_prompt") {
1045
+ throw new Error(`Note ${cell.id} must use rewrite_prompt`);
1046
+ }
1047
+ const { getAllToolDefs } = await import("../tools/index.js");
1048
+ const definition = getAllToolDefs(engine).find((candidate) => candidate.name === prepared.tool);
1049
+ if (!definition)
1050
+ throw new Error("rewrite_prompt is unavailable");
1051
+ const response = await definition.handler(prepared.input);
1052
+ if (response.isError) {
1053
+ throw new Error(response.content[0]?.text ?? "rewrite_prompt failed");
1054
+ }
1055
+ const value = textResult(response);
1056
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1057
+ throw new Error("rewrite_prompt returned an invalid result");
1058
+ }
1059
+ const rawResult = value;
1060
+ const resolvedPrompt = typeof rawResult.rewritten_prompt === "string"
1061
+ ? rawResult.rewritten_prompt.trim()
1062
+ : "";
1063
+ if (!resolvedPrompt) {
1064
+ throw new Error("rewrite_prompt did not return rewritten_prompt");
1065
+ }
1066
+ const artifact = await engine.artifacts.create({
1067
+ kind: "script",
1068
+ label: `script ${notebookGridTag(cell.slot)} resolved prompt`,
1069
+ });
1070
+ if (!artifact.ok)
1071
+ throw new Error(artifact.error.message);
1072
+ const written = await engine.files.write(artifact.value.artifactId, "prompt.json", JSON.stringify({
1073
+ version: 1,
1074
+ originalPrompt: cell.prompt ?? "",
1075
+ resolvedPrompt,
1076
+ request: prepared.input,
1077
+ result: rawResult,
1078
+ featureId: cell.inputs?.featureId ?? null,
1079
+ shotContext: cell.inputs?.shotContext ?? null,
1080
+ audioContext: cell.inputs?.audioContext ?? null,
1081
+ resolvedAt: new Date().toISOString(),
1082
+ }, null, 2));
1083
+ if (!written.ok)
1084
+ throw new Error(written.error.message);
1085
+ return {
1086
+ artifactId: artifact.value.artifactId,
1087
+ tool: prepared.tool,
1088
+ resolvedPrompt,
1089
+ rawResult,
1090
+ };
1091
+ }
1092
+ async function failedArtifact(engine, cell, message) {
1093
+ const kind = notebookCellMediaType(cell) ?? "script";
1094
+ const result = await engine.artifacts.create({
1095
+ kind,
1096
+ label: kind === "script"
1097
+ ? `script ${notebookGridTag(cell.slot)} failed`
1098
+ : `${notebookGridTag(cell.slot)} failed`,
1099
+ });
1100
+ if (!result.ok)
1101
+ throw new Error(result.error.message);
1102
+ const written = await engine.files.write(result.value.artifactId, "error.json", JSON.stringify({ message, failedAt: new Date().toISOString() }));
1103
+ if (!written.ok)
1104
+ throw new Error(written.error.message);
1105
+ return result.value.artifactId;
1106
+ }
1107
+ async function assembleCanonicalSequence(engine, document, cell) {
1108
+ const orderedClips = upstreamValues(document, cell)
1109
+ .filter((entry) => /^clip:\d+$/.test(entry.targetInput))
1110
+ .sort((left, right) => Number(left.targetInput.slice(5)) - Number(right.targetInput.slice(5)));
1111
+ if (orderedClips.length < 2) {
1112
+ throw new Error(`Concat cell ${cell.id} requires at least two clip inputs`);
1113
+ }
1114
+ for (let index = 0; index < orderedClips.length; index += 1) {
1115
+ const entry = orderedClips[index];
1116
+ if (entry.targetInput !== `clip:${index}`) {
1117
+ throw new Error(`Concat cell ${cell.id} requires contiguous clip:0 through clip:${orderedClips.length - 1}`);
1118
+ }
1119
+ if (!entry.output || notebookCellMediaType(entry.cell) !== "video") {
1120
+ throw new Error(`Concat input ${entry.targetInput} must be a completed video`);
1121
+ }
1122
+ }
1123
+ const clips = orderedClips.map((entry) => entry.output);
1124
+ const audioId = firstUpstreamArtifact(document, cell, "audio")
1125
+ ?? firstUpstreamArtifact(document, cell, "source-audio");
1126
+ let primary = engine.sequences.getPrimary();
1127
+ const videoTrack = primary.tracks.find((track) => track.kind === "video" && track.ordinal === 0)
1128
+ ?? primary.tracks.find((track) => track.kind === "video");
1129
+ if (!videoTrack) {
1130
+ throw new Error("Primary sequence has no video track for assembly");
1131
+ }
1132
+ if (!primary.tracks.some((track) => track.kind === "caption")) {
1133
+ const added = await engine.sequences.addTrack(primary.sequenceId, {
1134
+ kind: "caption",
1135
+ name: "Feature captions",
1136
+ });
1137
+ if (!added.ok)
1138
+ throw new Error(added.error.message);
1139
+ primary = engine.sequences.getPrimary();
1140
+ }
1141
+ const activeVideoTrack = primary.tracks.find((track) => track.trackId === videoTrack.trackId);
1142
+ if (!activeVideoTrack || activeVideoTrack.kind !== "video") {
1143
+ throw new Error("Primary video track changed during assembly");
1144
+ }
1145
+ const audioTrack = primary.tracks.find((track) => track.kind === "audio");
1146
+ const captionTrack = primary.tracks.find((track) => track.kind === "caption");
1147
+ const fps = primary.frameRate.numerator / primary.frameRate.denominator;
1148
+ const placements = [];
1149
+ const clipDurations = [];
1150
+ let timelineStartFrame = 0;
1151
+ for (const clipId of clips) {
1152
+ const manifest = await engine.files.manifest(clipId);
1153
+ if (!manifest.ok)
1154
+ throw new Error(manifest.error.message);
1155
+ const file = manifest.value.files.find((entry) => entry.name.startsWith("original."))
1156
+ ?? manifest.value.files[0];
1157
+ if (!file)
1158
+ throw new Error(`Clip artifact ${clipId} has no files`);
1159
+ let durationFrames = 90;
1160
+ const streams = engine.streams.list(clipId);
1161
+ if (streams.ok) {
1162
+ const videoStream = streams.value.find((stream) => stream.kind === "video")
1163
+ ?? streams.value[0];
1164
+ if (videoStream) {
1165
+ const seconds = videoStream.durationTicks
1166
+ * videoStream.timeBase.numerator
1167
+ / videoStream.timeBase.denominator;
1168
+ durationFrames = Math.max(1, Math.round(seconds * fps));
1169
+ placements.push({
1170
+ trackId: activeVideoTrack.trackId,
1171
+ timelineStartFrame,
1172
+ durationFrames,
1173
+ source: {
1174
+ kind: "timed",
1175
+ artifactId: clipId,
1176
+ range: {
1177
+ streamId: videoStream.streamId,
1178
+ objectHash: videoStream.objectHash,
1179
+ startTick: 0,
1180
+ durationTicks: videoStream.durationTicks,
1181
+ timeBase: videoStream.timeBase,
1182
+ },
1183
+ },
1184
+ speed: { numerator: 1, denominator: 1 },
1185
+ reverse: false,
1186
+ audioPolicy: "mute",
1187
+ audio: {
1188
+ gainDb: 0,
1189
+ muted: true,
1190
+ fadeInFrames: 0,
1191
+ fadeOutFrames: 0,
1192
+ },
1193
+ });
1194
+ clipDurations.push(durationFrames);
1195
+ timelineStartFrame += durationFrames;
1196
+ continue;
1197
+ }
1198
+ }
1199
+ placements.push({
1200
+ trackId: activeVideoTrack.trackId,
1201
+ timelineStartFrame,
1202
+ durationFrames,
1203
+ source: {
1204
+ kind: "still",
1205
+ artifactId: clipId,
1206
+ sourcePath: file.name,
1207
+ objectHash: file.objectHash,
1208
+ },
1209
+ });
1210
+ clipDurations.push(durationFrames);
1211
+ timelineStartFrame += durationFrames;
1212
+ }
1213
+ if (audioId && audioTrack) {
1214
+ const audioStreams = engine.streams.list(audioId);
1215
+ if (audioStreams.ok) {
1216
+ const audioStream = audioStreams.value.find((stream) => stream.kind === "audio");
1217
+ if (audioStream) {
1218
+ const seconds = audioStream.durationTicks
1219
+ * audioStream.timeBase.numerator
1220
+ / audioStream.timeBase.denominator;
1221
+ placements.push({
1222
+ trackId: audioTrack.trackId,
1223
+ timelineStartFrame: 0,
1224
+ durationFrames: Math.min(timelineStartFrame, Math.max(1, Math.round(seconds * fps))),
1225
+ source: {
1226
+ kind: "timed",
1227
+ artifactId: audioId,
1228
+ range: {
1229
+ streamId: audioStream.streamId,
1230
+ objectHash: audioStream.objectHash,
1231
+ startTick: 0,
1232
+ durationTicks: audioStream.durationTicks,
1233
+ timeBase: audioStream.timeBase,
1234
+ },
1235
+ },
1236
+ speed: { numerator: 1, denominator: 1 },
1237
+ reverse: false,
1238
+ audioPolicy: "preserve-pitch",
1239
+ audio: {
1240
+ gainDb: 0,
1241
+ muted: false,
1242
+ fadeInFrames: 0,
1243
+ fadeOutFrames: 0,
1244
+ },
1245
+ });
1246
+ }
1247
+ }
1248
+ }
1249
+ const targetTrackIds = [
1250
+ activeVideoTrack.trackId,
1251
+ ...(audioTrack ? [audioTrack.trackId] : []),
1252
+ ];
1253
+ const occupiedDuration = primary.clips
1254
+ .filter((clip) => targetTrackIds.includes(clip.trackId))
1255
+ .reduce((maximum, clip) => Math.max(maximum, clip.timelineStartFrame + clip.durationFrames), 0);
1256
+ const previousDuration = Number(cell.inputs?.assemblyDurationFrames ?? 0);
1257
+ const replacementDuration = Math.max(1, occupiedDuration, previousDuration, timelineStartFrame);
1258
+ const operations = [{
1259
+ kind: "batch-replace-range",
1260
+ range: {
1261
+ sequenceId: primary.sequenceId,
1262
+ startFrame: 0,
1263
+ durationFrames: replacementDuration,
1264
+ },
1265
+ trackIds: targetTrackIds,
1266
+ placements,
1267
+ ripple: false,
1268
+ }];
1269
+ const captionTexts = Array.isArray(cell.inputs?.captions)
1270
+ ? cell.inputs.captions.filter((value) => typeof value === "string" && value.trim().length > 0)
1271
+ : [];
1272
+ if (captionTrack) {
1273
+ let captionStartFrame = 0;
1274
+ for (let index = 0; index < clipDurations.length; index += 1) {
1275
+ const durationFrames = clipDurations[index];
1276
+ const text = captionTexts[index];
1277
+ if (text) {
1278
+ operations.push({
1279
+ kind: "upsert-caption-cue",
1280
+ cue: {
1281
+ cueId: uuidv5(`${primary.sequenceId}:notebook-feature-caption:${index}`, uuidv5.URL),
1282
+ trackId: captionTrack.trackId,
1283
+ timelineStartFrame: captionStartFrame,
1284
+ durationFrames,
1285
+ text,
1286
+ styleId: "high-contrast",
1287
+ },
1288
+ });
1289
+ }
1290
+ captionStartFrame += durationFrames;
1291
+ }
1292
+ }
1293
+ const intent = {
1294
+ intentVersion: MVP_CONTRACT_VERSION,
1295
+ commandId: uuidv7(),
1296
+ sequenceId: primary.sequenceId,
1297
+ baseRevision: engine.head,
1298
+ actor: "notebook-assembly",
1299
+ sourceSurface: "system",
1300
+ confirmationPolicy: "reversible-single-step",
1301
+ operations,
1302
+ };
1303
+ const preview = engine.edits.preview(intent);
1304
+ if (!preview.ok)
1305
+ throw new Error(preview.error.message);
1306
+ const commit = await engine.edits.commit(intent, preview.value.previewHash);
1307
+ if (!commit.ok)
1308
+ throw new Error(commit.error.message);
1309
+ const sequenceRevision = engine.head;
1310
+ const script = await engine.artifacts.create({
1311
+ kind: "script",
1312
+ label: `${notebookGridTag(cell.slot)} assembly`,
1313
+ });
1314
+ if (!script.ok)
1315
+ throw new Error(script.error.message);
1316
+ const written = await engine.files.write(script.value.artifactId, "assembly.json", JSON.stringify({
1317
+ sequenceId: primary.sequenceId,
1318
+ sequenceRevision,
1319
+ clipArtifactIds: clips,
1320
+ audioArtifactId: audioId,
1321
+ assemblyDurationFrames: timelineStartFrame,
1322
+ captionTexts,
1323
+ assembledAt: new Date().toISOString(),
1324
+ operationCount: operations.length,
1325
+ }));
1326
+ if (!written.ok)
1327
+ throw new Error(written.error.message);
1328
+ return {
1329
+ scriptArtifactId: script.value.artifactId,
1330
+ sequenceRevision,
1331
+ sequenceId: primary.sequenceId,
1332
+ clipArtifactIds: clips,
1333
+ audioArtifactId: audioId,
1334
+ assemblyDurationFrames: timelineStartFrame,
1335
+ };
1336
+ }
1337
+ async function executeLocalOrGeneration(engine, bookId, document, cell, options = {}) {
1338
+ if (cell.type === "split_video" && cell.operation === "split_ranges") {
1339
+ const sourceId = requiredVideoInputArtifact(document, cell);
1340
+ const groups = parseSplitOutputGroups(cell.inputs?.outputsSeconds);
1341
+ if (groups.length === 0) {
1342
+ throw new Error(`split_video cell ${cell.id} requires outputsSeconds range groups`);
1343
+ }
1344
+ for (const group of groups) {
1345
+ const total = group.reduce((sum, range) => sum + (range.endSeconds - range.startSeconds), 0);
1346
+ if (total < MINIMUM_SPLIT_PIECE_SECONDS) {
1347
+ throw new Error(`split_video cell ${cell.id} requires every output range to be at least ${MINIMUM_SPLIT_PIECE_SECONDS}s`);
1348
+ }
1349
+ }
1350
+ const outputCellIds = requiredIndexedSplitVideoOutputCellIds(document, cell, groups.length);
1351
+ const results = [];
1352
+ for (const [index, group] of groups.entries()) {
1353
+ const title = `${notebookGridTag(cell.slot)} output ${String(index + 1).padStart(2, "0")}`;
1354
+ const result = group.length === 1
1355
+ ? await splitSourceRange(engine, sourceId, group[0], title)
1356
+ : await concatSourceRanges(engine, sourceId, group, title);
1357
+ results.push(result);
1358
+ }
1359
+ const [firstFrame, lastFrame] = await Promise.all([
1360
+ captureFrame(engine, results[0].artifactId, {
1361
+ role: "first",
1362
+ title: `${notebookGridTag(cell.slot)} · first frame`,
1363
+ }),
1364
+ captureFrame(engine, results.at(-1).artifactId, {
1365
+ role: "last",
1366
+ title: `${notebookGridTag(cell.slot)} · last frame`,
1367
+ }),
1368
+ ]);
1369
+ let withOutputs = document;
1370
+ for (const [index, result] of results.entries()) {
1371
+ withOutputs = withMediaOutput(withOutputs, outputCellIds[index], result.artifactId, { durationSeconds: result.durationSeconds });
1372
+ }
1373
+ return {
1374
+ artifactId: results[0].artifactId,
1375
+ tool: "ffmpeg",
1376
+ document: withOutputs,
1377
+ cellInputs: {
1378
+ durationSeconds: results.reduce((total, result) => total + result.durationSeconds, 0),
1379
+ firstFrameArtifactId: firstFrame.artifactId,
1380
+ lastFrameArtifactId: lastFrame.artifactId,
1381
+ },
1382
+ };
1383
+ }
1384
+ if (cell.type === "split_video" && cell.operation === "split_at_points") {
1385
+ const sourceId = requiredVideoInputArtifact(document, cell);
1386
+ const rawCutPoints = cell.inputs?.cutPointsSeconds;
1387
+ const sourceDurationSeconds = Number(cell.inputs?.sourceDurationSeconds ?? cell.inputs?.endSeconds);
1388
+ if (!Array.isArray(rawCutPoints)
1389
+ || rawCutPoints.length === 0
1390
+ || !Number.isFinite(sourceDurationSeconds)) {
1391
+ throw new Error(`split_video cell ${cell.id} requires cut points and a source duration`);
1392
+ }
1393
+ const cutPoints = rawCutPoints.map((value) => typeof value === "number" ? value : Number.NaN);
1394
+ let previous = 0;
1395
+ for (const cutPoint of cutPoints) {
1396
+ if (!Number.isFinite(cutPoint)
1397
+ || cutPoint <= previous
1398
+ || sourceDurationSeconds - cutPoint < MINIMUM_SPLIT_PIECE_SECONDS
1399
+ || cutPoint - previous < MINIMUM_SPLIT_PIECE_SECONDS) {
1400
+ throw new Error(`split_video cell ${cell.id} requires ordered cut points at least ${MINIMUM_SPLIT_PIECE_SECONDS}s apart`);
1401
+ }
1402
+ previous = cutPoint;
1403
+ }
1404
+ const boundaries = [0, ...cutPoints, sourceDurationSeconds];
1405
+ const ranges = boundaries.slice(0, -1).map((startSeconds, index) => ({
1406
+ endSeconds: boundaries[index + 1],
1407
+ startSeconds,
1408
+ }));
1409
+ const outputCellIds = requiredIndexedSplitVideoOutputCellIds(document, cell, ranges.length);
1410
+ const results = await splitSourceRanges(engine, sourceId, ranges.map((range, index) => ({
1411
+ range,
1412
+ title: `${notebookGridTag(cell.slot)} chunk ${String(index + 1).padStart(2, "0")}`,
1413
+ })), 2);
1414
+ const [firstFrame, lastFrame] = await Promise.all([
1415
+ captureFrame(engine, results[0].artifactId, {
1416
+ role: "first",
1417
+ title: `${notebookGridTag(cell.slot)} · first frame`,
1418
+ }),
1419
+ captureFrame(engine, results.at(-1).artifactId, {
1420
+ role: "last",
1421
+ title: `${notebookGridTag(cell.slot)} · last frame`,
1422
+ }),
1423
+ ]);
1424
+ let withOutputs = document;
1425
+ for (const [index, result] of results.entries()) {
1426
+ withOutputs = withMediaOutput(withOutputs, outputCellIds[index], result.artifactId, { durationSeconds: result.durationSeconds });
1427
+ }
1428
+ return {
1429
+ artifactId: results[0].artifactId,
1430
+ tool: "ffmpeg",
1431
+ document: withOutputs,
1432
+ cellInputs: {
1433
+ durationSeconds: results.reduce((total, result) => total + result.durationSeconds, 0),
1434
+ firstFrameArtifactId: firstFrame.artifactId,
1435
+ lastFrameArtifactId: lastFrame.artifactId,
1436
+ },
1437
+ };
1438
+ }
1439
+ if (cell.type === "split_video"
1440
+ && cell.operation === "split_range"
1441
+ && typeof cell.inputs?.splitAtSeconds === "number") {
1442
+ const sourceId = requiredVideoInputArtifact(document, cell);
1443
+ const splitAtSeconds = cell.inputs.splitAtSeconds;
1444
+ const sourceDurationSeconds = Number(cell.inputs.sourceDurationSeconds ?? cell.inputs.endSeconds);
1445
+ if (!Number.isFinite(splitAtSeconds)
1446
+ || !Number.isFinite(sourceDurationSeconds)
1447
+ || !(splitAtSeconds > 0)
1448
+ || !(sourceDurationSeconds > splitAtSeconds)) {
1449
+ throw new Error(`split_video cell ${cell.id} requires a split boundary within the source duration`);
1450
+ }
1451
+ const outputCellIds = requiredTwoWaySplitVideoOutputCellIds(document, cell);
1452
+ const [first, remaining] = await Promise.all([
1453
+ splitSourceRange(engine, sourceId, { startSeconds: 0, endSeconds: splitAtSeconds }, `${notebookGridTag(cell.slot)} first`),
1454
+ splitSourceRange(engine, sourceId, {
1455
+ startSeconds: splitAtSeconds,
1456
+ endSeconds: sourceDurationSeconds,
1457
+ }, `${notebookGridTag(cell.slot)} remaining`),
1458
+ ]);
1459
+ const [firstFrame, lastFrame] = await Promise.all([
1460
+ captureFrame(engine, first.artifactId, {
1461
+ role: "first",
1462
+ title: `${notebookGridTag(cell.slot)} · first frame`,
1463
+ }),
1464
+ captureFrame(engine, remaining.artifactId, {
1465
+ role: "last",
1466
+ title: `${notebookGridTag(cell.slot)} · last frame`,
1467
+ }),
1468
+ ]);
1469
+ const withFirstOutput = withMediaOutput(document, outputCellIds.first, first.artifactId, { durationSeconds: first.durationSeconds });
1470
+ return {
1471
+ artifactId: first.artifactId,
1472
+ tool: "ffmpeg",
1473
+ document: withMediaOutput(withFirstOutput, outputCellIds.remaining, remaining.artifactId, { durationSeconds: remaining.durationSeconds }),
1474
+ cellInputs: {
1475
+ durationSeconds: first.durationSeconds + remaining.durationSeconds,
1476
+ firstFrameArtifactId: firstFrame.artifactId,
1477
+ lastFrameArtifactId: lastFrame.artifactId,
1478
+ },
1479
+ };
1480
+ }
1481
+ if (cell.type === "split_video" && cell.operation === "split_range") {
1482
+ const sourceId = requiredVideoInputArtifact(document, cell);
1483
+ const videoCellIds = requiredMediaOutputCellIds(document, cell, "video");
1484
+ const frameCellIds = splitFrameOutputCellIds(document, videoCellIds);
1485
+ const startSeconds = Number(cell.inputs?.startSeconds ?? 0);
1486
+ const endSeconds = Number(cell.inputs?.endSeconds ?? 0);
1487
+ const result = await splitSourceRange(engine, sourceId, { startSeconds, endSeconds }, notebookGridTag(cell.slot));
1488
+ const firstFrame = await captureFrame(engine, result.artifactId, {
1489
+ role: "first",
1490
+ title: `${notebookGridTag(cell.slot)} · first frame`,
1491
+ });
1492
+ const lastFrame = await captureFrame(engine, result.artifactId, {
1493
+ role: "last",
1494
+ title: `${notebookGridTag(cell.slot)} · last frame`,
1495
+ });
1496
+ return {
1497
+ artifactId: result.artifactId,
1498
+ tool: "ffmpeg",
1499
+ document: withSplitOutputs(document, {
1500
+ video: videoCellIds,
1501
+ first: frameCellIds.first,
1502
+ last: frameCellIds.last,
1503
+ }, {
1504
+ videoArtifactId: result.artifactId,
1505
+ firstFrameArtifactId: firstFrame.artifactId,
1506
+ lastFrameArtifactId: lastFrame.artifactId,
1507
+ durationSeconds: result.durationSeconds,
1508
+ firstAtSeconds: firstFrame.atSeconds,
1509
+ lastAtSeconds: lastFrame.atSeconds,
1510
+ }),
1511
+ cellInputs: {
1512
+ durationSeconds: result.durationSeconds,
1513
+ firstFrameArtifactId: firstFrame.artifactId,
1514
+ lastFrameArtifactId: lastFrame.artifactId,
1515
+ },
1516
+ };
1517
+ }
1518
+ if ((cell.type === "extract_audio" && cell.operation === "extract_audio")
1519
+ || cell.inputs?.mediaRole === "extract-audio-output") {
1520
+ const sourceId = requiredVideoInputArtifact(document, cell);
1521
+ const previous = cell.outputArtifactId && cell.inputs?.audioRange === undefined
1522
+ ? await engine.metadata.artifacts.read(cell.outputArtifactId, "original")
1523
+ : undefined;
1524
+ const previousRange = previous?.ok && previous.value.origin === "extract_audio"
1525
+ && previous.value.sourceArtifactId === sourceId && typeof previous.value.startSeconds === "number"
1526
+ ? previous.value : undefined;
1527
+ const savedRange = cell.inputs?.audioRange ?? previousRange;
1528
+ const rangeInputs = savedRange && typeof savedRange === "object" && !Array.isArray(savedRange)
1529
+ ? savedRange
1530
+ : undefined;
1531
+ const range = rangeInputs === undefined ? undefined : {
1532
+ startSeconds: Number(rangeInputs.startSeconds ?? 0),
1533
+ ...(rangeInputs.durationSeconds === undefined ? {} : { durationSeconds: Number(rangeInputs.durationSeconds) }),
1534
+ };
1535
+ const result = await extractSourceAudio(engine, sourceId, notebookGridTag(cell.slot), range);
1536
+ return {
1537
+ artifactId: result.artifactId,
1538
+ tool: "ffmpeg",
1539
+ cellInputs: {
1540
+ durationSeconds: result.durationSeconds,
1541
+ outputVersionArtifactIds: withOutputVersionIds(cell, result.artifactId),
1542
+ state: "completed",
1543
+ },
1544
+ };
1545
+ }
1546
+ if (cell.type === "analyze" && cell.operation === "analyze_source") {
1547
+ const sourceId = firstUpstreamArtifact(document, cell, "source")
1548
+ ?? firstUpstreamArtifact(document, cell);
1549
+ if (!sourceId)
1550
+ throw new Error(`Analyze cell ${cell.id} has no source artifact`);
1551
+ const fixture = analysisFixtureForCell(cell);
1552
+ const configuredDuration = Number(cell.inputs?.sourceDurationSeconds
1553
+ ?? (fixture && typeof fixture === "object"
1554
+ ? fixture.sourceDurationSeconds
1555
+ : undefined));
1556
+ const analysisRun = fixture !== null
1557
+ ? {
1558
+ rawOutput: fixture,
1559
+ sourceDurationSeconds: Number.isFinite(configuredDuration)
1560
+ ? configuredDuration
1561
+ : 60,
1562
+ request: {
1563
+ provider: "openai",
1564
+ model: INTELLIGENCE_MODEL,
1565
+ reasoning: { effort: ANALYSIS_REASONING_EFFORT },
1566
+ mode: "fixture",
1567
+ prompt: cell.prompt ?? "",
1568
+ },
1569
+ }
1570
+ : await analyzeNotebookSource(engine, sourceId, cell.prompt ?? "", {
1571
+ ...(Number.isFinite(configuredDuration)
1572
+ ? { sourceDurationSeconds: configuredDuration }
1573
+ : {}),
1574
+ });
1575
+ if (!Number.isFinite(analysisRun.sourceDurationSeconds)
1576
+ || analysisRun.sourceDurationSeconds <= 0) {
1577
+ throw new Error(`Analyze cell ${cell.id} requires a positive source duration`);
1578
+ }
1579
+ const next = await applyAnalysisResults(engine, document, cell.id, {
1580
+ rawOutput: analysisRun.rawOutput,
1581
+ sourceDurationSeconds: analysisRun.sourceDurationSeconds,
1582
+ request: analysisRun.request,
1583
+ sourceArtifactId: sourceId,
1584
+ ...("llmExchanges" in analysisRun
1585
+ ? { llmExchanges: analysisRun.llmExchanges }
1586
+ : {}),
1587
+ ...("evidence" in analysisRun
1588
+ ? { evidence: analysisRun.evidence }
1589
+ : {}),
1590
+ ...("boundaries" in analysisRun
1591
+ ? { boundaries: analysisRun.boundaries }
1592
+ : {}),
1593
+ });
1594
+ const completedAnalysis = next.cells.find((candidate) => candidate.id === cell.id);
1595
+ if (!completedAnalysis?.outputArtifactId) {
1596
+ throw new Error("Analysis did not produce a script artifact");
1597
+ }
1598
+ return {
1599
+ artifactId: completedAnalysis.outputArtifactId,
1600
+ tool: cell.tool ?? OPENAI_ANALYSIS_TOOL,
1601
+ document: next,
1602
+ };
1603
+ }
1604
+ if (cell.type === "concat" && cell.operation === "concat_video") {
1605
+ const assembled = await assembleCanonicalSequence(engine, document, cell);
1606
+ const currentSequence = engine.sequences.getPrimary();
1607
+ const frameSeconds = currentSequence.frameRate.denominator
1608
+ / currentSequence.frameRate.numerator;
1609
+ const captions = currentSequence.captions.map((caption) => ({
1610
+ text: caption.text,
1611
+ startSeconds: caption.timelineStartFrame * frameSeconds,
1612
+ durationSeconds: caption.durationFrames * frameSeconds,
1613
+ }));
1614
+ const result = await exportSequenceMaster(engine, assembled.clipArtifactIds, assembled.audioArtifactId, notebookGridTag(cell.slot), { captions });
1615
+ return {
1616
+ artifactId: result.artifactId,
1617
+ tool: "ffmpeg",
1618
+ cellInputs: {
1619
+ sequenceRevision: assembled.sequenceRevision,
1620
+ sequenceId: assembled.sequenceId,
1621
+ clipArtifactIds: assembled.clipArtifactIds,
1622
+ audioArtifactId: assembled.audioArtifactId,
1623
+ assemblyDurationFrames: assembled.assemblyDurationFrames,
1624
+ state: "completed",
1625
+ },
1626
+ };
1627
+ }
1628
+ if (cell.type === "splice"
1629
+ && (cell.operation === "splice_video"
1630
+ || cell.operation === "smooth_scene_concat")) {
1631
+ const result = await executeSpliceCell(engine, bookId, document, cell);
1632
+ return {
1633
+ artifactId: result.artifactId,
1634
+ tool: result.tool,
1635
+ cellInputs: {
1636
+ state: "completed",
1637
+ },
1638
+ };
1639
+ }
1640
+ if (cell.type === "prompt" && cell.operation === "rewrite_prompt") {
1641
+ if (!options.confirmPaid) {
1642
+ throw new Error(`Paid prompt rewrite for ${notebookGridTag(cell.slot)} requires confirmation before starting`);
1643
+ }
1644
+ const output = await rewritePromptCell(engine, document, cell, bookId);
1645
+ return {
1646
+ artifactId: output.artifactId,
1647
+ tool: output.tool,
1648
+ cellInputs: {
1649
+ resolvedPrompt: output.resolvedPrompt,
1650
+ rawRewriteResult: output.rawResult,
1651
+ state: "completed",
1652
+ },
1653
+ };
1654
+ }
1655
+ if ((cell.type === "generate_video"
1656
+ || cell.type === "generate_image"
1657
+ || cell.type === "generate_audio")
1658
+ && (notebookCellModel(cell) !== null
1659
+ || cell.tool === "edit_image_subjects")) {
1660
+ const requiresConfirmation = cell.inputs?.requiresConfirmation === true
1661
+ || (cell.tool === "edit_image_subjects" && cell.provider !== "local")
1662
+ || (cell.model ?? "").includes("seedance")
1663
+ || cell.model === LIGHTNING_VIDEO_MODEL;
1664
+ if (requiresConfirmation && !options.confirmPaid) {
1665
+ throw new Error(`Paid generation for ${notebookGridTag(cell.slot)} requires confirmation before starting`);
1666
+ }
1667
+ const output = await generationTool(engine, document, cell, bookId, {
1668
+ ...(options.runId ? { runId: options.runId } : {}),
1669
+ ...(options.outputCellId ? { outputCellId: options.outputCellId } : {}),
1670
+ });
1671
+ const surface = cell.type === "generate_image"
1672
+ ? "image_generate"
1673
+ : cell.type === "generate_video"
1674
+ ? "video_generate"
1675
+ : cell.type === "generate_audio"
1676
+ ? "audio_generate"
1677
+ : null;
1678
+ if (surface && output.resolvedPrompt) {
1679
+ void engine.prompts.record({
1680
+ surface,
1681
+ prompt: output.resolvedPrompt,
1682
+ context: {
1683
+ notebookId: document.id,
1684
+ cellId: cell.id,
1685
+ tool: output.tool,
1686
+ model: output.model,
1687
+ artifactId: output.artifactId,
1688
+ },
1689
+ }).catch(() => { });
1690
+ }
1691
+ return {
1692
+ artifactId: output.artifactId,
1693
+ tool: output.tool,
1694
+ provider: output.provider,
1695
+ model: output.model,
1696
+ awaitingOutput: true,
1697
+ cellInputs: {
1698
+ providerArtifactId: output.artifactId,
1699
+ ...(output.resolvedPrompt
1700
+ ? { resolvedPrompt: output.resolvedPrompt }
1701
+ : {}),
1702
+ state: "awaiting-output",
1703
+ },
1704
+ };
1705
+ }
1706
+ if (cell.type === "generate_video" || cell.type === "generate_image" || cell.type === "generate_audio") {
1707
+ throw new Error(`${notebookGridTag(cell.slot)} uses ${cell.model || cell.tool || "no model"}, which is not a generation model; pick one from list_generation_models`);
1708
+ }
1709
+ throw new Error(`Cell operation ${cell.operation ?? "unset"} is not runnable`);
1710
+ }
1711
+ export async function runNotebookCell(engine, bookId, notebookId, document, cell, options = {}) {
1712
+ let normalizedDocument = migrateStackedGenerationResults(engine, normalizeLegacyExtractCells(normalizeLegacyAnalyzeDocument(document)));
1713
+ let normalizedCell = normalizedDocument.cells.find((candidate) => candidate.id === cell.id) ?? normalizeLegacyAnalyzeCell(cell);
1714
+ const generateMediaOutput = normalizedCell.type === "generate_audio"
1715
+ ? ensureGenerationMediaOutput(engine, normalizedDocument, normalizedCell)
1716
+ : isConcurrentGenerationProducer(normalizedCell)
1717
+ ? findGenerationResultForRun(normalizedDocument, normalizedCell, {
1718
+ runId: options.runId,
1719
+ outputCellId: options.outputCellId,
1720
+ })
1721
+ : null;
1722
+ if (isConcurrentGenerationProducer(normalizedCell)
1723
+ && !generateMediaOutput) {
1724
+ if (options.outputCellId) {
1725
+ throw new Error(`Generation output cell not found for run: ${options.outputCellId}`);
1726
+ }
1727
+ const allocated = normalizedCell.type === "generate_image"
1728
+ ? allocateGenerateImageResult(engine, normalizedDocument, normalizedCell.id, {
1729
+ runId: options.runId ?? uuidv7(),
1730
+ acceptedAt: new Date().toISOString(),
1731
+ })
1732
+ : allocateGenerateVideoResult(engine, normalizedDocument, normalizedCell.id, {
1733
+ runId: options.runId ?? uuidv7(),
1734
+ acceptedAt: new Date().toISOString(),
1735
+ });
1736
+ const reserved = await mutateNotebook(engine, notebookId, normalizedCell.type === "generate_image"
1737
+ ? "allocate_generate_image_output"
1738
+ : "allocate_generate_video_output", () => allocated.document, { cellId: normalizedCell.id, outputCellId: allocated.outputCell.id });
1739
+ if (!reserved.ok)
1740
+ throw new Error(reserved.error.message);
1741
+ const refreshed = engine.notebooks.read(notebookId);
1742
+ if (!refreshed.ok)
1743
+ throw new Error(refreshed.error.message);
1744
+ normalizedDocument = refreshed.value;
1745
+ normalizedCell = normalizedDocument.cells.find((candidate) => candidate.id === cell.id) ?? normalizedCell;
1746
+ options = { ...options, outputCellId: allocated.outputCell.id };
1747
+ }
1748
+ if (generateMediaOutput
1749
+ && "document" in generateMediaOutput
1750
+ && generateMediaOutput.document !== normalizedDocument) {
1751
+ const repaired = await mutateNotebook(engine, notebookId, "ensure_generate_audio_output", (current) => ensureGenerationMediaOutput(engine, current, normalizedCell).document, { cellId: normalizedCell.id });
1752
+ if (!repaired.ok)
1753
+ throw new Error(repaired.error.message);
1754
+ const refreshed = engine.notebooks.read(notebookId);
1755
+ if (!refreshed.ok)
1756
+ throw new Error(refreshed.error.message);
1757
+ normalizedDocument = normalizeLegacyAnalyzeDocument(refreshed.value);
1758
+ normalizedCell = normalizedDocument.cells.find((candidate) => candidate.id === cell.id) ?? normalizedCell;
1759
+ }
1760
+ const nextFingerprint = notebookCellFingerprint(normalizedDocument, normalizedCell);
1761
+ const startedAt = new Date().toISOString();
1762
+ const runningState = {
1763
+ fingerprint: nextFingerprint,
1764
+ status: "running",
1765
+ ...(normalizedCell.outputArtifactId
1766
+ ? { outputArtifactId: normalizedCell.outputArtifactId }
1767
+ : {}),
1768
+ ...(options.runId ? { runId: options.runId } : {}),
1769
+ startedAt,
1770
+ updatedAt: startedAt,
1771
+ tool: normalizedCell.tool
1772
+ || normalizedCell.model
1773
+ || normalizedCell.operation,
1774
+ };
1775
+ const started = await mutateNotebook(engine, notebookId, "start_notebook_cell", (current) => withExecution(healGenerationMediaOutputResidue(normalizeLegacyAnalyzeDocument(current), normalizedCell, options.outputCellId), options.outputCellId && isConcurrentGenerationProducer(normalizedCell)
1776
+ ? options.outputCellId
1777
+ : normalizedCell.id, runningState), { cellId: normalizedCell.id, runId: options.runId, outputCellId: options.outputCellId });
1778
+ if (!started.ok)
1779
+ throw new Error(started.error.message);
1780
+ const activeDocument = withExecution(healGenerationMediaOutputResidue(normalizedDocument, normalizedCell, options.outputCellId), options.outputCellId && isConcurrentGenerationProducer(normalizedCell)
1781
+ ? options.outputCellId
1782
+ : normalizedCell.id, runningState);
1783
+ const activeCell = activeDocument.cells.find((candidate) => candidate.id === normalizedCell.id);
1784
+ if (!activeCell) {
1785
+ throw new Error(`Notebook cell not found: ${normalizedCell.id}`);
1786
+ }
1787
+ try {
1788
+ const output = await executeLocalOrGeneration(engine, bookId, activeDocument, activeCell, options);
1789
+ const updatedAt = new Date().toISOString();
1790
+ const status = output.awaitingOutput ? "awaiting-output" : "completed";
1791
+ const state = {
1792
+ fingerprint: nextFingerprint,
1793
+ status,
1794
+ outputArtifactId: output.artifactId,
1795
+ ...(output.awaitingOutput
1796
+ ? { providerArtifactId: output.artifactId }
1797
+ : { completedAt: updatedAt }),
1798
+ ...(options.runId ? { runId: options.runId } : {}),
1799
+ updatedAt,
1800
+ tool: output.tool,
1801
+ };
1802
+ const write = await mutateNotebook(engine, notebookId, "run_notebook_cell", (current) => {
1803
+ const base = output.document ?? current;
1804
+ const mediaOutput = findGenerationResultForRun(base, activeCell, {
1805
+ runId: options.runId,
1806
+ outputCellId: options.outputCellId,
1807
+ });
1808
+ // New-style generate_video / generate_image: artifact lives on the
1809
+ // dedicated output cell. Legacy/other cells keep inline
1810
+ // outputArtifactId on the action cell.
1811
+ const actionArtifactId = mediaOutput ? undefined : output.artifactId;
1812
+ const executionCellId = options.outputCellId
1813
+ && isConcurrentGenerationProducer(activeCell)
1814
+ ? options.outputCellId
1815
+ : activeCell.id;
1816
+ let withOutput = withExecution(base, executionCellId, state, actionArtifactId);
1817
+ const resolvedPrompt = typeof output.cellInputs?.resolvedPrompt === "string"
1818
+ ? output.cellInputs.resolvedPrompt
1819
+ : "";
1820
+ if (mediaOutput) {
1821
+ withOutput = withMediaOutput(withOutput, new Set([mediaOutput.outputCell.id]), output.artifactId, {
1822
+ state: status,
1823
+ ...(resolvedPrompt ? { resolvedPrompt } : {}),
1824
+ });
1825
+ }
1826
+ const actionBefore = withOutput.cells.find((candidate) => candidate.id === activeCell.id);
1827
+ const fillActionPrompt = Boolean(resolvedPrompt)
1828
+ && !actionBefore?.prompt;
1829
+ const fillActionModel = output.model !== undefined
1830
+ && actionBefore?.tool !== "edit_image_subjects"
1831
+ && output.model !== actionBefore?.model;
1832
+ const fillActionTool = fillActionModel && !actionBefore?.tool;
1833
+ const actionCellChanged = fillActionPrompt || fillActionModel;
1834
+ const concurrentProducer = isConcurrentGenerationProducer(activeCell);
1835
+ const withCells = {
1836
+ ...withOutput,
1837
+ cells: withOutput.cells.map((candidate) => {
1838
+ if (candidate.id === activeCell.id) {
1839
+ const nextInputs = { ...(candidate.inputs ?? {}) };
1840
+ if (!concurrentProducer && output.cellInputs) {
1841
+ Object.assign(nextInputs, output.cellInputs);
1842
+ }
1843
+ else if (concurrentProducer) {
1844
+ delete nextInputs.state;
1845
+ delete nextInputs.providerArtifactId;
1846
+ if (typeof output.cellInputs?.resolvedPrompt === "string"
1847
+ && output.cellInputs.resolvedPrompt) {
1848
+ nextInputs.resolvedPrompt = output.cellInputs.resolvedPrompt;
1849
+ }
1850
+ }
1851
+ return {
1852
+ ...candidate,
1853
+ ...(fillActionPrompt ? { prompt: resolvedPrompt } : {}),
1854
+ ...(fillActionModel ? { model: output.model } : {}),
1855
+ ...(fillActionTool ? { tool: output.tool } : {}),
1856
+ inputs: nextInputs,
1857
+ };
1858
+ }
1859
+ if (candidate.id === mediaOutput?.outputCell.id) {
1860
+ if (!resolvedPrompt && output.model === undefined) {
1861
+ return candidate;
1862
+ }
1863
+ return {
1864
+ ...candidate,
1865
+ ...(resolvedPrompt ? { prompt: resolvedPrompt } : {}),
1866
+ ...(output.model !== undefined ? { model: output.model } : {}),
1867
+ };
1868
+ }
1869
+ return candidate;
1870
+ }),
1871
+ };
1872
+ // Filling the empty prompt column changes the cell's fingerprint, so
1873
+ // the stored execution fingerprint must come from the updated
1874
+ // document — otherwise the cell would immediately read as stale.
1875
+ const finalCell = withCells.cells.find((candidate) => candidate.id === activeCell.id);
1876
+ const fingerprint = actionCellChanged && finalCell
1877
+ ? notebookCellFingerprint(withCells, finalCell)
1878
+ : state.fingerprint;
1879
+ if (!mediaOutput && !actionCellChanged)
1880
+ return withCells;
1881
+ const concurrent = isConcurrentGenerationProducer(activeCell);
1882
+ const nextExecution = { ...executionState(withCells) };
1883
+ if (concurrent
1884
+ && options.runId
1885
+ && nextExecution[activeCell.id]?.runId === options.runId) {
1886
+ delete nextExecution[activeCell.id];
1887
+ }
1888
+ return {
1889
+ ...withCells,
1890
+ execution: {
1891
+ ...nextExecution,
1892
+ ...(concurrent
1893
+ ? {}
1894
+ : {
1895
+ [activeCell.id]: {
1896
+ ...state,
1897
+ fingerprint,
1898
+ outputArtifactId: output.artifactId,
1899
+ },
1900
+ }),
1901
+ ...(mediaOutput
1902
+ ? {
1903
+ [mediaOutput.outputCell.id]: {
1904
+ fingerprint,
1905
+ status: status === "awaiting-output"
1906
+ ? "awaiting-output"
1907
+ : "completed",
1908
+ outputArtifactId: output.artifactId,
1909
+ ...(state.providerArtifactId
1910
+ ? { providerArtifactId: state.providerArtifactId }
1911
+ : {}),
1912
+ updatedAt: state.updatedAt,
1913
+ ...(state.completedAt
1914
+ ? { completedAt: state.completedAt }
1915
+ : {}),
1916
+ ...(state.runId ? { runId: state.runId } : {}),
1917
+ tool: state.tool,
1918
+ },
1919
+ }
1920
+ : {}),
1921
+ },
1922
+ };
1923
+ }, {
1924
+ cellId: activeCell.id,
1925
+ outputArtifactId: output.artifactId,
1926
+ tool: output.tool,
1927
+ status,
1928
+ runId: options.runId,
1929
+ }, { artifactId: output.artifactId });
1930
+ if (!write.ok)
1931
+ throw new Error(write.error.message);
1932
+ const read = engine.notebooks.read(notebookId);
1933
+ if (!read.ok)
1934
+ throw new Error(read.error.message);
1935
+ return {
1936
+ document: read.value,
1937
+ outputArtifactId: output.artifactId,
1938
+ status,
1939
+ };
1940
+ }
1941
+ catch (cause) {
1942
+ const message = cause instanceof Error ? cause.message : String(cause);
1943
+ await patchGeneration(engine, activeGenerationIdForRun(engine, notebookId, activeCell.id, options.runId, undefined), { status: "failed", error: truncateGenerationError(message) });
1944
+ const outputArtifactId = await failedArtifact(engine, activeCell, message);
1945
+ const state = {
1946
+ fingerprint: nextFingerprint,
1947
+ status: "error",
1948
+ outputArtifactId,
1949
+ completedAt: new Date().toISOString(),
1950
+ updatedAt: new Date().toISOString(),
1951
+ ...(options.runId ? { runId: options.runId } : {}),
1952
+ tool: activeCell.tool
1953
+ || activeCell.model
1954
+ || activeCell.operation,
1955
+ error: message,
1956
+ };
1957
+ const write = await mutateNotebook(engine, notebookId, "run_notebook_cell_failed", (current) => {
1958
+ const concurrent = isConcurrentGenerationProducer(activeCell);
1959
+ const targetCellId = concurrent && options.outputCellId
1960
+ ? options.outputCellId
1961
+ : activeCell.id;
1962
+ const withFailure = withExecution(current, targetCellId, state, concurrent ? undefined : outputArtifactId);
1963
+ if (!concurrent)
1964
+ return withFailure;
1965
+ const execution = { ...executionState(withFailure) };
1966
+ if (options.runId
1967
+ && execution[activeCell.id]?.runId === options.runId) {
1968
+ delete execution[activeCell.id];
1969
+ }
1970
+ const cells = withFailure.cells.map((candidate) => {
1971
+ if (candidate.id !== activeCell.id)
1972
+ return candidate;
1973
+ const inputs = { ...(candidate.inputs ?? {}) };
1974
+ delete inputs.state;
1975
+ delete inputs.providerArtifactId;
1976
+ return { ...candidate, inputs };
1977
+ });
1978
+ return { ...withFailure, execution, cells };
1979
+ }, {
1980
+ cellId: activeCell.id,
1981
+ outputArtifactId,
1982
+ error: message.slice(0, 500),
1983
+ runId: options.runId,
1984
+ tool: state.tool,
1985
+ }, { artifactId: outputArtifactId });
1986
+ if (!write.ok)
1987
+ throw new Error(write.error.message);
1988
+ throw new Error(message);
1989
+ }
1990
+ }
1991
+ export function notebookCellIsStale(engine, document, cell) {
1992
+ const execution = executionState(document)[cell.id];
1993
+ if (!execution)
1994
+ return true;
1995
+ if (execution.status === "running"
1996
+ || execution.status === "awaiting-output") {
1997
+ return false;
1998
+ }
1999
+ if (execution.fingerprint !== notebookCellFingerprint(document, cell))
2000
+ return true;
2001
+ if (execution.status === "error" || execution.error)
2002
+ return true;
2003
+ if (execution.status === "stale" || execution.stale)
2004
+ return true;
2005
+ // generate_video / generate_image may hold the artifact only on the output cell.
2006
+ const artifactId = resolveNotebookCellArtifactId(document, cell)
2007
+ ?? execution.outputArtifactId;
2008
+ if (!artifactId)
2009
+ return true;
2010
+ if (!engine.artifacts.get(artifactId).ok)
2011
+ return true;
2012
+ return false;
2013
+ }
2014
+ function notebookCellNeedsExecution(engine, document, cell) {
2015
+ if (notebookCellIsRunnable(cell)) {
2016
+ return notebookCellIsStale(engine, document, cell);
2017
+ }
2018
+ const artifactId = resolveNotebookCellArtifactId(document, cell)
2019
+ ?? cell.outputArtifactId
2020
+ ?? cell.outputEntityId;
2021
+ return !artifactId || !engine.artifacts.get(artifactId).ok;
2022
+ }
2023
+ function notebookCellHasReusableMediaOutput(engine, document, cell) {
2024
+ const mediaType = notebookCellMediaType(cell);
2025
+ if (!mediaType)
2026
+ return false;
2027
+ const artifactId = resolveNotebookCellArtifactId(document, cell)
2028
+ ?? cell.outputArtifactId
2029
+ ?? cell.outputEntityId;
2030
+ if (!artifactId)
2031
+ return false;
2032
+ const artifact = engine.artifacts.get(artifactId);
2033
+ const artifactMediaType = artifact.ok && artifact.value.kind === "final"
2034
+ ? "video"
2035
+ : artifact.ok ? artifact.value.kind : null;
2036
+ if (!artifact.ok || artifactMediaType !== mediaType)
2037
+ return false;
2038
+ if (!notebookCellIsRunnable(cell))
2039
+ return true;
2040
+ const mediaOutput = findGenerationMediaOutput(document, cell);
2041
+ if (mediaOutput) {
2042
+ const outputExecution = executionState(document)[mediaOutput.outputCell.id];
2043
+ if (mediaOutput.outputCell.inputs?.state === "completed"
2044
+ || outputExecution?.status === "completed") {
2045
+ return true;
2046
+ }
2047
+ }
2048
+ const execution = executionState(document)[cell.id];
2049
+ return execution?.status === "completed" && !execution.error;
2050
+ }
2051
+ function requiredNotebookCellIds(engine, document, cellIds, forceSelected) {
2052
+ const cells = new Map(document.cells.map((cell) => [cell.id, cell]));
2053
+ const parents = new Map();
2054
+ for (const edge of document.edges) {
2055
+ const list = parents.get(edge.target) ?? [];
2056
+ list.push(edge.source);
2057
+ parents.set(edge.target, list);
2058
+ }
2059
+ const required = new Set(cellIds);
2060
+ const expanded = new Set();
2061
+ const stack = cellIds.filter((cellId) => {
2062
+ const cell = cells.get(cellId);
2063
+ return forceSelected || !cell || notebookCellNeedsExecution(engine, document, cell);
2064
+ });
2065
+ while (stack.length > 0) {
2066
+ const id = stack.pop();
2067
+ if (!id || expanded.has(id))
2068
+ continue;
2069
+ expanded.add(id);
2070
+ for (const parentId of parents.get(id) ?? []) {
2071
+ const parent = cells.get(parentId);
2072
+ if (parent
2073
+ && notebookCellHasReusableMediaOutput(engine, document, parent)) {
2074
+ continue;
2075
+ }
2076
+ required.add(parentId);
2077
+ if (!parent)
2078
+ continue;
2079
+ if (!notebookCellNeedsExecution(engine, document, parent))
2080
+ continue;
2081
+ stack.push(parentId);
2082
+ }
2083
+ }
2084
+ return required;
2085
+ }
2086
+ function generationPlanClaimField(plan, field) {
2087
+ const claim = plan.executionClaim;
2088
+ return claim
2089
+ && typeof claim === "object"
2090
+ && !Array.isArray(claim)
2091
+ && typeof claim[field] === "string"
2092
+ ? claim[field]
2093
+ : null;
2094
+ }
2095
+ function generationPlanClaimRunId(plan) {
2096
+ return generationPlanClaimField(plan, "runId");
2097
+ }
2098
+ export function generationPlanClaimOutputCellId(plan) {
2099
+ return generationPlanClaimField(plan, "outputCellId");
2100
+ }
2101
+ function concurrentProducerIdForCell(cell) {
2102
+ if (isConcurrentGenerationProducer(cell))
2103
+ return cell.id;
2104
+ const producerId = cell.inputs?.producerCellId;
2105
+ if (isImageVideoGenerationResult(cell) && typeof producerId === "string") {
2106
+ return producerId;
2107
+ }
2108
+ return null;
2109
+ }
2110
+ function awaitJobCoverageIds(payload) {
2111
+ const ids = [];
2112
+ if (typeof payload.cellId === "string")
2113
+ ids.push(payload.cellId);
2114
+ if (typeof payload.outputCellId === "string")
2115
+ ids.push(payload.outputCellId);
2116
+ return [...new Set(ids)];
2117
+ }
2118
+ function awaitJobCoversExecution(input) {
2119
+ if (input.payload.notebookId !== input.notebookId)
2120
+ return false;
2121
+ if (typeof input.payload.runId === "string"
2122
+ && input.payload.runId !== input.runId) {
2123
+ return false;
2124
+ }
2125
+ const covered = awaitJobCoverageIds(input.payload);
2126
+ if (covered.includes(input.cellId))
2127
+ return true;
2128
+ if (input.producerCellId && covered.includes(input.producerCellId)) {
2129
+ const jobOutput = typeof input.payload.outputCellId === "string"
2130
+ ? input.payload.outputCellId
2131
+ : null;
2132
+ if (!jobOutput)
2133
+ return true;
2134
+ return jobOutput === input.cellId;
2135
+ }
2136
+ return false;
2137
+ }
2138
+ function resolveAwaitNotebookTarget(document, input) {
2139
+ const requested = document.cells.find((candidate) => candidate.id === input.cellId);
2140
+ if (!requested) {
2141
+ return { cell: undefined, outputCellId: input.outputCellId };
2142
+ }
2143
+ if (isImageVideoGenerationResult(requested)) {
2144
+ const producerId = requested.inputs?.producerCellId;
2145
+ const producer = typeof producerId === "string"
2146
+ ? document.cells.find((candidate) => candidate.id === producerId)
2147
+ : undefined;
2148
+ return {
2149
+ cell: producer ?? requested,
2150
+ outputCellId: input.outputCellId ?? requested.id,
2151
+ };
2152
+ }
2153
+ return { cell: requested, outputCellId: input.outputCellId };
2154
+ }
2155
+ export async function claimNotebookGenerationPlan(engine, notebookId, cellId, planId, runId, retryFailedCellId) {
2156
+ let claimedPlan = null;
2157
+ const claimedAt = new Date().toISOString();
2158
+ const result = await mutateNotebook(engine, notebookId, "claim_notebook_generation_plan", (document) => {
2159
+ const record = (document.generationPlans ?? []).find((candidate) => candidate.planId === planId && candidate.cellId === cellId);
2160
+ if (!record)
2161
+ throw new Error("Generation plan is not preserved in this notebook");
2162
+ if (record.status !== "planned") {
2163
+ throw new Error(`Generation plan is already ${record.status}`);
2164
+ }
2165
+ if (generationPlanClaimRunId(record.plan)) {
2166
+ throw new Error("Generation plan is already claimed");
2167
+ }
2168
+ const cell = document.cells.find((candidate) => candidate.id === cellId);
2169
+ if (!cell)
2170
+ throw new Error("Notebook cell not found");
2171
+ const expected = record.plan.cellDefinitionFingerprint
2172
+ ?? record.plan.cellFingerprint;
2173
+ if (expected !== notebookCellDefinitionFingerprint(document, cell)) {
2174
+ throw new Error("Generation plan is stale for the selected cell");
2175
+ }
2176
+ const execution = executionState(document)[cellId];
2177
+ const concurrent = isConcurrentGenerationProducer(cell);
2178
+ if (!concurrent
2179
+ && (execution?.status === "queued"
2180
+ || execution?.status === "running"
2181
+ || execution?.status === "awaiting-output")) {
2182
+ throw new Error(`Notebook cell ${notebookGridTag(cell.slot)} is already ${execution.status}`);
2183
+ }
2184
+ let working = retryFailedCellId
2185
+ ? removeCreditFailedOutput(engine, document, cellId, retryFailedCellId)
2186
+ : document;
2187
+ let outputCellId;
2188
+ if (cell.type === "generate_image" || cell.type === "generate_video"
2189
+ || (cell.type === "generate_audio" && retryFailedCellId)) {
2190
+ const allocate = cell.type === "generate_image" ? allocateGenerateImageResult
2191
+ : cell.type === "generate_audio" ? allocateGenerateAudioResult
2192
+ : allocateGenerateVideoResult;
2193
+ const allocated = allocate(engine, working, cell.id, { runId, planId, acceptedAt: claimedAt });
2194
+ working = allocated.document;
2195
+ outputCellId = allocated.outputCell.id;
2196
+ const retrySlot = document.cells.find((candidate) => candidate.id === retryFailedCellId)?.slot;
2197
+ if (retrySlot) {
2198
+ working = {
2199
+ ...working,
2200
+ cells: working.cells.map((candidate) => candidate.id === outputCellId
2201
+ ? { ...candidate, slot: retrySlot }
2202
+ : candidate),
2203
+ };
2204
+ }
2205
+ }
2206
+ claimedPlan = {
2207
+ ...record.plan,
2208
+ executionClaim: {
2209
+ runId,
2210
+ claimedAt,
2211
+ ...(outputCellId ? { outputCellId } : {}),
2212
+ },
2213
+ };
2214
+ const state = {
2215
+ fingerprint: notebookCellFingerprint(working, cell),
2216
+ status: "queued",
2217
+ runId,
2218
+ startedAt: claimedAt,
2219
+ updatedAt: claimedAt,
2220
+ tool: cell.tool || cell.model || cell.operation,
2221
+ };
2222
+ let queued = outputCellId
2223
+ ? withExecution(working, outputCellId, state)
2224
+ : withExecution(working, cellId, {
2225
+ ...state,
2226
+ ...(cell.outputArtifactId
2227
+ ? { outputArtifactId: cell.outputArtifactId }
2228
+ : {}),
2229
+ });
2230
+ if (outputCellId && !concurrent)
2231
+ queued = withExecution(queued, cellId, state);
2232
+ return {
2233
+ ...queued,
2234
+ generationPlans: (queued.generationPlans ?? []).map((candidate) => candidate.planId === planId
2235
+ ? {
2236
+ ...candidate,
2237
+ status: "queued",
2238
+ plan: claimedPlan,
2239
+ updatedAt: claimedAt,
2240
+ }
2241
+ : candidate),
2242
+ };
2243
+ }, { notebookId, cellId, planId, runId });
2244
+ if (!result.ok)
2245
+ throw new Error(result.error.message);
2246
+ if (!claimedPlan)
2247
+ throw new Error("Generation plan claim was not persisted");
2248
+ return claimedPlan;
2249
+ }
2250
+ export async function failQueuedNotebookCells(engine, notebookId, options) {
2251
+ const selected = new Set(options.cellIds);
2252
+ if (selected.size === 0
2253
+ && !options.generationPlanId
2254
+ && !options.runPlanId)
2255
+ return 0;
2256
+ const snapshot = engine.notebooks.read(notebookId);
2257
+ if (!snapshot.ok)
2258
+ return 0;
2259
+ const queuedCellIds = new Set([
2260
+ ...[...selected].filter((cellId) => {
2261
+ const state = executionState(snapshot.value)[cellId];
2262
+ return state?.status === "queued" && state.runId === options.runId;
2263
+ }),
2264
+ ...Object.entries(executionState(snapshot.value)).flatMap(([cellId, state]) => state?.status === "queued" && state.runId === options.runId ? [cellId] : []),
2265
+ ]);
2266
+ const failedCellIds = new Set([...selected].filter((cellId) => {
2267
+ const state = executionState(snapshot.value)[cellId];
2268
+ return state?.status === "error" && state.runId === options.runId;
2269
+ }));
2270
+ const terminalCellIds = new Set([...queuedCellIds, ...failedCellIds]);
2271
+ const hasGenerationPlan = (snapshot.value.generationPlans ?? []).some((record) => record.status === "queued"
2272
+ && terminalCellIds.has(record.cellId)
2273
+ && record.planId === options.generationPlanId
2274
+ && (generationPlanClaimRunId(record.plan) === options.runId
2275
+ || (options.allowLegacyUnboundGenerationPlan === true
2276
+ && generationPlanClaimRunId(record.plan) === null)));
2277
+ const hasRunPlan = Boolean(options.runPlanId
2278
+ && notebookRunPlanRecords(snapshot.value).some((record) => record.planId === options.runPlanId
2279
+ && record.status === "queued"
2280
+ && record.runId === options.runId
2281
+ && (selected.size === 0 || terminalCellIds.size > 0)));
2282
+ if (queuedCellIds.size === 0 && !hasGenerationPlan && !hasRunPlan)
2283
+ return 0;
2284
+ const updatedAt = new Date().toISOString();
2285
+ let terminalized = 0;
2286
+ const result = await mutateNotebook(engine, notebookId, "fail_queued_notebook_cells", (document) => {
2287
+ const execution = { ...executionState(document) };
2288
+ const transitionedCellIds = new Set();
2289
+ for (const [cellId, state] of Object.entries(execution)) {
2290
+ if (state?.status !== "queued" || state.runId !== options.runId)
2291
+ continue;
2292
+ if (selected.size > 0 && !queuedCellIds.has(cellId) && !selected.has(cellId)) {
2293
+ continue;
2294
+ }
2295
+ execution[cellId] = {
2296
+ ...state,
2297
+ status: "error",
2298
+ completedAt: updatedAt,
2299
+ updatedAt,
2300
+ error: options.error,
2301
+ };
2302
+ transitionedCellIds.add(cellId);
2303
+ }
2304
+ const planCellIds = new Set([...selected, ...transitionedCellIds]);
2305
+ for (const [cellId, state] of Object.entries(execution)) {
2306
+ if (state?.status === "error" && state.runId === options.runId) {
2307
+ planCellIds.add(cellId);
2308
+ }
2309
+ }
2310
+ terminalized = transitionedCellIds.size;
2311
+ return {
2312
+ ...document,
2313
+ execution,
2314
+ cells: document.cells.map((cell) => transitionedCellIds.has(cell.id)
2315
+ ? {
2316
+ ...cell,
2317
+ inputs: { ...(cell.inputs ?? {}), state: "error" },
2318
+ }
2319
+ : cell),
2320
+ generationPlans: (document.generationPlans ?? []).map((record) => record.status === "queued"
2321
+ && planCellIds.has(record.cellId)
2322
+ && record.planId === options.generationPlanId
2323
+ && (generationPlanClaimRunId(record.plan) === options.runId
2324
+ || (options.allowLegacyUnboundGenerationPlan === true
2325
+ && generationPlanClaimRunId(record.plan) === null))
2326
+ ? {
2327
+ ...record,
2328
+ status: "failed",
2329
+ updatedAt,
2330
+ error: truncateGenerationError(options.error),
2331
+ }
2332
+ : record),
2333
+ notebookRunPlans: notebookRunPlanRecords(document).map((record) => record.planId === options.runPlanId
2334
+ && record.status === "queued"
2335
+ && record.runId === options.runId
2336
+ && (selected.size === 0 || planCellIds.size > 0)
2337
+ ? {
2338
+ ...record,
2339
+ status: "failed",
2340
+ updatedAt,
2341
+ error: options.error,
2342
+ }
2343
+ : record),
2344
+ };
2345
+ }, {
2346
+ cellIds: [...selected],
2347
+ runId: options.runId,
2348
+ error: options.error.slice(0, 500),
2349
+ generationPlanId: options.generationPlanId,
2350
+ runPlanId: options.runPlanId,
2351
+ allowLegacyUnboundGenerationPlan: options.allowLegacyUnboundGenerationPlan,
2352
+ });
2353
+ if (!result.ok)
2354
+ throw new Error(result.error.message);
2355
+ return terminalized;
2356
+ }
2357
+ function notebookRunPlanRecords(document) {
2358
+ return document.notebookRunPlans ?? [];
2359
+ }
2360
+ async function updateNotebookRunPlanState(engine, notebookId, planId, status, details = {}) {
2361
+ const result = await mutateNotebook(engine, notebookId, "update_notebook_run_plan", (document) => ({
2362
+ ...document,
2363
+ notebookRunPlans: notebookRunPlanRecords(document).map((record) => record.planId === planId
2364
+ && typeof details.runId === "string"
2365
+ && record.runId === details.runId
2366
+ ? {
2367
+ ...record,
2368
+ status,
2369
+ ...details,
2370
+ updatedAt: new Date().toISOString(),
2371
+ }
2372
+ : record),
2373
+ }), { planId, status, ...details });
2374
+ if (!result.ok)
2375
+ throw new Error(result.error.message);
2376
+ }
2377
+ function paidCellAuthorized(document, cell, input) {
2378
+ if (!notebookCellIsPaid(cell))
2379
+ return true;
2380
+ const expectedFingerprint = notebookCellDefinitionFingerprint(document, cell);
2381
+ if (input.runPlanId) {
2382
+ const record = notebookRunPlanRecords(document).find((candidate) => candidate.planId === input.runPlanId);
2383
+ const fingerprints = record?.cellDefinitionFingerprints;
2384
+ return Boolean(record
2385
+ && (record.status === "queued" || record.status === "awaiting-output")
2386
+ && record.runId === input.runId
2387
+ && fingerprints?.[cell.id] === expectedFingerprint);
2388
+ }
2389
+ const planId = input.generationPlan?.planId;
2390
+ if (typeof planId !== "string")
2391
+ return false;
2392
+ const records = document.generationPlans ?? [];
2393
+ const record = records.find((candidate) => candidate.planId === planId && candidate.cellId === cell.id);
2394
+ const plan = record?.plan;
2395
+ return Boolean(record
2396
+ && (record.status === "queued" || record.status === "awaiting-output")
2397
+ && generationPlanClaimRunId(record.plan) === input.runId
2398
+ && plan
2399
+ && typeof plan === "object"
2400
+ && !Array.isArray(plan)
2401
+ && (plan.cellDefinitionFingerprint
2402
+ ?? plan.cellFingerprint) === expectedFingerprint);
2403
+ }
2404
+ function dispatchedPromptFor(cell, dispatchedPayload) {
2405
+ return submittedGenerationPrompt({
2406
+ dispatchedPrompt: payloadPrompt(dispatchedPayload),
2407
+ cellPrompt: cell.prompt,
2408
+ resolvedPrompt: cell.inputs?.resolvedPrompt,
2409
+ });
2410
+ }
2411
+ async function finishAwaitingNotebookCell(engine, input) {
2412
+ const read = engine.notebooks.read(input.notebookId);
2413
+ if (!read.ok)
2414
+ throw new Error(read.error.message);
2415
+ const resolvedTarget = resolveAwaitNotebookTarget(read.value, input);
2416
+ const cell = resolvedTarget.cell;
2417
+ if (!cell)
2418
+ throw new Error(`Awaiting notebook cell not found: ${input.cellId}`);
2419
+ input = {
2420
+ ...input,
2421
+ cellId: cell.id,
2422
+ ...(resolvedTarget.outputCellId
2423
+ ? { outputCellId: resolvedTarget.outputCellId }
2424
+ : {}),
2425
+ };
2426
+ const initialOutput = isConcurrentGenerationProducer(cell)
2427
+ ? findGenerationResultForRun(read.value, cell, {
2428
+ runId: input.runId,
2429
+ outputCellId: input.outputCellId,
2430
+ })
2431
+ : null;
2432
+ if (isConcurrentGenerationProducer(cell) && !initialOutput) {
2433
+ throw new Error(`Awaiting generation result not found for run ${input.runId}`);
2434
+ }
2435
+ const generationId = activeGenerationIdForRun(engine, input.notebookId, cell.id, input.runId, input.providerArtifactId);
2436
+ const current = executionState(read.value)[initialOutput?.outputCell.id ?? cell.id];
2437
+ if (current?.status === "completed"
2438
+ && current.outputArtifactId
2439
+ && engine.artifacts.get(current.outputArtifactId).ok) {
2440
+ await patchGeneration(engine, generationId, {
2441
+ status: "completed",
2442
+ outputArtifactId: current.outputArtifactId,
2443
+ });
2444
+ return {
2445
+ outputArtifactId: current.outputArtifactId,
2446
+ document: read.value,
2447
+ };
2448
+ }
2449
+ let outputArtifactId = input.providerArtifactId;
2450
+ const payloadResult = await engine.metadata.artifacts.read(input.providerArtifactId, "generation-payload");
2451
+ const dispatchedPayload = payloadResult.ok ? payloadResult.value : null;
2452
+ const resolvedPrompt = dispatchedPromptFor(cell, dispatchedPayload);
2453
+ const cellInputs = {
2454
+ providerArtifactId: input.providerArtifactId,
2455
+ ...(dispatchedPayload ? { dispatchedPayload } : {}),
2456
+ ...(resolvedPrompt ? { resolvedPrompt } : {}),
2457
+ state: "completed",
2458
+ };
2459
+ if (notebookCellMediaType(cell) === "video") {
2460
+ const audioArtifactId = firstUpstreamArtifact(read.value, cell, "replace_audio");
2461
+ if (audioArtifactId) {
2462
+ const durationSeconds = Math.max(1, Number(cell.inputs?.duration ?? 15));
2463
+ const normalized = await normalizeSceneVideo(engine, input.providerArtifactId, audioArtifactId, `${notebookGridTag(cell.slot)} normalized`, durationSeconds);
2464
+ outputArtifactId = normalized.artifactId;
2465
+ cellInputs.normalizedFromArtifactId = input.providerArtifactId;
2466
+ cellInputs.normalizedDurationSeconds = normalized.durationSeconds;
2467
+ }
2468
+ }
2469
+ const completedAt = new Date().toISOString();
2470
+ const state = {
2471
+ fingerprint: current?.fingerprint
2472
+ ?? notebookCellFingerprint(read.value, cell),
2473
+ status: "completed",
2474
+ outputArtifactId,
2475
+ providerArtifactId: input.providerArtifactId,
2476
+ runId: input.runId,
2477
+ completedAt,
2478
+ updatedAt: completedAt,
2479
+ tool: current?.tool ?? cell.tool ?? cell.model,
2480
+ };
2481
+ const updated = await mutateNotebook(engine, input.notebookId, "complete_notebook_provider_output", (document) => {
2482
+ let working = document;
2483
+ let resolvedOutput = findGenerationResultForRun(working, cell, {
2484
+ runId: input.runId,
2485
+ outputCellId: input.outputCellId,
2486
+ });
2487
+ if (cell.type === "generate_audio"
2488
+ && !resolvedOutput) {
2489
+ const ensured = ensureGenerationMediaOutput(engine, working, cell);
2490
+ working = ensured.document;
2491
+ resolvedOutput = {
2492
+ outputCell: ensured.outputCell,
2493
+ outputEdge: ensured.outputEdge,
2494
+ };
2495
+ }
2496
+ const concurrent = isConcurrentGenerationProducer(cell);
2497
+ const executionCellId = concurrent && resolvedOutput
2498
+ ? resolvedOutput.outputCell.id
2499
+ : cell.id;
2500
+ let withOutput = withExecution(working, executionCellId, state, concurrent || resolvedOutput ? undefined : outputArtifactId);
2501
+ if (resolvedOutput) {
2502
+ withOutput = withMediaOutput(withOutput, new Set([resolvedOutput.outputCell.id]), outputArtifactId, {
2503
+ state: "completed",
2504
+ ...(resolvedPrompt ? { resolvedPrompt } : {}),
2505
+ ...(dispatchedPayload ? { dispatchedPayload } : {}),
2506
+ });
2507
+ }
2508
+ const outputCellId = resolvedOutput?.outputCell.id;
2509
+ const actionBefore = withOutput.cells.find((candidate) => candidate.id === cell.id);
2510
+ const fillActionPrompt = Boolean(resolvedPrompt)
2511
+ && !actionBefore?.prompt;
2512
+ const withCells = {
2513
+ ...withOutput,
2514
+ cells: withOutput.cells.map((candidate) => {
2515
+ if (candidate.id === cell.id) {
2516
+ const nextInputs = { ...(candidate.inputs ?? {}) };
2517
+ if (!concurrent)
2518
+ Object.assign(nextInputs, cellInputs);
2519
+ else {
2520
+ delete nextInputs.state;
2521
+ delete nextInputs.providerArtifactId;
2522
+ if (resolvedPrompt) {
2523
+ nextInputs.resolvedPrompt = resolvedPrompt;
2524
+ }
2525
+ }
2526
+ return {
2527
+ ...candidate,
2528
+ ...(fillActionPrompt ? { prompt: resolvedPrompt } : {}),
2529
+ inputs: nextInputs,
2530
+ };
2531
+ }
2532
+ if (candidate.id === outputCellId && resolvedPrompt) {
2533
+ return { ...candidate, prompt: resolvedPrompt };
2534
+ }
2535
+ return candidate;
2536
+ }),
2537
+ };
2538
+ // Filling the empty prompt column changes the cell's fingerprint, so
2539
+ // the stored execution fingerprint must come from the updated document
2540
+ // — otherwise the completed cell would immediately read as stale.
2541
+ const finalCell = withCells.cells.find((candidate) => candidate.id === cell.id);
2542
+ const fingerprint = fillActionPrompt && finalCell
2543
+ ? notebookCellFingerprint(withCells, finalCell)
2544
+ : state.fingerprint;
2545
+ const nextExecution = { ...executionState(withCells) };
2546
+ if (concurrent && nextExecution[cell.id]?.runId === input.runId) {
2547
+ delete nextExecution[cell.id];
2548
+ }
2549
+ return {
2550
+ ...withCells,
2551
+ execution: {
2552
+ ...nextExecution,
2553
+ ...(concurrent
2554
+ ? {}
2555
+ : {
2556
+ [cell.id]: {
2557
+ ...state,
2558
+ fingerprint,
2559
+ outputArtifactId,
2560
+ },
2561
+ }),
2562
+ ...(outputCellId
2563
+ ? {
2564
+ [outputCellId]: {
2565
+ fingerprint,
2566
+ status: "completed",
2567
+ outputArtifactId,
2568
+ completedAt,
2569
+ updatedAt: completedAt,
2570
+ ...(input.runId ? { runId: input.runId } : {}),
2571
+ tool: state.tool,
2572
+ },
2573
+ }
2574
+ : {}),
2575
+ },
2576
+ };
2577
+ }, {
2578
+ cellId: cell.id,
2579
+ providerArtifactId: input.providerArtifactId,
2580
+ outputArtifactId,
2581
+ runId: input.runId,
2582
+ tool: state.tool,
2583
+ }, { artifactId: outputArtifactId });
2584
+ if (!updated.ok)
2585
+ throw new Error(updated.error.message);
2586
+ await patchGeneration(engine, generationId, {
2587
+ status: "completed",
2588
+ outputArtifactId,
2589
+ });
2590
+ const refreshed = engine.notebooks.read(input.notebookId);
2591
+ if (!refreshed.ok)
2592
+ throw new Error(refreshed.error.message);
2593
+ return { outputArtifactId, document: refreshed.value };
2594
+ }
2595
+ async function failAwaitingNotebookCell(engine, input, message) {
2596
+ const updatedAt = new Date().toISOString();
2597
+ const result = await mutateNotebook(engine, input.notebookId, "fail_notebook_provider_output", (document) => {
2598
+ const resolvedTarget = resolveAwaitNotebookTarget(document, input);
2599
+ const cell = resolvedTarget.cell;
2600
+ if (!cell)
2601
+ return document;
2602
+ const outputCellId = resolvedTarget.outputCellId;
2603
+ const concurrent = isConcurrentGenerationProducer(cell);
2604
+ const runMatchedOutput = concurrent
2605
+ ? document.cells.find((candidate) => candidate.id !== cell.id
2606
+ && (candidate.inputs?.generationRunId === input.runId
2607
+ || executionState(document)[candidate.id]?.runId === input.runId))
2608
+ : undefined;
2609
+ const failTargetId = (concurrent
2610
+ ? outputCellId ?? runMatchedOutput?.id
2611
+ : undefined) ?? cell.id;
2612
+ const current = executionState(document)[failTargetId]
2613
+ ?? executionState(document)[cell.id];
2614
+ const withFailure = withExecution(document, failTargetId, {
2615
+ fingerprint: current?.fingerprint
2616
+ ?? notebookCellFingerprint(document, cell),
2617
+ status: "error",
2618
+ outputArtifactId: input.providerArtifactId,
2619
+ providerArtifactId: input.providerArtifactId,
2620
+ runId: input.runId,
2621
+ completedAt: updatedAt,
2622
+ updatedAt,
2623
+ tool: current?.tool ?? cell.tool ?? cell.model,
2624
+ error: message,
2625
+ }, isConcurrentGenerationProducer(cell) ? undefined : input.providerArtifactId);
2626
+ if (isConcurrentGenerationProducer(cell)) {
2627
+ const execution = { ...executionState(withFailure) };
2628
+ if (execution[cell.id]?.runId === input.runId
2629
+ && execution[cell.id]?.status === "awaiting-output") {
2630
+ delete execution[cell.id];
2631
+ }
2632
+ return {
2633
+ ...withFailure,
2634
+ execution,
2635
+ cells: withFailure.cells.map((candidate) => candidate.id === failTargetId
2636
+ ? {
2637
+ ...candidate,
2638
+ inputs: {
2639
+ ...(candidate.inputs ?? {}),
2640
+ state: "error",
2641
+ },
2642
+ }
2643
+ : candidate),
2644
+ };
2645
+ }
2646
+ return rollbackGenerationMediaOutput(withFailure, cell, input.providerArtifactId, outputCellId);
2647
+ }, {
2648
+ cellId: input.cellId,
2649
+ providerArtifactId: input.providerArtifactId,
2650
+ runId: input.runId,
2651
+ error: message.slice(0, 500),
2652
+ }, { artifactId: input.providerArtifactId });
2653
+ if (!result.ok)
2654
+ throw new Error(result.error.message);
2655
+ await patchGeneration(engine, activeGenerationIdForRun(engine, input.notebookId, input.cellId, input.runId, input.providerArtifactId), { status: "failed", error: truncateGenerationError(message) });
2656
+ const read = engine.notebooks.read(input.notebookId);
2657
+ if (!read.ok)
2658
+ throw new Error(read.error.message);
2659
+ return read.value;
2660
+ }
2661
+ export async function failNotebookProviderOutput(engine, input) {
2662
+ return await failAwaitingNotebookCell(engine, input, notebookProviderFailureMessage(engine, input));
2663
+ }
2664
+ function runHasAwaitingCells(document, runId) {
2665
+ return Object.values(executionState(document)).some((execution) => execution.runId === runId && execution.status === "awaiting-output");
2666
+ }
2667
+ async function enqueueNotebookResume(input, document) {
2668
+ const producer = document.cells.find(cell => cell.id === input.cellId);
2669
+ if (producer?.inputs?.chainRunId === input.runId)
2670
+ return;
2671
+ if (runHasAwaitingCells(document, input.runId))
2672
+ return;
2673
+ await queue.enqueue(`resume-notebook-${input.runId}`, {
2674
+ type: "run_notebook",
2675
+ bookId: input.bookId,
2676
+ notebookId: input.notebookId,
2677
+ runId: input.runId,
2678
+ resume: true,
2679
+ force: false,
2680
+ ...(input.cellIds ? { cellIds: input.cellIds } : {}),
2681
+ ...(input.outputCellId ? { outputCellId: input.outputCellId } : {}),
2682
+ ...(input.runPlanId ? { runPlanId: input.runPlanId } : {}),
2683
+ ...(input.generationPlan ? { generationPlan: input.generationPlan } : {}),
2684
+ ...(input.actor ? { actor: input.actor } : {}),
2685
+ });
2686
+ }
2687
+ async function awaitNotebookOutput(engine, input, signal) {
2688
+ const deadline = Date.now() + 45 * 60_000;
2689
+ while (Date.now() < deadline) {
2690
+ signal.throwIfAborted();
2691
+ const status = await engine.status.get(input.providerArtifactId);
2692
+ if (!status.ok)
2693
+ throw new Error(status.error.message);
2694
+ if (status.value === "ready") {
2695
+ const completed = await finishAwaitingNotebookCell(engine, input);
2696
+ if (input.generationPlan
2697
+ && !input.runPlanId
2698
+ && typeof input.generationPlan.planId === "string") {
2699
+ await updateGenerationPlanState(engine, input.notebookId, input.generationPlan.planId, "continued", { outputArtifactId: completed.outputArtifactId, runId: input.runId });
2700
+ }
2701
+ await enqueueNotebookResume(input, completed.document);
2702
+ return;
2703
+ }
2704
+ if (status.value === "error") {
2705
+ const pending = engine.jobs.pending.read(input.providerArtifactId);
2706
+ const failure = engine.jobs.failures.read(input.providerArtifactId);
2707
+ if (!pending.ok || !pending.value || (failure.ok && failure.value)) {
2708
+ const document = await failNotebookProviderOutput(engine, input);
2709
+ await enqueueNotebookResume(input, document);
2710
+ return;
2711
+ }
2712
+ }
2713
+ await delay(2_000, undefined, { signal });
2714
+ }
2715
+ const document = await failAwaitingNotebookCell(engine, input, `Provider output ${input.providerArtifactId} timed out`);
2716
+ await enqueueNotebookResume(input, document);
2717
+ }
2718
+ export function registerRunNotebookJob(engine) {
2719
+ registerJobHandler("await_notebook_output", async (payload, context) => {
2720
+ const input = payload;
2721
+ try {
2722
+ await awaitNotebookOutput(engine, input, context.signal);
2723
+ return { status: "completed" };
2724
+ }
2725
+ catch (cause) {
2726
+ const reason = context.signal.aborted ? context.signal.reason : cause;
2727
+ const message = reason instanceof Error ? reason.message : String(reason);
2728
+ if (context.signal.aborted && message === "Queue stopped")
2729
+ throw reason;
2730
+ try {
2731
+ await failAwaitingNotebookCell(engine, input, message);
2732
+ }
2733
+ catch {
2734
+ // Best-effort: surface the original await failure if we cannot write
2735
+ // execution state (for example, notebook already deleted).
2736
+ }
2737
+ throw cause;
2738
+ }
2739
+ });
2740
+ const runNotebookHandler = async (payload) => {
2741
+ const input = payload;
2742
+ const runId = input.runId ?? uuidv7();
2743
+ const initial = engine.notebooks.read(input.notebookId);
2744
+ if (!initial.ok)
2745
+ throw new Error(initial.error.message);
2746
+ let document = normalizeLegacyAnalyzeDocument(initial.value);
2747
+ if (document !== initial.value) {
2748
+ const normalized = await mutateNotebook(engine, input.notebookId, "normalize_legacy_analyze_cells", normalizeLegacyAnalyzeDocument);
2749
+ if (!normalized.ok)
2750
+ throw new Error(normalized.error.message);
2751
+ const refreshed = engine.notebooks.read(input.notebookId);
2752
+ if (!refreshed.ok)
2753
+ throw new Error(refreshed.error.message);
2754
+ document = refreshed.value;
2755
+ }
2756
+ const selected = input.cellIds ? new Set(input.cellIds) : null;
2757
+ const required = selected
2758
+ ? requiredNotebookCellIds(engine, document, [...selected], input.force === true)
2759
+ : new Set();
2760
+ const ordered = topologicalOrder(document);
2761
+ const requestedOutputCellId = input.outputCellId
2762
+ ?? generationPlanClaimOutputCellId(input.generationPlan ?? {})
2763
+ ?? undefined;
2764
+ const outputs = {};
2765
+ const errors = [];
2766
+ const blocked = new Set();
2767
+ const waiting = new Set();
2768
+ for (const cell of ordered) {
2769
+ if (!notebookCellIsRunnable(cell)) {
2770
+ const output = resolveNotebookCellArtifactId(document, cell)
2771
+ ?? cell.outputArtifactId
2772
+ ?? cell.outputEntityId;
2773
+ if (output)
2774
+ outputs[cell.id] = output;
2775
+ continue;
2776
+ }
2777
+ if (selected && !required.has(cell.id))
2778
+ continue;
2779
+ const upstreamBlocked = document.edges
2780
+ .filter((edge) => edge.target === cell.id)
2781
+ .some((edge) => blocked.has(edge.source));
2782
+ if (upstreamBlocked) {
2783
+ blocked.add(cell.id);
2784
+ errors.push(`${notebookGridTag(cell.slot)}: blocked by failed upstream dependency`);
2785
+ continue;
2786
+ }
2787
+ const upstreamWaiting = document.edges
2788
+ .filter((edge) => edge.target === cell.id)
2789
+ .some((edge) => waiting.has(edge.source));
2790
+ if (upstreamWaiting) {
2791
+ waiting.add(cell.id);
2792
+ continue;
2793
+ }
2794
+ const invocationOutput = isConcurrentGenerationProducer(cell)
2795
+ ? findGenerationResultForRun(document, cell, {
2796
+ runId,
2797
+ outputCellId: requestedOutputCellId,
2798
+ })
2799
+ : null;
2800
+ const invocationExecution = invocationOutput
2801
+ ? executionState(document)[invocationOutput.outputCell.id]
2802
+ : undefined;
2803
+ const currentExecution = invocationExecution?.runId === runId
2804
+ ? invocationExecution
2805
+ : executionState(document)[cell.id];
2806
+ if (currentExecution?.status === "awaiting-output"
2807
+ || currentExecution?.status === "running") {
2808
+ waiting.add(cell.id);
2809
+ continue;
2810
+ }
2811
+ if (input.resume
2812
+ && currentExecution?.runId === runId
2813
+ && currentExecution.status === "error") {
2814
+ const message = currentExecution.error ?? "provider output failed";
2815
+ errors.push(`${notebookGridTag(cell.slot)}: ${message}`);
2816
+ blocked.add(cell.id);
2817
+ continue;
2818
+ }
2819
+ if (input.resume
2820
+ && invocationOutput
2821
+ && currentExecution?.runId === runId
2822
+ && currentExecution.status === "completed") {
2823
+ const artifactId = currentExecution.outputArtifactId
2824
+ ?? invocationOutput.outputCell.outputArtifactId;
2825
+ if (artifactId && engine.artifacts.get(artifactId).ok) {
2826
+ outputs[cell.id] = artifactId;
2827
+ continue;
2828
+ }
2829
+ }
2830
+ const stale = notebookCellIsStale(engine, document, cell);
2831
+ const forceCell = input.force === true
2832
+ && (!selected || selected.has(cell.id));
2833
+ if (!forceCell && !stale) {
2834
+ const artifactId = resolveNotebookCellArtifactId(document, cell)
2835
+ ?? cell.outputArtifactId;
2836
+ if (artifactId)
2837
+ outputs[cell.id] = artifactId;
2838
+ continue;
2839
+ }
2840
+ const paidAuthorized = paidCellAuthorized(document, cell, input);
2841
+ if (notebookCellIsPaid(cell) && !paidAuthorized) {
2842
+ const message = `${notebookGridTag(cell.slot)}: the saved plan no longer matches this cell's contents; rerun the cell to replan`;
2843
+ errors.push(message);
2844
+ blocked.add(cell.id);
2845
+ continue;
2846
+ }
2847
+ try {
2848
+ const result = await runNotebookCell(engine, input.bookId, input.notebookId, document, cell, {
2849
+ confirmPaid: paidAuthorized,
2850
+ runId,
2851
+ ...(requestedOutputCellId
2852
+ ? { outputCellId: requestedOutputCellId }
2853
+ : {}),
2854
+ });
2855
+ document = result.document;
2856
+ outputs[cell.id] = result.outputArtifactId;
2857
+ if (input.generationPlan
2858
+ && !input.runPlanId
2859
+ && typeof input.generationPlan.planId === "string") {
2860
+ await writeGenerationPlanProvenance(engine, input, cell, result.outputArtifactId);
2861
+ await updateGenerationPlanState(engine, input.notebookId, input.generationPlan.planId, result.status === "awaiting-output"
2862
+ ? "awaiting-output"
2863
+ : "continued", { outputArtifactId: result.outputArtifactId, runId });
2864
+ }
2865
+ if (result.status === "awaiting-output") {
2866
+ waiting.add(cell.id);
2867
+ await queue.enqueue(`await-notebook-${runId}-${cell.id}-${result.outputArtifactId}`, {
2868
+ type: "await_notebook_output",
2869
+ bookId: input.bookId,
2870
+ notebookId: input.notebookId,
2871
+ cellId: cell.id,
2872
+ providerArtifactId: result.outputArtifactId,
2873
+ runId,
2874
+ ...(requestedOutputCellId
2875
+ ? {
2876
+ outputCellId: requestedOutputCellId,
2877
+ }
2878
+ : {}),
2879
+ ...(input.runPlanId ? { runPlanId: input.runPlanId } : {}),
2880
+ ...(input.generationPlan
2881
+ ? { generationPlan: input.generationPlan }
2882
+ : {}),
2883
+ ...(input.actor ? { actor: input.actor } : {}),
2884
+ ...(input.cellIds ? { cellIds: input.cellIds } : {}),
2885
+ });
2886
+ }
2887
+ }
2888
+ catch (cause) {
2889
+ const message = cause instanceof Error ? cause.message : String(cause);
2890
+ errors.push(`${notebookGridTag(cell.slot)}: ${message}`);
2891
+ blocked.add(cell.id);
2892
+ if (input.generationPlan
2893
+ && !input.runPlanId
2894
+ && typeof input.generationPlan.planId === "string") {
2895
+ await updateGenerationPlanState(engine, input.notebookId, input.generationPlan.planId, "failed", { error: message, runId });
2896
+ }
2897
+ const refreshed = engine.notebooks.read(input.notebookId);
2898
+ if (refreshed.ok)
2899
+ document = refreshed.value;
2900
+ }
2901
+ }
2902
+ if (waiting.size > 0) {
2903
+ if (input.runPlanId) {
2904
+ await updateNotebookRunPlanState(engine, input.notebookId, input.runPlanId, "awaiting-output", {
2905
+ runId,
2906
+ outputs,
2907
+ ...(errors.length > 0 ? { error: errors.join("\n") } : {}),
2908
+ });
2909
+ }
2910
+ if (errors.length > 0) {
2911
+ throw new Error(`Notebook run is awaiting output with cell errors:\n${errors.join("\n")}`);
2912
+ }
2913
+ return {
2914
+ id: runId,
2915
+ notebookId: input.notebookId,
2916
+ status: "awaiting-output",
2917
+ cellOrder: ordered.map((cell) => cell.id),
2918
+ outputs,
2919
+ waitingCellIds: [...waiting],
2920
+ errors,
2921
+ };
2922
+ }
2923
+ const now = new Date().toISOString();
2924
+ const run = {
2925
+ id: runId,
2926
+ notebookId: input.notebookId,
2927
+ status: errors.length === 0 ? "completed" : "failed",
2928
+ startedAt: now,
2929
+ completedAt: now,
2930
+ cellOrder: ordered.map((cell) => cell.id),
2931
+ outputs,
2932
+ ...(errors.length > 0 ? { error: errors.join("\n") } : {}),
2933
+ };
2934
+ const recorded = await engine.notebooks.recordRun(run);
2935
+ if (!recorded.ok)
2936
+ throw new Error(recorded.error.message);
2937
+ if (input.runPlanId) {
2938
+ await updateNotebookRunPlanState(engine, input.notebookId, input.runPlanId, errors.length === 0 ? "completed" : "failed", {
2939
+ runId,
2940
+ outputs,
2941
+ ...(errors.length > 0 ? { error: errors.join("\n") } : {}),
2942
+ });
2943
+ }
2944
+ if (errors.length > 0)
2945
+ throw new Error(errors.join("\n"));
2946
+ return run;
2947
+ };
2948
+ registerJobHandler("run_notebook", async (payload) => {
2949
+ const input = payload;
2950
+ try {
2951
+ return await runNotebookHandler(payload);
2952
+ }
2953
+ catch (cause) {
2954
+ const message = cause instanceof Error ? cause.message : String(cause);
2955
+ const generationPlanId = typeof input.generationPlan?.planId === "string"
2956
+ ? input.generationPlan.planId
2957
+ : undefined;
2958
+ if (typeof input.runId === "string") {
2959
+ await failQueuedNotebookCells(engine, input.notebookId, {
2960
+ cellIds: input.cellIds ?? [],
2961
+ runId: input.runId,
2962
+ error: message,
2963
+ ...(generationPlanId ? { generationPlanId } : {}),
2964
+ ...(input.runPlanId ? { runPlanId: input.runPlanId } : {}),
2965
+ }).catch(() => undefined);
2966
+ }
2967
+ throw cause;
2968
+ }
2969
+ });
2970
+ }
2971
+ export async function reconcileAwaitingNotebookOutputs(engine, bookId) {
2972
+ const activeJobs = engine.jobs.queue.list({
2973
+ states: ["queued", "running", "completing"],
2974
+ type: "await_notebook_output",
2975
+ });
2976
+ const failedJobs = engine.jobs.queue.list({
2977
+ states: ["failed", "aborted"],
2978
+ type: "await_notebook_output",
2979
+ });
2980
+ let enqueued = 0;
2981
+ for (const notebook of engine.notebooks.list()) {
2982
+ const execution = executionState(notebook);
2983
+ for (const [cellId, state] of Object.entries(execution)) {
2984
+ if (state.status !== "awaiting-output"
2985
+ || !state.runId) {
2986
+ continue;
2987
+ }
2988
+ const stateRunId = state.runId;
2989
+ const cell = notebook.cells.find((candidate) => candidate.id === cellId);
2990
+ const providerArtifactId = state.providerArtifactId
2991
+ ?? state.outputArtifactId
2992
+ ?? cell?.outputArtifactId;
2993
+ if (!providerArtifactId)
2994
+ continue;
2995
+ const producerCellId = cell
2996
+ ? concurrentProducerIdForCell(cell)
2997
+ : null;
2998
+ const coveredByActive = activeJobs.some((job) => awaitJobCoversExecution({
2999
+ payload: job.payload,
3000
+ notebookId: notebook.id,
3001
+ cellId,
3002
+ producerCellId,
3003
+ runId: stateRunId,
3004
+ }));
3005
+ if (coveredByActive)
3006
+ continue;
3007
+ const matchingFailed = failedJobs
3008
+ .filter((job) => awaitJobCoversExecution({
3009
+ payload: job.payload,
3010
+ notebookId: notebook.id,
3011
+ cellId,
3012
+ producerCellId,
3013
+ runId: stateRunId,
3014
+ }))
3015
+ .sort((left, right) => right.id - left.id);
3016
+ const latestFailed = matchingFailed[0];
3017
+ const awaitProducerId = producerCellId ?? cellId;
3018
+ const awaitOutputCellId = cell && isImageVideoGenerationResult(cell)
3019
+ ? cellId
3020
+ : undefined;
3021
+ const interrupted = latestFailed?.state === "aborted"
3022
+ && latestFailed.error?.message === "Queue stopped";
3023
+ if (latestFailed && !interrupted) {
3024
+ const failure = engine.jobs.failures.read(providerArtifactId);
3025
+ const message = failure.ok && failure.value
3026
+ ? notebookProviderFailureMessage(engine, {
3027
+ notebookId: notebook.id, cellId: awaitProducerId, providerArtifactId,
3028
+ })
3029
+ : latestFailed.error?.message ?? `Notebook await ${latestFailed.state}`;
3030
+ await failAwaitingNotebookCell(engine, {
3031
+ type: "await_notebook_output",
3032
+ bookId,
3033
+ notebookId: notebook.id,
3034
+ cellId: awaitProducerId,
3035
+ providerArtifactId,
3036
+ runId: stateRunId,
3037
+ ...(awaitOutputCellId ? { outputCellId: awaitOutputCellId } : {}),
3038
+ }, message);
3039
+ continue;
3040
+ }
3041
+ const recovered = await recoverNotebookProviderTask(engine, providerArtifactId);
3042
+ if (!recovered) {
3043
+ const status = await engine.status.get(providerArtifactId);
3044
+ const failure = engine.jobs.failures.read(providerArtifactId);
3045
+ const active = engine.jobs.queue.list({
3046
+ artifactId: providerArtifactId, states: ["queued", "running", "completing"],
3047
+ });
3048
+ if (status.ok && status.value !== "ready" && failure.ok && !failure.value
3049
+ && active.length === 0) {
3050
+ const written = await engine.jobs.failures.write(providerArtifactId, {
3051
+ message: "Provider task could not be recovered after restart. No saved task ID or output is available.",
3052
+ });
3053
+ if (!written.ok)
3054
+ throw new Error(written.error.message);
3055
+ }
3056
+ }
3057
+ const runPlan = notebookRunPlanRecords(notebook).find((record) => record.runId === stateRunId
3058
+ && (record.status === "queued"
3059
+ || record.status === "awaiting-output"));
3060
+ const generationRecords = notebook.generationPlans ?? [];
3061
+ const generationRecord = generationRecords.find((candidate) => (candidate.cellId === awaitProducerId || candidate.cellId === cellId)
3062
+ && (candidate.status === "queued"
3063
+ || candidate.status === "awaiting-output")
3064
+ && (generationPlanClaimRunId(candidate.plan) === stateRunId
3065
+ || generationPlanClaimRunId(candidate.plan) === null));
3066
+ const generationPlan = runPlan?.plan ?? generationRecord?.plan;
3067
+ await queue.enqueue(`reconcile-await-notebook-${stateRunId}-${awaitProducerId}-${awaitOutputCellId ?? cellId}-${Date.now()}`, {
3068
+ type: "await_notebook_output",
3069
+ bookId,
3070
+ notebookId: notebook.id,
3071
+ cellId: awaitProducerId,
3072
+ providerArtifactId,
3073
+ runId: stateRunId,
3074
+ ...(awaitOutputCellId ? { outputCellId: awaitOutputCellId } : {}),
3075
+ ...(typeof runPlan?.planId === "string"
3076
+ ? { runPlanId: runPlan.planId }
3077
+ : {}),
3078
+ ...(generationPlan ? { generationPlan } : {}),
3079
+ cellIds: [awaitProducerId],
3080
+ actor: "notebook-reconciler",
3081
+ });
3082
+ enqueued += 1;
3083
+ }
3084
+ }
3085
+ return enqueued;
3086
+ }
3087
+ export async function reconcileGenerationMediaOutputs(engine) {
3088
+ let repaired = 0;
3089
+ for (const notebook of engine.notebooks.list()) {
3090
+ const normalized = ensureGenerationMediaOutputs(engine, notebook);
3091
+ if (normalized === notebook)
3092
+ continue;
3093
+ const result = await mutateNotebook(engine, notebook.id, "reconcile_generation_media_outputs", (current) => ensureGenerationMediaOutputs(engine, current));
3094
+ if (!result.ok)
3095
+ throw new Error(result.error.message);
3096
+ repaired += 1;
3097
+ }
3098
+ return repaired;
3099
+ }
3100
+ async function notebookRunHasLiveWork(engine, document, runId, runPlanId) {
3101
+ if (notebookRunPlanRecords(document).some((record) => (record.runId === runId || record.planId === runPlanId)
3102
+ && record.status === "awaiting-output"))
3103
+ return true;
3104
+ const activeJobs = engine.jobs.queue.list({
3105
+ states: ["queued", "running", "completing"],
3106
+ });
3107
+ if (activeJobs.some((job) => job.payload.notebookId === document.id
3108
+ && job.payload.runId === runId
3109
+ && (job.type === "run_notebook" || job.type === "await_notebook_output"))) {
3110
+ return true;
3111
+ }
3112
+ const pending = await engine.jobs.pending.findAll();
3113
+ if (!pending.ok)
3114
+ return true;
3115
+ const activeArtifactIds = new Set(pending.value.map((task) => task.artifactId));
3116
+ for (const job of activeJobs) {
3117
+ if (job.artifactId)
3118
+ activeArtifactIds.add(job.artifactId);
3119
+ }
3120
+ if (activeArtifactIds.size === 0)
3121
+ return false;
3122
+ const runArtifactIds = new Set();
3123
+ for (const state of Object.values(executionState(document))) {
3124
+ if (state.runId !== runId)
3125
+ continue;
3126
+ if (state.providerArtifactId)
3127
+ runArtifactIds.add(state.providerArtifactId);
3128
+ if (state.outputArtifactId)
3129
+ runArtifactIds.add(state.outputArtifactId);
3130
+ }
3131
+ for (const cell of document.cells) {
3132
+ const generations = engine.generations.listForCell(document.id, cell.id);
3133
+ if (!generations.ok)
3134
+ return true;
3135
+ for (const generation of generations.value) {
3136
+ if (generation.runId !== runId)
3137
+ continue;
3138
+ if (generation.providerArtifactId) {
3139
+ runArtifactIds.add(generation.providerArtifactId);
3140
+ }
3141
+ if (generation.outputArtifactId) {
3142
+ runArtifactIds.add(generation.outputArtifactId);
3143
+ }
3144
+ }
3145
+ }
3146
+ return [...runArtifactIds].some((artifactId) => activeArtifactIds.has(artifactId));
3147
+ }
3148
+ export async function reconcileQueuedNotebookCells(engine) {
3149
+ let terminalized = 0;
3150
+ for (const listed of engine.notebooks.list()) {
3151
+ for (const [cellId, listedState] of Object.entries(executionState(listed))) {
3152
+ if (listedState.status !== "queued" || !listedState.runId)
3153
+ continue;
3154
+ const read = engine.notebooks.read(listed.id);
3155
+ if (!read.ok)
3156
+ continue;
3157
+ const document = read.value;
3158
+ const state = executionState(document)[cellId];
3159
+ if (state?.status !== "queued" || state.runId !== listedState.runId)
3160
+ continue;
3161
+ const cell = document.cells.find((candidate) => candidate.id === cellId);
3162
+ const invocationResult = Boolean(cell && isImageVideoGenerationResult(cell));
3163
+ const producerCellId = cell
3164
+ ? concurrentProducerIdForCell(cell) ?? cell.id
3165
+ : cellId;
3166
+ const producer = document.cells.find((candidate) => candidate.id === producerCellId) ?? cell;
3167
+ if (!cell || (!invocationResult && !notebookCellIsRunnable(cell)))
3168
+ continue;
3169
+ const terminalJobs = engine.jobs.queue.list({
3170
+ states: ["done", "failed", "aborted"],
3171
+ type: "run_notebook",
3172
+ });
3173
+ const matchingTerminal = terminalJobs
3174
+ .filter((job) => {
3175
+ if (job.payload.notebookId !== document.id
3176
+ || job.payload.runId !== state.runId)
3177
+ return false;
3178
+ const cellIds = job.payload.cellIds;
3179
+ const jobOutputCellId = typeof job.payload.outputCellId === "string"
3180
+ ? job.payload.outputCellId
3181
+ : null;
3182
+ return !Array.isArray(cellIds)
3183
+ || cellIds.includes(cellId)
3184
+ || cellIds.includes(producerCellId)
3185
+ || jobOutputCellId === cellId;
3186
+ })
3187
+ .sort((left, right) => right.id - left.id);
3188
+ const terminalJob = matchingTerminal[0];
3189
+ if (terminalJob?.state === "done")
3190
+ continue;
3191
+ const claimedPlans = (document.generationPlans ?? []).filter((record) => record.cellId === producerCellId
3192
+ && record.status === "queued"
3193
+ && generationPlanClaimRunId(record.plan) === state.runId
3194
+ && (!invocationResult
3195
+ || generationPlanClaimOutputCellId(record.plan) === cellId
3196
+ || generationPlanClaimOutputCellId(record.plan) === null));
3197
+ if (!terminalJob
3198
+ && producer
3199
+ && notebookCellIsPaid(producer)
3200
+ && claimedPlans.length !== 1)
3201
+ continue;
3202
+ const runPlanId = typeof terminalJob?.payload.runPlanId === "string"
3203
+ ? terminalJob.payload.runPlanId
3204
+ : undefined;
3205
+ if (await notebookRunHasLiveWork(engine, document, state.runId, runPlanId))
3206
+ continue;
3207
+ const latest = engine.notebooks.read(document.id);
3208
+ if (!latest.ok)
3209
+ continue;
3210
+ const latestState = executionState(latest.value)[cellId];
3211
+ if (latestState?.status !== "queued" || latestState.runId !== state.runId) {
3212
+ continue;
3213
+ }
3214
+ if (await notebookRunHasLiveWork(engine, latest.value, state.runId, runPlanId))
3215
+ continue;
3216
+ const message = terminalJob?.error?.message
3217
+ ?? (terminalJob
3218
+ ? `Notebook run ${terminalJob.state} before queued cell ${notebookGridTag(cell.slot)} started`
3219
+ : `Queued notebook run has no persisted job for ${notebookGridTag(cell.slot)}`);
3220
+ const plan = terminalJob?.payload.generationPlan;
3221
+ const terminalPlanId = plan
3222
+ && typeof plan === "object"
3223
+ && !Array.isArray(plan)
3224
+ && typeof plan.planId === "string"
3225
+ ? plan.planId
3226
+ : undefined;
3227
+ const generationPlanId = terminalPlanId ?? claimedPlans[0]?.planId;
3228
+ terminalized += await failQueuedNotebookCells(engine, document.id, {
3229
+ cellIds: [...new Set([cellId, producerCellId])],
3230
+ runId: state.runId,
3231
+ error: message,
3232
+ ...(generationPlanId ? { generationPlanId } : {}),
3233
+ ...(runPlanId ? { runPlanId } : {}),
3234
+ ...(terminalJob ? { allowLegacyUnboundGenerationPlan: true } : {}),
3235
+ });
3236
+ }
3237
+ }
3238
+ for (const listed of engine.notebooks.list()) {
3239
+ for (const listedPlan of notebookRunPlanRecords(listed)) {
3240
+ if (listedPlan.status !== "queued" || !listedPlan.runId)
3241
+ continue;
3242
+ const read = engine.notebooks.read(listed.id);
3243
+ if (!read.ok)
3244
+ continue;
3245
+ const document = read.value;
3246
+ const plan = notebookRunPlanRecords(document).find((candidate) => candidate.planId === listedPlan.planId);
3247
+ if (plan?.status !== "queued"
3248
+ || plan.runId !== listedPlan.runId)
3249
+ continue;
3250
+ const terminalJobs = engine.jobs.queue.list({
3251
+ states: ["done", "failed", "aborted"],
3252
+ type: "run_notebook",
3253
+ });
3254
+ const terminalJob = terminalJobs
3255
+ .filter((job) => job.payload.notebookId === document.id
3256
+ && job.payload.runId === plan.runId)
3257
+ .sort((left, right) => right.id - left.id)[0];
3258
+ if (terminalJob?.state === "done")
3259
+ continue;
3260
+ if (await notebookRunHasLiveWork(engine, document, plan.runId, plan.planId))
3261
+ continue;
3262
+ const latest = engine.notebooks.read(document.id);
3263
+ if (!latest.ok)
3264
+ continue;
3265
+ const latestPlan = notebookRunPlanRecords(latest.value).find((candidate) => candidate.planId === plan.planId);
3266
+ if (latestPlan?.status !== "queued"
3267
+ || latestPlan.runId !== plan.runId)
3268
+ continue;
3269
+ if (await notebookRunHasLiveWork(engine, latest.value, plan.runId, plan.planId))
3270
+ continue;
3271
+ const message = terminalJob?.error?.message
3272
+ ?? (terminalJob
3273
+ ? `Notebook run ${terminalJob.state} before aggregate plan completed`
3274
+ : "Claimed aggregate notebook run has no persisted job");
3275
+ await failQueuedNotebookCells(engine, document.id, {
3276
+ cellIds: [],
3277
+ runId: plan.runId,
3278
+ runPlanId: plan.planId,
3279
+ error: message,
3280
+ });
3281
+ const reconciled = engine.notebooks.read(document.id);
3282
+ if (reconciled.ok
3283
+ && notebookRunPlanRecords(reconciled.value).some((candidate) => candidate.planId === plan.planId && candidate.status === "failed"))
3284
+ terminalized += 1;
3285
+ }
3286
+ }
3287
+ return terminalized;
3288
+ }
3289
+ export async function abortLeasedNotebookRunsOnStartup(engine) {
3290
+ const leased = engine.jobs.queue.list({
3291
+ states: ["running", "completing"],
3292
+ type: "run_notebook",
3293
+ });
3294
+ let aborted = 0;
3295
+ for (const job of leased) {
3296
+ if (await engine.jobs.queue.abort(job.id, "Server restarted before notebook run completed"))
3297
+ aborted += 1;
3298
+ }
3299
+ return aborted;
3300
+ }
3301
+ export function topologicalOrder(notebook) {
3302
+ const cells = new Map(notebook.cells.map((cell) => [cell.id, cell]));
3303
+ const incoming = new Map(notebook.cells.map((cell) => [cell.id, 0]));
3304
+ const outgoing = new Map();
3305
+ for (const edge of notebook.edges) {
3306
+ if (!cells.has(edge.source) || !cells.has(edge.target)) {
3307
+ throw new Error(`Notebook edge references a missing cell: ${edge.id}`);
3308
+ }
3309
+ incoming.set(edge.target, (incoming.get(edge.target) ?? 0) + 1);
3310
+ const targets = outgoing.get(edge.source) ?? [];
3311
+ targets.push(edge.target);
3312
+ outgoing.set(edge.source, targets);
3313
+ }
3314
+ const ready = notebook.cells
3315
+ .filter((cell) => incoming.get(cell.id) === 0)
3316
+ .map((cell) => cell.id);
3317
+ const ordered = [];
3318
+ while (ready.length > 0) {
3319
+ const id = ready.shift();
3320
+ if (!id)
3321
+ continue;
3322
+ const cell = cells.get(id);
3323
+ if (cell)
3324
+ ordered.push(cell);
3325
+ for (const target of outgoing.get(id) ?? []) {
3326
+ const count = (incoming.get(target) ?? 0) - 1;
3327
+ incoming.set(target, count);
3328
+ if (count === 0)
3329
+ ready.push(target);
3330
+ }
3331
+ }
3332
+ if (ordered.length !== notebook.cells.length)
3333
+ throw new Error("Notebook graph contains a cycle");
3334
+ return ordered;
3335
+ }