@vellumai/assistant 0.8.12 → 0.9.0-staging.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 (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -0,0 +1,593 @@
1
+ /**
2
+ * Lifecycle layer for the workflow orchestration engine.
3
+ *
4
+ * The {@link WorkflowRunManager} is the surface the workflow tool, scheduler,
5
+ * and routes (later PRs) drive. It owns everything the raw {@link executeWorkflow}
6
+ * engine deliberately does NOT:
7
+ *
8
+ * - **Feature-flag gate.** `start` hard-fails with {@link WorkflowsDisabledError}
9
+ * when the `workflows` flag is off, BEFORE any engine code path is reachable.
10
+ * - **Concurrent-run cap.** At most `config.workflows.maxConcurrentRuns` runs
11
+ * may be in flight; the (N+1)th `start` throws {@link WorkflowRunCapError}.
12
+ * - **Async launch.** `start` resolves capabilities, creates the journal run
13
+ * row, kicks off {@link executeWorkflow} WITHOUT awaiting it, and returns the
14
+ * `runId` immediately. Progress and completion are surfaced out-of-band.
15
+ * - **Progress + completion events.** The engine's `onProgress` callback is
16
+ * republished as `workflow_progress` server messages; on completion a
17
+ * `workflow_completed` message is published. Both go through the shared
18
+ * `broadcastMessage` / event hub.
19
+ * - **Completion injection.** On completion, a human-readable summary is
20
+ * surfaced back into the originating conversation via
21
+ * {@link wakeAgentForOpportunity} — the same mechanism scheduled tasks and
22
+ * background shell jobs use to report finished background work to a
23
+ * conversation.
24
+ *
25
+ * `status` and `list` are thin reads over the journal store (the engine flushes
26
+ * live counts to the run row, so `getRun` is the single source of truth).
27
+ */
28
+
29
+ import { createHash, randomUUID } from "node:crypto";
30
+
31
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
32
+ import { getConfig } from "../config/loader.js";
33
+ import type { AssistantConfig } from "../config/schema.js";
34
+ import {
35
+ FALLBACK_TURN_TRUST,
36
+ type TrustContext,
37
+ } from "../daemon/trust-context.js";
38
+ import { wakeAgentForOpportunity } from "../runtime/agent-wake.js";
39
+ import { broadcastMessage } from "../runtime/assistant-event-hub.js";
40
+ import { getLogger } from "../util/logger.js";
41
+ import type { CapabilityManifest } from "./capabilities.js";
42
+ import {
43
+ normalizeCapabilityManifest,
44
+ resolveCapabilities,
45
+ } from "./capabilities.js";
46
+ import {
47
+ executeWorkflow,
48
+ extractWorkflowMeta,
49
+ WorkflowNotFoundError,
50
+ } from "./engine.js";
51
+ import type { WorkflowRun } from "./journal-store.js";
52
+ import * as journalStore from "./journal-store.js";
53
+ import { runLeaf } from "./leaf-runner.js";
54
+ import * as library from "./library.js";
55
+
56
+ const log = getLogger("workflow-run-manager");
57
+
58
+ /** Source tag for the completion wake (shows up in the wake's structured log). */
59
+ const WORKFLOW_WAKE_SOURCE = "workflow_completed";
60
+
61
+ /** Thrown by `start` when the `workflows` feature flag is disabled. */
62
+ export class WorkflowsDisabledError extends Error {
63
+ readonly code = "workflows_disabled" as const;
64
+ constructor() {
65
+ super("Workflows are not enabled.");
66
+ this.name = "WorkflowsDisabledError";
67
+ }
68
+ }
69
+
70
+ /** Thrown by `start` when the concurrent-run cap is already reached. */
71
+ export class WorkflowRunCapError extends Error {
72
+ readonly code = "workflow_run_cap_exceeded" as const;
73
+ constructor(readonly limit: number) {
74
+ super(
75
+ `Cannot start workflow run: ${limit} run(s) are already in flight ` +
76
+ `(maxConcurrentRuns).`,
77
+ );
78
+ this.name = "WorkflowRunCapError";
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Thrown by `resume` when the target run cannot be resumed: it does not exist,
84
+ * is already in a terminal-but-not-resumable state, or is currently in flight.
85
+ * Only an `interrupted` run (a `running` row reconciled at startup after a
86
+ * crash) is resumable.
87
+ */
88
+ export class WorkflowResumeNotPossibleError extends Error {
89
+ readonly code = "workflow_resume_not_possible" as const;
90
+ constructor(
91
+ readonly runId: string,
92
+ readonly reason: "not_found" | "not_interrupted" | "in_flight",
93
+ readonly status?: WorkflowRun["status"],
94
+ ) {
95
+ super(
96
+ reason === "not_found"
97
+ ? `Workflow run ${runId} not found.`
98
+ : reason === "in_flight"
99
+ ? `Workflow run ${runId} is already in flight.`
100
+ : `Workflow run ${runId} is not resumable (status: ${status}). ` +
101
+ `Only interrupted runs can be resumed.`,
102
+ );
103
+ this.name = "WorkflowResumeNotPossibleError";
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Start a run from EITHER an inline `scriptSource` OR a saved workflow `name`
109
+ * (exactly one). When `name` is given the source is resolved from the saved
110
+ * workflow library; an unknown name throws {@link WorkflowNotFoundError}.
111
+ */
112
+ export type StartWorkflowOptions = StartWorkflowCommon &
113
+ (
114
+ | { scriptSource: string; name?: undefined }
115
+ | { name: string; scriptSource?: undefined }
116
+ );
117
+
118
+ interface StartWorkflowCommon {
119
+ /** Verbatim run input, exposed to the script as `args`. */
120
+ args: unknown;
121
+ /** Per-run capability declaration (the single consent point). */
122
+ manifest: CapabilityManifest;
123
+ /**
124
+ * Conversation that originated the run; receives the completion summary via
125
+ * an agent wake. Omit for runs with no originating conversation (e.g. a
126
+ * scheduled run with no chat surface) — the summary is then events-only.
127
+ */
128
+ conversationId?: string;
129
+ /** Human-readable label for display; defaults to the run id. */
130
+ label?: string;
131
+ /** Trust/auth context forwarded to every leaf. */
132
+ trustContext: TrustContext;
133
+ }
134
+
135
+ /**
136
+ * Dependencies injected for testing. Production callers omit this entirely and
137
+ * rely on the real engine, leaf runner, journal store, config, and wake.
138
+ */
139
+ export interface WorkflowRunManagerDeps {
140
+ executeWorkflow: typeof executeWorkflow;
141
+ leafRunner: typeof runLeaf;
142
+ journal: typeof journalStore;
143
+ getConfig: () => AssistantConfig;
144
+ isFlagEnabled: (config: AssistantConfig) => boolean;
145
+ wake: typeof wakeAgentForOpportunity;
146
+ broadcast: typeof broadcastMessage;
147
+ newRunId: () => string;
148
+ /** Resolve a saved workflow's source by name (for `start({ name })`). */
149
+ getWorkflow: typeof library.getWorkflow;
150
+ }
151
+
152
+ function defaultDeps(): WorkflowRunManagerDeps {
153
+ return {
154
+ executeWorkflow,
155
+ leafRunner: runLeaf,
156
+ journal: journalStore,
157
+ getConfig,
158
+ isFlagEnabled: (config) =>
159
+ isAssistantFeatureFlagEnabled("workflows", config),
160
+ wake: wakeAgentForOpportunity,
161
+ broadcast: broadcastMessage,
162
+ newRunId: () => randomUUID(),
163
+ getWorkflow: library.getWorkflow,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Owns the lifecycle of workflow runs. One instance per daemon (see
169
+ * {@link getWorkflowRunManager}); tests construct their own with injected deps.
170
+ */
171
+ export class WorkflowRunManager {
172
+ private readonly deps: WorkflowRunManagerDeps;
173
+ /** runId → AbortController for every currently in-flight run. */
174
+ private readonly inflight = new Map<string, AbortController>();
175
+
176
+ constructor(deps?: Partial<WorkflowRunManagerDeps>) {
177
+ this.deps = { ...defaultDeps(), ...deps };
178
+ }
179
+
180
+ /**
181
+ * Launch a workflow run. Gates on the `workflows` flag and the concurrent-run
182
+ * cap (both throw before any engine code is reachable), resolves capabilities,
183
+ * creates the journal run row, and kicks off {@link executeWorkflow}
184
+ * asynchronously. Returns the `runId` immediately — completion is surfaced via
185
+ * events and a conversation wake.
186
+ */
187
+ start(opts: StartWorkflowOptions): { runId: string } {
188
+ const config = this.deps.getConfig();
189
+ if (!this.deps.isFlagEnabled(config)) {
190
+ throw new WorkflowsDisabledError();
191
+ }
192
+
193
+ const limit = config.workflows.maxConcurrentRuns;
194
+ if (this.inflight.size >= limit) {
195
+ throw new WorkflowRunCapError(limit);
196
+ }
197
+
198
+ // Resolve the script source: either inline (`scriptSource`) or by name from
199
+ // the saved-workflow library (`name`). An unknown saved name throws
200
+ // synchronously, leaving no orphaned `running` row behind.
201
+ const scriptSource = this.resolveScriptSource(opts);
202
+
203
+ // Resolve capabilities and extract meta BEFORE creating the run row so a
204
+ // manifest authoring error (unknown/forbidden tool) or a malformed script
205
+ // `meta` surfaces synchronously to the caller and leaves no orphaned
206
+ // `running` row behind.
207
+ const capabilities = resolveCapabilities(opts.manifest);
208
+ const meta = extractWorkflowMeta(scriptSource);
209
+
210
+ const runId = this.deps.newRunId();
211
+ const label = opts.label ?? meta.name;
212
+
213
+ // Create the row up front (so `status`/`list` see it immediately) with the
214
+ // same name + script hash the engine would compute, so its idempotent
215
+ // re-open of the existing row is a true no-op rather than a divergent
216
+ // overwrite. We persist the MANIFEST (a plain serializable grant), not the
217
+ // resolved capabilities (which carry live Tool objects that don't survive a
218
+ // round-trip), so a later `resume` can re-resolve it deterministically. We
219
+ // also persist the originating TRUST CONTEXT (trust metadata, no secret
220
+ // material) so a crash-orphaned `resume` reconstructs the exact trust class
221
+ // the run started under — NEVER escalating to guardian on resume.
222
+ this.deps.journal.createRun({
223
+ id: runId,
224
+ name: meta.name,
225
+ scriptSource,
226
+ scriptHash: createHash("sha256").update(scriptSource).digest("hex"),
227
+ args: opts.args,
228
+ capabilities: opts.manifest,
229
+ status: "running",
230
+ conversationId: opts.conversationId ?? null,
231
+ trust: opts.trustContext,
232
+ });
233
+
234
+ const controller = new AbortController();
235
+ this.inflight.set(runId, controller);
236
+
237
+ // Fire-and-forget: the engine owns its own try/catch and always finishes
238
+ // the run row. We never await it — `start` must return synchronously.
239
+ void this.runToCompletion(
240
+ runId,
241
+ label,
242
+ scriptSource,
243
+ capabilities,
244
+ controller,
245
+ {
246
+ args: opts.args,
247
+ conversationId: opts.conversationId,
248
+ trustContext: opts.trustContext,
249
+ },
250
+ );
251
+
252
+ return { runId };
253
+ }
254
+
255
+ /**
256
+ * Mark every `running` run `interrupted` at daemon startup. A row left in
257
+ * `running` means the process died mid-run (the engine always finishes its
258
+ * row on exit, so nothing else leaves one `running`). This makes the run
259
+ * eligible for an explicit {@link resume}; it does NOT auto-resume (that would
260
+ * be surprising and possibly unsafe). STATUS ONLY — accounting counters are
261
+ * never touched (see {@link journalStore.markRunningAsInterrupted}). Returns
262
+ * the number of runs reconciled.
263
+ */
264
+ reconcileOrphanedRuns(): number {
265
+ return this.deps.journal.markRunningAsInterrupted();
266
+ }
267
+
268
+ /**
269
+ * Resume an `interrupted` run with the SAME `runId`, re-invoking the engine so
270
+ * it replays the journaled prefix and continues from the first changed/new
271
+ * leaf. Reconstructs the run's inputs from the persisted row (script source,
272
+ * args, capability manifest, conversation) and the trust context from the
273
+ * persisted `trust` snapshot — so a resumed run NEVER runs with more trust
274
+ * than it started under. Legacy rows with no persisted trust fall back to the
275
+ * low-trust {@link FALLBACK_TURN_TRUST} (never guardian). Enforces the
276
+ * `workflows` flag and the concurrent-run cap (a resume occupies a run slot).
277
+ * Returns the `runId` immediately — completion is surfaced via events and a
278
+ * conversation wake exactly like {@link start}.
279
+ *
280
+ * Throws {@link WorkflowResumeNotPossibleError} when the run is missing, not
281
+ * `interrupted`, or already in flight.
282
+ */
283
+ resume(runId: string): { runId: string } {
284
+ const config = this.deps.getConfig();
285
+ if (!this.deps.isFlagEnabled(config)) {
286
+ throw new WorkflowsDisabledError();
287
+ }
288
+
289
+ if (this.inflight.has(runId)) {
290
+ throw new WorkflowResumeNotPossibleError(runId, "in_flight");
291
+ }
292
+
293
+ const run = this.deps.journal.getRun(runId);
294
+ if (!run) {
295
+ throw new WorkflowResumeNotPossibleError(runId, "not_found");
296
+ }
297
+ // Gate to `interrupted`: only a startup-reconciled crash leaves a resumable
298
+ // row. A `running` row that is NOT in our in-flight map is a stale row from
299
+ // a still-booting/other process — refuse rather than racing it.
300
+ if (run.status !== "interrupted") {
301
+ throw new WorkflowResumeNotPossibleError(
302
+ runId,
303
+ "not_interrupted",
304
+ run.status,
305
+ );
306
+ }
307
+
308
+ const limit = config.workflows.maxConcurrentRuns;
309
+ if (this.inflight.size >= limit) {
310
+ throw new WorkflowRunCapError(limit);
311
+ }
312
+
313
+ // Re-resolve capabilities from the persisted manifest (re-resolution throws
314
+ // synchronously on an unknown/forbidden tool, leaving the row interrupted).
315
+ // The engine re-validates the script's `meta` on re-invoke, so we don't
316
+ // re-extract it here — the persisted `name` is the display label.
317
+ const capabilities = resolveCapabilities(
318
+ normalizeCapabilityManifest(run.capabilities),
319
+ );
320
+ const label = run.name ?? runId;
321
+ const trustContext = reconstructResumeTrustContext(run.trust);
322
+
323
+ // Flip back to `running` before launching so `status`/`list` reflect the
324
+ // in-flight resume; the engine re-opens the same row idempotently.
325
+ this.deps.journal.updateRun(runId, { status: "running" });
326
+
327
+ const controller = new AbortController();
328
+ this.inflight.set(runId, controller);
329
+
330
+ void this.runToCompletion(
331
+ runId,
332
+ label,
333
+ run.scriptSource,
334
+ capabilities,
335
+ controller,
336
+ {
337
+ args: run.args,
338
+ conversationId: run.conversationId ?? undefined,
339
+ trustContext,
340
+ },
341
+ );
342
+
343
+ return { runId };
344
+ }
345
+
346
+ /**
347
+ * Resolve the run's script source from `StartWorkflowOptions`: returns
348
+ * `opts.scriptSource` verbatim, or — when `name` is given — the source of the
349
+ * matching saved workflow. Throws {@link WorkflowNotFoundError} if the name
350
+ * does not resolve.
351
+ */
352
+ private resolveScriptSource(opts: StartWorkflowOptions): string {
353
+ if (opts.scriptSource !== undefined) return opts.scriptSource;
354
+ const saved = this.deps.getWorkflow(opts.name);
355
+ if (!saved) throw new WorkflowNotFoundError(opts.name);
356
+ return saved.source;
357
+ }
358
+
359
+ /** Abort an in-flight run by signalling its {@link AbortController}. No-op for unknown/finished runs. */
360
+ abort(runId: string): void {
361
+ this.inflight.get(runId)?.abort();
362
+ }
363
+
364
+ /** Read the current run row (live counts + status), or null if unknown. */
365
+ status(runId: string): WorkflowRun | null {
366
+ return this.deps.journal.getRun(runId);
367
+ }
368
+
369
+ /** List runs newest-first, optionally filtered by status. */
370
+ list(options?: {
371
+ limit?: number;
372
+ status?: WorkflowRun["status"];
373
+ }): WorkflowRun[] {
374
+ return this.deps.journal.listRuns({
375
+ limit: options?.limit ?? 50,
376
+ ...(options?.status ? { status: options.status } : {}),
377
+ });
378
+ }
379
+
380
+ /** Number of currently in-flight runs (for tests / diagnostics). */
381
+ inflightCount(): number {
382
+ return this.inflight.size;
383
+ }
384
+
385
+ /**
386
+ * Drive a single run to completion: stream progress events, await the engine,
387
+ * publish completion, and inject the summary into the originating
388
+ * conversation. Always clears the in-flight slot in a finally block so a cap
389
+ * slot is never leaked even if the engine throws (it shouldn't — it owns its
390
+ * own error handling — but defense in depth).
391
+ */
392
+ private async runToCompletion(
393
+ runId: string,
394
+ label: string,
395
+ scriptSource: string,
396
+ capabilities: ReturnType<typeof resolveCapabilities>,
397
+ controller: AbortController,
398
+ ctx: RunContext,
399
+ ): Promise<void> {
400
+ const config = this.deps.getConfig();
401
+ try {
402
+ const result = await this.deps.executeWorkflow({
403
+ runId,
404
+ scriptSource,
405
+ args: ctx.args,
406
+ capabilities,
407
+ config: config.workflows,
408
+ journal: this.deps.journal,
409
+ leafRunner: this.deps.leafRunner,
410
+ trustContext: ctx.trustContext,
411
+ signal: controller.signal,
412
+ onProgress: (event) => {
413
+ const run = this.deps.journal.getRun(runId);
414
+ const agentsSpawned = run?.agentsSpawned ?? 0;
415
+ this.deps.broadcast({
416
+ type: "workflow_progress",
417
+ runId,
418
+ label,
419
+ agentsSpawned,
420
+ ...(event.type === "phase"
421
+ ? { phase: event.title }
422
+ : { message: event.message }),
423
+ });
424
+ },
425
+ });
426
+
427
+ const summary = buildCompletionSummary(
428
+ runId,
429
+ label,
430
+ result,
431
+ this.deps.journal.getRun(runId),
432
+ );
433
+
434
+ this.deps.broadcast({
435
+ type: "workflow_completed",
436
+ runId,
437
+ status: result.status,
438
+ agentsSpawned: result.agentsSpawned,
439
+ inputTokens: result.inputTokens,
440
+ outputTokens: result.outputTokens,
441
+ summary,
442
+ });
443
+
444
+ await this.injectCompletionSummary(ctx, summary);
445
+ } catch (err) {
446
+ // The engine finishes the run row even on internal errors, so this only
447
+ // fires for an unexpected throw (e.g. a bug in the manager glue). Log and
448
+ // move on — the in-flight slot is released in finally.
449
+ log.error({ err, runId }, "Workflow run manager: run threw unexpectedly");
450
+ } finally {
451
+ this.inflight.delete(runId);
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Surface the completion summary back into the originating conversation,
457
+ * REUSING {@link wakeAgentForOpportunity} — the canonical "background job
458
+ * finished, tell this conversation" path (shared with scheduled tasks and
459
+ * background shell tools). Best-effort: a wake failure is logged, never
460
+ * thrown (the run already completed; events already fired).
461
+ */
462
+ private async injectCompletionSummary(
463
+ ctx: RunContext,
464
+ summary: string,
465
+ ): Promise<void> {
466
+ if (!ctx.conversationId) return;
467
+ try {
468
+ await this.deps.wake({
469
+ conversationId: ctx.conversationId,
470
+ hint: summary,
471
+ source: WORKFLOW_WAKE_SOURCE,
472
+ trustContext: ctx.trustContext,
473
+ });
474
+ } catch (err) {
475
+ log.warn(
476
+ { err, conversationId: ctx.conversationId },
477
+ "Workflow run manager: completion wake failed (non-fatal)",
478
+ );
479
+ }
480
+ }
481
+ }
482
+
483
+ /**
484
+ * The per-run inputs `runToCompletion` needs, decoupled from how the run was
485
+ * launched. Both {@link WorkflowRunManager.start} (from `StartWorkflowOptions`)
486
+ * and {@link WorkflowRunManager.resume} (from the persisted run row) build one.
487
+ */
488
+ interface RunContext {
489
+ args: unknown;
490
+ conversationId: string | undefined;
491
+ trustContext: TrustContext;
492
+ }
493
+
494
+ /** The trust classes a persisted trust snapshot may legitimately carry. */
495
+ const VALID_TRUST_CLASSES: ReadonlySet<TrustContext["trustClass"]> = new Set([
496
+ "guardian",
497
+ "trusted_contact",
498
+ "unknown",
499
+ ]);
500
+
501
+ /**
502
+ * Reconstruct the {@link TrustContext} for a resumed run from its persisted
503
+ * `trust` snapshot. This is the security boundary on resume: a run must NEVER
504
+ * resume with more trust than it started under. When the snapshot is absent
505
+ * (legacy rows written before the `trust_json` column existed) or unusable
506
+ * (unparseable JSON, or a missing/unrecognized `trustClass`), we fall back to
507
+ * the low-trust {@link FALLBACK_TURN_TRUST} — biased to `unknown` — matching the
508
+ * start path's no-elevation discipline. We deliberately do NOT fall back to the
509
+ * internal guardian context, which would clear the side-effect approval gate.
510
+ */
511
+ function reconstructResumeTrustContext(persisted: unknown): TrustContext {
512
+ if (persisted && typeof persisted === "object") {
513
+ const candidate = persisted as Partial<TrustContext>;
514
+ if (
515
+ typeof candidate.trustClass === "string" &&
516
+ VALID_TRUST_CLASSES.has(candidate.trustClass)
517
+ ) {
518
+ // The snapshot is a well-formed trust context — replay it verbatim.
519
+ return candidate as TrustContext;
520
+ }
521
+ }
522
+ return FALLBACK_TURN_TRUST;
523
+ }
524
+
525
+ /**
526
+ * Build the human-readable completion summary injected into the originating
527
+ * conversation and carried on the `workflow_completed` event. Reports the run's
528
+ * terminal status, agent/token counts, and a result-or-error tail.
529
+ */
530
+ function buildCompletionSummary(
531
+ runId: string,
532
+ label: string,
533
+ result: Awaited<ReturnType<typeof executeWorkflow>>,
534
+ run: WorkflowRun | null,
535
+ ): string {
536
+ const lines = [
537
+ `[workflow "${label}" ${result.status}]`,
538
+ `Run ${runId} finished with status: ${result.status}.`,
539
+ `Agents spawned: ${result.agentsSpawned}. ` +
540
+ `Tokens: ${result.inputTokens} in / ${result.outputTokens} out.`,
541
+ ];
542
+ if (result.status === "completed") {
543
+ lines.push(
544
+ `Result: ${truncateForSummary(stringifyResult(result.result), runId)}`,
545
+ );
546
+ } else {
547
+ lines.push(`Outcome: ${run?.error ?? result.status}`);
548
+ }
549
+ return lines.join("\n");
550
+ }
551
+
552
+ /**
553
+ * Max characters of a workflow result echoed into the completion summary. The
554
+ * summary rides on the `workflow_completed` SSE event AND is injected into the
555
+ * originating conversation via a wake, so an unbounded fan-out result (a large
556
+ * array/object/string) could blow the event or prompt size limit. The full,
557
+ * untruncated result is retained on the durable run row.
558
+ */
559
+ const MAX_SUMMARY_RESULT_CHARS = 2000;
560
+
561
+ /** Bound a result tail for the summary; the full value stays on the run row. */
562
+ function truncateForSummary(s: string, runId: string): string {
563
+ if (s.length <= MAX_SUMMARY_RESULT_CHARS) return s;
564
+ const omitted = s.length - MAX_SUMMARY_RESULT_CHARS;
565
+ return (
566
+ `${s.slice(0, MAX_SUMMARY_RESULT_CHARS)}… ` +
567
+ `[truncated ${omitted} chars — fetch the full result with manage_workflows ` +
568
+ `{ action: "get_result", run_id: "${runId}" }]`
569
+ );
570
+ }
571
+
572
+ /** Compact a workflow result for the summary — JSON for objects, string otherwise. */
573
+ function stringifyResult(value: unknown): string {
574
+ if (value == null) return "(no result)";
575
+ if (typeof value === "string") return value;
576
+ try {
577
+ return JSON.stringify(value);
578
+ } catch {
579
+ return String(value);
580
+ }
581
+ }
582
+
583
+ // ── Process-level singleton ───────────────────────────────────────────────
584
+
585
+ let _instance: WorkflowRunManager | null = null;
586
+
587
+ /** Singleton {@link WorkflowRunManager} shared across the daemon. */
588
+ export function getWorkflowRunManager(): WorkflowRunManager {
589
+ if (!_instance) {
590
+ _instance = new WorkflowRunManager();
591
+ }
592
+ return _instance;
593
+ }