tulingcode 0.1.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 (1119) hide show
  1. package/AGENTS.md +134 -0
  2. package/Dockerfile +18 -0
  3. package/README.md +15 -0
  4. package/bin/tuling +179 -0
  5. package/bunfig.toml +7 -0
  6. package/drizzle.config.ts +10 -0
  7. package/git +0 -0
  8. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  9. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  10. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  11. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  12. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  13. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  14. package/migration/20260225215848_workspace/migration.sql +7 -0
  15. package/migration/20260225215848_workspace/snapshot.json +959 -0
  16. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  17. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  18. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  19. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  20. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  21. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  22. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  23. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  24. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  25. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  26. package/migration/20260323234822_events/migration.sql +13 -0
  27. package/migration/20260323234822_events/snapshot.json +1271 -0
  28. package/migration/20260410174513_workspace-name/migration.sql +16 -0
  29. package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
  30. package/migration/20260413175956_chief_energizer/migration.sql +13 -0
  31. package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
  32. package/migration/20260422160000_context_inheritance/migration.sql +3 -0
  33. package/migration/20260422170000_task_registry/migration.sql +18 -0
  34. package/migration/20260423145421_remove_session_entry/migration.sql +4 -0
  35. package/migration/20260515000000_actor_rename/migration.sql +7 -0
  36. package/migration/20260515010000_memory_fts/migration.sql +33 -0
  37. package/migration/20260515020000_user_task/migration.sql +29 -0
  38. package/migration/20260519000000_last_checkpoint_message_id/migration.sql +1 -0
  39. package/migration/20260521000000_message_agent_id/migration.sql +2 -0
  40. package/migration/20260521000100_actor_registry_v6/migration.sql +25 -0
  41. package/migration/20260521010000_memory_fts_v6/migration.sql +33 -0
  42. package/migration/20260521020000_memory_fts_triggers/migration.sql +17 -0
  43. package/migration/20260526000000_agent_id_main/migration.sql +14 -0
  44. package/migration/20260527000000_actor_lifecycle/migration.sql +8 -0
  45. package/migration/20260527000100_inbox/migration.sql +12 -0
  46. package/migration/20260529000000_task_todo_redesign/migration.sql +16 -0
  47. package/migration/20260603000000_task_in_progress_owner/migration.sql +1 -0
  48. package/migration/20260603000000_workflow_run/migration.sql +17 -0
  49. package/migration/20260604000000_workflow_script_sha/migration.sql +1 -0
  50. package/migration/20260608000000_claude_import/migration.sql +7 -0
  51. package/migration/20260608010000_claude_import_message_ids/migration.sql +1 -0
  52. package/migration/20260609000000_history_fts/migration.sql +29 -0
  53. package/migration/20260609230000_workflow_agent_timeout/migration.sql +1 -0
  54. package/package.json +196 -0
  55. package/parsers-config.ts +290 -0
  56. package/script/build.ts +267 -0
  57. package/script/check-migrations.ts +16 -0
  58. package/script/fix-node-pty.ts +28 -0
  59. package/script/generate.ts +23 -0
  60. package/script/postinstall.mjs +102 -0
  61. package/script/publish.ts +60 -0
  62. package/script/run-workspace-server +106 -0
  63. package/script/schema.ts +63 -0
  64. package/script/time.ts +6 -0
  65. package/script/trace-imports.ts +153 -0
  66. package/script/upgrade-opentui.ts +64 -0
  67. package/src/account/account.sql.ts +39 -0
  68. package/src/account/account.ts +456 -0
  69. package/src/account/repo.ts +166 -0
  70. package/src/account/schema.ts +99 -0
  71. package/src/account/url.ts +8 -0
  72. package/src/acp/README.md +174 -0
  73. package/src/acp/agent.ts +1783 -0
  74. package/src/acp/session.ts +116 -0
  75. package/src/acp/types.ts +24 -0
  76. package/src/actor/actor.sql.ts +38 -0
  77. package/src/actor/events.ts +67 -0
  78. package/src/actor/index.ts +2 -0
  79. package/src/actor/registry.ts +412 -0
  80. package/src/actor/return-header.ts +24 -0
  81. package/src/actor/schema.ts +47 -0
  82. package/src/actor/spawn-ref.ts +16 -0
  83. package/src/actor/spawn.ts +741 -0
  84. package/src/actor/turn.ts +49 -0
  85. package/src/actor/waiter.ts +166 -0
  86. package/src/agent/agent.ts +554 -0
  87. package/src/agent/config.ts +5 -0
  88. package/src/agent/generate.txt +75 -0
  89. package/src/agent/prompt/checkpoint-writer.txt +167 -0
  90. package/src/agent/prompt/compaction.txt +9 -0
  91. package/src/agent/prompt/distill.txt +199 -0
  92. package/src/agent/prompt/dream.txt +155 -0
  93. package/src/agent/prompt/explore.txt +18 -0
  94. package/src/agent/prompt/summary.txt +11 -0
  95. package/src/agent/prompt/title.txt +44 -0
  96. package/src/audio.d.ts +9 -0
  97. package/src/auth/index.ts +97 -0
  98. package/src/bus/bus-event.ts +33 -0
  99. package/src/bus/global.ts +12 -0
  100. package/src/bus/index.ts +193 -0
  101. package/src/cli/bootstrap.ts +33 -0
  102. package/src/cli/cmd/account.ts +258 -0
  103. package/src/cli/cmd/acp.ts +70 -0
  104. package/src/cli/cmd/agent.ts +248 -0
  105. package/src/cli/cmd/cmd.ts +7 -0
  106. package/src/cli/cmd/db.ts +120 -0
  107. package/src/cli/cmd/debug/agent.ts +192 -0
  108. package/src/cli/cmd/debug/config.ts +17 -0
  109. package/src/cli/cmd/debug/file.ts +100 -0
  110. package/src/cli/cmd/debug/index.ts +48 -0
  111. package/src/cli/cmd/debug/lsp.ts +61 -0
  112. package/src/cli/cmd/debug/ripgrep.ts +105 -0
  113. package/src/cli/cmd/debug/scrap.ts +16 -0
  114. package/src/cli/cmd/debug/skill.ts +23 -0
  115. package/src/cli/cmd/debug/snapshot.ts +53 -0
  116. package/src/cli/cmd/export.ts +306 -0
  117. package/src/cli/cmd/generate.ts +50 -0
  118. package/src/cli/cmd/github.ts +1647 -0
  119. package/src/cli/cmd/import.ts +208 -0
  120. package/src/cli/cmd/mcp.ts +812 -0
  121. package/src/cli/cmd/models.ts +88 -0
  122. package/src/cli/cmd/plug.ts +233 -0
  123. package/src/cli/cmd/pr.ts +138 -0
  124. package/src/cli/cmd/providers.ts +705 -0
  125. package/src/cli/cmd/run-completion.ts +77 -0
  126. package/src/cli/cmd/run.ts +694 -0
  127. package/src/cli/cmd/serve.ts +21 -0
  128. package/src/cli/cmd/session.ts +181 -0
  129. package/src/cli/cmd/stats.ts +413 -0
  130. package/src/cli/cmd/tui/app.tsx +1130 -0
  131. package/src/cli/cmd/tui/asset/TEN_VAD_LICENSE +12 -0
  132. package/src/cli/cmd/tui/asset/charge.wav +0 -0
  133. package/src/cli/cmd/tui/asset/pulse-a.wav +0 -0
  134. package/src/cli/cmd/tui/asset/pulse-b.wav +0 -0
  135. package/src/cli/cmd/tui/asset/pulse-c.wav +0 -0
  136. package/src/cli/cmd/tui/asset/ten_vad.wasm +0 -0
  137. package/src/cli/cmd/tui/asset/ten_vad_loader.js +30 -0
  138. package/src/cli/cmd/tui/attach.ts +83 -0
  139. package/src/cli/cmd/tui/component/background-image.tsx +150 -0
  140. package/src/cli/cmd/tui/component/bg-pulse.tsx +130 -0
  141. package/src/cli/cmd/tui/component/border.tsx +21 -0
  142. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  143. package/src/cli/cmd/tui/component/dialog-command.tsx +208 -0
  144. package/src/cli/cmd/tui/component/dialog-console-org.tsx +103 -0
  145. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +157 -0
  146. package/src/cli/cmd/tui/component/dialog-image-list.tsx +111 -0
  147. package/src/cli/cmd/tui/component/dialog-logo-design.tsx +37 -0
  148. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  149. package/src/cli/cmd/tui/component/dialog-mimo-login.tsx +224 -0
  150. package/src/cli/cmd/tui/component/dialog-model.tsx +253 -0
  151. package/src/cli/cmd/tui/component/dialog-provider.tsx +490 -0
  152. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +101 -0
  153. package/src/cli/cmd/tui/component/dialog-session-list.tsx +269 -0
  154. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  155. package/src/cli/cmd/tui/component/dialog-skill.tsx +42 -0
  156. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  157. package/src/cli/cmd/tui/component/dialog-status.tsx +170 -0
  158. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  159. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  160. package/src/cli/cmd/tui/component/dialog-variant.tsx +39 -0
  161. package/src/cli/cmd/tui/component/dialog-workflows.tsx +62 -0
  162. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +289 -0
  163. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +81 -0
  164. package/src/cli/cmd/tui/component/dialog-worktree.tsx +90 -0
  165. package/src/cli/cmd/tui/component/error-component.tsx +92 -0
  166. package/src/cli/cmd/tui/component/logo.tsx +961 -0
  167. package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
  168. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +684 -0
  169. package/src/cli/cmd/tui/component/prompt/cwd.ts +0 -0
  170. package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
  171. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  172. package/src/cli/cmd/tui/component/prompt/index.tsx +1812 -0
  173. package/src/cli/cmd/tui/component/prompt/part.ts +16 -0
  174. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  175. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  176. package/src/cli/cmd/tui/component/starry-background.tsx +305 -0
  177. package/src/cli/cmd/tui/component/startup-loading.tsx +67 -0
  178. package/src/cli/cmd/tui/component/task-item.tsx +63 -0
  179. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  180. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  181. package/src/cli/cmd/tui/config/cwd.ts +5 -0
  182. package/src/cli/cmd/tui/config/tui-migrate.ts +151 -0
  183. package/src/cli/cmd/tui/config/tui-schema.ts +38 -0
  184. package/src/cli/cmd/tui/config/tui.ts +219 -0
  185. package/src/cli/cmd/tui/context/args.tsx +16 -0
  186. package/src/cli/cmd/tui/context/directory.ts +15 -0
  187. package/src/cli/cmd/tui/context/event.ts +45 -0
  188. package/src/cli/cmd/tui/context/exit.tsx +65 -0
  189. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  190. package/src/cli/cmd/tui/context/keybind.tsx +105 -0
  191. package/src/cli/cmd/tui/context/kv.tsx +76 -0
  192. package/src/cli/cmd/tui/context/language.tsx +91 -0
  193. package/src/cli/cmd/tui/context/local.tsx +455 -0
  194. package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
  195. package/src/cli/cmd/tui/context/project.tsx +109 -0
  196. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  197. package/src/cli/cmd/tui/context/route.tsx +61 -0
  198. package/src/cli/cmd/tui/context/sdk.tsx +150 -0
  199. package/src/cli/cmd/tui/context/sync.tsx +828 -0
  200. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  201. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  202. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  203. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +230 -0
  204. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +230 -0
  205. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  206. package/src/cli/cmd/tui/context/theme/cobalt2.json +225 -0
  207. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  208. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  209. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  210. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  211. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  212. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  213. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  214. package/src/cli/cmd/tui/context/theme/lucent-orng.json +234 -0
  215. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  216. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  217. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  218. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  219. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  220. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  221. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  222. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  223. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  224. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  225. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  226. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  227. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  228. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  229. package/src/cli/cmd/tui/context/theme/tulingcode.json +245 -0
  230. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  231. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  232. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  233. package/src/cli/cmd/tui/context/theme.tsx +1298 -0
  234. package/src/cli/cmd/tui/context/thinking.ts +48 -0
  235. package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
  236. package/src/cli/cmd/tui/event.ts +56 -0
  237. package/src/cli/cmd/tui/feature-plugins/home/footer.tsx +93 -0
  238. package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +193 -0
  239. package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +54 -0
  240. package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +114 -0
  241. package/src/cli/cmd/tui/feature-plugins/sidebar/cwd.tsx +45 -0
  242. package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +62 -0
  243. package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +93 -0
  244. package/src/cli/cmd/tui/feature-plugins/sidebar/goal.tsx +84 -0
  245. package/src/cli/cmd/tui/feature-plugins/sidebar/instructions.tsx +54 -0
  246. package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +66 -0
  247. package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +98 -0
  248. package/src/cli/cmd/tui/feature-plugins/sidebar/task.tsx +95 -0
  249. package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +51 -0
  250. package/src/cli/cmd/tui/feature-plugins/sidebar/tps.ts +31 -0
  251. package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +274 -0
  252. package/src/cli/cmd/tui/i18n/en.ts +397 -0
  253. package/src/cli/cmd/tui/i18n/es.ts +433 -0
  254. package/src/cli/cmd/tui/i18n/fr.ts +440 -0
  255. package/src/cli/cmd/tui/i18n/ja.ts +392 -0
  256. package/src/cli/cmd/tui/i18n/locales.ts +82 -0
  257. package/src/cli/cmd/tui/i18n/ru.ts +452 -0
  258. package/src/cli/cmd/tui/i18n/zh.ts +390 -0
  259. package/src/cli/cmd/tui/i18n/zht.ts +360 -0
  260. package/src/cli/cmd/tui/layer.ts +6 -0
  261. package/src/cli/cmd/tui/plugin/api.tsx +402 -0
  262. package/src/cli/cmd/tui/plugin/index.ts +3 -0
  263. package/src/cli/cmd/tui/plugin/internal.ts +35 -0
  264. package/src/cli/cmd/tui/plugin/runtime.ts +1030 -0
  265. package/src/cli/cmd/tui/plugin/slots.tsx +60 -0
  266. package/src/cli/cmd/tui/routes/home.tsx +165 -0
  267. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  268. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +116 -0
  269. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +47 -0
  270. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  271. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  272. package/src/cli/cmd/tui/routes/session/index.tsx +2532 -0
  273. package/src/cli/cmd/tui/routes/session/permission.tsx +691 -0
  274. package/src/cli/cmd/tui/routes/session/question.tsx +488 -0
  275. package/src/cli/cmd/tui/routes/session/sidebar.tsx +97 -0
  276. package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +142 -0
  277. package/src/cli/cmd/tui/thread.ts +246 -0
  278. package/src/cli/cmd/tui/ui/dialog-alert.tsx +61 -0
  279. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +95 -0
  280. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +223 -0
  281. package/src/cli/cmd/tui/ui/dialog-help.tsx +42 -0
  282. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +123 -0
  283. package/src/cli/cmd/tui/ui/dialog-select.tsx +452 -0
  284. package/src/cli/cmd/tui/ui/dialog.tsx +207 -0
  285. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  286. package/src/cli/cmd/tui/ui/spinner.ts +378 -0
  287. package/src/cli/cmd/tui/ui/toast.tsx +102 -0
  288. package/src/cli/cmd/tui/util/clipboard.ts +203 -0
  289. package/src/cli/cmd/tui/util/editor.ts +35 -0
  290. package/src/cli/cmd/tui/util/image-protocol.ts +35 -0
  291. package/src/cli/cmd/tui/util/index.ts +6 -0
  292. package/src/cli/cmd/tui/util/model.ts +23 -0
  293. package/src/cli/cmd/tui/util/provider-origin.ts +7 -0
  294. package/src/cli/cmd/tui/util/revert-diff.ts +18 -0
  295. package/src/cli/cmd/tui/util/scroll.ts +23 -0
  296. package/src/cli/cmd/tui/util/selection.ts +23 -0
  297. package/src/cli/cmd/tui/util/signal.ts +41 -0
  298. package/src/cli/cmd/tui/util/sound.ts +154 -0
  299. package/src/cli/cmd/tui/util/system-locale.ts +209 -0
  300. package/src/cli/cmd/tui/util/terminal.ts +110 -0
  301. package/src/cli/cmd/tui/util/transcript.ts +112 -0
  302. package/src/cli/cmd/tui/util/vad.ts +229 -0
  303. package/src/cli/cmd/tui/util/voice.ts +360 -0
  304. package/src/cli/cmd/tui/win32.ts +130 -0
  305. package/src/cli/cmd/tui/worker.ts +104 -0
  306. package/src/cli/cmd/uninstall.ts +351 -0
  307. package/src/cli/cmd/upgrade.ts +79 -0
  308. package/src/cli/cmd/web.ts +81 -0
  309. package/src/cli/effect/prompt.ts +25 -0
  310. package/src/cli/error.ts +82 -0
  311. package/src/cli/heap.ts +59 -0
  312. package/src/cli/i18n.ts +15 -0
  313. package/src/cli/logo.ts +53 -0
  314. package/src/cli/network.ts +62 -0
  315. package/src/cli/ui.ts +133 -0
  316. package/src/cli/upgrade.ts +41 -0
  317. package/src/command/index.ts +276 -0
  318. package/src/command/template/initialize.txt +66 -0
  319. package/src/command/template/review.txt +101 -0
  320. package/src/config/agent.ts +197 -0
  321. package/src/config/command.ts +69 -0
  322. package/src/config/config.ts +1024 -0
  323. package/src/config/console-state.ts +16 -0
  324. package/src/config/entry-name.ts +16 -0
  325. package/src/config/error.ts +21 -0
  326. package/src/config/formatter.ts +17 -0
  327. package/src/config/history.ts +21 -0
  328. package/src/config/index.ts +16 -0
  329. package/src/config/keybinds.ts +127 -0
  330. package/src/config/layout.ts +10 -0
  331. package/src/config/lsp.ts +45 -0
  332. package/src/config/managed.ts +70 -0
  333. package/src/config/markdown.ts +97 -0
  334. package/src/config/mcp.ts +172 -0
  335. package/src/config/model-id.ts +14 -0
  336. package/src/config/parse.ts +44 -0
  337. package/src/config/paths.ts +73 -0
  338. package/src/config/permission.ts +76 -0
  339. package/src/config/plugin.ts +88 -0
  340. package/src/config/provider.ts +118 -0
  341. package/src/config/server.ts +20 -0
  342. package/src/config/skills.ts +16 -0
  343. package/src/config/variable.ts +90 -0
  344. package/src/control-plane/adaptors/index.ts +52 -0
  345. package/src/control-plane/adaptors/worktree.ts +47 -0
  346. package/src/control-plane/dev/debug-workspace-plugin.ts +73 -0
  347. package/src/control-plane/schema.ts +19 -0
  348. package/src/control-plane/sse.ts +66 -0
  349. package/src/control-plane/types.ts +34 -0
  350. package/src/control-plane/util.ts +37 -0
  351. package/src/control-plane/workspace-context.ts +26 -0
  352. package/src/control-plane/workspace.sql.ts +17 -0
  353. package/src/control-plane/workspace.ts +615 -0
  354. package/src/effect/app-runtime.ts +146 -0
  355. package/src/effect/bootstrap-runtime.ts +33 -0
  356. package/src/effect/bridge.ts +48 -0
  357. package/src/effect/cross-spawn-spawner.ts +514 -0
  358. package/src/effect/index.ts +5 -0
  359. package/src/effect/instance-ref.ts +11 -0
  360. package/src/effect/instance-registry.ts +12 -0
  361. package/src/effect/instance-state.ts +81 -0
  362. package/src/effect/logger.ts +73 -0
  363. package/src/effect/memo-map.ts +3 -0
  364. package/src/effect/observability.ts +107 -0
  365. package/src/effect/run-service.ts +52 -0
  366. package/src/effect/runner.ts +210 -0
  367. package/src/effect/runtime.ts +19 -0
  368. package/src/env/index.ts +37 -0
  369. package/src/file/ignore.ts +81 -0
  370. package/src/file/index.ts +664 -0
  371. package/src/file/protected.ts +59 -0
  372. package/src/file/ripgrep.ts +485 -0
  373. package/src/file/watcher.ts +163 -0
  374. package/src/flag/flag.ts +164 -0
  375. package/src/format/formatter.ts +403 -0
  376. package/src/format/index.ts +203 -0
  377. package/src/git/index.ts +260 -0
  378. package/src/global/index.ts +54 -0
  379. package/src/history/backfill.ts +162 -0
  380. package/src/history/extract.ts +67 -0
  381. package/src/history/fts-query.ts +15 -0
  382. package/src/history/fts.sql.ts +20 -0
  383. package/src/history/index.ts +10 -0
  384. package/src/history/resolve.ts +65 -0
  385. package/src/history/service.ts +258 -0
  386. package/src/history/writer.ts +112 -0
  387. package/src/id/id.ts +87 -0
  388. package/src/ide/index.ts +73 -0
  389. package/src/inbox/inbox-ref.ts +38 -0
  390. package/src/inbox/inbox.sql.ts +26 -0
  391. package/src/inbox/inbox.ts +223 -0
  392. package/src/inbox/index.ts +3 -0
  393. package/src/inbox/render.ts +40 -0
  394. package/src/index.ts +260 -0
  395. package/src/installation/index.ts +351 -0
  396. package/src/installation/version.ts +8 -0
  397. package/src/lsp/client.ts +249 -0
  398. package/src/lsp/diagnostic.ts +29 -0
  399. package/src/lsp/index.ts +3 -0
  400. package/src/lsp/language.ts +120 -0
  401. package/src/lsp/launch.ts +21 -0
  402. package/src/lsp/lsp.ts +519 -0
  403. package/src/lsp/server.ts +1956 -0
  404. package/src/mcp/auth.ts +144 -0
  405. package/src/mcp/index.ts +944 -0
  406. package/src/mcp/oauth-callback.ts +232 -0
  407. package/src/mcp/oauth-provider.ts +214 -0
  408. package/src/memory/fts-query.ts +37 -0
  409. package/src/memory/fts.sql.ts +19 -0
  410. package/src/memory/index.ts +1 -0
  411. package/src/memory/paths.ts +116 -0
  412. package/src/memory/reconcile.ts +144 -0
  413. package/src/memory/service.ts +144 -0
  414. package/src/metrics/client.ts +40 -0
  415. package/src/metrics/event.ts +43 -0
  416. package/src/metrics/index.ts +5 -0
  417. package/src/metrics/installation.ts +18 -0
  418. package/src/metrics/subscriber.ts +58 -0
  419. package/src/metrics/util.ts +9 -0
  420. package/src/node.ts +6 -0
  421. package/src/npm/config.ts +0 -0
  422. package/src/npm/index.ts +293 -0
  423. package/src/npmcli-config.d.ts +43 -0
  424. package/src/patch/index.ts +680 -0
  425. package/src/permission/arity.ts +163 -0
  426. package/src/permission/evaluate.ts +15 -0
  427. package/src/permission/index.ts +379 -0
  428. package/src/permission/schema.ts +17 -0
  429. package/src/plugin/checkpoint-splitover.ts +60 -0
  430. package/src/plugin/cloud-ai.ts +329 -0
  431. package/src/plugin/cloudflare.ts +76 -0
  432. package/src/plugin/codex.ts +607 -0
  433. package/src/plugin/github-copilot/copilot.ts +368 -0
  434. package/src/plugin/github-copilot/models.ts +153 -0
  435. package/src/plugin/index.ts +493 -0
  436. package/src/plugin/install.ts +439 -0
  437. package/src/plugin/loader.ts +216 -0
  438. package/src/plugin/matcher.ts +33 -0
  439. package/src/plugin/meta.ts +188 -0
  440. package/src/plugin/mimo-free.ts +153 -0
  441. package/src/plugin/mimo.ts +124 -0
  442. package/src/plugin/shared.ts +323 -0
  443. package/src/plugin/subagent-progress-checker.ts +147 -0
  444. package/src/project/bootstrap.ts +59 -0
  445. package/src/project/index.ts +2 -0
  446. package/src/project/instance.ts +190 -0
  447. package/src/project/project-id.ts +48 -0
  448. package/src/project/project.sql.ts +16 -0
  449. package/src/project/project.ts +501 -0
  450. package/src/project/schema.ts +15 -0
  451. package/src/project/vcs.ts +227 -0
  452. package/src/provider/auth.ts +234 -0
  453. package/src/provider/error.ts +216 -0
  454. package/src/provider/index.ts +5 -0
  455. package/src/provider/models.ts +180 -0
  456. package/src/provider/provider.ts +1782 -0
  457. package/src/provider/schema.ts +36 -0
  458. package/src/provider/sdk/copilot/README.md +5 -0
  459. package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  460. package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  461. package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  462. package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  463. package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  464. package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  465. package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  466. package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  467. package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  468. package/src/provider/sdk/copilot/index.ts +2 -0
  469. package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  470. package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +335 -0
  471. package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  472. package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  473. package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  474. package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +214 -0
  475. package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1770 -0
  476. package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +173 -0
  477. package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  478. package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +87 -0
  479. package/src/provider/sdk/copilot/responses/tool/file-search.ts +127 -0
  480. package/src/provider/sdk/copilot/responses/tool/image-generation.ts +114 -0
  481. package/src/provider/sdk/copilot/responses/tool/local-shell.ts +64 -0
  482. package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +103 -0
  483. package/src/provider/sdk/copilot/responses/tool/web-search.ts +102 -0
  484. package/src/provider/transform.ts +1322 -0
  485. package/src/pty/index.ts +364 -0
  486. package/src/pty/pty.bun.ts +26 -0
  487. package/src/pty/pty.node.ts +27 -0
  488. package/src/pty/pty.ts +25 -0
  489. package/src/pty/schema.ts +17 -0
  490. package/src/question/index.ts +252 -0
  491. package/src/question/schema.ts +17 -0
  492. package/src/server/adapter.bun.ts +40 -0
  493. package/src/server/adapter.node.ts +66 -0
  494. package/src/server/adapter.ts +21 -0
  495. package/src/server/error.ts +53 -0
  496. package/src/server/event.ts +7 -0
  497. package/src/server/fence.ts +81 -0
  498. package/src/server/mdns.ts +60 -0
  499. package/src/server/middleware.ts +92 -0
  500. package/src/server/projectors.ts +28 -0
  501. package/src/server/proxy.ts +171 -0
  502. package/src/server/routes/control/index.ts +160 -0
  503. package/src/server/routes/control/workspace.ts +203 -0
  504. package/src/server/routes/global.ts +287 -0
  505. package/src/server/routes/instance/bash-interactive.ts +82 -0
  506. package/src/server/routes/instance/config.ts +89 -0
  507. package/src/server/routes/instance/event.ts +88 -0
  508. package/src/server/routes/instance/experimental.ts +408 -0
  509. package/src/server/routes/instance/file.ts +190 -0
  510. package/src/server/routes/instance/httpapi/config.ts +51 -0
  511. package/src/server/routes/instance/httpapi/permission.ts +72 -0
  512. package/src/server/routes/instance/httpapi/project.ts +62 -0
  513. package/src/server/routes/instance/httpapi/provider.ts +142 -0
  514. package/src/server/routes/instance/httpapi/question.ts +121 -0
  515. package/src/server/routes/instance/httpapi/server.ts +136 -0
  516. package/src/server/routes/instance/index.ts +301 -0
  517. package/src/server/routes/instance/mcp.ts +260 -0
  518. package/src/server/routes/instance/middleware.ts +35 -0
  519. package/src/server/routes/instance/permission.ts +73 -0
  520. package/src/server/routes/instance/project.ts +122 -0
  521. package/src/server/routes/instance/provider.ts +158 -0
  522. package/src/server/routes/instance/pty.ts +247 -0
  523. package/src/server/routes/instance/question.ts +162 -0
  524. package/src/server/routes/instance/session.ts +1296 -0
  525. package/src/server/routes/instance/sync.ts +143 -0
  526. package/src/server/routes/instance/trace.ts +59 -0
  527. package/src/server/routes/instance/tui.ts +384 -0
  528. package/src/server/routes/instance/workflows.ts +72 -0
  529. package/src/server/routes/ui.ts +55 -0
  530. package/src/server/server.ts +136 -0
  531. package/src/server/workspace.ts +122 -0
  532. package/src/session/auto-dream.ts +123 -0
  533. package/src/session/boundary.ts +77 -0
  534. package/src/session/budgeted-read.ts +118 -0
  535. package/src/session/checkpoint-align.ts +29 -0
  536. package/src/session/checkpoint-context.ts +36 -0
  537. package/src/session/checkpoint-paths.ts +86 -0
  538. package/src/session/checkpoint-progress-reconcile.ts +111 -0
  539. package/src/session/checkpoint-retry.ts +192 -0
  540. package/src/session/checkpoint-templates.ts +114 -0
  541. package/src/session/checkpoint-validator.ts +259 -0
  542. package/src/session/checkpoint.ts +1478 -0
  543. package/src/session/classify.ts +92 -0
  544. package/src/session/claude-import.sql.ts +13 -0
  545. package/src/session/claude-import.ts +379 -0
  546. package/src/session/compaction.ts +543 -0
  547. package/src/session/goal.ts +232 -0
  548. package/src/session/index.ts +1 -0
  549. package/src/session/instruction.ts +276 -0
  550. package/src/session/last-message-info.ts +32 -0
  551. package/src/session/llm-request-prefix.ts +82 -0
  552. package/src/session/llm.ts +735 -0
  553. package/src/session/max-mode.ts +397 -0
  554. package/src/session/message-v2.ts +1136 -0
  555. package/src/session/message.ts +191 -0
  556. package/src/session/overflow.ts +53 -0
  557. package/src/session/prefix-capture-ref.ts +48 -0
  558. package/src/session/processor.ts +962 -0
  559. package/src/session/projectors.ts +137 -0
  560. package/src/session/prompt/anthropic.txt +154 -0
  561. package/src/session/prompt/beast.txt +155 -0
  562. package/src/session/prompt/build-switch.txt +5 -0
  563. package/src/session/prompt/codex.txt +79 -0
  564. package/src/session/prompt/compose.txt +115 -0
  565. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  566. package/src/session/prompt/default.txt +151 -0
  567. package/src/session/prompt/gemini.txt +155 -0
  568. package/src/session/prompt/gpt.txt +107 -0
  569. package/src/session/prompt/kimi.txt +95 -0
  570. package/src/session/prompt/max-steps.txt +16 -0
  571. package/src/session/prompt/trinity.txt +97 -0
  572. package/src/session/prompt.ts +3355 -0
  573. package/src/session/prune.ts +481 -0
  574. package/src/session/retry.ts +166 -0
  575. package/src/session/revert.ts +161 -0
  576. package/src/session/run-state.ts +135 -0
  577. package/src/session/schema.ts +36 -0
  578. package/src/session/session.sql.ts +110 -0
  579. package/src/session/session.ts +908 -0
  580. package/src/session/status.ts +89 -0
  581. package/src/session/summary.ts +163 -0
  582. package/src/session/system.ts +86 -0
  583. package/src/session/todo.ts +77 -0
  584. package/src/share/index.ts +2 -0
  585. package/src/share/session.ts +57 -0
  586. package/src/share/share-next.ts +381 -0
  587. package/src/share/share.sql.ts +13 -0
  588. package/src/shell/shell.ts +110 -0
  589. package/src/skill/compose/.bundle/ask/SKILL.md +58 -0
  590. package/src/skill/compose/.bundle/brainstorm/SKILL.md +220 -0
  591. package/src/skill/compose/.bundle/brainstorm/scripts/frame-template.html +214 -0
  592. package/src/skill/compose/.bundle/brainstorm/scripts/helper.js +88 -0
  593. package/src/skill/compose/.bundle/brainstorm/scripts/server.cjs +354 -0
  594. package/src/skill/compose/.bundle/brainstorm/scripts/start-server.sh +148 -0
  595. package/src/skill/compose/.bundle/brainstorm/scripts/stop-server.sh +56 -0
  596. package/src/skill/compose/.bundle/brainstorm/spec-document-reviewer-prompt.md +50 -0
  597. package/src/skill/compose/.bundle/brainstorm/visual-companion.md +287 -0
  598. package/src/skill/compose/.bundle/debug/CREATION-LOG.md +119 -0
  599. package/src/skill/compose/.bundle/debug/SKILL.md +297 -0
  600. package/src/skill/compose/.bundle/debug/condition-based-waiting-example.ts +158 -0
  601. package/src/skill/compose/.bundle/debug/condition-based-waiting.md +115 -0
  602. package/src/skill/compose/.bundle/debug/defense-in-depth.md +122 -0
  603. package/src/skill/compose/.bundle/debug/find-polluter.sh +63 -0
  604. package/src/skill/compose/.bundle/debug/root-cause-tracing.md +169 -0
  605. package/src/skill/compose/.bundle/debug/test-academic.md +14 -0
  606. package/src/skill/compose/.bundle/debug/test-pressure-1.md +58 -0
  607. package/src/skill/compose/.bundle/debug/test-pressure-2.md +68 -0
  608. package/src/skill/compose/.bundle/debug/test-pressure-3.md +69 -0
  609. package/src/skill/compose/.bundle/execute/SKILL.md +71 -0
  610. package/src/skill/compose/.bundle/feedback/SKILL.md +214 -0
  611. package/src/skill/compose/.bundle/merge/SKILL.md +252 -0
  612. package/src/skill/compose/.bundle/new-skill/SKILL.md +656 -0
  613. package/src/skill/compose/.bundle/new-skill/anthropic-best-practices.md +1150 -0
  614. package/src/skill/compose/.bundle/new-skill/examples/CLAUDE_MD_TESTING.md +189 -0
  615. package/src/skill/compose/.bundle/new-skill/graphviz-conventions.dot +172 -0
  616. package/src/skill/compose/.bundle/new-skill/persuasion-principles.md +187 -0
  617. package/src/skill/compose/.bundle/new-skill/render-graphs.js +168 -0
  618. package/src/skill/compose/.bundle/new-skill/testing-skills-with-subagents.md +384 -0
  619. package/src/skill/compose/.bundle/parallel/SKILL.md +182 -0
  620. package/src/skill/compose/.bundle/plan/SKILL.md +161 -0
  621. package/src/skill/compose/.bundle/plan/plan-document-reviewer-prompt.md +50 -0
  622. package/src/skill/compose/.bundle/report/SKILL.md +180 -0
  623. package/src/skill/compose/.bundle/review/SKILL.md +104 -0
  624. package/src/skill/compose/.bundle/review/code-reviewer.md +171 -0
  625. package/src/skill/compose/.bundle/subagent/SKILL.md +344 -0
  626. package/src/skill/compose/.bundle/subagent/code-quality-reviewer-prompt.md +24 -0
  627. package/src/skill/compose/.bundle/subagent/implementer-prompt.md +126 -0
  628. package/src/skill/compose/.bundle/subagent/spec-reviewer-prompt.md +112 -0
  629. package/src/skill/compose/.bundle/tdd/SKILL.md +372 -0
  630. package/src/skill/compose/.bundle/tdd/testing-anti-patterns.md +299 -0
  631. package/src/skill/compose/.bundle/verify/SKILL.md +140 -0
  632. package/src/skill/compose/.bundle/worktree/SKILL.md +234 -0
  633. package/src/skill/compose/LICENSE-karpathy +28 -0
  634. package/src/skill/compose/LICENSE-superpowers +26 -0
  635. package/src/skill/compose/bundle.macro.ts +30 -0
  636. package/src/skill/compose/extract.ts +85 -0
  637. package/src/skill/discovery.ts +116 -0
  638. package/src/skill/index.ts +311 -0
  639. package/src/snapshot/index.ts +777 -0
  640. package/src/sql.d.ts +4 -0
  641. package/src/storage/db.bun.ts +8 -0
  642. package/src/storage/db.node.ts +8 -0
  643. package/src/storage/db.ts +172 -0
  644. package/src/storage/index.ts +26 -0
  645. package/src/storage/json-migration.ts +426 -0
  646. package/src/storage/schema.sql.ts +10 -0
  647. package/src/storage/schema.ts +7 -0
  648. package/src/storage/storage.ts +331 -0
  649. package/src/sync/README.md +179 -0
  650. package/src/sync/event.sql.ts +16 -0
  651. package/src/sync/index.ts +278 -0
  652. package/src/sync/schema.ts +14 -0
  653. package/src/task/events.ts +28 -0
  654. package/src/task/gate-state.ts +54 -0
  655. package/src/task/gate.ts +116 -0
  656. package/src/task/index.ts +1 -0
  657. package/src/task/registry.ts +387 -0
  658. package/src/task/schema.ts +43 -0
  659. package/src/task/task.sql.ts +50 -0
  660. package/src/team/events.ts +22 -0
  661. package/src/team/index.ts +113 -0
  662. package/src/team/schema.ts +31 -0
  663. package/src/temporary.ts +33 -0
  664. package/src/tool/actor.shell.txt +72 -0
  665. package/src/tool/actor.ts +803 -0
  666. package/src/tool/actor.txt +103 -0
  667. package/src/tool/apply_patch.ts +308 -0
  668. package/src/tool/apply_patch.txt +33 -0
  669. package/src/tool/bash-interactive.ts +183 -0
  670. package/src/tool/bash.ts +696 -0
  671. package/src/tool/bash.txt +123 -0
  672. package/src/tool/change-directory.ts +91 -0
  673. package/src/tool/codesearch.ts +63 -0
  674. package/src/tool/codesearch.txt +12 -0
  675. package/src/tool/edit.ts +685 -0
  676. package/src/tool/edit.txt +10 -0
  677. package/src/tool/external-directory.ts +132 -0
  678. package/src/tool/glob.ts +100 -0
  679. package/src/tool/glob.txt +6 -0
  680. package/src/tool/grep.ts +145 -0
  681. package/src/tool/grep.txt +8 -0
  682. package/src/tool/history.ts +146 -0
  683. package/src/tool/history.txt +17 -0
  684. package/src/tool/index.ts +4 -0
  685. package/src/tool/invalid.ts +20 -0
  686. package/src/tool/invocation-style.ts +17 -0
  687. package/src/tool/lsp.ts +91 -0
  688. package/src/tool/lsp.txt +19 -0
  689. package/src/tool/mcp-exa.ts +78 -0
  690. package/src/tool/memory-path-guard.ts +162 -0
  691. package/src/tool/memory.ts +81 -0
  692. package/src/tool/memory.txt +69 -0
  693. package/src/tool/multiedit.ts +61 -0
  694. package/src/tool/multiedit.txt +41 -0
  695. package/src/tool/plan-enter.txt +14 -0
  696. package/src/tool/plan-exit.txt +13 -0
  697. package/src/tool/plan.ts +90 -0
  698. package/src/tool/question.ts +67 -0
  699. package/src/tool/question.txt +10 -0
  700. package/src/tool/read.ts +327 -0
  701. package/src/tool/read.txt +14 -0
  702. package/src/tool/registry.ts +413 -0
  703. package/src/tool/schema.ts +17 -0
  704. package/src/tool/session-cwd.ts +35 -0
  705. package/src/tool/shell-tokenize.ts +346 -0
  706. package/src/tool/shell-wrap.ts +190 -0
  707. package/src/tool/skill.ts +76 -0
  708. package/src/tool/skill.txt +5 -0
  709. package/src/tool/task.shell.txt +57 -0
  710. package/src/tool/task.ts +456 -0
  711. package/src/tool/task.txt +56 -0
  712. package/src/tool/tool.ts +153 -0
  713. package/src/tool/truncate.ts +201 -0
  714. package/src/tool/truncation-dir.ts +4 -0
  715. package/src/tool/webfetch.ts +199 -0
  716. package/src/tool/webfetch.txt +13 -0
  717. package/src/tool/websearch/index.ts +104 -0
  718. package/src/tool/websearch/mimo.ts +118 -0
  719. package/src/tool/websearch/websearch.txt +14 -0
  720. package/src/tool/workflow.ts +164 -0
  721. package/src/tool/workflow.txt +25 -0
  722. package/src/tool/write.ts +88 -0
  723. package/src/tool/write.txt +9 -0
  724. package/src/util/abort.ts +35 -0
  725. package/src/util/archive.ts +15 -0
  726. package/src/util/color.ts +17 -0
  727. package/src/util/data-url.ts +9 -0
  728. package/src/util/defer.ts +10 -0
  729. package/src/util/effect-http-client.ts +11 -0
  730. package/src/util/effect-zod.ts +367 -0
  731. package/src/util/error.ts +78 -0
  732. package/src/util/filesystem.ts +243 -0
  733. package/src/util/fn.ts +21 -0
  734. package/src/util/format.ts +20 -0
  735. package/src/util/iife.ts +3 -0
  736. package/src/util/index.ts +12 -0
  737. package/src/util/keybind.ts +101 -0
  738. package/src/util/lazy.ts +18 -0
  739. package/src/util/local-context.ts +23 -0
  740. package/src/util/locale.ts +79 -0
  741. package/src/util/lock.ts +96 -0
  742. package/src/util/log.ts +197 -0
  743. package/src/util/media.ts +26 -0
  744. package/src/util/mimo-process.ts +24 -0
  745. package/src/util/network.ts +9 -0
  746. package/src/util/process.ts +174 -0
  747. package/src/util/queue.ts +32 -0
  748. package/src/util/record.ts +3 -0
  749. package/src/util/rpc.ts +64 -0
  750. package/src/util/schema.ts +53 -0
  751. package/src/util/scrap.ts +10 -0
  752. package/src/util/signal.ts +12 -0
  753. package/src/util/timeout.ts +14 -0
  754. package/src/util/token.ts +5 -0
  755. package/src/util/update-schema.ts +13 -0
  756. package/src/util/which.ts +14 -0
  757. package/src/util/wildcard.ts +57 -0
  758. package/src/workflow/builtin/deep-research.js +391 -0
  759. package/src/workflow/builtin.ts +54 -0
  760. package/src/workflow/events.ts +72 -0
  761. package/src/workflow/meta.ts +335 -0
  762. package/src/workflow/persistence.ts +312 -0
  763. package/src/workflow/resolve.ts +45 -0
  764. package/src/workflow/runtime-ref.ts +18 -0
  765. package/src/workflow/runtime.ts +1234 -0
  766. package/src/workflow/sandbox.ts +280 -0
  767. package/src/workflow/workflow.sql.ts +31 -0
  768. package/src/workflow/workspace.ts +69 -0
  769. package/src/worktree/index.ts +614 -0
  770. package/sst-env.d.ts +10 -0
  771. package/test/AGENTS.md +133 -0
  772. package/test/account/repo.test.ts +352 -0
  773. package/test/account/service.test.ts +456 -0
  774. package/test/acp/agent-interface.test.ts +51 -0
  775. package/test/acp/event-subscription.test.ts +725 -0
  776. package/test/actor/cancel-cascade.test.ts +432 -0
  777. package/test/actor/no-completion-listener.test.ts +41 -0
  778. package/test/actor/poststop-progress-write-permission.repro.test.ts +414 -0
  779. package/test/actor/registry-render.test.ts +113 -0
  780. package/test/actor/registry-status.test.ts +111 -0
  781. package/test/actor/registry.test.ts +619 -0
  782. package/test/actor/return-header.test.ts +40 -0
  783. package/test/actor/spawn-lifecycle.test.ts +346 -0
  784. package/test/actor/spawn-no-deadlock.test.ts +340 -0
  785. package/test/actor/spawn-notification.test.ts +393 -0
  786. package/test/actor/spawn-task-autostart.test.ts +530 -0
  787. package/test/actor/spawn.test.ts +1072 -0
  788. package/test/actor/status-event-payload.test.ts +132 -0
  789. package/test/actor/terminology.test.ts +39 -0
  790. package/test/actor/turn.test.ts +125 -0
  791. package/test/actor/waiter.test.ts +246 -0
  792. package/test/agent/agent.test.ts +874 -0
  793. package/test/agent/allowlist.test.ts +45 -0
  794. package/test/auth/auth.test.ts +86 -0
  795. package/test/bus/bus-effect.test.ts +162 -0
  796. package/test/bus/bus-integration.test.ts +87 -0
  797. package/test/bus/bus.test.ts +219 -0
  798. package/test/cli/account.test.ts +26 -0
  799. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  800. package/test/cli/error.test.ts +18 -0
  801. package/test/cli/github-action.test.ts +198 -0
  802. package/test/cli/github-remote.test.ts +80 -0
  803. package/test/cli/import.test.ts +54 -0
  804. package/test/cli/plugin-auth-picker.test.ts +120 -0
  805. package/test/cli/run-completion.test.ts +131 -0
  806. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  807. package/test/cli/tui/plugin-add.test.ts +111 -0
  808. package/test/cli/tui/plugin-install.test.ts +87 -0
  809. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  810. package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
  811. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  812. package/test/cli/tui/plugin-loader.test.ts +816 -0
  813. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  814. package/test/cli/tui/revert-diff.test.ts +35 -0
  815. package/test/cli/tui/route-agent-id.test.ts +26 -0
  816. package/test/cli/tui/sidebar-tps.test.ts +63 -0
  817. package/test/cli/tui/slot-replace.test.tsx +47 -0
  818. package/test/cli/tui/sync-bucket.test.ts +29 -0
  819. package/test/cli/tui/theme-store.test.ts +51 -0
  820. package/test/cli/tui/thread.test.ts +121 -0
  821. package/test/cli/tui/transcript.test.ts +426 -0
  822. package/test/cli/tui/use-event.test.tsx +175 -0
  823. package/test/cli/tui/voice.test.ts +269 -0
  824. package/test/command/deep-research-command.test.ts +16 -0
  825. package/test/config/agent-color.test.ts +77 -0
  826. package/test/config/checkpoint-fork.test.ts +21 -0
  827. package/test/config/config.test.ts +2577 -0
  828. package/test/config/fixtures/empty-frontmatter.md +4 -0
  829. package/test/config/fixtures/frontmatter.md +28 -0
  830. package/test/config/fixtures/markdown-header.md +11 -0
  831. package/test/config/fixtures/no-frontmatter.md +1 -0
  832. package/test/config/fixtures/weird-model-id.md +13 -0
  833. package/test/config/lsp.test.ts +87 -0
  834. package/test/config/markdown.test.ts +228 -0
  835. package/test/config/plugin.test.ts +0 -0
  836. package/test/config/tui.test.ts +627 -0
  837. package/test/control-plane/adaptors.test.ts +71 -0
  838. package/test/control-plane/sse.test.ts +56 -0
  839. package/test/effect/app-runtime-logger.test.ts +92 -0
  840. package/test/effect/cross-spawn-spawner.test.ts +411 -0
  841. package/test/effect/instance-state.test.ts +482 -0
  842. package/test/effect/observability.test.ts +46 -0
  843. package/test/effect/run-service.test.ts +46 -0
  844. package/test/effect/runner-warn-log.test.ts +111 -0
  845. package/test/effect/runner.test.ts +494 -0
  846. package/test/fake/provider.ts +90 -0
  847. package/test/file/fsmonitor.test.ts +68 -0
  848. package/test/file/ignore.test.ts +10 -0
  849. package/test/file/index.test.ts +956 -0
  850. package/test/file/path-traversal.test.ts +204 -0
  851. package/test/file/ripgrep.test.ts +214 -0
  852. package/test/file/watcher.test.ts +249 -0
  853. package/test/filesystem/filesystem.test.ts +319 -0
  854. package/test/fixture/db.ts +11 -0
  855. package/test/fixture/fixture.test.ts +58 -0
  856. package/test/fixture/fixture.ts +190 -0
  857. package/test/fixture/flock-worker.ts +72 -0
  858. package/test/fixture/lsp/fake-lsp-server.js +75 -0
  859. package/test/fixture/plug-worker.ts +93 -0
  860. package/test/fixture/plugin-meta-worker.ts +19 -0
  861. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  862. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  863. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  864. package/test/fixture/skills/index.json +6 -0
  865. package/test/fixture/tui-plugin.ts +328 -0
  866. package/test/fixture/tui-runtime.ts +31 -0
  867. package/test/format/format.test.ts +244 -0
  868. package/test/git/git.test.ts +128 -0
  869. package/test/global/fixture/global-paths-worker.ts +17 -0
  870. package/test/global/mimocode-home.test.ts +143 -0
  871. package/test/history/backfill.test.ts +149 -0
  872. package/test/history/extract.test.ts +106 -0
  873. package/test/history/fts-query.test.ts +30 -0
  874. package/test/history/resolve.test.ts +130 -0
  875. package/test/history/service.test.ts +210 -0
  876. package/test/history/writer.test.ts +163 -0
  877. package/test/ide/ide.test.ts +82 -0
  878. package/test/inbox/drain-in-loop.test.ts +230 -0
  879. package/test/inbox/fork-agent-compat.test.ts +387 -0
  880. package/test/inbox/gc-on-init.test.ts +167 -0
  881. package/test/inbox/send-no-block.test.ts +120 -0
  882. package/test/inbox/sender-cancel-independence.test.ts +160 -0
  883. package/test/inbox/wake-matrix.test.ts +141 -0
  884. package/test/installation/installation.test.ts +226 -0
  885. package/test/keybind.test.ts +421 -0
  886. package/test/lib/effect.ts +53 -0
  887. package/test/lib/filesystem.ts +10 -0
  888. package/test/lib/llm-server.ts +770 -0
  889. package/test/lib/scripted-llm-server.ts +245 -0
  890. package/test/lsp/client.test.ts +98 -0
  891. package/test/lsp/index.test.ts +109 -0
  892. package/test/lsp/launch.test.ts +22 -0
  893. package/test/lsp/lifecycle.test.ts +184 -0
  894. package/test/mcp/headers.test.ts +178 -0
  895. package/test/mcp/lifecycle.test.ts +824 -0
  896. package/test/mcp/oauth-auto-connect.test.ts +281 -0
  897. package/test/mcp/oauth-browser.test.ts +268 -0
  898. package/test/mcp/oauth-callback.test.ts +34 -0
  899. package/test/memory/abort-leak-webfetch.ts +49 -0
  900. package/test/memory/abort-leak.test.ts +127 -0
  901. package/test/memory/cc-frontmatter.test.ts +85 -0
  902. package/test/memory/cc-paths.test.ts +60 -0
  903. package/test/memory/cc-reconcile.test.ts +239 -0
  904. package/test/memory/cc-search.test.ts +151 -0
  905. package/test/memory/fts-query.test.ts +48 -0
  906. package/test/memory/fts-rowid-stability.test.ts +271 -0
  907. package/test/memory/paths.test.ts +210 -0
  908. package/test/memory/reconcile.test.ts +115 -0
  909. package/test/memory/service.test.ts +169 -0
  910. package/test/npm.test.ts +18 -0
  911. package/test/patch/patch.test.ts +348 -0
  912. package/test/permission/abort.test.ts +116 -0
  913. package/test/permission/arity.test.ts +33 -0
  914. package/test/permission/disabled.test.ts +51 -0
  915. package/test/permission/next.test.ts +1080 -0
  916. package/test/permission/non-interactive.test.ts +55 -0
  917. package/test/permission-task.test.ts +326 -0
  918. package/test/plugin/actor-hooks.test.ts +1471 -0
  919. package/test/plugin/auth-override.test.ts +79 -0
  920. package/test/plugin/checkpoint-splitover.test.ts +434 -0
  921. package/test/plugin/cloudflare.test.ts +68 -0
  922. package/test/plugin/codex.test.ts +123 -0
  923. package/test/plugin/github-copilot-models.test.ts +163 -0
  924. package/test/plugin/install-concurrency.test.ts +140 -0
  925. package/test/plugin/install.test.ts +570 -0
  926. package/test/plugin/loader-shared.test.ts +1169 -0
  927. package/test/plugin/matcher.test.ts +97 -0
  928. package/test/plugin/meta.test.ts +137 -0
  929. package/test/plugin/mimo.test.ts +257 -0
  930. package/test/plugin/shared.test.ts +88 -0
  931. package/test/plugin/subagent-progress-checker.test.ts +227 -0
  932. package/test/plugin/trigger.test.ts +116 -0
  933. package/test/plugin/workspace-adaptor.test.ts +109 -0
  934. package/test/preload.ts +102 -0
  935. package/test/project/migrate-global.test.ts +150 -0
  936. package/test/project/project-id.test.ts +64 -0
  937. package/test/project/project.test.ts +481 -0
  938. package/test/project/vcs.test.ts +286 -0
  939. package/test/project/worktree-remove.test.ts +126 -0
  940. package/test/project/worktree.test.ts +214 -0
  941. package/test/provider/amazon-bedrock.test.ts +462 -0
  942. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  943. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  944. package/test/provider/error.test.ts +160 -0
  945. package/test/provider/gitlab-duo.test.ts +413 -0
  946. package/test/provider/model-groups.test.ts +389 -0
  947. package/test/provider/provider-chunk-timeout.test.ts +23 -0
  948. package/test/provider/provider.test.ts +2648 -0
  949. package/test/provider/transform.test.ts +3379 -0
  950. package/test/pty/pty-output-isolation.test.ts +146 -0
  951. package/test/pty/pty-session.test.ts +102 -0
  952. package/test/pty/pty-shell.test.ts +69 -0
  953. package/test/question/question.test.ts +464 -0
  954. package/test/server/global-session-list.test.ts +105 -0
  955. package/test/server/project-init-git.test.ts +122 -0
  956. package/test/server/session-actions.test.ts +49 -0
  957. package/test/server/session-list.test.ts +110 -0
  958. package/test/server/session-messages.test.ts +220 -0
  959. package/test/server/session-prompt-busy.test.ts +146 -0
  960. package/test/server/session-select.test.ts +100 -0
  961. package/test/server/session-task-route.test.ts +165 -0
  962. package/test/server/summarize-route-main-slice.test.ts +99 -0
  963. package/test/server/trace-attributes.test.ts +76 -0
  964. package/test/server/workflows-route.test.ts +279 -0
  965. package/test/session/bootstrap-skip-system.test.ts +121 -0
  966. package/test/session/boundary.test.ts +33 -0
  967. package/test/session/budgeted-read.test.ts +74 -0
  968. package/test/session/checkpoint-align.test.ts +58 -0
  969. package/test/session/checkpoint-boundary.test.ts +186 -0
  970. package/test/session/checkpoint-child-session.test.ts +508 -0
  971. package/test/session/checkpoint-context.test.ts +141 -0
  972. package/test/session/checkpoint-drain.test.ts +188 -0
  973. package/test/session/checkpoint-extract-titles.test.ts +58 -0
  974. package/test/session/checkpoint-fork-mode.test.ts +576 -0
  975. package/test/session/checkpoint-main-slice.test.ts +259 -0
  976. package/test/session/checkpoint-paths.test.ts +78 -0
  977. package/test/session/checkpoint-permission.test.ts +136 -0
  978. package/test/session/checkpoint-progress-reconcile.test.ts +219 -0
  979. package/test/session/checkpoint-rebuild-unify.test.ts +143 -0
  980. package/test/session/checkpoint-rebuild-v3.test.ts +248 -0
  981. package/test/session/checkpoint-render-verify.test.ts +512 -0
  982. package/test/session/checkpoint-retry.test.ts +150 -0
  983. package/test/session/checkpoint-splitover-integration.test.ts +533 -0
  984. package/test/session/checkpoint-templates.test.ts +51 -0
  985. package/test/session/checkpoint-thresholds.test.ts +120 -0
  986. package/test/session/checkpoint-validator.test.ts +189 -0
  987. package/test/session/classify-integration.test.ts +476 -0
  988. package/test/session/classify.test.ts +335 -0
  989. package/test/session/compaction-agent-scope.test.ts +164 -0
  990. package/test/session/context-inheritance.test.ts +46 -0
  991. package/test/session/fork-prefix-invariant.test.ts +116 -0
  992. package/test/session/goal.test.ts +106 -0
  993. package/test/session/instruction.test.ts +387 -0
  994. package/test/session/invalid-output-continuation.test.ts +150 -0
  995. package/test/session/last-message-info.test.ts +47 -0
  996. package/test/session/length-tool-safety.test.ts +121 -0
  997. package/test/session/llm-request-prefix.test.ts +197 -0
  998. package/test/session/llm-retry.test.ts +59 -0
  999. package/test/session/llm-system-prompt.test.ts +479 -0
  1000. package/test/session/llm.test.ts +1272 -0
  1001. package/test/session/main-lifecycle.test.ts +51 -0
  1002. package/test/session/main-runloop-history-invariant.test.ts +182 -0
  1003. package/test/session/max-mode-econnreset.test.ts +229 -0
  1004. package/test/session/max-mode.test.ts +54 -0
  1005. package/test/session/message-v2-filter.test.ts +197 -0
  1006. package/test/session/message-v2.test.ts +1119 -0
  1007. package/test/session/messages-default-main.test.ts +105 -0
  1008. package/test/session/messages-pagination.test.ts +888 -0
  1009. package/test/session/overflow.test.ts +576 -0
  1010. package/test/session/processor-effect.test.ts +853 -0
  1011. package/test/session/prompt-effect.test.ts +1574 -0
  1012. package/test/session/prompt-rebuild-loop.test.ts +108 -0
  1013. package/test/session/prompt-rebuild-reset.test.ts +67 -0
  1014. package/test/session/prompt-sweep.test.ts +145 -0
  1015. package/test/session/prompt-task-gate.test.ts +127 -0
  1016. package/test/session/prompt.test.ts +703 -0
  1017. package/test/session/prune-main-slice.test.ts +272 -0
  1018. package/test/session/prune-skip-system.test.ts +346 -0
  1019. package/test/session/prune.test.ts +419 -0
  1020. package/test/session/rebuild-microcompact.test.ts +318 -0
  1021. package/test/session/recall-reminder.test.ts +37 -0
  1022. package/test/session/retry.test.ts +410 -0
  1023. package/test/session/revert-compact.test.ts +639 -0
  1024. package/test/session/run-state-tuple-key.test.ts +152 -0
  1025. package/test/session/session-create-registers-main.test.ts +70 -0
  1026. package/test/session/session.test.ts +181 -0
  1027. package/test/session/snapshot-tool-race.test.ts +301 -0
  1028. package/test/session/structured-output-integration.test.ts +264 -0
  1029. package/test/session/structured-output-retry.test.ts +127 -0
  1030. package/test/session/structured-output.test.ts +397 -0
  1031. package/test/session/summary-main-slice.test.ts +170 -0
  1032. package/test/session/system.test.ts +72 -0
  1033. package/test/share/share-next.test.ts +332 -0
  1034. package/test/shell/shell.test.ts +73 -0
  1035. package/test/skill/compose-review.test.ts +141 -0
  1036. package/test/skill/discovery.test.ts +116 -0
  1037. package/test/skill/skill.test.ts +465 -0
  1038. package/test/snapshot/snapshot.test.ts +1531 -0
  1039. package/test/storage/db.test.ts +16 -0
  1040. package/test/storage/json-migration.test.ts +831 -0
  1041. package/test/storage/storage.test.ts +293 -0
  1042. package/test/sync/index.test.ts +237 -0
  1043. package/test/task/gate-state.test.ts +66 -0
  1044. package/test/task/gate.test.ts +167 -0
  1045. package/test/task/registry.test.ts +152 -0
  1046. package/test/task/state-machine.test.ts +292 -0
  1047. package/test/team/migrate-to-inbox.test.ts +124 -0
  1048. package/test/team/team.test.ts +75 -0
  1049. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  1050. package/test/tool/actor-cancel.test.ts +206 -0
  1051. package/test/tool/actor-recover.test.ts +50 -0
  1052. package/test/tool/actor-send.test.ts +200 -0
  1053. package/test/tool/actor-status.test.ts +296 -0
  1054. package/test/tool/actor-wait.test.ts +193 -0
  1055. package/test/tool/actor.shell.test.ts +250 -0
  1056. package/test/tool/actor.test.ts +748 -0
  1057. package/test/tool/apply_patch.test.ts +626 -0
  1058. package/test/tool/bash.test.ts +1195 -0
  1059. package/test/tool/describe-workflow.test.ts +12 -0
  1060. package/test/tool/edit.test.ts +691 -0
  1061. package/test/tool/external-directory.test.ts +207 -0
  1062. package/test/tool/fixtures/large-image.png +0 -0
  1063. package/test/tool/fixtures/models-api.json +65179 -0
  1064. package/test/tool/glob.test.ts +81 -0
  1065. package/test/tool/grep.test.ts +114 -0
  1066. package/test/tool/history.test.ts +144 -0
  1067. package/test/tool/invocation-style.test.ts +30 -0
  1068. package/test/tool/memory-edit-ask-skip.test.ts +62 -0
  1069. package/test/tool/memory-path-guard.test.ts +594 -0
  1070. package/test/tool/memory.test.ts +71 -0
  1071. package/test/tool/question.test.ts +167 -0
  1072. package/test/tool/read.test.ts +483 -0
  1073. package/test/tool/registry-invocation-style.test.ts +121 -0
  1074. package/test/tool/registry.test.ts +164 -0
  1075. package/test/tool/shell-tokenize.test.ts +273 -0
  1076. package/test/tool/shell-wrap-missing-script.test.ts +128 -0
  1077. package/test/tool/shell-wrap.test.ts +257 -0
  1078. package/test/tool/skill.test.ts +99 -0
  1079. package/test/tool/task-recover.test.ts +36 -0
  1080. package/test/tool/task.shell.test.ts +234 -0
  1081. package/test/tool/task.test.ts +296 -0
  1082. package/test/tool/tool-def-shell-shape.test.ts +23 -0
  1083. package/test/tool/tool-define.test.ts +59 -0
  1084. package/test/tool/truncation.test.ts +253 -0
  1085. package/test/tool/webfetch.test.ts +103 -0
  1086. package/test/tool/whitelist.test.ts +373 -0
  1087. package/test/tool/write.test.ts +244 -0
  1088. package/test/util/data-url.test.ts +14 -0
  1089. package/test/util/effect-zod.test.ts +869 -0
  1090. package/test/util/error.test.ts +38 -0
  1091. package/test/util/filesystem.test.ts +656 -0
  1092. package/test/util/format.test.ts +59 -0
  1093. package/test/util/glob.test.ts +164 -0
  1094. package/test/util/iife.test.ts +36 -0
  1095. package/test/util/lazy.test.ts +50 -0
  1096. package/test/util/lock.test.ts +72 -0
  1097. package/test/util/log.test.ts +44 -0
  1098. package/test/util/module.test.ts +59 -0
  1099. package/test/util/process.test.ts +128 -0
  1100. package/test/util/timeout.test.ts +21 -0
  1101. package/test/util/which.test.ts +100 -0
  1102. package/test/util/wildcard.test.ts +90 -0
  1103. package/test/workflow/builtin.test.ts +22 -0
  1104. package/test/workflow/deep-research-cluster.test.ts +47 -0
  1105. package/test/workflow/lib.ts +243 -0
  1106. package/test/workflow/meta.test.ts +142 -0
  1107. package/test/workflow/model-routing.test.ts +68 -0
  1108. package/test/workflow/persistence.test.ts +229 -0
  1109. package/test/workflow/resolve.test.ts +37 -0
  1110. package/test/workflow/runtime-nested.test.ts +419 -0
  1111. package/test/workflow/runtime-worktree.test.ts +261 -0
  1112. package/test/workflow/runtime.test.ts +1078 -0
  1113. package/test/workflow/sandbox.test.ts +259 -0
  1114. package/test/workflow/tool.test.ts +473 -0
  1115. package/test/workflow/verify-wow.test.ts +144 -0
  1116. package/test/workflow/workspace.test.ts +88 -0
  1117. package/test/workspace/workspace-restore.test.ts +281 -0
  1118. package/test/worktree/index.test.ts +30 -0
  1119. package/tsconfig.json +24 -0
