@bastani/atomic 0.9.13 → 0.9.14-alpha.2

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 (762) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +3 -3
  3. package/dist/builtin/intercom/package.json +2 -2
  4. package/dist/builtin/mcp/package.json +3 -3
  5. package/dist/builtin/subagents/CHANGELOG.md +14 -0
  6. package/dist/builtin/subagents/README.md +6 -4
  7. package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
  8. package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
  9. package/dist/builtin/subagents/agents/codebase-locator.md +1 -1
  10. package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
  11. package/dist/builtin/subagents/agents/codebase-pattern-finder.md +1 -1
  12. package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
  13. package/dist/builtin/subagents/agents/codebase-research-locator.md +1 -1
  14. package/dist/builtin/subagents/agents/debugger.md +1 -1
  15. package/dist/builtin/subagents/agents/worker.md +1 -1
  16. package/dist/builtin/subagents/package.json +6 -5
  17. package/dist/builtin/subagents/src/agents/agent-discovery.ts +17 -11
  18. package/dist/builtin/subagents/src/agents/agent-loaders.ts +115 -43
  19. package/dist/builtin/subagents/src/agents/agent-management-helpers.ts +10 -8
  20. package/dist/builtin/subagents/src/agents/agent-management.ts +8 -7
  21. package/dist/builtin/subagents/src/agents/agent-overrides.ts +2 -2
  22. package/dist/builtin/subagents/src/agents/agent-scope.ts +1 -1
  23. package/dist/builtin/subagents/src/agents/agent-selection.ts +1 -1
  24. package/dist/builtin/subagents/src/agents/agent-serializer.ts +24 -3
  25. package/dist/builtin/subagents/src/agents/agent-types.ts +3 -1
  26. package/dist/builtin/subagents/src/agents/agents.ts +7 -5
  27. package/dist/builtin/subagents/src/agents/frontmatter.ts +67 -22
  28. package/dist/builtin/subagents/src/agents/identity.ts +1 -1
  29. package/dist/builtin/subagents/src/agents/skills-paths.ts +1 -1
  30. package/dist/builtin/subagents/src/agents/skills.ts +64 -6
  31. package/dist/builtin/subagents/src/extension/config.ts +1 -1
  32. package/dist/builtin/subagents/src/extension/control-notices.ts +2 -2
  33. package/dist/builtin/subagents/src/extension/doctor.ts +15 -5
  34. package/dist/builtin/subagents/src/extension/fanout-child.ts +12 -12
  35. package/dist/builtin/subagents/src/extension/index.ts +23 -23
  36. package/dist/builtin/subagents/src/extension/notification-content.ts +1 -1
  37. package/dist/builtin/subagents/src/extension/schemas.ts +1 -1
  38. package/dist/builtin/subagents/src/extension/startup-maintenance.ts +3 -3
  39. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +3 -3
  40. package/dist/builtin/subagents/src/intercom/result-intercom.ts +1 -1
  41. package/dist/builtin/subagents/src/intercom/supervisor-authorization.ts +1 -1
  42. package/dist/builtin/subagents/src/runs/foreground/completion-notification.ts +2 -2
  43. package/dist/builtin/subagents/src/runs/foreground/execution-detach-reservations.ts +1 -1
  44. package/dist/builtin/subagents/src/runs/foreground/execution-intercom-detach.ts +3 -3
  45. package/dist/builtin/subagents/src/runs/foreground/execution-run-sync.ts +1 -1
  46. package/dist/builtin/subagents/src/runs/foreground/execution-updates.ts +2 -2
  47. package/dist/builtin/subagents/src/runs/foreground/execution-utils.ts +1 -1
  48. package/dist/builtin/subagents/src/runs/foreground/execution.ts +2 -2
  49. package/dist/builtin/subagents/src/runs/foreground/inprocess-run-sync.ts +27 -11
  50. package/dist/builtin/subagents/src/runs/foreground/notify.ts +3 -3
  51. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-context.ts +11 -11
  52. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-input.ts +3 -3
  53. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +15 -14
  54. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +19 -19
  55. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-runtime.ts +2 -2
  56. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +14 -13
  57. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-status.ts +9 -9
  58. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-types.ts +7 -7
  59. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-worktree.ts +6 -6
  60. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +24 -23
  61. package/dist/builtin/subagents/src/runs/inprocess/control-registry.ts +1 -1
  62. package/dist/builtin/subagents/src/runs/inprocess/control-status.ts +3 -3
  63. package/dist/builtin/subagents/src/runs/inprocess/index.ts +3 -3
  64. package/dist/builtin/subagents/src/runs/inprocess/prompt-behavior.ts +17 -8
  65. package/dist/builtin/subagents/src/runs/inprocess/runner.ts +60 -12
  66. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-api.ts +8 -8
  67. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-control.ts +3 -3
  68. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-core.ts +4 -4
  69. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-projection.ts +4 -4
  70. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-registry.ts +5 -5
  71. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-rendering.ts +3 -3
  72. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-sanitize.ts +3 -3
  73. package/dist/builtin/subagents/src/runs/inprocess.ts +1 -1
  74. package/dist/builtin/subagents/src/runs/shared/long-running-guard.ts +1 -1
  75. package/dist/builtin/subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +1 -1
  76. package/dist/builtin/subagents/src/runs/shared/model-candidate-filter.ts +3 -3
  77. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +2 -2
  78. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -1
  79. package/dist/builtin/subagents/src/runs/shared/single-output.ts +1 -1
  80. package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +1 -1
  81. package/dist/builtin/subagents/src/shared/artifacts.ts +2 -2
  82. package/dist/builtin/subagents/src/shared/event-jsonl-writer.ts +1 -1
  83. package/dist/builtin/subagents/src/shared/fast-mode.ts +1 -1
  84. package/dist/builtin/subagents/src/shared/formatters.ts +2 -2
  85. package/dist/builtin/subagents/src/shared/jsonl-writer.ts +1 -1
  86. package/dist/builtin/subagents/src/shared/model-resolution.ts +1 -1
  87. package/dist/builtin/subagents/src/shared/session-tokens.ts +1 -1
  88. package/dist/builtin/subagents/src/shared/settings.ts +4 -4
  89. package/dist/builtin/subagents/src/shared/status-format.ts +1 -1
  90. package/dist/builtin/subagents/src/shared/types-config.ts +16 -4
  91. package/dist/builtin/subagents/src/shared/types-depth.ts +1 -1
  92. package/dist/builtin/subagents/src/shared/types-nested.ts +1 -1
  93. package/dist/builtin/subagents/src/shared/types-output.ts +1 -1
  94. package/dist/builtin/subagents/src/shared/types-results.ts +1 -1
  95. package/dist/builtin/subagents/src/shared/types-runtime.ts +1 -1
  96. package/dist/builtin/subagents/src/shared/types.ts +6 -6
  97. package/dist/builtin/subagents/src/shared/utils.ts +3 -3
  98. package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +1 -1
  99. package/dist/builtin/subagents/src/slash/slash-bridge.ts +3 -3
  100. package/dist/builtin/subagents/src/slash/slash-commands.ts +6 -6
  101. package/dist/builtin/subagents/src/slash/slash-live-state.ts +3 -3
  102. package/dist/builtin/subagents/src/tui/render-event-formatting.ts +2 -2
  103. package/dist/builtin/subagents/src/tui/render-layout.ts +1 -1
  104. package/dist/builtin/subagents/src/tui/render-progress.ts +2 -2
  105. package/dist/builtin/subagents/src/tui/render-result-compact.ts +7 -7
  106. package/dist/builtin/subagents/src/tui/render-result.ts +10 -10
  107. package/dist/builtin/subagents/src/tui/render-stable-output.ts +1 -1
  108. package/dist/builtin/subagents/src/tui/render-status-progress.ts +5 -5
  109. package/dist/builtin/subagents/src/tui/render.ts +4 -4
  110. package/dist/builtin/web-access/package.json +2 -2
  111. package/dist/builtin/workflows/CHANGELOG.md +57 -0
  112. package/dist/builtin/workflows/README.md +39 -8
  113. package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +16 -5
  114. package/dist/builtin/workflows/builtin/adversarial-verification.ts +4 -0
  115. package/dist/builtin/workflows/builtin/classify-and-act.ts +4 -0
  116. package/dist/builtin/workflows/builtin/fan-out-and-synthesize.ts +4 -0
  117. package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +12 -4
  118. package/dist/builtin/workflows/builtin/generate-and-filter.ts +4 -0
  119. package/dist/builtin/workflows/builtin/goal-models.ts +8 -10
  120. package/dist/builtin/workflows/builtin/goal.ts +4 -0
  121. package/dist/builtin/workflows/builtin/index.d.ts +0 -4
  122. package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +4 -2
  123. package/dist/builtin/workflows/builtin/loop-until-done.ts +4 -0
  124. package/dist/builtin/workflows/builtin/open-claude-design-live-protocol.ts +305 -0
  125. package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +115 -171
  126. package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +9 -25
  127. package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +19 -86
  128. package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +0 -6
  129. package/dist/builtin/workflows/builtin/open-claude-design.d.ts +0 -5
  130. package/dist/builtin/workflows/builtin/open-claude-design.ts +11 -15
  131. package/dist/builtin/workflows/builtin/ralph-models.ts +24 -29
  132. package/dist/builtin/workflows/builtin/ralph.ts +4 -0
  133. package/dist/builtin/workflows/builtin/tournament-runner.ts +6 -3
  134. package/dist/builtin/workflows/builtin/tournament.ts +4 -0
  135. package/dist/builtin/workflows/package.json +2 -2
  136. package/dist/builtin/workflows/skills/impeccable/SKILL.md +3 -3
  137. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +25 -31
  138. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +1 -0
  139. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +16 -10
  140. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +2 -2
  141. package/dist/builtin/workflows/skills/impeccable/reference/android.md +6 -0
  142. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +3 -0
  143. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +3 -1
  144. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +6 -1
  145. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +18 -2
  146. package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +24 -30
  147. package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +1 -0
  148. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +16 -10
  149. package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +2 -2
  150. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +1 -1
  151. package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +1 -0
  152. package/dist/builtin/workflows/skills/impeccable/reference/document.md +1 -1
  153. package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
  154. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +12 -6
  155. package/dist/builtin/workflows/skills/impeccable/reference/init.md +12 -3
  156. package/dist/builtin/workflows/skills/impeccable/reference/ios.md +6 -0
  157. package/dist/builtin/workflows/skills/impeccable/reference/live-setup.md +102 -0
  158. package/dist/builtin/workflows/skills/impeccable/reference/live.md +113 -521
  159. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +39 -26
  160. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
  161. package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -3
  162. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +1 -1
  163. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +25 -11
  164. package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +332 -221
  165. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +10 -19
  166. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +91 -38
  167. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +19 -10
  168. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +56 -6
  169. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +10 -16
  170. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +140 -2
  171. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +1174 -694
  172. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
  173. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +5 -1
  174. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +431 -45
  175. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +10 -1
  176. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +47 -6
  177. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +23 -22
  178. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +0 -10
  179. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +384 -384
  180. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +466 -2
  181. package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +2 -0
  182. package/dist/builtin/workflows/skills/impeccable/scripts/embed-prompt.mjs +270 -0
  183. package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +51 -6
  184. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +70 -10
  185. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +336 -90
  186. package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +38 -3
  187. package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +77 -11
  188. package/dist/builtin/workflows/skills/impeccable/scripts/lib/design-parser.mjs +102 -19
  189. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +44 -59
  190. package/dist/builtin/workflows/skills/impeccable/scripts/lib/is-generated.mjs +3 -0
  191. package/dist/builtin/workflows/skills/impeccable/scripts/lib/open-system-browser.mjs +57 -0
  192. package/dist/builtin/workflows/skills/impeccable/scripts/lib/roll-selection.mjs +369 -0
  193. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +31 -3
  194. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +73 -2
  195. package/dist/builtin/workflows/skills/impeccable/scripts/live/accept-css.mjs +617 -0
  196. package/dist/builtin/workflows/skills/impeccable/scripts/live/accept-verify.mjs +71 -0
  197. package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +29 -2
  198. package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +54 -7
  199. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/astro.mjs +47 -0
  200. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/detect-utils.mjs +157 -0
  201. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/index.mjs +143 -0
  202. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/journal.mjs +205 -0
  203. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
  204. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/nuxt.mjs +170 -0
  205. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/script-src.mjs +26 -0
  206. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
  207. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
  208. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
  209. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
  210. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
  211. package/dist/builtin/workflows/skills/impeccable/scripts/live/instructions.mjs +169 -0
  212. package/dist/builtin/workflows/skills/impeccable/scripts/live/roots.mjs +670 -0
  213. package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +232 -35
  214. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-ast.mjs +969 -0
  215. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +621 -74
  216. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +66 -28
  217. package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +12 -12
  218. package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-surfaces.mjs +75 -0
  219. package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +135 -0
  220. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +23 -37
  221. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +945 -115
  222. package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +33 -1
  223. package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
  224. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +173 -425
  225. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +2 -0
  226. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +16 -3
  227. package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +39 -10
  228. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +194 -27
  229. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +10 -4
  230. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +50 -31
  231. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +91 -28
  232. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +8 -5
  233. package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +759 -109
  234. package/dist/builtin/workflows/src/authoring/workflow.ts +9 -0
  235. package/dist/builtin/workflows/src/authoring.d.ts +3 -0
  236. package/dist/builtin/workflows/src/durable/completed-catalog.ts +23 -2
  237. package/dist/builtin/workflows/src/durable/dbos-backend.ts +30 -17
  238. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +14 -0
  239. package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +60 -57
  240. package/dist/builtin/workflows/src/durable/dbos-process-owner.ts +87 -0
  241. package/dist/builtin/workflows/src/durable/dbos-registration-diagnostics.ts +113 -0
  242. package/dist/builtin/workflows/src/durable/factory.ts +11 -8
  243. package/dist/builtin/workflows/src/durable/tool-failure-checkpoint.ts +5 -0
  244. package/dist/builtin/workflows/src/durable/tool-primitive.ts +186 -20
  245. package/dist/builtin/workflows/src/durable/types.ts +4 -0
  246. package/dist/builtin/workflows/src/durable/workflow-heartbeat-anchor.ts +153 -0
  247. package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +28 -0
  248. package/dist/builtin/workflows/src/engine/run-tool-control-registry.ts +9 -1
  249. package/dist/builtin/workflows/src/engine/run.ts +1 -0
  250. package/dist/builtin/workflows/src/extension/adopt-session-run-state.ts +20 -0
  251. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +9 -30
  252. package/dist/builtin/workflows/src/extension/extension-factory.ts +3 -0
  253. package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +28 -10
  254. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +138 -0
  255. package/dist/builtin/workflows/src/extension/index.bundle.mjs +3365 -1341
  256. package/dist/builtin/workflows/src/extension/workflow-heartbeat-delivery.ts +197 -0
  257. package/dist/builtin/workflows/src/extension/workflow-heartbeat-notice.ts +115 -0
  258. package/dist/builtin/workflows/src/extension/workflow-heartbeat-scheduler.ts +962 -0
  259. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +4 -1
  260. package/dist/builtin/workflows/src/extension/workflow-schema.ts +29 -5
  261. package/dist/builtin/workflows/src/extension/workflow-tool-send.ts +32 -1
  262. package/dist/builtin/workflows/src/runs/background/cancellation-registry.ts +10 -1
  263. package/dist/builtin/workflows/src/runs/background/job-tracker.ts +10 -1
  264. package/dist/builtin/workflows/src/runs/foreground/executor-prompt-nodes.ts +16 -11
  265. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +31 -1
  266. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +41 -10
  267. package/dist/builtin/workflows/src/runs/foreground/stage-runner-messages.ts +48 -0
  268. package/dist/builtin/workflows/src/runs/foreground/stage-runner-output.ts +4 -1
  269. package/dist/builtin/workflows/src/runs/foreground/stage-runner-structured-output.ts +45 -12
  270. package/dist/builtin/workflows/src/sdk-surface.ts +9 -0
  271. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +10 -0
  272. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +10 -0
  273. package/dist/builtin/workflows/src/shared/graph-store-snapshot.ts +14 -2
  274. package/dist/builtin/workflows/src/shared/persistence-restore.ts +12 -0
  275. package/dist/builtin/workflows/src/shared/prompt-answer.ts +130 -0
  276. package/dist/builtin/workflows/src/shared/session-scoped-singleton.ts +98 -0
  277. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +9 -1
  278. package/dist/builtin/workflows/src/shared/store-factory.ts +9 -1
  279. package/dist/builtin/workflows/src/shared/store-public-types.ts +2 -1
  280. package/dist/builtin/workflows/src/shared/store-tool-node-methods.ts +7 -0
  281. package/dist/builtin/workflows/src/shared/store-types.ts +18 -1
  282. package/dist/builtin/workflows/src/shared/tool-payload-bounds.ts +576 -0
  283. package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +1 -0
  284. package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +1 -0
  285. package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.d.ts +21 -0
  286. package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.ts +25 -0
  287. package/dist/builtin/workflows/src/tui/graph-theme.ts +8 -1
  288. package/dist/builtin/workflows/src/tui/graph-view-constants.ts +11 -0
  289. package/dist/builtin/workflows/src/tui/graph-view-input.ts +100 -5
  290. package/dist/builtin/workflows/src/tui/graph-view-layout.ts +42 -11
  291. package/dist/builtin/workflows/src/tui/graph-view-render-helpers.ts +55 -6
  292. package/dist/builtin/workflows/src/tui/graph-view-render.ts +71 -6
  293. package/dist/builtin/workflows/src/tui/graph-view-state.ts +43 -0
  294. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
  295. package/dist/builtin/workflows/src/tui/node-card.ts +5 -1
  296. package/dist/builtin/workflows/src/tui/stage-chat-view-archive-history.ts +11 -2
  297. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +36 -10
  298. package/dist/builtin/workflows/src/tui/switcher.ts +11 -4
  299. package/dist/builtin/workflows/src/tui/tool-detail.ts +348 -0
  300. package/dist/builtin/workflows/src/tui/workflow-status.ts +5 -2
  301. package/dist/cli/args.d.ts +1 -0
  302. package/dist/cli/args.d.ts.map +1 -1
  303. package/dist/cli/args.js +11 -0
  304. package/dist/cli/args.js.map +1 -1
  305. package/dist/cli/auth-check.d.ts +1 -1
  306. package/dist/cli/auth-check.js.map +1 -1
  307. package/dist/cli/credential-print.d.ts +1 -1
  308. package/dist/cli/credential-print.js.map +1 -1
  309. package/dist/cli/list-models.d.ts +1 -1
  310. package/dist/cli/list-models.js.map +1 -1
  311. package/dist/cli/startup-ui.d.ts +9 -0
  312. package/dist/cli/startup-ui.d.ts.map +1 -1
  313. package/dist/cli/startup-ui.js +7 -1
  314. package/dist/cli/startup-ui.js.map +1 -1
  315. package/dist/core/agent-session-custom-message-commit.js +2 -2
  316. package/dist/core/agent-session-custom-message-commit.js.map +1 -1
  317. package/dist/core/agent-session-export.d.ts +6 -1
  318. package/dist/core/agent-session-export.d.ts.map +1 -1
  319. package/dist/core/agent-session-export.js +6 -3
  320. package/dist/core/agent-session-export.js.map +1 -1
  321. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  322. package/dist/core/agent-session-extension-bindings.js +6 -4
  323. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  324. package/dist/core/agent-session-methods.d.ts +5 -2
  325. package/dist/core/agent-session-methods.d.ts.map +1 -1
  326. package/dist/core/agent-session-methods.js.map +1 -1
  327. package/dist/core/agent-session-prompt.d.ts +12 -1
  328. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  329. package/dist/core/agent-session-prompt.js +31 -10
  330. package/dist/core/agent-session-prompt.js.map +1 -1
  331. package/dist/core/agent-session-runtime-auth.js.map +1 -1
  332. package/dist/core/agent-session-services.d.ts +1 -1
  333. package/dist/core/agent-session-services.js.map +1 -1
  334. package/dist/core/agent-session-skill-block.d.ts +1 -0
  335. package/dist/core/agent-session-skill-block.d.ts.map +1 -1
  336. package/dist/core/agent-session-skill-block.js +18 -2
  337. package/dist/core/agent-session-skill-block.js.map +1 -1
  338. package/dist/core/agent-session-state.d.ts.map +1 -1
  339. package/dist/core/agent-session-state.js +2 -1
  340. package/dist/core/agent-session-state.js.map +1 -1
  341. package/dist/core/agent-session-types.d.ts +2 -1
  342. package/dist/core/agent-session-types.d.ts.map +1 -1
  343. package/dist/core/agent-session-types.js.map +1 -1
  344. package/dist/core/agent-session.d.ts +1 -1
  345. package/dist/core/agent-session.js.map +1 -1
  346. package/dist/core/atomic-guide-command.js +2 -2
  347. package/dist/core/atomic-guide-command.js.map +1 -1
  348. package/dist/core/builtin-packages.d.ts.map +1 -1
  349. package/dist/core/builtin-packages.js +0 -6
  350. package/dist/core/builtin-packages.js.map +1 -1
  351. package/dist/core/cloudflare-gateway-binding.d.ts +11 -0
  352. package/dist/core/cloudflare-gateway-binding.d.ts.map +1 -0
  353. package/dist/core/cloudflare-gateway-binding.js +11 -0
  354. package/dist/core/cloudflare-gateway-binding.js.map +1 -0
  355. package/dist/core/codex-fast-mode-transport.d.ts +12 -0
  356. package/dist/core/codex-fast-mode-transport.d.ts.map +1 -0
  357. package/dist/core/codex-fast-mode-transport.js +130 -0
  358. package/dist/core/codex-fast-mode-transport.js.map +1 -0
  359. package/dist/core/codex-fast-mode.d.ts +20 -6
  360. package/dist/core/codex-fast-mode.d.ts.map +1 -1
  361. package/dist/core/codex-fast-mode.js +111 -3
  362. package/dist/core/codex-fast-mode.js.map +1 -1
  363. package/dist/core/diagnostics.d.ts +4 -0
  364. package/dist/core/diagnostics.d.ts.map +1 -1
  365. package/dist/core/diagnostics.js.map +1 -1
  366. package/dist/core/event-bus.d.ts +12 -0
  367. package/dist/core/event-bus.d.ts.map +1 -1
  368. package/dist/core/event-bus.js +22 -0
  369. package/dist/core/event-bus.js.map +1 -1
  370. package/dist/core/experimental.d.ts +25 -0
  371. package/dist/core/experimental.d.ts.map +1 -1
  372. package/dist/core/experimental.js +24 -0
  373. package/dist/core/experimental.js.map +1 -1
  374. package/dist/core/export-html/template-js/tree-filter-render.js +4 -3
  375. package/dist/core/export-html/template.js +4 -3
  376. package/dist/core/extension-session-state.d.ts +28 -0
  377. package/dist/core/extension-session-state.d.ts.map +1 -0
  378. package/dist/core/extension-session-state.js +55 -0
  379. package/dist/core/extension-session-state.js.map +1 -0
  380. package/dist/core/extensions/api-types.d.ts +6 -0
  381. package/dist/core/extensions/api-types.d.ts.map +1 -1
  382. package/dist/core/extensions/api-types.js.map +1 -1
  383. package/dist/core/extensions/context-types.d.ts +4 -0
  384. package/dist/core/extensions/context-types.d.ts.map +1 -1
  385. package/dist/core/extensions/context-types.js.map +1 -1
  386. package/dist/core/extensions/index.d.ts +1 -0
  387. package/dist/core/extensions/index.d.ts.map +1 -1
  388. package/dist/core/extensions/index.js +1 -0
  389. package/dist/core/extensions/index.js.map +1 -1
  390. package/dist/core/extensions/loader-api.d.ts +1 -1
  391. package/dist/core/extensions/loader-api.d.ts.map +1 -1
  392. package/dist/core/extensions/loader-api.js +16 -10
  393. package/dist/core/extensions/loader-api.js.map +1 -1
  394. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  395. package/dist/core/extensions/loader-virtual-modules.js +12 -1
  396. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  397. package/dist/core/extensions/runner-context.d.ts +2 -0
  398. package/dist/core/extensions/runner-context.d.ts.map +1 -1
  399. package/dist/core/extensions/runner-context.js +8 -0
  400. package/dist/core/extensions/runner-context.js.map +1 -1
  401. package/dist/core/extensions/runner.d.ts +1 -0
  402. package/dist/core/extensions/runner.d.ts.map +1 -1
  403. package/dist/core/extensions/runner.js +3 -0
  404. package/dist/core/extensions/runner.js.map +1 -1
  405. package/dist/core/extensions/runtime-types.d.ts +3 -0
  406. package/dist/core/extensions/runtime-types.d.ts.map +1 -1
  407. package/dist/core/extensions/runtime-types.js.map +1 -1
  408. package/dist/core/extensions/ui-types.d.ts +26 -0
  409. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  410. package/dist/core/extensions/ui-types.js +17 -1
  411. package/dist/core/extensions/ui-types.js.map +1 -1
  412. package/dist/core/http-dispatcher.d.ts.map +1 -1
  413. package/dist/core/http-dispatcher.js +5 -0
  414. package/dist/core/http-dispatcher.js.map +1 -1
  415. package/dist/core/keybindings.d.ts +24 -0
  416. package/dist/core/keybindings.d.ts.map +1 -1
  417. package/dist/core/keybindings.js +6 -0
  418. package/dist/core/keybindings.js.map +1 -1
  419. package/dist/core/model-config.d.ts +5 -0
  420. package/dist/core/model-config.d.ts.map +1 -1
  421. package/dist/core/model-config.js +1 -0
  422. package/dist/core/model-config.js.map +1 -1
  423. package/dist/core/model-registry.d.ts +1 -1
  424. package/dist/core/model-registry.js.map +1 -1
  425. package/dist/core/model-resolver-cli.d.ts +1 -1
  426. package/dist/core/model-resolver-cli.js.map +1 -1
  427. package/dist/core/model-resolver-defaults.js +3 -3
  428. package/dist/core/model-resolver-defaults.js.map +1 -1
  429. package/dist/core/model-resolver-initial.d.ts +1 -1
  430. package/dist/core/model-resolver-initial.js.map +1 -1
  431. package/dist/core/model-resolver-scope.d.ts +1 -1
  432. package/dist/core/model-resolver-scope.js.map +1 -1
  433. package/dist/core/model-runtime.d.ts +54 -0
  434. package/dist/core/model-runtime.d.ts.map +1 -1
  435. package/dist/core/model-runtime.js +96 -13
  436. package/dist/core/model-runtime.js.map +1 -1
  437. package/dist/core/oauth-login.js.map +1 -1
  438. package/dist/core/project-trust.d.ts +10 -0
  439. package/dist/core/project-trust.d.ts.map +1 -1
  440. package/dist/core/project-trust.js +12 -0
  441. package/dist/core/project-trust.js.map +1 -1
  442. package/dist/core/resource-loader-assets.d.ts.map +1 -1
  443. package/dist/core/resource-loader-assets.js +14 -5
  444. package/dist/core/resource-loader-assets.js.map +1 -1
  445. package/dist/core/resource-loader-core.d.ts +3 -0
  446. package/dist/core/resource-loader-core.d.ts.map +1 -1
  447. package/dist/core/resource-loader-core.js +5 -0
  448. package/dist/core/resource-loader-core.js.map +1 -1
  449. package/dist/core/resource-loader-internals.d.ts +2 -0
  450. package/dist/core/resource-loader-internals.d.ts.map +1 -1
  451. package/dist/core/resource-loader-internals.js.map +1 -1
  452. package/dist/core/resource-loader-reload.d.ts.map +1 -1
  453. package/dist/core/resource-loader-reload.js +2 -0
  454. package/dist/core/resource-loader-reload.js.map +1 -1
  455. package/dist/core/resource-loader-types.d.ts +2 -0
  456. package/dist/core/resource-loader-types.d.ts.map +1 -1
  457. package/dist/core/resource-loader-types.js.map +1 -1
  458. package/dist/core/resource-loader.d.ts +2 -0
  459. package/dist/core/resource-loader.d.ts.map +1 -1
  460. package/dist/core/resource-loader.js +1 -0
  461. package/dist/core/resource-loader.js.map +1 -1
  462. package/dist/core/sdk-types.d.ts +7 -6
  463. package/dist/core/sdk-types.d.ts.map +1 -1
  464. package/dist/core/sdk-types.js.map +1 -1
  465. package/dist/core/sdk.d.ts.map +1 -1
  466. package/dist/core/sdk.js +17 -6
  467. package/dist/core/sdk.js.map +1 -1
  468. package/dist/core/session-manager-core.d.ts +7 -1
  469. package/dist/core/session-manager-core.d.ts.map +1 -1
  470. package/dist/core/session-manager-core.js +11 -2
  471. package/dist/core/session-manager-core.js.map +1 -1
  472. package/dist/core/session-manager-entries.d.ts +2 -2
  473. package/dist/core/session-manager-entries.d.ts.map +1 -1
  474. package/dist/core/session-manager-entries.js +6 -4
  475. package/dist/core/session-manager-entries.js.map +1 -1
  476. package/dist/core/session-manager-list.d.ts.map +1 -1
  477. package/dist/core/session-manager-list.js +5 -1
  478. package/dist/core/session-manager-list.js.map +1 -1
  479. package/dist/core/session-manager-types.d.ts +25 -2
  480. package/dist/core/session-manager-types.d.ts.map +1 -1
  481. package/dist/core/session-manager-types.js.map +1 -1
  482. package/dist/core/session-manager.d.ts +1 -1
  483. package/dist/core/session-manager.d.ts.map +1 -1
  484. package/dist/core/session-manager.js.map +1 -1
  485. package/dist/core/settings-manager-basic-accessors.d.ts +1 -0
  486. package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
  487. package/dist/core/settings-manager-basic-accessors.js +8 -0
  488. package/dist/core/settings-manager-basic-accessors.js.map +1 -1
  489. package/dist/core/settings-manager-ui-accessors.d.ts +3 -1
  490. package/dist/core/settings-manager-ui-accessors.d.ts.map +1 -1
  491. package/dist/core/settings-manager-ui-accessors.js +9 -0
  492. package/dist/core/settings-manager-ui-accessors.js.map +1 -1
  493. package/dist/core/settings-manager.d.ts +1 -1
  494. package/dist/core/settings-manager.d.ts.map +1 -1
  495. package/dist/core/settings-manager.js.map +1 -1
  496. package/dist/core/settings-types.d.ts +4 -0
  497. package/dist/core/settings-types.d.ts.map +1 -1
  498. package/dist/core/settings-types.js.map +1 -1
  499. package/dist/core/skill-catalog.d.ts +35 -0
  500. package/dist/core/skill-catalog.d.ts.map +1 -0
  501. package/dist/core/skill-catalog.js +264 -0
  502. package/dist/core/skill-catalog.js.map +1 -0
  503. package/dist/core/skills-async.d.ts.map +1 -1
  504. package/dist/core/skills-async.js +12 -6
  505. package/dist/core/skills-async.js.map +1 -1
  506. package/dist/core/skills.d.ts +2 -0
  507. package/dist/core/skills.d.ts.map +1 -1
  508. package/dist/core/skills.js +8 -5
  509. package/dist/core/skills.js.map +1 -1
  510. package/dist/core/slash-commands.d.ts.map +1 -1
  511. package/dist/core/slash-commands.js +1 -5
  512. package/dist/core/slash-commands.js.map +1 -1
  513. package/dist/core/system-prompt.js +2 -2
  514. package/dist/core/system-prompt.js.map +1 -1
  515. package/dist/core/tools/ask-user-question/ask-user-question.d.ts +25 -2
  516. package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
  517. package/dist/core/tools/ask-user-question/ask-user-question.js +47 -18
  518. package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
  519. package/dist/core/tools/ask-user-question/view/components/multi-select-view.d.ts.map +1 -1
  520. package/dist/core/tools/ask-user-question/view/components/multi-select-view.js +5 -2
  521. package/dist/core/tools/ask-user-question/view/components/multi-select-view.js.map +1 -1
  522. package/dist/core/tools/ask-user-question/view/components/submit-picker.d.ts.map +1 -1
  523. package/dist/core/tools/ask-user-question/view/components/submit-picker.js +3 -1
  524. package/dist/core/tools/ask-user-question/view/components/submit-picker.js.map +1 -1
  525. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
  526. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +8 -1
  527. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
  528. package/dist/core/tools/bash.d.ts.map +1 -1
  529. package/dist/core/tools/bash.js +2 -0
  530. package/dist/core/tools/bash.js.map +1 -1
  531. package/dist/core/tools/edit.d.ts.map +1 -1
  532. package/dist/core/tools/edit.js +2 -0
  533. package/dist/core/tools/edit.js.map +1 -1
  534. package/dist/core/tools/find.d.ts.map +1 -1
  535. package/dist/core/tools/find.js +3 -1
  536. package/dist/core/tools/find.js.map +1 -1
  537. package/dist/core/tools/grep.js +1 -1
  538. package/dist/core/tools/grep.js.map +1 -1
  539. package/dist/core/tools/hashline-engine/format.d.ts +2 -0
  540. package/dist/core/tools/hashline-engine/format.d.ts.map +1 -1
  541. package/dist/core/tools/hashline-engine/format.js +4 -2
  542. package/dist/core/tools/hashline-engine/format.js.map +1 -1
  543. package/dist/core/tools/hashline-engine/messages.d.ts +2 -0
  544. package/dist/core/tools/hashline-engine/messages.d.ts.map +1 -1
  545. package/dist/core/tools/hashline-engine/messages.js +4 -2
  546. package/dist/core/tools/hashline-engine/messages.js.map +1 -1
  547. package/dist/core/tools/hashline-engine/parser.d.ts.map +1 -1
  548. package/dist/core/tools/hashline-engine/parser.js +16 -4
  549. package/dist/core/tools/hashline-engine/parser.js.map +1 -1
  550. package/dist/core/tools/hashline-engine/tokenizer.d.ts.map +1 -1
  551. package/dist/core/tools/hashline-engine/tokenizer.js +37 -27
  552. package/dist/core/tools/hashline-engine/tokenizer.js.map +1 -1
  553. package/dist/core/tools/hashline.d.ts.map +1 -1
  554. package/dist/core/tools/hashline.js +8 -2
  555. package/dist/core/tools/hashline.js.map +1 -1
  556. package/dist/core/tools/ls.d.ts.map +1 -1
  557. package/dist/core/tools/ls.js +2 -0
  558. package/dist/core/tools/ls.js.map +1 -1
  559. package/dist/core/tools/read.d.ts.map +1 -1
  560. package/dist/core/tools/read.js +8 -1
  561. package/dist/core/tools/read.js.map +1 -1
  562. package/dist/core/tools/search.d.ts.map +1 -1
  563. package/dist/core/tools/search.js +2 -0
  564. package/dist/core/tools/search.js.map +1 -1
  565. package/dist/core/tools/todos.d.ts +3 -12
  566. package/dist/core/tools/todos.d.ts.map +1 -1
  567. package/dist/core/tools/todos.js +19 -4
  568. package/dist/core/tools/todos.js.map +1 -1
  569. package/dist/core/tools/write.d.ts.map +1 -1
  570. package/dist/core/tools/write.js +2 -0
  571. package/dist/core/tools/write.js.map +1 -1
  572. package/dist/index-extensions.d.ts +1 -1
  573. package/dist/index-extensions.d.ts.map +1 -1
  574. package/dist/index-extensions.js +1 -1
  575. package/dist/index-extensions.js.map +1 -1
  576. package/dist/index.d.ts +8 -5
  577. package/dist/index.d.ts.map +1 -1
  578. package/dist/index.js +6 -2
  579. package/dist/index.js.map +1 -1
  580. package/dist/main-runtime-api-key.d.ts +1 -1
  581. package/dist/main-runtime-api-key.js.map +1 -1
  582. package/dist/main-session-options.d.ts +1 -1
  583. package/dist/main-session-options.js.map +1 -1
  584. package/dist/main.d.ts.map +1 -1
  585. package/dist/main.js +10 -2
  586. package/dist/main.js.map +1 -1
  587. package/dist/modes/interactive/components/chat-session-host-rendering.js +1 -1
  588. package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
  589. package/dist/modes/interactive/components/chat-session-host-state.d.ts +8 -0
  590. package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
  591. package/dist/modes/interactive/components/chat-session-host-state.js +8 -0
  592. package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
  593. package/dist/modes/interactive/components/chat-session-host.d.ts +26 -0
  594. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  595. package/dist/modes/interactive/components/chat-session-host.js +38 -0
  596. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  597. package/dist/modes/interactive/components/chat-transcript.d.ts +37 -0
  598. package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
  599. package/dist/modes/interactive/components/chat-transcript.js +54 -1
  600. package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
  601. package/dist/modes/interactive/components/fast-mode-selector.js +1 -1
  602. package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -1
  603. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  604. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  605. package/dist/modes/interactive/components/model-selector.js +2 -1
  606. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  607. package/dist/modes/interactive/components/oauth-selector.d.ts +1 -1
  608. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  609. package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts +95 -0
  610. package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts.map +1 -0
  611. package/dist/modes/interactive/components/reserved-bottom-overlay.js +234 -0
  612. package/dist/modes/interactive/components/reserved-bottom-overlay.js.map +1 -0
  613. package/dist/modes/interactive/components/session-selector-search.d.ts +4 -0
  614. package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
  615. package/dist/modes/interactive/components/session-selector-search.js +4 -0
  616. package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
  617. package/dist/modes/interactive/components/settings-selector-handlers.d.ts.map +1 -1
  618. package/dist/modes/interactive/components/settings-selector-handlers.js +3 -0
  619. package/dist/modes/interactive/components/settings-selector-handlers.js.map +1 -1
  620. package/dist/modes/interactive/components/settings-selector-items.d.ts.map +1 -1
  621. package/dist/modes/interactive/components/settings-selector-items.js +7 -0
  622. package/dist/modes/interactive/components/settings-selector-items.js.map +1 -1
  623. package/dist/modes/interactive/components/settings-selector-types.d.ts +3 -1
  624. package/dist/modes/interactive/components/settings-selector-types.d.ts.map +1 -1
  625. package/dist/modes/interactive/components/settings-selector-types.js.map +1 -1
  626. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  627. package/dist/modes/interactive/components/tool-execution.js +12 -1
  628. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  629. package/dist/modes/interactive/interactive-auth-login.js.map +1 -1
  630. package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
  631. package/dist/modes/interactive/interactive-autocomplete.js +5 -4
  632. package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  633. package/dist/modes/interactive/interactive-bash-compact.d.ts.map +1 -1
  634. package/dist/modes/interactive/interactive-bash-compact.js +2 -2
  635. package/dist/modes/interactive/interactive-bash-compact.js.map +1 -1
  636. package/dist/modes/interactive/interactive-extension-custom-ui.js +94 -6
  637. package/dist/modes/interactive/interactive-extension-custom-ui.js.map +1 -1
  638. package/dist/modes/interactive/interactive-mode-base.d.ts +41 -3
  639. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  640. package/dist/modes/interactive/interactive-mode-base.js +80 -11
  641. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  642. package/dist/modes/interactive/interactive-mode-surface.d.ts +9 -1
  643. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  644. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  645. package/dist/modes/interactive/interactive-mode-types.d.ts +2 -0
  646. package/dist/modes/interactive/interactive-mode-types.d.ts.map +1 -1
  647. package/dist/modes/interactive/interactive-mode-types.js.map +1 -1
  648. package/dist/modes/interactive/interactive-model-catalog-startup.d.ts +8 -1
  649. package/dist/modes/interactive/interactive-model-catalog-startup.d.ts.map +1 -1
  650. package/dist/modes/interactive/interactive-model-catalog-startup.js +13 -3
  651. package/dist/modes/interactive/interactive-model-catalog-startup.js.map +1 -1
  652. package/dist/modes/interactive/interactive-model-routing.js +3 -4
  653. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  654. package/dist/modes/interactive/interactive-render-chat.js +6 -0
  655. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  656. package/dist/modes/interactive/interactive-resource-disclosure.js +2 -2
  657. package/dist/modes/interactive/interactive-resource-disclosure.js.map +1 -1
  658. package/dist/modes/interactive/interactive-selectors.js +7 -3
  659. package/dist/modes/interactive/interactive-selectors.js.map +1 -1
  660. package/dist/modes/interactive/interactive-session-runtime.js +7 -1
  661. package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
  662. package/dist/modes/interactive/interactive-slash-commands.js +2 -2
  663. package/dist/modes/interactive/interactive-slash-commands.js.map +1 -1
  664. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  665. package/dist/modes/interactive/interactive-startup.js +34 -9
  666. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  667. package/dist/modes/interactive/interactive-tui.d.ts +21 -1
  668. package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
  669. package/dist/modes/interactive/interactive-tui.js +140 -38
  670. package/dist/modes/interactive/interactive-tui.js.map +1 -1
  671. package/dist/modes/interactive/model-catalog-refresh.d.ts +17 -0
  672. package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
  673. package/dist/modes/interactive/model-catalog-refresh.js +84 -0
  674. package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
  675. package/dist/modes/interactive/theme/dark.json +2 -0
  676. package/dist/modes/interactive/theme/light.json +2 -0
  677. package/dist/modes/interactive/theme/theme-class.d.ts +12 -3
  678. package/dist/modes/interactive/theme/theme-class.d.ts.map +1 -1
  679. package/dist/modes/interactive/theme/theme-class.js +6 -1
  680. package/dist/modes/interactive/theme/theme-class.js.map +1 -1
  681. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  682. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  683. package/dist/modes/interactive/theme/theme-controller.js +23 -10
  684. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  685. package/dist/modes/interactive/theme/theme-loading.d.ts.map +1 -1
  686. package/dist/modes/interactive/theme/theme-loading.js +1 -0
  687. package/dist/modes/interactive/theme/theme-loading.js.map +1 -1
  688. package/dist/modes/interactive/theme/theme-schema.d.ts +8 -0
  689. package/dist/modes/interactive/theme/theme-schema.d.ts.map +1 -1
  690. package/dist/modes/interactive/theme/theme-schema.js +3 -1
  691. package/dist/modes/interactive/theme/theme-schema.js.map +1 -1
  692. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  693. package/dist/modes/interactive-engine/engine-custom-ui.d.ts +2 -0
  694. package/dist/modes/interactive-engine/engine-custom-ui.d.ts.map +1 -1
  695. package/dist/modes/interactive-engine/engine-custom-ui.js +1 -0
  696. package/dist/modes/interactive-engine/engine-custom-ui.js.map +1 -1
  697. package/dist/modes/interactive-engine/protocol.d.ts +1 -0
  698. package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
  699. package/dist/modes/interactive-engine/protocol.js +1 -0
  700. package/dist/modes/interactive-engine/protocol.js.map +1 -1
  701. package/dist/modes/interactive-engine/remote-component.d.ts +3 -1
  702. package/dist/modes/interactive-engine/remote-component.d.ts.map +1 -1
  703. package/dist/modes/interactive-engine/remote-component.js +3 -2
  704. package/dist/modes/interactive-engine/remote-component.js.map +1 -1
  705. package/dist/modes/json-event.d.ts +6 -1
  706. package/dist/modes/json-event.d.ts.map +1 -1
  707. package/dist/modes/json-event.js +17 -2
  708. package/dist/modes/json-event.js.map +1 -1
  709. package/dist/modes/rpc/rpc-client.js.map +1 -1
  710. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  711. package/dist/modes/rpc/rpc-command-handler.js +5 -4
  712. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  713. package/dist/modes/rpc/rpc-provider-auth.js.map +1 -1
  714. package/dist/modes/rpc/rpc-responses.js.map +1 -1
  715. package/dist/modes/rpc/rpc-types.d.ts +1 -1
  716. package/dist/modes/rpc/rpc-types.js.map +1 -1
  717. package/dist/package-manager-cli.js.map +1 -1
  718. package/dist/server/create-harness.d.ts.map +1 -1
  719. package/dist/server/create-harness.js +11 -0
  720. package/dist/server/create-harness.js.map +1 -1
  721. package/dist/utils/abort.d.ts +9 -0
  722. package/dist/utils/abort.d.ts.map +1 -0
  723. package/dist/utils/abort.js +19 -0
  724. package/dist/utils/abort.js.map +1 -0
  725. package/dist/utils/tools-manager.d.ts +16 -1
  726. package/dist/utils/tools-manager.d.ts.map +1 -1
  727. package/dist/utils/tools-manager.js +19 -19
  728. package/dist/utils/tools-manager.js.map +1 -1
  729. package/docs/custom-provider.md +9 -8
  730. package/docs/development.md +0 -1
  731. package/docs/docs.json +0 -1
  732. package/docs/environment-variables.md +3 -0
  733. package/docs/extensions.md +100 -2
  734. package/docs/json.md +3 -1
  735. package/docs/keybindings.md +8 -3
  736. package/docs/models/artificial-analysis-index.md +2 -2
  737. package/docs/models/model-selection.md +32 -23
  738. package/docs/models/pareto-efficiency.md +25 -24
  739. package/docs/providers.md +95 -2
  740. package/docs/quickstart.md +4 -2
  741. package/docs/rpc.md +21 -7
  742. package/docs/sdk.md +26 -1
  743. package/docs/session-format.md +5 -2
  744. package/docs/settings.md +34 -3
  745. package/docs/skills.md +16 -1
  746. package/docs/terminal-setup.md +15 -2
  747. package/docs/themes.md +22 -4
  748. package/docs/tools.md +3 -1
  749. package/docs/tui.md +2 -0
  750. package/docs/usage.md +5 -4
  751. package/docs/workflows.md +96 -19
  752. package/npm-shrinkwrap.json +211 -232
  753. package/package.json +8 -8
  754. package/dist/builtin/i-have-adhd/CHANGELOG.md +0 -30
  755. package/dist/builtin/i-have-adhd/LICENSE +0 -21
  756. package/dist/builtin/i-have-adhd/README.md +0 -18
  757. package/dist/builtin/i-have-adhd/index.ts +0 -238
  758. package/dist/builtin/i-have-adhd/package.json +0 -56
  759. package/dist/builtin/i-have-adhd/skills/i-have-adhd/SKILL.md +0 -140
  760. package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +0 -359
  761. package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +0 -180
  762. package/docs/i-have-adhd.md +0 -36
@@ -0,0 +1,962 @@
1
+ import type { SessionEntry } from "../shared/persistence-restore.js";
2
+ import { effectiveRunStatus } from "../shared/returned-run-status.js";
3
+ import { isTopLevelWorkflowRun } from "../shared/run-visibility.js";
4
+ import type { Store } from "../shared/store.js";
5
+ import { isTerminalRunStatus } from "../shared/store-internal.js";
6
+ import { readGraphStoreSnapshot, subscribeStoreInvalidation } from "../shared/store-observation.js";
7
+ import type { RunSnapshot, StoreSnapshot } from "../shared/store-types.js";
8
+ import {
9
+ WORKFLOW_HEARTBEAT_CUSTOM_TYPE,
10
+ type WorkflowHeartbeatEventDetails,
11
+ type WorkflowHeartbeatIdentity,
12
+ } from "../shared/workflow-heartbeat-contract.js";
13
+ import type { ExtensionAPI } from "./index.js";
14
+ import {
15
+ createWorkflowHeartbeatDelivery,
16
+ defaultWorkflowHeartbeatTimerApi,
17
+ type WorkflowHeartbeatTimerApi,
18
+ type WorkflowHeartbeatTimerHandle,
19
+ } from "./workflow-heartbeat-delivery.js";
20
+ import { formatWorkflowHeartbeatNoticeText } from "./workflow-heartbeat-notice.js";
21
+
22
+ /**
23
+ * Longest delay a host timer accepts before it overflows to a 1 ms fire. A
24
+ * boundary further out than this is armed in one clamped hop; the wake-up finds
25
+ * nothing due and re-arms for the remainder.
26
+ */
27
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
28
+
29
+ const MS_PER_MINUTE = 60_000;
30
+
31
+ /** Minimal run shape the schedule door reads. */
32
+ export type WorkflowHeartbeatRun = Pick<RunSnapshot, "id" | "name" | "startedAt" | "status">;
33
+
34
+ export interface WorkflowHeartbeatScheduleDisabled {
35
+ readonly kind: "disabled";
36
+ }
37
+
38
+ export interface WorkflowHeartbeatScheduleScheduled {
39
+ readonly kind: "scheduled";
40
+ readonly scheduledAt: number;
41
+ }
42
+
43
+ export type WorkflowHeartbeatScheduleResult = WorkflowHeartbeatScheduleDisabled | WorkflowHeartbeatScheduleScheduled;
44
+
45
+ export type WorkflowHeartbeatSuppressionReason = "missing" | "terminal" | "paused" | "duplicate" | "unavailable";
46
+
47
+ export interface WorkflowHeartbeatEnqueued {
48
+ readonly kind: "enqueued";
49
+ readonly identity: WorkflowHeartbeatIdentity;
50
+ }
51
+
52
+ export interface WorkflowHeartbeatSuppressed {
53
+ readonly kind: "suppressed";
54
+ readonly reason: WorkflowHeartbeatSuppressionReason;
55
+ }
56
+
57
+ export type WorkflowHeartbeatEnqueueResult = WorkflowHeartbeatEnqueued | WorkflowHeartbeatSuppressed;
58
+
59
+ export interface WorkflowHeartbeatCleared {
60
+ readonly kind: "cleared";
61
+ }
62
+
63
+ export interface WorkflowHeartbeatAlreadyClear {
64
+ readonly kind: "already-clear";
65
+ }
66
+
67
+ export type WorkflowHeartbeatClearResult = WorkflowHeartbeatCleared | WorkflowHeartbeatAlreadyClear;
68
+
69
+ /**
70
+ * Scheduler state.
71
+ *
72
+ * The cadence is derived, never stored twice: a boundary is a pure function of
73
+ * the run's original start time and the `heartbeatIntervalMinutes` frozen at the
74
+ * authoring door. Session restore already preserves the start time verbatim
75
+ * (`shared/persistence-restore.ts`).
76
+ *
77
+ * One durable record exists, and only because one input is genuinely not
78
+ * derivable: a durable resume re-dispatches under the original workflow id but
79
+ * mints a fresh `RunSnapshot.startedAt`, so the original start survives nowhere
80
+ * else. `durable/workflow-heartbeat-anchor.ts` carries it, write-once, and is
81
+ * read as `min(run.startedAt, anchorAt)` — it can only restore the original
82
+ * anchor, never advance it.
83
+ *
84
+ * `pending` is deliberately never persisted: after a process exit the host
85
+ * session and its queue are gone, so re-delivering that identity would backfill
86
+ * a missed boundary, which the spec forbids.
87
+ */
88
+ export interface WorkflowHeartbeatSchedulerState {
89
+ /** Next due boundary per enabled active run. At most one entry per run. */
90
+ readonly scheduled: Map<string, WorkflowHeartbeatEventDetails>;
91
+ /** Outstanding heartbeat per run — in flight, or admitted and awaiting pickup. */
92
+ readonly pending: Map<string, WorkflowHeartbeatIdentity>;
93
+ /** Exact identity of each admitted heartbeat the parent has not consumed yet, by run. */
94
+ readonly awaitingParentPickup: Map<string, WorkflowHeartbeatIdentity>;
95
+ /** Most recently enqueued boundary per run, so a boundary is never re-raised. */
96
+ readonly lastEnqueuedAt: Map<string, number>;
97
+ /** Resolved cadence anchor per run, memoized after its first durable read. */
98
+ readonly anchorAt: Map<string, number>;
99
+ /** Runs whose launch record is durably acknowledged, so the write stops retrying. */
100
+ readonly anchorPersisted: Set<string>;
101
+ /** Runs with a launch-record write in flight, so at most one is outstanding. */
102
+ readonly anchorWritesPending: Set<string>;
103
+ /**
104
+ * Cadence each run launched with, captured the first time it resolved.
105
+ *
106
+ * An in-flight run keeps the interval its own definition was authored with.
107
+ * Re-reading the live registry every pass would let a mid-run `/workflow
108
+ * reload`, rename, edit, or delete change or stop an already-enabled run —
109
+ * and nothing else in the system lets a reload reach a run in flight, because
110
+ * the executor closes over its own definition for the life of the run.
111
+ */
112
+ readonly intervalMinutes: Map<string, number>;
113
+ }
114
+
115
+ export function createWorkflowHeartbeatSchedulerState(): WorkflowHeartbeatSchedulerState {
116
+ return {
117
+ scheduled: new Map<string, WorkflowHeartbeatEventDetails>(),
118
+ pending: new Map<string, WorkflowHeartbeatIdentity>(),
119
+ awaitingParentPickup: new Map<string, WorkflowHeartbeatIdentity>(),
120
+ lastEnqueuedAt: new Map<string, number>(),
121
+ anchorAt: new Map<string, number>(),
122
+ anchorPersisted: new Set<string>(),
123
+ anchorWritesPending: new Set<string>(),
124
+ intervalMinutes: new Map<string, number>(),
125
+ };
126
+ }
127
+
128
+ export function resetWorkflowHeartbeatSchedulerState(state: WorkflowHeartbeatSchedulerState): void {
129
+ state.scheduled.clear();
130
+ state.pending.clear();
131
+ state.awaitingParentPickup.clear();
132
+ state.lastEnqueuedAt.clear();
133
+ state.anchorAt.clear();
134
+ state.anchorPersisted.clear();
135
+ state.anchorWritesPending.clear();
136
+ state.intervalMinutes.clear();
137
+ }
138
+
139
+ export interface WorkflowHeartbeatSchedulerOptions {
140
+ readonly store: Store;
141
+ readonly sendMessage?: ExtensionAPI["sendMessage"];
142
+ /**
143
+ * Resolved cadence for a workflow name, read from the compiled definition.
144
+ * `undefined` means the definition is not available, which schedules nothing.
145
+ */
146
+ readonly resolveIntervalMinutes: (workflowName: string) => number | undefined;
147
+ /**
148
+ * Whether the host reports heartbeat consumption, by routing its `message_end`
149
+ * event to `notifyHeartbeatConsumed()`.
150
+ *
151
+ * The host resolves `sendMessage` when the card is admitted into the parent's
152
+ * queue, not when the parent reads it. When consumption is reported, an
153
+ * admitted heartbeat holds its slot until the card is actually injected into
154
+ * the conversation, with no deadline — that is what "retain exactly one
155
+ * pending event" requires, and a deadline of any length would breach it. When
156
+ * it is not reported nothing could ever release the slot, so an admitted
157
+ * heartbeat releases it at once rather than silencing the run.
158
+ */
159
+ readonly parentAvailabilityReported?: boolean;
160
+ /** Persisted cadence anchor, best-effort. Omitted disables the durable record. */
161
+ readonly anchorStore?: WorkflowHeartbeatAnchorStore;
162
+ readonly state?: WorkflowHeartbeatSchedulerState;
163
+ readonly now?: () => number;
164
+ readonly timers?: WorkflowHeartbeatTimerApi;
165
+ }
166
+
167
+ /** What a run launched with, recovered across a process boundary. */
168
+ export interface WorkflowHeartbeatLaunchRecord {
169
+ /** The run's original start time. */
170
+ readonly anchorAt: number;
171
+ /** The cadence its own definition declared. Absent on pre-existing records. */
172
+ readonly intervalMinutes?: number;
173
+ }
174
+
175
+ /**
176
+ * Durable launch-record seam. Both sides are best-effort: a backend that is not
177
+ * ready must degrade to the run's own `startedAt` and the live definition rather
178
+ * than fail a background pass.
179
+ */
180
+ export interface WorkflowHeartbeatAnchorStore {
181
+ /** What the run launched with, when a record was written. */
182
+ readAnchorAt(runId: string): WorkflowHeartbeatLaunchRecord | undefined;
183
+ /**
184
+ * Persist what the run launched with. Write-once; later calls are no-ops.
185
+ * Returns whether the record is durably readable afterwards, so the caller can
186
+ * stop retrying. Never called for a non-positive cadence.
187
+ */
188
+ recordAnchorAt(runId: string, record: WorkflowHeartbeatLaunchRecord): Promise<boolean>;
189
+ }
190
+
191
+ export interface WorkflowHeartbeatScheduler {
192
+ /** Maintain at most one next-due heartbeat schedule for one enabled active run. */
193
+ scheduleWorkflowHeartbeats(run: WorkflowHeartbeatRun, intervalMinutes: number): WorkflowHeartbeatScheduleResult;
194
+ /** Queue one due heartbeat for the parent chat without interrupting it. */
195
+ enqueueWorkflowHeartbeat(payload: WorkflowHeartbeatEventDetails): WorkflowHeartbeatEnqueueResult;
196
+ /**
197
+ * Terminal cleanup (issue #1975): leave no deliverable heartbeat schedule,
198
+ * timer, launch memo, or queued heartbeat for one run.
199
+ *
200
+ * Idempotent by construction — every step is an unconditional delete on a map,
201
+ * a set, or the delivery queue, none of which throws for an absent key and
202
+ * none of which writes anything back. A repeat call therefore reports
203
+ * `already-clear` and cannot resurrect a schedule.
204
+ *
205
+ * What it cannot do is withdraw a card the host has already admitted into the
206
+ * parent's queue: no extension-facing seam exposes that. That one is caught at
207
+ * the other end instead — {@link workflowHeartbeatContextInvalidation} runs
208
+ * when the parent reads the card and keeps a finished run's heartbeat out of
209
+ * the model's context. Locally, the run's slot is released and a later
210
+ * consumption signal for it releases and re-arms nothing.
211
+ */
212
+ clearWorkflowHeartbeats(runId: string): WorkflowHeartbeatClearResult;
213
+ /**
214
+ * The parent chat consumed this exact heartbeat identity: the host injected
215
+ * its typed reconciliation into the conversation. Releases only that run's
216
+ * matching held slot and re-arms it at its first future boundary — never at a
217
+ * missed one.
218
+ *
219
+ * Consumption, not turn completion, is the release condition. The host emits
220
+ * its turn-settled event even when a paused queue held the card back, so
221
+ * releasing on that would let a second card stack behind a parked first one.
222
+ */
223
+ notifyHeartbeatConsumed(identity: WorkflowHeartbeatIdentity): void;
224
+ readonly state: WorkflowHeartbeatSchedulerState;
225
+ dispose(): void;
226
+ }
227
+
228
+ /** Largest cadence in minutes whose millisecond conversion is still representable. */
229
+ export const MAX_REPRESENTABLE_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES = Number.MAX_VALUE / MS_PER_MINUTE;
230
+
231
+ /**
232
+ * First cadence boundary strictly after `after`, on the `startedAt + n × I`
233
+ * series. Never derived from a previous delivery, so a retry, a pause, or a
234
+ * process restart cannot shift the cadence.
235
+ *
236
+ * The arithmetic `n` is tried first, then `n + 1` for the ordinary rounding
237
+ * case. Neither suffices for a cadence far finer than one ULP at the anchor:
238
+ * at a real epoch anchor near `1.7e12` one ULP is about `0.000244 ms`, so a
239
+ * `1e-9`-minute cadence (`0.00006 ms`) first lands a representable step at
240
+ * `n = 3`. Those two probes would call an authoring-valid interval
241
+ * unschedulable, so a bounded bracket-and-narrow finds the smallest such `n`
242
+ * while keeping the boundary exactly on the anchored series.
243
+ *
244
+ * Below one ULP the naming `n` itself stops being representable — at
245
+ * `Number.MIN_VALUE` minutes the first `n` that moves the sum is about
246
+ * `4.1e314`, past `Number.MAX_VALUE`. The series member still exists and still
247
+ * rounds to exactly one ULP past the anchor, so that is what is returned. This
248
+ * is not an invented boundary: every value this function produces is the
249
+ * correctly-rounded `startedAt + n × I`, and this case only applies the same
250
+ * rounding when no `n` can be written down.
251
+ *
252
+ * Returns `undefined` only when no representable boundary exists at all: a
253
+ * non-positive or non-finite interval, or a cadence above
254
+ * {@link MAX_REPRESENTABLE_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES}
255
+ * (~`3e303` minutes), where `intervalMs` is already `Infinity` and every
256
+ * `startedAt + n × I` is `Infinity` too. Authoring accepts those values, so no
257
+ * schedulable boundary is the narrow, non-throwing answer — but it is warned
258
+ * about under `ATOMIC_WORKFLOW_DEBUG` rather than being silent.
259
+ */
260
+ export function nextWorkflowHeartbeatBoundary(
261
+ startedAt: number,
262
+ intervalMinutes: number,
263
+ after: number,
264
+ ): number | undefined {
265
+ if (!Number.isFinite(intervalMinutes) || intervalMinutes <= 0) return undefined;
266
+ if (!Number.isFinite(startedAt) || !Number.isFinite(after)) return undefined;
267
+ const intervalMs = intervalMinutes * MS_PER_MINUTE;
268
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
269
+ warnWorkflowHeartbeatUnschedulableInterval(intervalMinutes);
270
+ return undefined;
271
+ }
272
+ const boundaryAt = (n: number): number => startedAt + n * intervalMs;
273
+ const elapsed = after - startedAt;
274
+ const estimate = elapsed < 0 ? 1 : Math.floor(elapsed / intervalMs) + 1;
275
+ if (!Number.isFinite(estimate)) return undefined;
276
+ if (boundaryAt(estimate) > after) return finiteBoundaryAfter(boundaryAt(estimate), after);
277
+ // Floating-point rounding can land the computed boundary exactly on or a hair
278
+ // before `after`; step one whole interval rather than shaving the anchor.
279
+ if (boundaryAt(estimate + 1) > after) return finiteBoundaryAfter(boundaryAt(estimate + 1), after);
280
+ // Sub-ULP cadence. Double until the anchored multiple is representably past
281
+ // `after`, then narrow back to the smallest one, so the boundary raised is
282
+ // still the first on the series rather than an arbitrary later multiple.
283
+ let low = estimate + 1;
284
+ let high = low;
285
+ while (boundaryAt(high) <= after) {
286
+ low = high;
287
+ high *= 2;
288
+ // `n` itself is no longer representable, but the series member it would
289
+ // name is: it rounds to the next representable instant after `after`.
290
+ if (!Number.isFinite(high)) return nextRepresentableAfter(after);
291
+ }
292
+ while (high - low > 1) {
293
+ const mid = Math.floor(low / 2 + high / 2);
294
+ // No integer strictly between them is representable at this magnitude.
295
+ if (mid <= low || mid >= high) break;
296
+ if (boundaryAt(mid) > after) high = mid;
297
+ else low = mid;
298
+ }
299
+ return finiteBoundaryAfter(boundaryAt(high), after);
300
+ }
301
+
302
+ function finiteBoundaryAfter(scheduledAt: number, after: number): number | undefined {
303
+ return Number.isFinite(scheduledAt) && scheduledAt > after ? scheduledAt : undefined;
304
+ }
305
+
306
+ /**
307
+ * The next representable instant after `at`, one unit in the last place away.
308
+ *
309
+ * JavaScript has no `Math.nextafter`, so the double is stepped at the bit level.
310
+ * Used only for a cadence finer than one ULP, where the anchored series member
311
+ * exists but no representable `n` names it.
312
+ */
313
+ function nextRepresentableAfter(at: number): number | undefined {
314
+ if (!Number.isFinite(at)) return undefined;
315
+ const view = new DataView(new ArrayBuffer(8));
316
+ view.setFloat64(0, at);
317
+ // Stepping the bit pattern upward walks away from zero for a positive double
318
+ // and toward zero for a negative one, which is what "next larger" means on
319
+ // each side. A run's timestamps are positive, so the negative branch is a
320
+ // completeness guard rather than a live path.
321
+ const bits = view.getBigUint64(0);
322
+ view.setBigUint64(0, at >= 0 ? bits + 1n : bits - 1n);
323
+ const next = view.getFloat64(0);
324
+ return Number.isFinite(next) && next > at ? next : undefined;
325
+ }
326
+
327
+ /**
328
+ * Whether a run may hold a heartbeat schedule right now. An effectively
329
+ * blocked run is not progressing, so it receives no new heartbeat even though
330
+ * its stored `running` status still makes it resumable.
331
+ */
332
+ export function isWorkflowHeartbeatEligibleRun(run: RunSnapshot): boolean {
333
+ if (!isTopLevelWorkflowRun(run)) return false;
334
+ if (run.status !== "running") return false;
335
+ if (run.pausedAt !== undefined) return false;
336
+ return !isTerminalRunStatus(effectiveRunStatus(run));
337
+ }
338
+
339
+ /**
340
+ * Whether a run is genuinely over and no longer owns heartbeat state.
341
+ * `recordRunBlocked` keeps a recoverable block at stored status `running`, so
342
+ * cleanup must use the store's terminal status rather than its effective
343
+ * lifecycle-notice status.
344
+ */
345
+ export function isWorkflowHeartbeatTerminalRun(run: RunSnapshot): boolean {
346
+ return isTerminalRunStatus(run.status);
347
+ }
348
+
349
+ export function installWorkflowHeartbeatScheduler(
350
+ options: WorkflowHeartbeatSchedulerOptions,
351
+ ): WorkflowHeartbeatScheduler {
352
+ const state = options.state ?? createWorkflowHeartbeatSchedulerState();
353
+ const now = options.now ?? Date.now;
354
+ const timers = options.timers ?? defaultWorkflowHeartbeatTimerApi;
355
+ const send = options.sendMessage;
356
+ const parentAvailabilityReported = options.parentAvailabilityReported === true;
357
+ const anchorStore = options.anchorStore;
358
+ let active = true;
359
+ let timerHandle: WorkflowHeartbeatTimerHandle | undefined;
360
+
361
+ const emit = (details: WorkflowHeartbeatEventDetails): boolean | Promise<boolean> => {
362
+ if (typeof send !== "function") return false;
363
+ try {
364
+ const result = send(
365
+ {
366
+ customType: WORKFLOW_HEARTBEAT_CUSTOM_TYPE,
367
+ content: formatWorkflowHeartbeatNoticeText(details),
368
+ display: true,
369
+ details,
370
+ },
371
+ // Identical to the lifecycle notice options: the parent's active
372
+ // response is never aborted; the steer waits for the next
373
+ // protocol-safe boundary and is persisted meanwhile.
374
+ { triggerTurn: true, deliverAs: "steer", persistWhenStreaming: true },
375
+ );
376
+ if (result === undefined) return true;
377
+ return Promise.resolve(result).then(
378
+ () => true,
379
+ (error: unknown) => {
380
+ warnWorkflowHeartbeatSendFailure(error);
381
+ return false;
382
+ },
383
+ );
384
+ } catch (error) {
385
+ warnWorkflowHeartbeatSendFailure(error);
386
+ return false;
387
+ }
388
+ };
389
+
390
+ /**
391
+ * Live re-check taken before every attempt, retries included. The two
392
+ * objective-named guards are the pre-process batch read and the pre-enqueue
393
+ * read; this one keeps the second guard true across the retry window, where
394
+ * the run could otherwise reach a terminal state between attempt 1 and
395
+ * attempt 5 and still be sent. A card the host has already admitted into its
396
+ * queue is past this guard: terminal cleanup (issue #1975) discards the
397
+ * identities still waiting here, and no host seam withdraws an admitted one,
398
+ * so that one is caught later instead — see
399
+ * {@link workflowHeartbeatContextInvalidation}, which runs when the parent
400
+ * reads the card.
401
+ */
402
+ const canDeliverWorkflowHeartbeat = (details: WorkflowHeartbeatEventDetails): boolean => {
403
+ const run = findRun(readGraphStoreSnapshot(options.store), details.runId);
404
+ return run !== undefined && isWorkflowHeartbeatEligibleRun(run);
405
+ };
406
+
407
+ const delivery = createWorkflowHeartbeatDelivery({
408
+ emit,
409
+ timers,
410
+ canDeliver: canDeliverWorkflowHeartbeat,
411
+ onSettled: (details, delivered) => {
412
+ const identity = state.pending.get(details.runId);
413
+ if (identity?.scheduledAt !== details.scheduledAt) return;
414
+ if (!delivered) {
415
+ // Nothing reached the parent's queue, so the slot is free at once.
416
+ state.pending.delete(details.runId);
417
+ state.awaitingParentPickup.delete(details.runId);
418
+ refresh();
419
+ return;
420
+ }
421
+ if (!parentAvailabilityReported) {
422
+ // This host reports no consumption signal, so nothing could ever
423
+ // release a held slot. Releasing on admission is the only choice that
424
+ // keeps the run heartbeating at all.
425
+ state.pending.delete(details.runId);
426
+ refresh();
427
+ return;
428
+ }
429
+ // The host resolves `sendMessage` once the card is admitted into the
430
+ // parent's queue, not once the parent reads it — and its turn-settled
431
+ // event fires even when a paused queue held the card back. Releasing on
432
+ // either would let the next boundary stack a second card behind the
433
+ // first, so the slot is held — with no deadline — until the card is
434
+ // actually consumed into the conversation. The cadence pauses rather
435
+ // than stacking.
436
+ //
437
+ // The exact scheduled identity is retained until the host injects the
438
+ // typed hidden reconciliation. Rendered text is display-only and never
439
+ // decides slot ownership or release.
440
+ state.awaitingParentPickup.set(details.runId, identity);
441
+ refresh();
442
+ },
443
+ });
444
+
445
+ /**
446
+ * Drop everything one run owns, in one place.
447
+ *
448
+ * Every per-run map and set is listed deliberately rather than filtered: a
449
+ * field left out here is a leak that survives the run that owns it, which is
450
+ * precisely what slice 3 exists to close. `Map.delete`/`Set.delete` report
451
+ * whether a key was there and never throw, so the union of those answers is
452
+ * the `Cleared`/`AlreadyClear` result with no extra bookkeeping — and no
453
+ * tombstone to grow without bound for the life of the process.
454
+ *
455
+ * Late callbacks cannot re-create what this removed. `onSettled` returns
456
+ * early once `pending` no longer holds the identity, and the launch-record
457
+ * write only marks a run persisted when its own in-flight marker was still
458
+ * there to remove.
459
+ *
460
+ * Does not arm: the public cleanup door re-arms after this helper removes
461
+ * every per-run field and queued identity.
462
+ */
463
+ const clearRunHeartbeatState = (runId: string): boolean => {
464
+ let cleared = state.scheduled.delete(runId);
465
+ cleared = state.pending.delete(runId) || cleared;
466
+ cleared = state.awaitingParentPickup.delete(runId) || cleared;
467
+ cleared = state.lastEnqueuedAt.delete(runId) || cleared;
468
+ cleared = state.anchorAt.delete(runId) || cleared;
469
+ cleared = state.anchorPersisted.delete(runId) || cleared;
470
+ cleared = state.anchorWritesPending.delete(runId) || cleared;
471
+ cleared = state.intervalMinutes.delete(runId) || cleared;
472
+ return delivery.discard(runId) || cleared;
473
+ };
474
+
475
+ const clearWorkflowHeartbeats = (runId: string): WorkflowHeartbeatClearResult => {
476
+ if (!clearRunHeartbeatState(runId)) return { kind: "already-clear" };
477
+ // The global wake-up may have been this run's; re-derive it from what is
478
+ // left rather than leaving a timer for a boundary nobody owns.
479
+ arm();
480
+ return { kind: "cleared" };
481
+ };
482
+
483
+ const notifyHeartbeatConsumed = (identity: WorkflowHeartbeatIdentity): void => {
484
+ if (!active) return;
485
+ const held = state.awaitingParentPickup.get(identity.runId);
486
+ const pending = state.pending.get(identity.runId);
487
+ if (held?.scheduledAt !== identity.scheduledAt || pending?.scheduledAt !== identity.scheduledAt) return;
488
+ state.awaitingParentPickup.delete(identity.runId);
489
+ state.pending.delete(identity.runId);
490
+ // `refresh()` re-arms from `max(now, lastEnqueuedAt)`, so the cadence
491
+ // resumes at the first future boundary and never at a missed one.
492
+ refresh();
493
+ };
494
+
495
+ const scheduleWorkflowHeartbeats = (
496
+ run: WorkflowHeartbeatRun,
497
+ intervalMinutes: number,
498
+ ): WorkflowHeartbeatScheduleResult => {
499
+ state.scheduled.delete(run.id);
500
+ // A resolved interval of 0 creates no timer and no schedule record at all.
501
+ if (intervalMinutes <= 0) return { kind: "disabled" };
502
+ if (run.status !== "running") return { kind: "disabled" };
503
+ // Recovery and resume both land here: the floor is `now`, so missed
504
+ // boundaries are skipped rather than replayed.
505
+ const floor = Math.max(now(), state.lastEnqueuedAt.get(run.id) ?? Number.NEGATIVE_INFINITY);
506
+ const anchorAt = resolveAnchorAt(run);
507
+ const scheduledAt = nextWorkflowHeartbeatBoundary(anchorAt, intervalMinutes, floor);
508
+ if (scheduledAt === undefined) return { kind: "disabled" };
509
+ // Persist the cadence anchor here rather than at the first delivery. A run
510
+ // that gains durable progress and then pauses, quits, or crashes before its
511
+ // first boundary would otherwise have no record, and a durable resume —
512
+ // which reclaims the original run id but remints `startedAt` — would put it
513
+ // on a fresh series. The write is a no-op until the run has durable progress
514
+ // of its own. Active progress is retried on later store refreshes; the forced
515
+ // pause/quit checkpoint covers its already-paused edge at the recorder that
516
+ // creates that progress (`engine/run-durable-stage-session.ts`).
517
+ persistLaunchRecordOnce(run.id, anchorAt, intervalMinutes);
518
+ state.scheduled.set(run.id, {
519
+ runId: run.id,
520
+ scheduledAt,
521
+ workflowName: run.name,
522
+ // The anchor, not the snapshot's own `startedAt`: after a durable
523
+ // resume those differ, and the payload must stay consistent with the
524
+ // series the boundary came from.
525
+ startedAt: anchorAt,
526
+ intervalMinutes,
527
+ });
528
+ return { kind: "scheduled", scheduledAt };
529
+ };
530
+
531
+ const enqueueWorkflowHeartbeat = (payload: WorkflowHeartbeatEventDetails): WorkflowHeartbeatEnqueueResult => {
532
+ if (!active || typeof send !== "function") return { kind: "suppressed", reason: "unavailable" };
533
+ // Pre-enqueue guard: an independent read of live status, so a run that
534
+ // finished between the processing batch and this call is suppressed.
535
+ const run = findRun(readGraphStoreSnapshot(options.store), payload.runId);
536
+ if (run === undefined) return { kind: "suppressed", reason: "missing" };
537
+ if (isTerminalRunStatus(effectiveRunStatus(run))) return { kind: "suppressed", reason: "terminal" };
538
+ if (!isWorkflowHeartbeatEligibleRun(run)) return { kind: "suppressed", reason: "paused" };
539
+ // One pending event per run, and the oldest one wins: a newer boundary is
540
+ // not enqueued while an identity is still in flight.
541
+ if (state.pending.has(payload.runId)) return { kind: "suppressed", reason: "duplicate" };
542
+ if ((state.lastEnqueuedAt.get(payload.runId) ?? Number.NEGATIVE_INFINITY) >= payload.scheduledAt) {
543
+ return { kind: "suppressed", reason: "duplicate" };
544
+ }
545
+ const identity: WorkflowHeartbeatIdentity = { runId: payload.runId, scheduledAt: payload.scheduledAt };
546
+ state.pending.set(payload.runId, identity);
547
+ state.lastEnqueuedAt.set(payload.runId, payload.scheduledAt);
548
+ delivery.deliver(payload);
549
+ return { kind: "enqueued", identity };
550
+ };
551
+
552
+ const arm = (): void => {
553
+ if (timerHandle !== undefined) {
554
+ timers.clearTimeout(timerHandle);
555
+ timerHandle = undefined;
556
+ }
557
+ if (!active) return;
558
+ // One globally-next-due wake-up, not one recurring timer per run.
559
+ const next = earliestScheduled(state);
560
+ if (next === undefined) return;
561
+ const nextAt = next.scheduledAt;
562
+ // A held slot has no deadline, so it contributes no wake-up of its own.
563
+ const delay = Math.min(Math.max(1, Math.ceil(nextAt - now())), MAX_TIMER_DELAY_MS);
564
+ const handle = timers.setTimeout(() => {
565
+ timerHandle = undefined;
566
+ processDue();
567
+ }, delay);
568
+ handle.unref?.();
569
+ timerHandle = handle;
570
+ };
571
+
572
+ const refresh = (): void => {
573
+ if (!active || typeof send !== "function") return;
574
+ const snapshot = readGraphStoreSnapshot(options.store);
575
+ const observed = new Set<string>();
576
+ for (const run of snapshot.runs) {
577
+ if (!isTopLevelWorkflowRun(run)) continue;
578
+ // Terminal cleanup and restart recovery (issue #1975). The trigger is
579
+ // observed state, not a transition event, because several paths reach a
580
+ // terminal snapshot without ever calling `recordRunEnd` — durable replay,
581
+ // completed-catalog reconstruction, and a restore that inserts an
582
+ // already-failed run. This pass also runs once at install, so it is the
583
+ // boot sweep: a stale durable anchor or a leftover pending record for a
584
+ // run that is already terminal is discarded rather than replayed, and the
585
+ // anchor is neither read nor written for such a run.
586
+ if (isWorkflowHeartbeatTerminalRun(run)) {
587
+ clearWorkflowHeartbeats(run.id);
588
+ continue;
589
+ }
590
+ observed.add(run.id);
591
+ if (state.pending.has(run.id) || !isWorkflowHeartbeatEligibleRun(run)) {
592
+ state.scheduled.delete(run.id);
593
+ continue;
594
+ }
595
+ // An entry that already came due is owed and is kept as it is. Re-deriving
596
+ // it here would floor at `now` and silently advance it to the following
597
+ // boundary, so any ordinary store mutation landing between a boundary and
598
+ // its armed callback would drop that heartbeat. A sticky entry carries the
599
+ // `intervalMinutes` captured when it was derived, so a workflow reload in
600
+ // that window does not retro-fit a new cadence onto an owed boundary.
601
+ const owed = state.scheduled.get(run.id);
602
+ if (owed !== undefined && owed.scheduledAt <= now()) continue;
603
+ const launched = readLaunchRecord(run.id);
604
+ const intervalMinutes = resolveRunIntervalMinutes(
605
+ state,
606
+ options.resolveIntervalMinutes,
607
+ run,
608
+ launched?.intervalMinutes,
609
+ );
610
+ if (intervalMinutes === undefined) {
611
+ state.scheduled.delete(run.id);
612
+ continue;
613
+ }
614
+ scheduleWorkflowHeartbeats(run, intervalMinutes);
615
+ }
616
+ // A run that vanished from the store — cleared session state, a discarded
617
+ // graph — owns nothing either. The sweep covers every per-run field, not
618
+ // just the schedule, so no timer, slot, or queued identity outlives its run.
619
+ for (const runId of trackedRunIds(state)) {
620
+ if (!observed.has(runId)) clearWorkflowHeartbeats(runId);
621
+ }
622
+ arm();
623
+ };
624
+
625
+ /** The run's durable launch record, read at most once per run per process. */
626
+ function readLaunchRecord(runId: string): WorkflowHeartbeatLaunchRecord | undefined {
627
+ try {
628
+ return anchorStore?.readAnchorAt(runId);
629
+ } catch {
630
+ // A durable backend that is not ready falls back to live values.
631
+ return undefined;
632
+ }
633
+ }
634
+
635
+ /**
636
+ * The run's cadence anchor: its original start time. A durable resume mints a
637
+ * fresh `RunSnapshot.startedAt` under the original workflow id, so the
638
+ * persisted anchor is what keeps the resumed run on its original series.
639
+ * Taking the minimum means the record can only restore the original anchor,
640
+ * never advance it.
641
+ *
642
+ * A failed-run continuation legitimately starts a new series: it carries a
643
+ * fresh run id, and the slice-1 identity is `runId + scheduledAt`, so it is a
644
+ * different run and reads no record of its own.
645
+ */
646
+ function resolveAnchorAt(run: WorkflowHeartbeatRun): number {
647
+ const cached = state.anchorAt.get(run.id);
648
+ if (cached !== undefined) return Math.min(cached, run.startedAt);
649
+ const stored = readLaunchRecord(run.id)?.anchorAt;
650
+ const anchorAt = stored === undefined ? run.startedAt : Math.min(stored, run.startedAt);
651
+ state.anchorAt.set(run.id, anchorAt);
652
+ return anchorAt;
653
+ }
654
+
655
+ /**
656
+ * Write what the run launched with, at most once per process, best-effort.
657
+ *
658
+ * The write is asynchronous and its result is only trusted once the durable
659
+ * backend acknowledges it. The DBOS backend updates its in-memory mirror
660
+ * before the real write is queued, so a synchronous read-back would mark a
661
+ * rejected storage write as persisted and never retry it.
662
+ *
663
+ * `recordAnchorAt` is itself a no-op until the run has durable progress of its
664
+ * own, so this is called on every schedule pass until it is acknowledged and
665
+ * then never again. Bounding it matters because a schedule pass runs on every
666
+ * store invalidation for every running run; `anchorWritesPending` keeps at
667
+ * most one write in flight per run while a pass is waiting.
668
+ *
669
+ * A failure clears only the pending marker and leaves the retry to the next
670
+ * natural schedule pass. It deliberately does not call `refresh()`, which
671
+ * would turn a persistently failing backend into a retry loop.
672
+ *
673
+ * The in-flight marker is also what makes a late acknowledgement safe after
674
+ * terminal cleanup (issue #1975): cleanup removes the marker, so a write that
675
+ * resolves afterwards finds nothing to remove and records nothing, rather
676
+ * than re-creating an `anchorPersisted` entry for a run that is finished.
677
+ *
678
+ * Only reached for a positive cadence, because `scheduleWorkflowHeartbeats`
679
+ * returns `disabled` above this point — so a run launched with heartbeats off
680
+ * leaves no durable artifact of any kind, which is what the acceptance
681
+ * criterion "disabled (0) creates no timer or record" requires.
682
+ */
683
+ function persistLaunchRecordOnce(runId: string, anchorAt: number, intervalMinutes: number): void {
684
+ if (anchorStore === undefined) return;
685
+ if (state.anchorPersisted.has(runId) || state.anchorWritesPending.has(runId)) return;
686
+ state.anchorWritesPending.add(runId);
687
+ let write: Promise<boolean>;
688
+ try {
689
+ write = Promise.resolve(anchorStore.recordAnchorAt(runId, { anchorAt, intervalMinutes }));
690
+ } catch {
691
+ // A backend that throws synchronously must not stop a heartbeat.
692
+ state.anchorWritesPending.delete(runId);
693
+ return;
694
+ }
695
+ void write.then(
696
+ (acknowledged) => {
697
+ // `Set.delete` reports whether this run was still tracked. Cleanup
698
+ // removes the marker, so a resolution arriving after it adds nothing.
699
+ const tracked = state.anchorWritesPending.delete(runId);
700
+ if (acknowledged && tracked) state.anchorPersisted.add(runId);
701
+ },
702
+ () => {
703
+ state.anchorWritesPending.delete(runId);
704
+ },
705
+ );
706
+ }
707
+
708
+ const processDue = (): void => {
709
+ if (!active) return;
710
+ const at = now();
711
+ const due = [...state.scheduled.values()]
712
+ .filter((details) => details.scheduledAt <= at)
713
+ .sort(compareWorkflowHeartbeatOrder);
714
+ // Pre-process guard: live status read taken immediately before the batch,
715
+ // separate from the per-enqueue read below.
716
+ const snapshot = readGraphStoreSnapshot(options.store);
717
+ for (const details of due) {
718
+ state.scheduled.delete(details.runId);
719
+ const run = findRun(snapshot, details.runId);
720
+ if (run === undefined || !isWorkflowHeartbeatEligibleRun(run)) continue;
721
+ enqueueWorkflowHeartbeat(details);
722
+ }
723
+ refresh();
724
+ };
725
+
726
+ const unsubscribe = subscribeStoreInvalidation(options.store, refresh);
727
+ refresh();
728
+
729
+ return {
730
+ scheduleWorkflowHeartbeats,
731
+ enqueueWorkflowHeartbeat,
732
+ clearWorkflowHeartbeats,
733
+ notifyHeartbeatConsumed,
734
+ state,
735
+ dispose() {
736
+ active = false;
737
+ unsubscribe();
738
+ if (timerHandle !== undefined) {
739
+ timers.clearTimeout(timerHandle);
740
+ timerHandle = undefined;
741
+ }
742
+ state.scheduled.clear();
743
+ delivery.dispose();
744
+ },
745
+ };
746
+ }
747
+
748
+ /** Due heartbeats process in `scheduledAt` order, with `runId` as the stable tie-break. */
749
+ export function compareWorkflowHeartbeatOrder(a: WorkflowHeartbeatIdentity, b: WorkflowHeartbeatIdentity): number {
750
+ if (a.scheduledAt !== b.scheduledAt) return a.scheduledAt - b.scheduledAt;
751
+ if (a.runId === b.runId) return 0;
752
+ return a.runId < b.runId ? -1 : 1;
753
+ }
754
+
755
+ function earliestScheduled(state: WorkflowHeartbeatSchedulerState): WorkflowHeartbeatEventDetails | undefined {
756
+ let earliest: WorkflowHeartbeatEventDetails | undefined;
757
+ for (const details of state.scheduled.values()) {
758
+ if (earliest === undefined || compareWorkflowHeartbeatOrder(details, earliest) < 0) earliest = details;
759
+ }
760
+ return earliest;
761
+ }
762
+
763
+ /**
764
+ * Every run this state still holds anything for, as a snapshot safe to mutate
765
+ * under. Listing all eight fields is deliberate: the sweep that uses it exists
766
+ * to prove nothing outlives its run, and a field omitted here would be the one
767
+ * thing that did.
768
+ */
769
+ function trackedRunIds(state: WorkflowHeartbeatSchedulerState): string[] {
770
+ const ids = new Set<string>([
771
+ ...state.scheduled.keys(),
772
+ ...state.pending.keys(),
773
+ ...state.awaitingParentPickup.keys(),
774
+ ...state.lastEnqueuedAt.keys(),
775
+ ...state.anchorAt.keys(),
776
+ ...state.anchorPersisted,
777
+ ...state.anchorWritesPending,
778
+ ...state.intervalMinutes.keys(),
779
+ ]);
780
+ return [...ids];
781
+ }
782
+
783
+ /**
784
+ * The cadence for a run: the one it launched with, resolved from its definition
785
+ * the first time and memoized thereafter.
786
+ *
787
+ * Only a successful resolution memoizes, so a run observed during startup
788
+ * warmup — before discovery has finished — picks its cadence up on a later pass
789
+ * rather than being frozen as unresolvable. Once resolved, a `/workflow reload`,
790
+ * rename, edit, or delete cannot change or stop that run.
791
+ */
792
+ function resolveRunIntervalMinutes(
793
+ state: WorkflowHeartbeatSchedulerState,
794
+ resolve: (workflowName: string) => number | undefined,
795
+ run: WorkflowHeartbeatRun,
796
+ persistedIntervalMinutes?: number,
797
+ ): number | undefined {
798
+ const memoized = state.intervalMinutes.get(run.id);
799
+ // A memoized `0` means the run launched disabled, and stays disabled for its
800
+ // whole life however the definition is later edited.
801
+ if (memoized !== undefined) return memoized > 0 ? memoized : undefined;
802
+ // A persisted launch cadence outranks the live registry: it is what this run
803
+ // launched with, recovered across a process boundary. Absent on records
804
+ // written by earlier builds, which fall back to the registry.
805
+ const resolved = persistedIntervalMinutes ?? resolve(run.name);
806
+ // Not resolvable yet — startup discovery may still be warming up, so nothing
807
+ // is memoized and the next pass tries again.
808
+ if (resolved === undefined || !Number.isFinite(resolved)) return undefined;
809
+ // Memoize before the positivity test, so a run that launched disabled is
810
+ // remembered as such. Editing that workflow to a positive cadence mid-session
811
+ // must not start heartbeating a run that launched with heartbeats off, just as
812
+ // editing to `0` must not silence one that launched enabled.
813
+ state.intervalMinutes.set(run.id, resolved);
814
+ return resolved > 0 ? resolved : undefined;
815
+ }
816
+
817
+ function findRun(snapshot: StoreSnapshot, runId: string): RunSnapshot | undefined {
818
+ return snapshot.runs.find((run) => run.id === runId);
819
+ }
820
+
821
+ function warnWorkflowHeartbeatSendFailure(error: unknown): void {
822
+ if (process.env.ATOMIC_WORKFLOW_DEBUG !== "1") return;
823
+ const message = error instanceof Error ? error.message : String(error);
824
+ console.warn("[workflows] workflow heartbeat send failed", message);
825
+ }
826
+
827
+ /**
828
+ * A cadence so large that its first boundary is not a representable time.
829
+ * Authoring accepts every positive finite value, so this cannot be rejected at
830
+ * the door — but it must not be silent either, because the run simply never
831
+ * heartbeats.
832
+ */
833
+ function warnWorkflowHeartbeatUnschedulableInterval(intervalMinutes: number): void {
834
+ if (process.env.ATOMIC_WORKFLOW_DEBUG !== "1") return;
835
+ console.warn(
836
+ `[workflows] heartbeatIntervalMinutes ${intervalMinutes} exceeds the largest representable cadence ` +
837
+ `(${MAX_REPRESENTABLE_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES} minutes); no boundary can be scheduled for it`,
838
+ );
839
+ }
840
+
841
+ /**
842
+ * A host message as far as heartbeat consumption is concerned.
843
+ *
844
+ * `details` stays a record because its contents differ per custom type and are
845
+ * validated by the reader that knows which type it asked for.
846
+ */
847
+ interface HostCustomMessage {
848
+ readonly role: string;
849
+ readonly customType?: string;
850
+ readonly details?: Record<string, unknown>;
851
+ /** The message exactly as the host produced it, for key-preserving replacement. */
852
+ readonly raw: Record<string, unknown>;
853
+ }
854
+
855
+ /** The `message_end` payload the workflows-side handler receives. */
856
+ interface HostMessageEndEvent {
857
+ readonly message: HostCustomMessage;
858
+ }
859
+
860
+ function isRecord(value: unknown): value is Record<string, unknown> {
861
+ return typeof value === "object" && value !== null;
862
+ }
863
+
864
+ /**
865
+ * Narrow an untyped `message_end` payload to the declared host shape.
866
+ *
867
+ * The handler signature is `(event?: unknown, ctx?) => unknown`, so `unknown` is
868
+ * the honest type of what arrives; it is validated once here and every consumer
869
+ * downstream reads a declared type instead of re-casting at each field. A
870
+ * payload that does not match is rejected exactly as before rather than trusted
871
+ * because a type claims a shape.
872
+ */
873
+ function asHostMessageEndEvent(event: unknown): HostMessageEndEvent | undefined {
874
+ if (!isRecord(event)) return undefined;
875
+ const message = event.message;
876
+ if (!isRecord(message)) return undefined;
877
+ const { role, customType, details } = message;
878
+ if (typeof role !== "string") return undefined;
879
+ return {
880
+ message: {
881
+ role,
882
+ ...(typeof customType === "string" ? { customType } : {}),
883
+ ...(isRecord(details) ? { details } : {}),
884
+ raw: message,
885
+ },
886
+ };
887
+ }
888
+
889
+ /**
890
+ * Host custom type of the hidden twin queued alongside a protected card.
891
+ *
892
+ * Declared locally rather than imported: `packages/workflows` never imports
893
+ * `packages/coding-agent` source, the same reason `"message_end"` and
894
+ * `deliverAs: "steer"` are string literals here.
895
+ *
896
+ * cross-ref: packages/coding-agent/src/core/agent-session-persistent-custom-messages.ts
897
+ */
898
+ const PROTECTED_RECONCILIATION_CUSTOM_TYPE = "atomic:protected-streaming-reconciliation";
899
+
900
+ /**
901
+ * The exact heartbeat identity this consumed message belongs to, or
902
+ * `undefined` when it is not a heartbeat reconciliation.
903
+ *
904
+ * The parent's queue holds a hidden reconciliation whose
905
+ * `details.protectedReconciliationOf` points to the visible intent entry.
906
+ * Identity comes only from a typed `workflows:workflow-heartbeat` intent, never
907
+ * from rendered text or from another extension's custom message.
908
+ */
909
+ export function workflowHeartbeatConsumedIdentity(
910
+ event: unknown,
911
+ entries: readonly SessionEntry[] | undefined,
912
+ ): WorkflowHeartbeatIdentity | undefined {
913
+ if (entries === undefined) return undefined;
914
+ const parsed = asHostMessageEndEvent(event);
915
+ if (parsed === undefined) return undefined;
916
+ const { role, customType, details } = parsed.message;
917
+ if (role !== "custom" || customType !== PROTECTED_RECONCILIATION_CUSTOM_TYPE) return undefined;
918
+ if (details === undefined) return undefined;
919
+ const intentEntryId = details.protectedReconciliationOf;
920
+ if (typeof intentEntryId !== "string") return undefined;
921
+ const intent = entries.find((entry) => entry.id === intentEntryId);
922
+ if (intent?.customType !== WORKFLOW_HEARTBEAT_CUSTOM_TYPE) return undefined;
923
+ if (!isRecord(intent.details)) return undefined;
924
+ const { runId, scheduledAt } = intent.details;
925
+ if (typeof runId !== "string" || typeof scheduledAt !== "number") return undefined;
926
+ return { runId, scheduledAt };
927
+ }
928
+
929
+ /**
930
+ * The `message_end` replacement that invalidates a heartbeat whose run no longer
931
+ * owns it, or `undefined` to leave the message exactly as the host produced it.
932
+ *
933
+ * Terminal cleanup reaches everything the scheduler owns, but the parent's queue
934
+ * is the host's: once `sendMessage` resolves, the visible card is committed to
935
+ * the transcript and a hidden reconciliation is queued behind it, and no host
936
+ * seam withdraws either. Consumption is the last moment before that steer joins
937
+ * the model's context, and a `message_end` handler may return a replacement of
938
+ * the same role — so this is where a heartbeat whose exact identity is no longer
939
+ * owned is invalidated (issue #1975).
940
+ *
941
+ * `excludeFromContext` rather than rewritten text: the host includes a custom
942
+ * message in the provider request only when that flag is not `true`, so setting
943
+ * it removes the steer from the model's view outright instead of putting
944
+ * different words in the parent's context. The visible card is deliberately
945
+ * untouched — it is already in the user's scrollback, already excluded from
946
+ * context itself, and rewriting a transcript after the fact would be worse than
947
+ * leaving a true record of what was raised.
948
+ *
949
+ * Every key of the original message is carried over: the host replaces by
950
+ * deleting each key of the target before assigning, so an omitted one is lost.
951
+ */
952
+ export function workflowHeartbeatContextInvalidation(
953
+ event: unknown,
954
+ entries: readonly SessionEntry[] | undefined,
955
+ isIdentityOwned: (identity: WorkflowHeartbeatIdentity) => boolean,
956
+ ): { readonly message: Record<string, unknown> } | undefined {
957
+ const identity = workflowHeartbeatConsumedIdentity(event, entries);
958
+ if (identity === undefined || isIdentityOwned(identity)) return undefined;
959
+ const parsed = asHostMessageEndEvent(event);
960
+ if (parsed === undefined) return undefined;
961
+ return { message: { ...parsed.message.raw, excludeFromContext: true } };
962
+ }