@@ -0,0 +1,1234 @@
1
+ import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect"
2
+ import os from "node:os"
3
+ import { createHash } from "node:crypto"
4
+ import { spawnRef } from "@/actor/spawn-ref"
5
+ import { workflowRef } from "./runtime-ref"
6
+ import { Config } from "@/config"
7
+ import { EffectBridge } from "@/effect"
8
+ import { Bus } from "@/bus"
9
+ import { Inbox } from "@/inbox"
10
+ import { Worktree } from "@/worktree"
11
+ import { Provider } from "@/provider"
12
+ import { InstanceRef } from "@/effect/instance-ref"
13
+ import { Instance } from "@/project/instance"
14
+ import { Identifier } from "@/id/id"
15
+ import type { SessionID } from "@/session/schema"
16
+ import type { ProviderID, ModelID } from "@/provider/schema"
17
+ import { parseMeta } from "./meta"
18
+ import { evalScript, type HostFn } from "./sandbox"
19
+ import { makeFileHooks, resolveInWorkspace } from "./workspace"
20
+ import { isInlineScript, resolveWorkflowScript } from "./resolve"
21
+ import { WorkflowAgentFailed, WorkflowChildFailed, WorkflowFinished, WorkflowLog, WorkflowPhase, WorkflowStarted } from "./events"
22
+ import { WorkflowPersistence, journalKeyBase } from "./persistence"
23
+ import type { RunSummary } from "./persistence"
24
+ import { Log, Lock } from "@/util"
25
+
26
+ const log = Log.create({ service: "workflow.runtime" })
27
+
28
+ /** Default wall-clock budget for a whole workflow script (12h research default). */
29
+ const SCRIPT_DEADLINE_MS = 12 * 60 * 60 * 1000
30
+ /** Unique sentinel for the per-agent timeout race: a timeout winner can never
31
+ * collide with an agent deliverable (those are object | string | null). */
32
+ const STRAGGLER_TIMEOUT = Symbol("straggler-timeout")
33
+ /** Hard ceiling on total agents a single run may spawn (lifecycle cap). */
34
+ const MAX_LIFECYCLE_AGENTS = 1000
35
+ /** Default soft cap on concurrent agents when the caller does not specify one. */
36
+ const DEFAULT_MAX_CONCURRENT = 16
37
+ /** Marker prefix on errors from STRUCTURAL workflow faults (cycle, over-depth,
38
+ * unknown name) — workflow-wiring bugs that must fail the whole tree loud rather
39
+ * than degrade to the never-throw null that a child's RUNTIME failure yields. The
40
+ * workflow() hook re-propagates any child outcome whose error carries this marker,
41
+ * so the fault surfaces at the root run the user launched. */
42
+ const WORKFLOW_STRUCTURAL_ERROR = "WorkflowStructuralError"
43
+
44
+ type RunStatus = "running" | "completed" | "failed" | "cancelled"
45
+
46
+ export type RunOutcome =
47
+ | { status: "completed"; result: unknown }
48
+ | { status: "failed"; error: string }
49
+ | { status: "cancelled" }
50
+
51
+ interface RunEntry {
52
+ runID: string
53
+ sessionID: SessionID
54
+ status: RunStatus
55
+ deferred: Deferred.Deferred<RunOutcome>
56
+ fiber: Fiber.Fiber<void> | undefined
57
+ childActorIDs: Set<string>
58
+ worktrees: Set<string> // worktree directories pending disposition, for cancel cleanup
59
+ childRunIDs: Set<string> // child workflow runIDs, for recursive cancel/reclaim
60
+ name: string
61
+ running: number
62
+ succeeded: number
63
+ failed: number
64
+ agentCount: number
65
+ capWarned: boolean
66
+ // Model refs already warned about this run, so an unresolvable ref (e.g. a
67
+ // workflow using "lite" with no model_groups.lite configured) logs ONCE per
68
+ // run instead of once per agent spawn. Per-run, not layer-global, so a later
69
+ // run re-warns. See resolveAgentModel.
70
+ warnedModelRefs: Set<string>
71
+ currentPhase: string | undefined
72
+ }
73
+
74
+ interface StartInput {
75
+ script: string
76
+ sessionID: SessionID
77
+ parentActorID: string
78
+ args?: unknown
79
+ model?: { providerID: ProviderID; modelID: ModelID }
80
+ maxConcurrentAgents?: number
81
+ // Hard ceiling on total agents this run may spawn (lifecycle cap). Defaults to
82
+ // MAX_LIFECYCLE_AGENTS (1000). Over-cap agent() calls return null (graceful
83
+ // degradation, never-throw), NOT throw — so a fan-out that wants more agents
84
+ // than the cap degrades to the cap-limited subset instead of aborting the run.
85
+ // Lowerable for tests; tunable in prod.
86
+ maxLifecycleAgents?: number
87
+ /** Per-agent wall-clock timeout (ms). When an individual agent() call's spawned
88
+ * child produces no terminal outcome within this window, it is gracefully
89
+ * cancelled and agent() resolves to null (the never-throw failure sentinel), so
90
+ * one hung agent (e.g. an LLM TTFT wall) cannot stall a parallel/pipeline barrier
91
+ * indefinitely. Default undefined = OFF (only the global scriptDeadlineMs bounds a
92
+ * run). A per-call agent(prompt,{timeoutMs}) overrides this. */
93
+ agentTimeoutMs?: number
94
+ scriptDeadlineMs?: number
95
+ // Internal (resume-only): when true, launch ignores any persisted journal and
96
+ // truncates the stale `.jsonl` before the run appends. resume() sets this on the
97
+ // script-change path (stored script_sha != current script's sha, MR104 P1-2) so
98
+ // an EDITED script never replays results journaled against the OLD body. start()
99
+ // never sets it (a fresh runID has no prior journal — nothing to invalidate).
100
+ freshJournal?: boolean
101
+ /** Root dir the guest's file primitives (readFile/writeFile/glob/exists) are
102
+ * jailed to. Defaults to the caller's worktree. A child workflow inherits the
103
+ * parent's workspace unless its workflow() opts override it. */
104
+ workspace?: string
105
+ /** Resolved names of ancestor workflows (root = empty). A workflow() whose
106
+ * resolved child name is already here is a cycle → throw. */
107
+ lineage?: readonly string[]
108
+ /** Current nesting depth (root run = 0). */
109
+ depth?: number
110
+ /** Max nesting depth before workflow() throws. Defaults to config (8). */
111
+ maxDepth?: number
112
+ }
113
+
114
+ /** Options the guest may pass to `agent(prompt, opts?)`. */
115
+ interface AgentOpts {
116
+ agentType?: string
117
+ tools?: readonly string[]
118
+ /** A model reference resolved host-side via Provider.resolveModelRef: either a
119
+ * "provider/model" literal or a configured tier/group name (e.g. "lite").
120
+ * Omitted → the run's default model. Unknown group → falls back to the run
121
+ * default (never throws to the guest). */
122
+ model?: string
123
+ schema?: Record<string, unknown>
124
+ isolation?: "worktree"
125
+ label?: string
126
+ phase?: string
127
+ /** Per-call override of the run's agentTimeoutMs (ms). */
128
+ timeoutMs?: number
129
+ }
130
+
131
+ export interface Interface {
132
+ readonly start: (input: StartInput) => Effect.Effect<{ runID: string }>
133
+ readonly status: (input: {
134
+ runID: string
135
+ }) => Effect.Effect<{ status: RunStatus | "unknown"; agentCount: number; currentPhase?: string }>
136
+ readonly wait: (input: { runID: string; timeoutMs?: number }) => Effect.Effect<RunOutcome>
137
+ readonly cancel: (input: { runID: string }) => Effect.Effect<void>
138
+ readonly list: (input?: { sessionID?: SessionID }) => Effect.Effect<RunSummary[]>
139
+ readonly resume: (input: { runID: string; agentTimeoutMs?: number }) => Effect.Effect<{ runID: string; resumed: boolean }>
140
+ }
141
+
142
+ export class Service extends Context.Service<Service, Interface>()("@tulingcode/WorkflowRuntime") {}
143
+
144
+ /** A plain promise-based semaphore: at most `max` concurrent `run` callbacks. */
145
+ function makeSemaphore(max: number) {
146
+ let active = 0
147
+ const queue: Array<() => void> = []
148
+ const release = () => {
149
+ active--
150
+ const next = queue.shift()
151
+ if (next) next()
152
+ }
153
+ return {
154
+ run<T>(fn: () => Promise<T>): Promise<T> {
155
+ return new Promise<T>((resolve, reject) => {
156
+ const attempt = () => {
157
+ active++
158
+ fn().then(
159
+ (value) => {
160
+ release()
161
+ resolve(value)
162
+ },
163
+ (err) => {
164
+ release()
165
+ reject(err)
166
+ },
167
+ )
168
+ }
169
+ if (active < max) attempt()
170
+ else queue.push(attempt)
171
+ })
172
+ },
173
+ }
174
+ }
175
+
176
+ function cpuCount(): number {
177
+ const n = os.cpus().length
178
+ return n > 0 ? n : 4
179
+ }
180
+
181
+ export const layer = Layer.effect(
182
+ Service,
183
+ Effect.gen(function* () {
184
+ const bus = yield* Bus.Service
185
+ const inbox = yield* Inbox.Service
186
+ const worktree = yield* Worktree.Service
187
+ const provider = yield* Provider.Service
188
+ // Resolve the Config service handle at layer scope (a legitimate layer dep,
189
+ // satisfied by Config.defaultLayer) so the requirement is discharged here and
190
+ // does NOT leak into start/resume's effect signatures. Only config.get() runs
191
+ // lazily below — it reads the per-instance ALS context and returns Effect<Info>
192
+ // with no requirement, so it stays out of the public method types.
193
+ const config = yield* Config.Service
194
+ const scope = yield* Scope.Scope
195
+ const runs = new Map<string, RunEntry>()
196
+
197
+ // Resolve a guest-supplied model ref (a "provider/model" literal OR a
198
+ // tier/group name like "lite") to a concrete {providerID, modelID} via the
199
+ // Provider service — host-side, inside the runtime's own Layer scope (so it
200
+ // survives resume(), which re-reads the script with no fresh StartInput).
201
+ // NEVER throws to the guest: an unknown group (resolveModelRef throwing
202
+ // ModelGroupNotFoundError) falls back to the run default, matching agent()'s
203
+ // never-throw contract. undefined ref → the run default unchanged.
204
+ const resolveAgentModel = (
205
+ ref: string | undefined,
206
+ fallback: { providerID: ProviderID; modelID: ModelID } | undefined,
207
+ warned: Set<string>,
208
+ ): Effect.Effect<{ providerID: ProviderID; modelID: ModelID } | undefined> =>
209
+ ref === undefined
210
+ ? Effect.succeed(fallback)
211
+ : provider.resolveModelRef(ref).pipe(
212
+ Effect.map((m) => ({ providerID: m.providerID, modelID: m.id })),
213
+ Effect.catchCause(() =>
214
+ Effect.sync(() => {
215
+ // Leave a breadcrumb so a bad ref isn't pure silence: an unknown
216
+ // group/tier, a typo, or an out-of-tree script passing the old
217
+ // {providerID, modelID} object (not a string) all land here and
218
+ // silently use the run default. Warn ONCE per unique ref per run
219
+ // (a fan-out like deep-research would otherwise log on every
220
+ // agent spawn). For a non-string ref, log its sorted keys (e.g.
221
+ // "modelID,providerID") so the operator sees it's the legacy
222
+ // object shape — keys are schema names, no user data.
223
+ const shown =
224
+ typeof ref === "string"
225
+ ? ref
226
+ : `{${Object.keys(ref as object)
227
+ .sort()
228
+ .join(",")}}`
229
+ if (!warned.has(shown)) {
230
+ warned.add(shown)
231
+ log.warn("workflow agent model ref did not resolve — using run default", { ref: shown })
232
+ }
233
+ return fallback
234
+ }),
235
+ ),
236
+ )
237
+
238
+ // Process-wide concurrency ceiling: ONE semaphore shared by every run
239
+ // (including nested children), so tree-wide concurrent agents can never
240
+ // exceed it regardless of nesting depth. It is a PURE process/config property,
241
+ // sized SOLELY from config.workflow.maxConcurrentAgents (falling back to the
242
+ // min(16, 2×cores) default) — NEVER seeded or raised by any per-launch
243
+ // maxConcurrentAgents input. A per-run input only ever NARROWS that run's own
244
+ // semaphore (clamped ≤ global, below); it can neither raise the global nor bind
245
+ // a later run to an earlier run's cap. Resolved LAZILY on the first launch
246
+ // (config.get reads the per-instance ALS context, live inside launch but NOT at
247
+ // layer-build time) and memoized at service scope so every subsequent launch() —
248
+ // including nested children — shares the same semaphore. `cfg`/`globalMax`/
249
+ // `globalSem` are reused by later tasks (T12 maxDepth, T14 maxLifecycleAgents).
250
+ let cfg: Config.Info | undefined
251
+ let globalMax = 0
252
+ let globalSem: ReturnType<typeof makeSemaphore> | undefined
253
+ const ensureGlobal = Effect.fn("WorkflowRuntime.ensureGlobal")(function* () {
254
+ if (globalSem) return globalSem
255
+ // Resolve config once (this is the only suspension point). Cached on `cfg`
256
+ // for reuse by later per-run reads (maxDepth, maxLifecycleAgents).
257
+ cfg ??= yield* config.get()
258
+ globalMax = Math.max(
259
+ 1,
260
+ cfg.workflow?.maxConcurrentAgents ?? Math.min(DEFAULT_MAX_CONCURRENT, 2 * Math.max(1, cpuCount())),
261
+ )
262
+ // Assign synchronously with ??= so two concurrent first-launches that both
263
+ // passed the guard above (the `config.get()` await is a suspension point)
264
+ // converge on ONE semaphore instead of transiently doubling the ceiling.
265
+ // Frozen for the process lifetime: a later config change to
266
+ // maxConcurrentAgents does NOT rebuild it (acceptable while workflow is
267
+ // experimental — the global ceiling is a process/config property).
268
+ globalSem ??= makeSemaphore(globalMax)
269
+ return globalSem
270
+ })
271
+
272
+ // Debounced counter flush: coalesce high-rate running/succeeded/failed updates
273
+ // to at most one DB write per ~250ms per run. flushNow is the synchronous final
274
+ // flush on terminal. All best-effort.
275
+ const flushTimers = new Map<string, ReturnType<typeof setTimeout>>()
276
+ const flushNow = (entry: RunEntry) => {
277
+ const t = flushTimers.get(entry.runID)
278
+ if (t) {
279
+ clearTimeout(t)
280
+ flushTimers.delete(entry.runID)
281
+ }
282
+ return WorkflowPersistence.flushCounters({
283
+ runID: entry.runID,
284
+ running: entry.running,
285
+ succeeded: entry.succeeded,
286
+ failed: entry.failed,
287
+ }).pipe(Effect.ignore)
288
+ }
289
+ const scheduleFlush = (entry: RunEntry) => {
290
+ if (flushTimers.has(entry.runID)) return
291
+ flushTimers.set(
292
+ entry.runID,
293
+ setTimeout(() => {
294
+ flushTimers.delete(entry.runID)
295
+ Effect.runFork(
296
+ WorkflowPersistence.flushCounters({
297
+ runID: entry.runID,
298
+ running: entry.running,
299
+ succeeded: entry.succeeded,
300
+ failed: entry.failed,
301
+ }).pipe(Effect.ignore),
302
+ )
303
+ }, 250),
304
+ )
305
+ }
306
+
307
+ // Best-effort cleanup for a NON-SUCCESS terminal (cancel, deadline, script
308
+ // failure): graceful-cancel any in-flight child agents and remove every
309
+ // worktree the run still owns, then clear the set. NEVER throws — a reclaim
310
+ // failure must not mask the original terminal cause. NOT called on success:
311
+ // kept (success+changed) worktrees are the deliverable and must survive.
312
+ const reclaim = (entry: RunEntry) =>
313
+ Effect.gen(function* () {
314
+ const actor = spawnRef.current
315
+ if (actor) {
316
+ yield* Effect.forEach(
317
+ [...entry.childActorIDs],
318
+ (childID) => actor.cancel(entry.sessionID, childID, "graceful").pipe(Effect.ignore),
319
+ { concurrency: "unbounded", discard: true },
320
+ )
321
+ }
322
+ yield* Effect.forEach(
323
+ [...entry.worktrees],
324
+ (directory) => worktree.remove({ directory }).pipe(Effect.ignore),
325
+ { concurrency: "unbounded", discard: true },
326
+ )
327
+ entry.worktrees.clear()
328
+ // Recurse into child workflow RUNS (populated by workflow()). Cancelling the
329
+ // orchestrator tears down the whole tree — a child still "running" here is
330
+ // cancelled via cancelEntry (mutually recursive with reclaim).
331
+ // SAFETY: childRunIDs edges are parent→child only (added solely at the
332
+ // workflow() call site with a freshly-minted child runID), so the graph is a
333
+ // tree and this recursion is finite. The status-flip guard alone does NOT stop
334
+ // a cycle (a node's flip is post-order, after its reclaim returns), so
335
+ // acyclicity is load-bearing — the workflow() cycle guard (Task 12) is what
336
+ // keeps it true as the call graph grows.
337
+ yield* Effect.forEach(
338
+ [...entry.childRunIDs],
339
+ (childRunID) =>
340
+ Effect.gen(function* () {
341
+ const child = runs.get(childRunID)
342
+ if (child && child.status === "running") yield* cancelEntry(child)
343
+ }).pipe(Effect.ignore),
344
+ { concurrency: "unbounded", discard: true },
345
+ )
346
+ })
347
+
348
+ const cancelEntry = (entry: RunEntry): Effect.Effect<void> =>
349
+ Effect.gen(function* () {
350
+ if (entry.status !== "running") return
351
+ yield* reclaim(entry)
352
+ yield* flushNow(entry)
353
+ yield* WorkflowPersistence.recordTerminal({ runID: entry.runID, status: "cancelled" }).pipe(Effect.ignore)
354
+ if (entry.fiber) yield* Fiber.interrupt(entry.fiber)
355
+ entry.status = "cancelled"
356
+ yield* Deferred.succeed(entry.deferred, { status: "cancelled" })
357
+ yield* bus.publish(WorkflowFinished, { sessionID: entry.sessionID, runID: entry.runID, status: "cancelled" })
358
+ })
359
+
360
+ const waitFor = (childRunID: string) =>
361
+ Effect.gen(function* () {
362
+ const child = runs.get(childRunID)
363
+ if (!child) return { status: "failed" as const, error: "child run missing" }
364
+ return yield* Deferred.await(child.deferred)
365
+ })
366
+
367
+ const launch = Effect.fn("WorkflowRuntime.launch")(function* (input: StartInput, runID: string, name: string) {
368
+ // The guest body is the script with the `meta` literal blanked out (parseMeta
369
+ // preserves line numbers). start already validated meta and resume only loads
370
+ // a previously-validated script, so this parse is purely to extract the body;
371
+ // it never gates here. Fall back to the raw script if parse somehow fails.
372
+ const parsed = parseMeta(input.script)
373
+ const body = parsed.ok ? parsed.body : input.script
374
+ // Resolve the workspace root ONCE at launch (the Instance ALS context is
375
+ // live here — the bridge below captures it). Default = the caller's
376
+ // worktree. Captured in the closure so the file hooks read it synchronously
377
+ // and never touch ALS from inside the forked work fiber.
378
+ const workspaceRoot = input.workspace ?? Instance.worktree
379
+ const fileHooks = makeFileHooks(workspaceRoot)
380
+ const deferred = yield* Deferred.make<RunOutcome>()
381
+ const entry: RunEntry = {
382
+ runID,
383
+ sessionID: input.sessionID,
384
+ status: "running",
385
+ deferred,
386
+ fiber: undefined,
387
+ childActorIDs: new Set<string>(),
388
+ worktrees: new Set<string>(),
389
+ childRunIDs: new Set<string>(),
390
+ name,
391
+ running: 0,
392
+ succeeded: 0,
393
+ failed: 0,
394
+ agentCount: 0,
395
+ capWarned: false,
396
+ warnedModelRefs: new Set<string>(),
397
+ currentPhase: undefined,
398
+ }
399
+ runs.set(runID, entry)
400
+ // Stamp a sha256 of the FULL script body (the exact bytes writeScript persists
401
+ // and resume's readScript reads back), so resume can detect a between-cycle
402
+ // edit by comparing this to the current file's sha — apples-to-apples, MR104
403
+ // P1-2. recordStart re-stamps it on every (re)launch, so a changed-script
404
+ // relaunch overwrites the stale sha and a subsequent resume replays correctly.
405
+ const scriptSha = createHash("sha256").update(input.script).digest("hex")
406
+ yield* WorkflowPersistence.recordStart({
407
+ runID,
408
+ sessionID: input.sessionID,
409
+ name,
410
+ parentActorID: input.parentActorID,
411
+ args: input.args,
412
+ scriptSha,
413
+ agentTimeoutMs: input.agentTimeoutMs,
414
+ }).pipe(Effect.ignore)
415
+ yield* WorkflowPersistence.writeScript(runID, input.script).pipe(Effect.ignore)
416
+
417
+ // Replay journal: prior agent() results (empty on a fresh run). On resume,
418
+ // a cache hit returns instantly with no spawn; misses spawn + append. The
419
+ // occ counter disambiguates byte-identical calls into distinct slots.
420
+ // freshJournal (resume's script-change path) truncates the stale `.jsonl`
421
+ // FIRST so loadJournal returns empty AND the run's appends don't interleave
422
+ // with results journaled against the old script body — a later resume would
423
+ // otherwise read both and replay the wrong results.
424
+ if (input.freshJournal) yield* WorkflowPersistence.clearJournal(runID).pipe(Effect.ignore)
425
+ const journal = yield* WorkflowPersistence.loadJournal(runID)
426
+ const occ = new Map<string, number>()
427
+ const pass = journal.pass
428
+
429
+ // Capture the bridge BEFORE forking so it snapshots the caller's
430
+ // Instance/Workspace context — the quickjs Promise boundary in agent()
431
+ // would otherwise lose it.
432
+ const bridge = yield* EffectBridge.make()
433
+
434
+ // Resolve the process-wide ceiling NOW (under the live Instance context) so
435
+ // its semaphore object exists before any spawn site closes over it. Sized
436
+ // PURELY from config (memoized after the first launch); a per-launch
437
+ // maxConcurrentAgents never seeds or raises it — it only narrows this run's
438
+ // own semaphore below.
439
+ const globalSemLocal = yield* ensureGlobal()
440
+ // Nesting safety (T12): carried through every run. lineage = resolved names of
441
+ // ancestor workflows (root = empty); depth = this run's level (root = 0). A
442
+ // workflow() whose child name is already in lineage is a cycle, and a child
443
+ // beyond maxDepth is over-deep — both throw at the call site (workflowHook).
444
+ // maxDepth precedence: explicit per-run input > config > module default 8.
445
+ const lineage = input.lineage ?? []
446
+ const depth = input.depth ?? 0
447
+ const maxDepth = input.maxDepth ?? cfg?.workflow?.maxDepth ?? 8
448
+ // Per-run soft cap: defaults to the global ceiling, clamped to ≤ global so a
449
+ // child can shrink its own concurrency but never exceed the process ceiling.
450
+ // The 2×cores clamp is GONE — the global semaphore is the real throttle.
451
+ const requested = input.maxConcurrentAgents ?? globalMax
452
+ const max = Math.max(1, Math.min(requested, globalMax))
453
+ const sem = makeSemaphore(max)
454
+ // Lifecycle cap (total agents over the run's life). Resolved once here so
455
+ // both spawn paths (shared + isolated) share it; over-cap calls return null.
456
+ const lifecycleCap = input.maxLifecycleAgents ?? cfg?.workflow?.maxLifecycleAgents ?? MAX_LIFECYCLE_AGENTS
457
+ // Over-cap → null (see maxLifecycleAgents doc): warn ONCE per run so the
458
+ // dropped work is visible without spamming a log line per over-cap call.
459
+ const warnCapOnce = () => {
460
+ if (entry.capWarned) return
461
+ entry.capWarned = true
462
+ log.warn("workflow lifecycle agent cap reached — over-cap agents return null", {
463
+ runID,
464
+ cap: lifecycleCap,
465
+ })
466
+ }
467
+ // Per-agent wall-clock timeout. Run-level default (OFF unless set); a per-call
468
+ // opts.timeoutMs overrides it. Resolved per agent() call since opts is per-call.
469
+ const runAgentTimeoutMs = input.agentTimeoutMs
470
+ // Race a child's outcome-await against the effective per-agent timeout. On a
471
+ // TRUE timeout: gracefully cancel that one child (the lever reclaim uses) and
472
+ // yield null — the never-throw sentinel the guest already tolerates, so a hung
473
+ // agent can't stall a parallel/pipeline barrier. A genuine null deliverable
474
+ // (agent failed fast) is NOT a timeout → no cancel. No timeout configured
475
+ // (undefined / <=0) ⇒ await unbounded (current behavior, only scriptDeadline bounds).
476
+ const awaitWithTimeout = <A>(
477
+ actorID: string,
478
+ opts: AgentOpts,
479
+ await_: Effect.Effect<A | null>,
480
+ // Optional side-channel: set when the timeout branch wins, so the caller
481
+ // can distinguish a TRUE timeout (reason="timeout") from a fast actor-error
482
+ // null. Never throws; called once at most. Pure observability.
483
+ onTimeout?: () => void,
484
+ ) => {
485
+ const ms = opts.timeoutMs ?? runAgentTimeoutMs
486
+ if (!ms || ms <= 0) return await_
487
+ return Effect.raceFirst(
488
+ await_,
489
+ Effect.sleep(`${ms} millis`).pipe(Effect.as(STRAGGLER_TIMEOUT as unknown as A | null)),
490
+ ).pipe(
491
+ Effect.flatMap((r) =>
492
+ r === (STRAGGLER_TIMEOUT as unknown)
493
+ ? (spawnRef.current
494
+ ? spawnRef.current.cancel(input.sessionID, actorID, "graceful").pipe(Effect.ignore)
495
+ : Effect.void
496
+ ).pipe(
497
+ Effect.tap(() =>
498
+ Effect.sync(() => {
499
+ try {
500
+ onTimeout?.()
501
+ } catch {
502
+ /* observability must never escape */
503
+ }
504
+ }),
505
+ ),
506
+ Effect.as(null),
507
+ )
508
+ : Effect.succeed(r),
509
+ ),
510
+ )
511
+ }
512
+
513
+ // Publish a WorkflowAgentFailed event for an agent() call that resolved to
514
+ // null. Pure observability — counters and the agent() return value are
515
+ // unaffected. Wrapped in try/catch so a bus problem can never break a run.
516
+ type FailReason = "over-cap" | "spawn-reject" | "timeout" | "actor-error" | "no-deliverable"
517
+ const publishAgentFailed = (
518
+ o: AgentOpts,
519
+ reason: FailReason,
520
+ info: { actorID?: string; errorMessage?: string } = {},
521
+ ) => {
522
+ try {
523
+ Effect.runFork(
524
+ bus
525
+ .publish(WorkflowAgentFailed, {
526
+ sessionID: input.sessionID,
527
+ runID,
528
+ actorID: info.actorID,
529
+ agentType: o.agentType ?? "general",
530
+ label: o.label,
531
+ phase: o.phase ?? entry.currentPhase,
532
+ reason,
533
+ errorMessage: info.errorMessage,
534
+ })
535
+ .pipe(Effect.ignore),
536
+ )
537
+ } catch {
538
+ /* observability must never escape */
539
+ }
540
+ }
541
+
542
+ yield* bus.publish(WorkflowStarted, { sessionID: input.sessionID, runID, name })
543
+
544
+ // Observability-only spawn description from label/phase: "[Phase] label",
545
+ // or just one of them, or undefined (then spawn falls back to agentType —
546
+ // see spawn.ts `input.description ?? input.agentType`). label/phase NEVER
547
+ // touch currentPhase/counters/schema — they are purely the per-agent tag
548
+ // the actor registry stores and the /workflows view surfaces.
549
+ const spawnDescription = (o: AgentOpts) =>
550
+ o.label ? (o.phase ? `[${o.phase}] ${o.label}` : o.label) : o.phase ? `[${o.phase}]` : undefined
551
+
552
+ // Shared-tree spawn (default): the existing behavior. SUBAGENT mode — the
553
+ // worker shares the run's parent session (cheaper, no per-agent session).
554
+ // Safe since lastAssistant is agent-scoped (fix 59597264): each subagent's
555
+ // result is extracted by its own agentID, so concurrent same-session
556
+ // subagents don't cross-contaminate. context:"none" keeps each worker free
557
+ // of parent history (parallel fan-out is the use case). NEVER throw to the
558
+ // guest for spawn/turn failures — resolve to null so the script continues.
559
+ const spawnShared = async (
560
+ actor: NonNullable<typeof spawnRef.current>,
561
+ prompt: string,
562
+ o: AgentOpts,
563
+ resolvedModel: { providerID: ProviderID; modelID: ModelID } | undefined,
564
+ ) => {
565
+ // COUNTER INVARIANT: running++ exactly once BEFORE the spawn attempt, and
566
+ // running-- + (succeeded XOR failed)++ exactly once AFTER it settles. The
567
+ // bookkeeping lives OUTSIDE the bridge so it still runs when the bridge
568
+ // result is the spawn-reject sentinel (null). Counters settle on whether a
569
+ // DELIVERABLE was produced (value !== null) — the exact thing the guest
570
+ // observes. An agent whose turn errored finishes with status:"success" but
571
+ // no finalText/structured, so its deliverable is null and the guest sees a
572
+ // failure; the counter must agree. A spawn reject also yields null → failed.
573
+ entry.running++
574
+ scheduleFlush(entry)
575
+ // Failure-reason refs: defaults to "actor-error" (the broad catch-all) and
576
+ // is narrowed at known branch points. Read once at the end, on null return,
577
+ // to publish WorkflowAgentFailed. agent()'s null contract is unchanged.
578
+ let reason: FailReason = "actor-error"
579
+ let actorID: string | undefined
580
+ let errorMessage: string | undefined
581
+ const value = await bridge
582
+ .promise(
583
+ Effect.gen(function* () {
584
+ const spawned = yield* actor.spawn({
585
+ mode: "subagent",
586
+ sessionID: input.sessionID,
587
+ agentType: o.agentType ?? "general",
588
+ description: spawnDescription(o),
589
+ task: prompt,
590
+ context: "none",
591
+ tools: o.tools ? [...o.tools] : "INHERIT",
592
+ background: true,
593
+ parentActorID: input.parentActorID,
594
+ model: resolvedModel,
595
+ // Register the child in the reclaim set the instant the actor
596
+ // exists — synchronously inside the spawn Effect, BEFORE its work
597
+ // fiber detaches. A cancel racing this spawn would otherwise miss
598
+ // it (the child runs detached in the actor scope, so interrupting
599
+ // the workflow fiber can't stop it) and leak an orphan. MR104 #2.
600
+ onActorID: (id) => entry.childActorIDs.add(id),
601
+ ...(o.schema ? { format: { type: "json_schema" as const, schema: o.schema, retryCount: 2 } } : {}),
602
+ })
603
+ actorID = spawned.actorID
604
+ // Bound the outcome-await by the per-agent timeout: a hung child times
605
+ // out to null (and is cancelled) rather than stalling the barrier. The
606
+ // deliverable is computed inside the awaited Effect so the timeout wraps
607
+ // the whole await→extract. schema requested ⇒ structured ?? null (never
608
+ // prose finalText: prose breaks `r.fields`-style scripts + our pipeline).
609
+ const deliverable = yield* awaitWithTimeout(
610
+ spawned.actorID,
611
+ o,
612
+ Deferred.await(spawned.outcome).pipe(
613
+ Effect.map((outcome) => {
614
+ if (outcome.status !== "success") {
615
+ reason = "actor-error"
616
+ errorMessage = (outcome as { error?: string }).error
617
+ return null
618
+ }
619
+ const v = o.schema
620
+ ? (outcome.structured ?? null)
621
+ : (outcome.structured ?? outcome.finalText ?? null)
622
+ if (v === null) reason = "no-deliverable"
623
+ return v
624
+ }),
625
+ ),
626
+ () => {
627
+ reason = "timeout"
628
+ },
629
+ )
630
+ entry.childActorIDs.delete(spawned.actorID)
631
+ return deliverable
632
+ }),
633
+ )
634
+ .catch((e) => {
635
+ reason = "spawn-reject"
636
+ errorMessage = e instanceof Error ? e.message : String(e)
637
+ return null
638
+ })
639
+ entry.running--
640
+ if (value !== null) entry.succeeded++
641
+ else {
642
+ entry.failed++
643
+ publishAgentFailed(o, reason, { actorID, errorMessage })
644
+ }
645
+ scheduleFlush(entry)
646
+ return value
647
+ }
648
+
649
+ // Isolated spawn: fresh worktree, file tools rebound to it via Instance.provide.
650
+ const spawnIsolated = async (
651
+ actor: NonNullable<typeof spawnRef.current>,
652
+ prompt: string,
653
+ o: AgentOpts,
654
+ resolvedModel: { providerID: ProviderID; modelID: ModelID } | undefined,
655
+ ) => {
656
+ // Failure-reason refs (parallel to spawnShared); see there for rationale.
657
+ let reason: FailReason = "actor-error"
658
+ let actorIDOut: string | undefined
659
+ let errorMessage: string | undefined
660
+ // 1) Create + fully populate a worktree (createFromInfo awaits boot).
661
+ const info = await bridge
662
+ .promise(
663
+ Effect.gen(function* () {
664
+ const i = yield* worktree.makeWorktreeInfo()
665
+ yield* worktree.createFromInfo(i)
666
+ return i
667
+ }),
668
+ )
669
+ .catch((e) => {
670
+ errorMessage = e instanceof Error ? e.message : String(e)
671
+ return null
672
+ })
673
+ if (!info) {
674
+ publishAgentFailed(o, "spawn-reject", { errorMessage })
675
+ return null
676
+ }
677
+ // Register the worktree for cleanup the moment it exists on disk — BEFORE
678
+ // the spawn attempt. If spawn rejects or the agent fails, cancel-cleanup
679
+ // (and the disposition below) can still reclaim it; nothing orphans.
680
+ entry.worktrees.add(info.directory)
681
+ const base = await bridge.promise(worktree.head(info.directory)).catch(() => "")
682
+ // 2) A bridge bound to the worktree's InstanceContext: provide InstanceRef =
683
+ // worktree ctx so Effect-side reads resolve there; the Instance.provide
684
+ // wrap below covers raw-ALS tool reads (the load-bearing part). The outer
685
+ // Instance.provide is what reroutes the agent's file tools; wtBridge is
686
+ // defense-in-depth for any Effect-side InstanceRef read during dispatch.
687
+ const wtCtx = await Instance.provide({
688
+ directory: info.directory,
689
+ fn: () => Promise.resolve(Instance.current),
690
+ })
691
+ const wtBridge = await bridge.promise(EffectBridge.make().pipe(Effect.provideService(InstanceRef, wtCtx)))
692
+ // 3) Spawn + await INSIDE Instance.provide({worktree}) — AsyncLocalStorage
693
+ // propagates the worktree dir across the actor's forked work fiber, so the
694
+ // agent's read/write/bash resolve to the worktree, not the parent tree.
695
+ // COUNTER INVARIANT (isolated path): running++ here, BEFORE the spawn
696
+ // attempt, so it pairs with the settle below regardless of spawn-reject
697
+ // (spawned === null). The settle (running-- + succeeded/failed++) runs once
698
+ // on every disposition path after `succeeded` is known.
699
+ entry.running++
700
+ scheduleFlush(entry)
701
+ const spawned = await Instance.provide({
702
+ directory: info.directory,
703
+ fn: () =>
704
+ wtBridge
705
+ .promise(
706
+ Effect.gen(function* () {
707
+ const s = yield* actor.spawn({
708
+ mode: "subagent",
709
+ sessionID: input.sessionID,
710
+ agentType: o.agentType ?? "general",
711
+ description: spawnDescription(o),
712
+ task: prompt,
713
+ context: "none",
714
+ tools: o.tools ? [...o.tools] : "INHERIT",
715
+ background: true,
716
+ parentActorID: input.parentActorID,
717
+ model: resolvedModel,
718
+ // Same MR104 #2 fix as spawnShared: register the child in the
719
+ // reclaim set synchronously inside the spawn Effect, before its
720
+ // work fiber detaches, so a racing cancel never orphans it.
721
+ onActorID: (id) => entry.childActorIDs.add(id),
722
+ ...(o.schema ? { format: { type: "json_schema" as const, schema: o.schema, retryCount: 2 } } : {}),
723
+ })
724
+ actorIDOut = s.actorID
725
+ // Bound the await by the per-agent timeout. On timeout the helper
726
+ // cancels the child and yields null; we surface that as a null
727
+ // `spawned` so the disposition below takes the same path as a
728
+ // spawn-reject/failure (worktree reclaimed, value null, failed++) —
729
+ // a hung isolated agent can't stall the barrier or leak a worktree.
730
+ const outcome = yield* awaitWithTimeout(s.actorID, o, Deferred.await(s.outcome), () => {
731
+ reason = "timeout"
732
+ })
733
+ entry.childActorIDs.delete(s.actorID)
734
+ if (outcome === null) return null
735
+ if (outcome.status !== "success") {
736
+ reason = "actor-error"
737
+ errorMessage = (outcome as { error?: string }).error
738
+ }
739
+ return { actorID: s.actorID, outcome }
740
+ }),
741
+ )
742
+ .catch((e) => {
743
+ reason = "spawn-reject"
744
+ errorMessage = e instanceof Error ? e.message : String(e)
745
+ return null
746
+ }),
747
+ }).catch((e) => {
748
+ reason = "spawn-reject"
749
+ errorMessage = e instanceof Error ? e.message : String(e)
750
+ return null
751
+ })
752
+ // 4) Disposition. KEEP the worktree only when the agent SUCCEEDED and left
753
+ // changes (the deliverable, surfaced via _worktree). In every other case
754
+ // — pristine (untouched), spawn rejected, or agent failed/cancelled —
755
+ // remove it so nothing leaks on disk. Guard: an empty base means head()
756
+ // failed at create time; treat as CHANGED (never trust an unreliable
757
+ // pristine check to authorize a delete).
758
+ const succeeded = !!spawned && spawned.outcome.status === "success"
759
+ // Settle the counter once here — after `succeeded` is known and before any
760
+ // disposition branch — so it runs exactly once on every path (spawn-reject
761
+ // → spawned===null → failed++, keep, remove). Pairs with the running++ above.
762
+ // We REUSE the existing `succeeded` discriminant (read-only; the worktree
763
+ // disposition below owns it) rather than the returned deliverable: in the
764
+ // isolated path a successful agent's work is its worktree, so a status
765
+ // success is a success even when it returned no text.
766
+ entry.running--
767
+ if (succeeded) entry.succeeded++
768
+ else {
769
+ entry.failed++
770
+ publishAgentFailed(o, reason, { actorID: actorIDOut, errorMessage })
771
+ }
772
+ scheduleFlush(entry)
773
+ // The success deliverable. When a schema was requested it MUST be the
774
+ // validated structured object — never prose finalText (see the shared-spawn
775
+ // path above for why: prose breaks `r.fields`-style scripts + our pipeline
776
+ // null-injection). schema requested ⇒ structured ?? null.
777
+ const value =
778
+ spawned && spawned.outcome.status === "success"
779
+ ? o.schema
780
+ ? (spawned.outcome.structured ?? null)
781
+ : (spawned.outcome.structured ?? spawned.outcome.finalText ?? null)
782
+ : null
783
+ const pristine =
784
+ base !== "" && (await bridge.promise(worktree.isPristine(info.directory, base)).catch(() => false))
785
+ const keep = succeeded && !pristine
786
+ if (!keep) {
787
+ await bridge.promise(worktree.remove({ directory: info.directory })).catch(() => undefined)
788
+ entry.worktrees.delete(info.directory)
789
+ return succeeded ? value : null
790
+ }
791
+ // keep: the worktree stays on disk and tracked until an integrate step or
792
+ // cancel reclaims it; surface its branch so the script can act on it.
793
+ const wt = { branch: info.branch, directory: info.directory, changed: true }
794
+ if (value && typeof value === "object" && !Array.isArray(value)) return { ...(value as object), _worktree: wt }
795
+ return { _worktree: wt, result: value }
796
+ }
797
+
798
+ const agent: HostFn = (prompt: unknown, opts?: unknown) => {
799
+ const o = (opts ?? {}) as AgentOpts
800
+ const promptStr = String(prompt)
801
+ // Isolated agents are never journaled in v1 (their deliverable is a
802
+ // worktree the journal can't reconstruct) — always spawn.
803
+ if (o.isolation !== "worktree") {
804
+ const base = journalKeyBase(promptStr, {
805
+ agentType: o.agentType,
806
+ model: o.model,
807
+ schema: o.schema,
808
+ phase: o.phase,
809
+ })
810
+ const n = occ.get(base) ?? 0
811
+ occ.set(base, n + 1)
812
+ const key = base + ":" + n
813
+ if (journal.results.has(key)) {
814
+ // Cache hit: no spawn, no agentCount increment (would hit the 1000
815
+ // cap on replays alone). Outcome counter DOES climb so the live view
816
+ // reflects reality as replay proceeds.
817
+ entry.succeeded++
818
+ scheduleFlush(entry)
819
+ return Promise.resolve(journal.results.get(key))
820
+ }
821
+ return (async () => {
822
+ // Spawn UNDER the semaphore (governs concurrency). The journal append
823
+ // happens AFTER the slot is released, so file IO never holds a slot.
824
+ const result = await sem.run(async () =>
825
+ globalSemLocal.run(async () => {
826
+ if (entry.agentCount >= lifecycleCap) {
827
+ warnCapOnce()
828
+ publishAgentFailed(o, "over-cap")
829
+ return null
830
+ }
831
+ entry.agentCount++
832
+ const actor = spawnRef.current
833
+ if (!actor) throw new Error("Actor service unavailable")
834
+ // Resolve the guest's model ref host-side AFTER the journal key was
835
+ // computed above (the key hashes the raw `o.model` ref, NOT the
836
+ // resolved struct, so resume keys stay stable across config changes).
837
+ // Never-throws: an unknown group falls back to input.model.
838
+ const resolvedModel = await bridge.promise(resolveAgentModel(o.model, input.model, entry.warnedModelRefs))
839
+ return spawnShared(actor, promptStr, o, resolvedModel)
840
+ }),
841
+ )
842
+ // Cache successful results only (null = failure/spawn-reject/killed →
843
+ // not journaled → re-runs on resume, self-heal). SYNCHRONOUS append so
844
+ // the result is durable the instant it resolves: a mid-run process exit
845
+ // / SIGKILL / deadline leaves a journal with every completed agent, which
846
+ // is the whole point of resume. A sync write (unlike an awaited async
847
+ // Effect.promise(fs)) does NOT starve the quickjs sandbox pump — verified.
848
+ // Effect.ignore'd so a write failure can't break the agent.
849
+ if (result !== null) {
850
+ await Effect.runPromise(
851
+ WorkflowPersistence.appendJournalSync(runID, [{ t: "agent", key, result, pass }]).pipe(Effect.ignore),
852
+ )
853
+ }
854
+ return result
855
+ })()
856
+ }
857
+ return sem.run(async () =>
858
+ globalSemLocal.run(async () => {
859
+ if (entry.agentCount >= lifecycleCap) {
860
+ warnCapOnce()
861
+ publishAgentFailed(o, "over-cap")
862
+ return null
863
+ }
864
+ entry.agentCount++
865
+ const actor = spawnRef.current
866
+ if (!actor) throw new Error("Actor service unavailable")
867
+ // Resolve the guest's model ref host-side (isolated agents aren't
868
+ // journaled, so there's no key to keep stable here). Never-throws.
869
+ const resolvedModel = await bridge.promise(resolveAgentModel(o.model, input.model, entry.warnedModelRefs))
870
+ return spawnIsolated(actor, promptStr, o, resolvedModel)
871
+ }),
872
+ )
873
+ }
874
+
875
+ const phase: HostFn = (title: unknown) => {
876
+ entry.currentPhase = String(title)
877
+ Effect.runFork(WorkflowPersistence.recordPhase({ runID, phase: String(title) }).pipe(Effect.ignore))
878
+ Effect.runFork(WorkflowPersistence.appendJournal(runID, { t: "phase", title: String(title), pass }).pipe(Effect.ignore))
879
+ Effect.runFork(bus.publish(WorkflowPhase, { sessionID: input.sessionID, runID, title: String(title) }))
880
+ return undefined
881
+ }
882
+
883
+ const logHook: HostFn = (message: unknown) => {
884
+ Effect.runFork(WorkflowPersistence.appendJournal(runID, { t: "log", msg: String(message), pass }).pipe(Effect.ignore))
885
+ Effect.runFork(bus.publish(WorkflowLog, { sessionID: input.sessionID, runID, message: String(message) }))
886
+ return undefined
887
+ }
888
+
889
+ // workflow(nameOrScript, args?, opts?) — schedule a CHILD workflow as its
890
+ // own independent sub-run, awaited inline. Mirrors agent()→Actor.spawn one
891
+ // level up: mint a deterministic child runID (stable across resume so the
892
+ // parent journal can find the child), resolve name→script, launch it, await
893
+ // its RunOutcome. A child that fails resolves to null (never-throw, like
894
+ // agent()) so parallel/pipeline over children degrade gracefully. An unknown
895
+ // name THROWS (Effect.die → the guest call rejects → the run fails loud).
896
+ const workflowOcc = new Map<string, number>()
897
+ const workflowHook: HostFn = (nameOrScript: unknown, childArgs?: unknown, opts?: unknown) => {
898
+ const spec = String(nameOrScript)
899
+ const o = (opts ?? {}) as { workspace?: string; maxConcurrentAgents?: number }
900
+ // Content key over the SEMANTIC inputs that reach the child (spec + args).
901
+ // occ disambiguates byte-identical workflow() calls into distinct slots.
902
+ const base = createHash("sha256")
903
+ .update(JSON.stringify({ spec, args: childArgs ?? null }))
904
+ .digest("hex")
905
+ const n = workflowOcc.get(base) ?? 0
906
+ workflowOcc.set(base, n + 1)
907
+ const key = base + ":" + n
908
+ // Parent-journal hit: a completed child replays its result with NO relaunch
909
+ // (the two-level resume short-circuit — parent journal skips the whole child
910
+ // sub-run; the child's own journal would handle agent-level skip if it were
911
+ // re-run). Counts as a succeeded outcome so the live view reflects replay
912
+ // progress. The "wf:" prefix keeps this slot namespace disjoint from agent() keys.
913
+ if (journal.results.has("wf:" + key)) {
914
+ entry.succeeded++
915
+ scheduleFlush(entry)
916
+ return Promise.resolve(journal.results.get("wf:" + key))
917
+ }
918
+ const childRunID = "wf_" + createHash("sha256").update(runID + key).digest("hex")
919
+ return bridge.promise(
920
+ Effect.gen(function* () {
921
+ const childScript = isInlineScript(spec)
922
+ ? spec
923
+ : yield* Effect.promise(() => resolveWorkflowScript(spec, workspaceRoot, Instance.worktree))
924
+ if (childScript === null)
925
+ return yield* Effect.die(new Error(`${WORKFLOW_STRUCTURAL_ERROR}: unknown workflow: ${JSON.stringify(spec)}`))
926
+ // Nesting guards (T12) — LAUNCH path only (a journal HIT early-returned
927
+ // above without deriving childName/childRunID, and a cached child already
928
+ // completed in a prior pass, so re-validating would be wrong). The child's
929
+ // lineage name is its resolved saved name, or a content-hash label for an
930
+ // inline body so distinct inline children don't collide AND an inline body
931
+ // that re-invokes itself is still caught as a cycle. Over-depth and cycle
932
+ // are SCRIPT-LOGIC errors → Effect.die (fail loud), same posture as the
933
+ // unknown-name die above. The guest await rejects → the orchestrator script
934
+ // throws → the parent run fails with this message.
935
+ // NOTE: saved names key on the name alone (args-independent), so saved
936
+ // A→A with different args IS a cycle; an inline body keys on its content
937
+ // hash WHICH INCLUDES args, so inline A→A with different args is NOT a
938
+ // cycle and is bounded only by maxDepth.
939
+ const childName = isInlineScript(spec) ? "inline:" + base.slice(0, 12) : spec
940
+ if (depth + 1 > maxDepth) {
941
+ return yield* Effect.die(new Error(`${WORKFLOW_STRUCTURAL_ERROR}: workflow nesting exceeds maxDepth (${maxDepth})`))
942
+ }
943
+ if (lineage.includes(childName)) {
944
+ return yield* Effect.die(
945
+ new Error(`${WORKFLOW_STRUCTURAL_ERROR}: workflow cycle detected: ${childName} is already an ancestor`),
946
+ )
947
+ }
948
+ entry.childRunIDs.add(childRunID)
949
+ // The child is an independent sub-run: it gets its own per-run lifecycle
950
+ // cap + per-agent timeout (defaults), deliberately NOT inherited from the
951
+ // parent. Tree-wide concurrency is bounded by the global semaphore,
952
+ // not by propagating these per-run knobs.
953
+ yield* launch(
954
+ {
955
+ script: childScript,
956
+ sessionID: input.sessionID,
957
+ parentActorID: input.parentActorID,
958
+ args: childArgs,
959
+ model: input.model,
960
+ // A child may narrow its workspace to a subdir but never widen it
961
+ // beyond the parent's root — resolveInWorkspace throws on escape
962
+ // (a script-logic error → fail loud), same posture as the jail itself.
963
+ workspace: o.workspace ? resolveInWorkspace(workspaceRoot, String(o.workspace)) : workspaceRoot,
964
+ maxConcurrentAgents: o.maxConcurrentAgents,
965
+ scriptDeadlineMs: input.scriptDeadlineMs,
966
+ // Extend the nesting context for the child (T12): append this child to
967
+ // the ancestor lineage, increment depth, carry the same cap down.
968
+ lineage: [...lineage, childName],
969
+ depth: depth + 1,
970
+ maxDepth,
971
+ },
972
+ childRunID,
973
+ isInlineScript(spec) ? "inline" : spec,
974
+ )
975
+ const childOutcome = yield* waitFor(childRunID)
976
+ // Structural faults (cycle / depth / unknown-name) are workflow-wiring
977
+ // BUGS, not runtime conditions — propagate them loud instead of degrading
978
+ // to null like a child's runtime failure, so the fault surfaces at the root
979
+ // run. Each ancestor re-dies in turn; slice from the marker so the message
980
+ // doesn't accrete a "workflow script rejected:" prefix at every level.
981
+ if (childOutcome.status === "failed" && childOutcome.error.includes(WORKFLOW_STRUCTURAL_ERROR)) {
982
+ const idx = childOutcome.error.indexOf(WORKFLOW_STRUCTURAL_ERROR)
983
+ return yield* Effect.die(new Error(childOutcome.error.slice(idx)))
984
+ }
985
+ // Runtime failure (NOT structural — that path re-died above): the child's
986
+ // agents failed, it hit its deadline, or it was cancelled. workflow() still
987
+ // returns null (never-throw); this event records WHY for triage. Mirrors
988
+ // WorkflowAgentFailed. Fire-and-forget so a bus problem can't break the run.
989
+ if (childOutcome.status !== "completed") {
990
+ yield* bus
991
+ .publish(WorkflowChildFailed, {
992
+ sessionID: input.sessionID,
993
+ runID,
994
+ childRunID,
995
+ name: isInlineScript(spec) ? "inline" : spec,
996
+ status: childOutcome.status, // "failed" | "cancelled"
997
+ ...(childOutcome.status === "failed" ? { error: childOutcome.error } : {}),
998
+ })
999
+ .pipe(Effect.ignore)
1000
+ }
1001
+ const value = childOutcome.status === "completed" ? (childOutcome.result ?? null) : null
1002
+ // Journal ONLY a successful child (null = failure → not cached → re-runs
1003
+ // on resume, self-heal — same contract as agent()). Synchronous append so
1004
+ // it survives a mid-run kill.
1005
+ if (value !== null) {
1006
+ yield* WorkflowPersistence.appendJournalSync(runID, [
1007
+ { t: "agent", key: "wf:" + key, result: value, pass },
1008
+ ]).pipe(Effect.ignore)
1009
+ }
1010
+ return value
1011
+ }),
1012
+ )
1013
+ }
1014
+
1015
+ const hooks: Record<string, HostFn> = {
1016
+ agent,
1017
+ phase,
1018
+ log: logHook,
1019
+ workflow: workflowHook,
1020
+ readFile: fileHooks.readFile,
1021
+ writeFile: fileHooks.writeFile,
1022
+ glob: fileHooks.glob,
1023
+ exists: fileHooks.exists,
1024
+ }
1025
+
1026
+ const work = Effect.gen(function* () {
1027
+ // Object-form tryPromise: bare tryPromise wraps any rejection as an
1028
+ // UnknownError whose .message is the useless "An error occurred in
1029
+ // Effect.tryPromise" (the real error lands in .cause), so the failed-run
1030
+ // error field / WorkflowFinished.error below would be opaque. Catching to
1031
+ // the raw Error makes result.failure the sandbox Error itself, whose
1032
+ // .message already carries the guest {name,message,stack} (vm.dump
1033
+ // preserves it through the sandbox throw site) — a script-logic crash is
1034
+ // then diagnosable from the run's error alone, no repro needed.
1035
+ // Per-run PRNG seed = first 4 bytes of sha1(runID). runID is unique-per-run
1036
+ // and persisted, so resume of the SAME run derives the SAME seed → guest
1037
+ // Math.random replays identically (the replay invariant). Two UNRELATED runs
1038
+ // of the same script get DIFFERENT runIDs → different seeds → different
1039
+ // sequences, so sampling-style scripts get fresh coverage instead of
1040
+ // repeating the same picks. Bun's lifetime-classify verification sample
1041
+ // is the motivating use case.
1042
+ const seed = createHash("sha1").update(runID).digest().readUInt32BE(0)
1043
+ const result = yield* Effect.tryPromise({
1044
+ try: () => evalScript(body, hooks, { deadlineMs: input.scriptDeadlineMs ?? SCRIPT_DEADLINE_MS, args: input.args, seed }),
1045
+ catch: (e) => (e instanceof Error ? e : new Error(String(e))),
1046
+ }).pipe(Effect.result)
1047
+
1048
+ if (result._tag === "Success") {
1049
+ entry.status = "completed"
1050
+ yield* flushNow(entry)
1051
+ yield* WorkflowPersistence.recordTerminal({ runID, status: "completed" }).pipe(Effect.ignore)
1052
+ yield* Deferred.succeed(deferred, { status: "completed", result: result.success })
1053
+ yield* bus.publish(WorkflowFinished, { sessionID: input.sessionID, runID, status: "completed" })
1054
+ // Notify the parent so its next turn drains a completion message, the
1055
+ // same way background actors notify on terminal (see actor/spawn.ts
1056
+ // forkWork.notify). Fire-and-forget: a notify failure (e.g. parent row
1057
+ // gone) must never fail the run, and wait-ers are already unblocked
1058
+ // above by Deferred.succeed.
1059
+ yield* inbox
1060
+ .send({
1061
+ receiverSessionID: input.sessionID,
1062
+ receiverActorID: input.parentActorID,
1063
+ senderSessionID: input.sessionID,
1064
+ senderActorID: "workflow",
1065
+ type: "actor_notification",
1066
+ content: `Workflow completed. run_id: ${runID}\n` + JSON.stringify(result.success ?? null).slice(0, 4000),
1067
+ })
1068
+ .pipe(Effect.ignore)
1069
+ return
1070
+ }
1071
+ // Non-success terminal: reclaim in-flight agents + worktrees so a
1072
+ // deadline-fire / script throw leaves a clean slate for a convergent
1073
+ // re-run. Success path does NOT reclaim — kept worktrees are the deliverable.
1074
+ yield* reclaim(entry)
1075
+ const error = result.failure instanceof Error ? result.failure.message : String(result.failure)
1076
+ entry.status = "failed"
1077
+ log.warn("workflow run failed", { runID, error })
1078
+ yield* flushNow(entry)
1079
+ yield* WorkflowPersistence.recordTerminal({ runID, status: "failed", error }).pipe(Effect.ignore)
1080
+ yield* Deferred.succeed(deferred, { status: "failed", error })
1081
+ yield* bus.publish(WorkflowFinished, { sessionID: input.sessionID, runID, status: "failed", error })
1082
+ yield* inbox
1083
+ .send({
1084
+ receiverSessionID: input.sessionID,
1085
+ receiverActorID: input.parentActorID,
1086
+ senderSessionID: input.sessionID,
1087
+ senderActorID: "workflow",
1088
+ type: "actor_notification",
1089
+ content: `Workflow failed. run_id: ${runID}\nerror: ${error}`,
1090
+ })
1091
+ .pipe(Effect.ignore)
1092
+ })
1093
+
1094
+ entry.fiber = yield* work.pipe(Effect.forkIn(scope))
1095
+ return { runID }
1096
+ })
1097
+
1098
+ const start = Effect.fn("WorkflowRuntime.start")(function* (input: StartInput) {
1099
+ const parsed = parseMeta(input.script)
1100
+ if (!parsed.ok) return yield* Effect.die(parsed.error)
1101
+ const runID = Identifier.descending("workflow")
1102
+ return yield* launch(input, runID, parsed.meta.name)
1103
+ })
1104
+
1105
+ const status = Effect.fn("WorkflowRuntime.status")(function* (input: { runID: string }) {
1106
+ const entry = runs.get(input.runID)
1107
+ if (!entry) return { status: "unknown" as const, agentCount: 0 }
1108
+ return {
1109
+ status: entry.status,
1110
+ agentCount: entry.agentCount,
1111
+ ...(entry.currentPhase !== undefined ? { currentPhase: entry.currentPhase } : {}),
1112
+ }
1113
+ })
1114
+
1115
+ const wait = Effect.fn("WorkflowRuntime.wait")(function* (input: { runID: string; timeoutMs?: number }) {
1116
+ const entry = runs.get(input.runID)
1117
+ if (!entry) return { status: "failed" as const, error: `unknown runID ${input.runID}` }
1118
+ if (input.timeoutMs === undefined) return yield* Deferred.await(entry.deferred)
1119
+ const raced = yield* Deferred.await(entry.deferred).pipe(
1120
+ Effect.timeout(input.timeoutMs),
1121
+ Effect.catchTag("TimeoutError", () => Effect.succeed(null)),
1122
+ )
1123
+ if (raced === null) return { status: "failed" as const, error: "workflow wait timed out" }
1124
+ return raced
1125
+ })
1126
+
1127
+ const cancel = Effect.fn("WorkflowRuntime.cancel")(function* (input: { runID: string }) {
1128
+ const entry = runs.get(input.runID)
1129
+ if (!entry) return
1130
+ yield* cancelEntry(entry)
1131
+ })
1132
+
1133
+ const list = Effect.fn("WorkflowRuntime.list")(function* (input?: { sessionID?: SessionID }) {
1134
+ return yield* WorkflowPersistence.list(input)
1135
+ })
1136
+
1137
+ // Re-launch a persisted run under the SAME runID via the shared launch path.
1138
+ // recordStart's onConflictDoUpdate flips the existing row back to "running" and
1139
+ // runs.set overwrites the stale terminal entry (its old fiber is already done).
1140
+ // model/concurrency/deadline are not persisted in v1 — launch applies defaults.
1141
+ const resume = Effect.fn("WorkflowRuntime.resume")(function* (input: { runID: string; agentTimeoutMs?: number }) {
1142
+ // SERIALIZE same-runID resume with the repo's in-process reader/writer lock
1143
+ // (util/lock.ts: a module-global Map mutex). The live-guard below is a
1144
+ // check-then-act (read runs.get → decide → launch) and is NOT atomic on its
1145
+ // own: two concurrent resume(sameRunID) of a completed run would BOTH read
1146
+ // status !== "running", BOTH pass the guard, and BOTH launch() — and launch
1147
+ // does runs.set(runID, entry), so the second clobbers the first (orphaned
1148
+ // fiber, raced counter flush) and both append to the same .jsonl journal.
1149
+ // Holding the write lock across the guard THROUGH launch closes that window:
1150
+ // the first waiter launches and flips the entry to "running" before releasing,
1151
+ // so the second waiter sees status "running" at the guard and bails. We do NOT
1152
+ // hold it for the whole run (launch forks the work fiber and returns once the
1153
+ // entry is "running") — only the resume decision + entry creation is serialized.
1154
+ // LIMITATION: this is in-process only. Two SEPARATE processes resuming the same
1155
+ // runID against the same DB (e.g. two server instances) are NOT covered — there
1156
+ // is no shared/file-lock infra in this repo to reuse, and cross-process resume
1157
+ // is out of scope for MR104 P2-1.
1158
+ // Acquire as a JS Promise<Disposable> (Lock.write is promise-based; there is no
1159
+ // existing Effect-context consumer to mirror, so we bridge via Effect.promise),
1160
+ // and release in Effect.ensuring so it ALWAYS releases — even if load /
1161
+ // readScript / launch throws — otherwise a failed resume would deadlock every
1162
+ // future resume of this runID.
1163
+ const lock = yield* Effect.promise(() => Lock.write("workflow-resume:" + input.runID))
1164
+ return yield* Effect.gen(function* () {
1165
+ // Refuse to resume a run that is still LIVE in this process: launch would
1166
+ // runs.set() over the live entry, orphaning the running fiber (double parent
1167
+ // notify, raced counter flush, unreclaimable by cancel). The DB row is NOT the
1168
+ // signal — a process-exited run still reads "running" there and IS resumable;
1169
+ // a live `runs` entry means a fiber is actually executing here.
1170
+ const live = runs.get(input.runID)
1171
+ if (live && live.status === "running") return { runID: input.runID, resumed: false }
1172
+ const row = yield* WorkflowPersistence.load(input.runID)
1173
+ if (!row) return { runID: input.runID, resumed: false }
1174
+ // readScript is Effect.promise — a missing file rejects as a DEFECT, which
1175
+ // Effect.exit captures (Effect.result/option/catchAll do not catch defects in
1176
+ // this effect version). Treat a missing or empty script as not-resumable.
1177
+ const read = yield* WorkflowPersistence.readScript(input.runID).pipe(Effect.exit)
1178
+ const script = Exit.isSuccess(read) ? read.value : ""
1179
+ if (!script) return { runID: input.runID, resumed: false }
1180
+ // Script-change invalidation (MR104 P1-2): the journal keys results by
1181
+ // {prompt,agentType,model,schema,phase}+occ, NOT by the script body — so a
1182
+ // between-cycle edit would replay OLD results onto NEW code paths (silent
1183
+ // divergence). Compare the persisted sha (stamped at the prior launch) to the
1184
+ // CURRENT script's sha; on any mismatch — including a null stored sha (a run
1185
+ // recorded before this column existed → "unknown" → treat as changed) — pass
1186
+ // freshJournal so launch truncates the stale journal and runs from scratch,
1187
+ // re-stamping the new sha for the next resume. A match → normal replay.
1188
+ const currentSha = createHash("sha256").update(script).digest("hex")
1189
+ const freshJournal = row.scriptSha !== currentSha
1190
+ yield* launch(
1191
+ {
1192
+ script,
1193
+ sessionID: row.sessionID,
1194
+ parentActorID: row.parentActorID ?? "main",
1195
+ args: row.args,
1196
+ freshJournal,
1197
+ // Per-agent timeout: caller's explicit override > persisted value > undefined (off).
1198
+ // The row's agent_timeout_ms was stamped at the original launch (or last resume
1199
+ // that supplied an explicit override), so a UI-side resume that doesn't know
1200
+ // the original launch params (e.g. TUI's /workflows resume) inherits the
1201
+ // original timeout instead of silently dropping to unbounded — which used to
1202
+ // let a wedged mimo TTFT stall the resumed run forever.
1203
+ agentTimeoutMs: input.agentTimeoutMs ?? row.agentTimeoutMs,
1204
+ },
1205
+ input.runID,
1206
+ row.name,
1207
+ )
1208
+ return { runID: input.runID, resumed: true }
1209
+ }).pipe(Effect.ensuring(Effect.sync(() => lock[Symbol.dispose]())))
1210
+ })
1211
+
1212
+ const impl = Service.of({ start, status, wait, cancel, list, resume })
1213
+ // Late-bind the impl so the `workflow` tool can resolve it without forcing a
1214
+ // WorkflowRuntime.Service requirement onto ToolRegistry.layer. See
1215
+ // runtime-ref.ts for rationale.
1216
+ workflowRef.current = impl
1217
+ yield* Effect.addFinalizer(() =>
1218
+ Effect.sync(() => {
1219
+ if (workflowRef.current === impl) workflowRef.current = undefined
1220
+ }),
1221
+ )
1222
+ return impl
1223
+ }),
1224
+ )
1225
+
1226
+ export const defaultLayer = layer.pipe(
1227
+ Layer.provide(Bus.defaultLayer),
1228
+ Layer.provide(Inbox.defaultLayer),
1229
+ Layer.provide(Worktree.defaultLayer),
1230
+ Layer.provide(Provider.defaultLayer),
1231
+ Layer.provide(Config.defaultLayer),
1232
+ )
1233
+
1234
+ export * as WorkflowRuntime from "./runtime"