@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,711 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ // Silence the logger.
4
+ mock.module("../util/logger.js", () => ({
5
+ getLogger: () =>
6
+ new Proxy({} as Record<string, unknown>, {
7
+ get: () => () => {},
8
+ }),
9
+ }));
10
+
11
+ import type { AssistantConfig } from "../config/schema.js";
12
+ import type { TrustContext } from "../daemon/trust-context.js";
13
+ import type { ExecuteWorkflowOptions } from "./engine.js";
14
+ import { WorkflowNotFoundError } from "./engine.js";
15
+ import type {
16
+ CreateRunInput,
17
+ WorkflowRun,
18
+ WorkflowRunStatus,
19
+ } from "./journal-store.js";
20
+ import {
21
+ WorkflowResumeNotPossibleError,
22
+ WorkflowRunCapError,
23
+ WorkflowRunManager,
24
+ type WorkflowRunManagerDeps,
25
+ WorkflowsDisabledError,
26
+ } from "./run-manager.js";
27
+
28
+ const TRUST: TrustContext = {
29
+ sourceChannel: "vellum",
30
+ trustClass: "guardian",
31
+ };
32
+
33
+ /** Minimal config exposing only the fields the manager reads. */
34
+ function makeConfig(maxConcurrentRuns = 3): AssistantConfig {
35
+ return {
36
+ workflows: {
37
+ maxAgentsPerRun: 500,
38
+ maxConcurrentLeaves: 6,
39
+ maxConcurrentRuns,
40
+ journalRetentionDays: 30,
41
+ },
42
+ } as unknown as AssistantConfig;
43
+ }
44
+
45
+ /**
46
+ * In-memory journal store standing in for `./journal-store.js`. Implements only
47
+ * the surface the manager touches (`createRun`, `getRun`, `listRuns`) plus the
48
+ * engine's surface (unused here since we also fake the engine).
49
+ */
50
+ function makeFakeJournal() {
51
+ const rows = new Map<string, WorkflowRun>();
52
+ const setStats = (id: string, patch: Partial<WorkflowRun>): void => {
53
+ const existing = rows.get(id);
54
+ if (existing) rows.set(id, { ...existing, ...patch });
55
+ };
56
+ return {
57
+ rows,
58
+ setStats,
59
+ journal: {
60
+ createRun: (input: CreateRunInput): WorkflowRun => {
61
+ const run: WorkflowRun = {
62
+ id: input.id,
63
+ name: input.name ?? null,
64
+ scriptSource: input.scriptSource,
65
+ scriptHash: input.scriptHash,
66
+ args: input.args ?? null,
67
+ capabilities: input.capabilities ?? null,
68
+ status: input.status ?? "running",
69
+ conversationId: input.conversationId ?? null,
70
+ trust: input.trust ?? null,
71
+ agentsSpawned: 0,
72
+ inputTokens: 0,
73
+ outputTokens: 0,
74
+ result: null,
75
+ error: null,
76
+ createdAt: Date.now(),
77
+ updatedAt: Date.now(),
78
+ finishedAt: null,
79
+ };
80
+ rows.set(input.id, run);
81
+ return run;
82
+ },
83
+ getRun: (id: string): WorkflowRun | null => rows.get(id) ?? null,
84
+ updateRun: (
85
+ id: string,
86
+ patch: Partial<WorkflowRun>,
87
+ ): WorkflowRun | null => {
88
+ const existing = rows.get(id);
89
+ if (!existing) return null;
90
+ const updated = { ...existing, ...patch, updatedAt: Date.now() };
91
+ rows.set(id, updated);
92
+ return updated;
93
+ },
94
+ markRunningAsInterrupted: (): number => {
95
+ let n = 0;
96
+ for (const [id, run] of rows) {
97
+ if (run.status === "running") {
98
+ rows.set(id, { ...run, status: "interrupted" });
99
+ n += 1;
100
+ }
101
+ }
102
+ return n;
103
+ },
104
+ listRuns: ({
105
+ limit,
106
+ status,
107
+ }: {
108
+ limit: number;
109
+ status?: WorkflowRunStatus;
110
+ }): WorkflowRun[] => {
111
+ const all = [...rows.values()].sort(
112
+ (a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0),
113
+ );
114
+ return (status ? all.filter((r) => r.status === status) : all).slice(
115
+ 0,
116
+ limit,
117
+ );
118
+ },
119
+ } as unknown as WorkflowRunManagerDeps["journal"],
120
+ };
121
+ }
122
+
123
+ interface ManagerHarness {
124
+ manager: WorkflowRunManager;
125
+ executeCalls: ExecuteWorkflowOptions[];
126
+ broadcasts: Array<{ type: string; [k: string]: unknown }>;
127
+ wakes: Array<{ conversationId: string; hint: string; source: string }>;
128
+ fake: ReturnType<typeof makeFakeJournal>;
129
+ /** Resolve the latest in-flight engine run with a given result. */
130
+ resolveLatest: (result: EngineResult) => Promise<void>;
131
+ }
132
+
133
+ interface EngineResult {
134
+ status: WorkflowRunStatus;
135
+ result: unknown;
136
+ agentsSpawned: number;
137
+ inputTokens: number;
138
+ outputTokens: number;
139
+ }
140
+
141
+ /**
142
+ * Build a manager with a fully-injected, provider-free seam: a fake engine that
143
+ * surfaces a resolver hook (so a test can drive progress/abort/completion
144
+ * deterministically), an in-memory journal, and spies for broadcast + wake.
145
+ */
146
+ function makeHarness(opts?: {
147
+ flagEnabled?: boolean;
148
+ maxConcurrentRuns?: number;
149
+ /** Custom engine impl; defaults to the deferred-resolver fake. */
150
+ engine?: WorkflowRunManagerDeps["executeWorkflow"];
151
+ /** Saved-workflow resolver for the `start({ name })` path. */
152
+ getWorkflow?: WorkflowRunManagerDeps["getWorkflow"];
153
+ }): ManagerHarness {
154
+ const fake = makeFakeJournal();
155
+ const executeCalls: ExecuteWorkflowOptions[] = [];
156
+ const broadcasts: ManagerHarness["broadcasts"] = [];
157
+ const wakes: ManagerHarness["wakes"] = [];
158
+
159
+ // Deferred-resolution engine: capture each invocation and let the test settle
160
+ // it. Tracks the resolver so `resolveLatest` can fire completion on demand.
161
+ const resolvers: Array<(r: EngineResult) => void> = [];
162
+ const defaultEngine: WorkflowRunManagerDeps["executeWorkflow"] = (
163
+ options,
164
+ ) => {
165
+ executeCalls.push(options);
166
+ // Reflect the engine's count-flushing so `getRun` mirrors mid-flight state.
167
+ return new Promise<EngineResult>((resolve) => {
168
+ resolvers.push((r) => {
169
+ fake.setStats(options.runId, {
170
+ status: r.status,
171
+ agentsSpawned: r.agentsSpawned,
172
+ inputTokens: r.inputTokens,
173
+ outputTokens: r.outputTokens,
174
+ result: r.status === "completed" ? r.result : null,
175
+ error:
176
+ r.status === "completed"
177
+ ? null
178
+ : `Workflow ended with status ${r.status}`,
179
+ finishedAt: Date.now(),
180
+ });
181
+ resolve(r);
182
+ });
183
+ }) as ReturnType<WorkflowRunManagerDeps["executeWorkflow"]>;
184
+ };
185
+
186
+ const deps: Partial<WorkflowRunManagerDeps> = {
187
+ executeWorkflow: opts?.engine ?? defaultEngine,
188
+ leafRunner: (() => {
189
+ throw new Error("leafRunner must not be called in run-manager tests");
190
+ }) as unknown as WorkflowRunManagerDeps["leafRunner"],
191
+ journal: fake.journal,
192
+ getConfig: () => makeConfig(opts?.maxConcurrentRuns),
193
+ isFlagEnabled: () => opts?.flagEnabled ?? true,
194
+ wake: (async (wakeOpts) => {
195
+ wakes.push({
196
+ conversationId: wakeOpts.conversationId,
197
+ hint: wakeOpts.hint,
198
+ source: wakeOpts.source,
199
+ });
200
+ return { invoked: true, producedToolCalls: false };
201
+ }) as WorkflowRunManagerDeps["wake"],
202
+ broadcast: ((msg) => {
203
+ broadcasts.push(msg as { type: string });
204
+ }) as WorkflowRunManagerDeps["broadcast"],
205
+ newRunId: (() => {
206
+ let n = 0;
207
+ return () => `run-${++n}`;
208
+ })(),
209
+ ...(opts?.getWorkflow ? { getWorkflow: opts.getWorkflow } : {}),
210
+ };
211
+
212
+ const manager = new WorkflowRunManager(deps);
213
+
214
+ return {
215
+ manager,
216
+ executeCalls,
217
+ broadcasts,
218
+ wakes,
219
+ fake,
220
+ resolveLatest: async (result) => {
221
+ const resolve = resolvers[resolvers.length - 1];
222
+ if (!resolve) throw new Error("no in-flight engine run to resolve");
223
+ resolve(result);
224
+ // Let the manager's post-completion microtasks (broadcast + wake) settle.
225
+ await new Promise((r) => setTimeout(r, 0));
226
+ },
227
+ };
228
+ }
229
+
230
+ describe("WorkflowRunManager.start — flag gate", () => {
231
+ test("flag off → start throws and the engine is never invoked", () => {
232
+ const h = makeHarness({ flagEnabled: false });
233
+ expect(() =>
234
+ h.manager.start({
235
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
236
+ args: {},
237
+ manifest: { tools: [], hostFunctions: [], persona: false },
238
+ trustContext: TRUST,
239
+ }),
240
+ ).toThrow(WorkflowsDisabledError);
241
+ expect(h.executeCalls).toHaveLength(0);
242
+ expect(h.fake.rows.size).toBe(0);
243
+ });
244
+ });
245
+
246
+ describe("WorkflowRunManager.start — concurrent-run cap", () => {
247
+ test("the (N+1)th concurrent start is rejected", () => {
248
+ const h = makeHarness({ maxConcurrentRuns: 2 });
249
+ const startOne = () =>
250
+ h.manager.start({
251
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
252
+ args: {},
253
+ manifest: { tools: [], hostFunctions: [], persona: false },
254
+ trustContext: TRUST,
255
+ });
256
+
257
+ startOne();
258
+ startOne();
259
+ expect(h.manager.inflightCount()).toBe(2);
260
+ expect(() => startOne()).toThrow(WorkflowRunCapError);
261
+ // The rejected start created no extra run row.
262
+ expect(h.fake.rows.size).toBe(2);
263
+ });
264
+
265
+ test("a finished run frees a cap slot", async () => {
266
+ const h = makeHarness({ maxConcurrentRuns: 1 });
267
+ const start = () =>
268
+ h.manager.start({
269
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
270
+ args: {},
271
+ manifest: { tools: [], hostFunctions: [], persona: false },
272
+ trustContext: TRUST,
273
+ });
274
+
275
+ start();
276
+ expect(() => start()).toThrow(WorkflowRunCapError);
277
+
278
+ await h.resolveLatest({
279
+ status: "completed",
280
+ result: "ok",
281
+ agentsSpawned: 1,
282
+ inputTokens: 10,
283
+ outputTokens: 5,
284
+ });
285
+
286
+ expect(h.manager.inflightCount()).toBe(0);
287
+ // Slot freed: a second run now starts without throwing.
288
+ expect(() => start()).not.toThrow();
289
+ });
290
+ });
291
+
292
+ describe("WorkflowRunManager.abort", () => {
293
+ test("abort signals the engine's AbortController", () => {
294
+ const h = makeHarness();
295
+ const { runId } = h.manager.start({
296
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
297
+ args: {},
298
+ manifest: { tools: [], hostFunctions: [], persona: false },
299
+ trustContext: TRUST,
300
+ });
301
+
302
+ const passedSignal = h.executeCalls[0]!.signal!;
303
+ expect(passedSignal.aborted).toBe(false);
304
+ h.manager.abort(runId);
305
+ expect(passedSignal.aborted).toBe(true);
306
+ });
307
+
308
+ test("an aborted run resolves with status 'aborted' and persists it", async () => {
309
+ const h = makeHarness();
310
+ const { runId } = h.manager.start({
311
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
312
+ args: {},
313
+ manifest: { tools: [], hostFunctions: [], persona: false },
314
+ trustContext: TRUST,
315
+ });
316
+
317
+ h.manager.abort(runId);
318
+ // The (faked) engine observes the abort and reports the terminal status.
319
+ await h.resolveLatest({
320
+ status: "aborted",
321
+ result: null,
322
+ agentsSpawned: 2,
323
+ inputTokens: 0,
324
+ outputTokens: 0,
325
+ });
326
+
327
+ expect(h.manager.status(runId)?.status).toBe("aborted");
328
+ const completed = h.broadcasts.find((b) => b.type === "workflow_completed");
329
+ expect(completed?.status).toBe("aborted");
330
+ });
331
+ });
332
+
333
+ describe("WorkflowRunManager — progress events", () => {
334
+ test("onProgress phase/log are republished as workflow_progress", async () => {
335
+ const h = makeHarness();
336
+ h.manager.start({
337
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
338
+ args: {},
339
+ manifest: { tools: [], hostFunctions: [], persona: false },
340
+ label: "My Flow",
341
+ trustContext: TRUST,
342
+ });
343
+
344
+ const onProgress = h.executeCalls[0]!.onProgress!;
345
+ onProgress({ type: "phase", title: "Gathering" });
346
+ onProgress({ type: "log", message: "step done" });
347
+
348
+ const progress = h.broadcasts.filter((b) => b.type === "workflow_progress");
349
+ expect(progress).toHaveLength(2);
350
+ expect(progress[0]).toMatchObject({
351
+ type: "workflow_progress",
352
+ label: "My Flow",
353
+ phase: "Gathering",
354
+ });
355
+ expect(progress[1]).toMatchObject({
356
+ type: "workflow_progress",
357
+ message: "step done",
358
+ });
359
+ });
360
+ });
361
+
362
+ describe("WorkflowRunManager — completion", () => {
363
+ test("completion publishes workflow_completed and injects into the conversation", async () => {
364
+ const h = makeHarness();
365
+ const { runId } = h.manager.start({
366
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
367
+ args: {},
368
+ manifest: { tools: [], hostFunctions: [], persona: false },
369
+ conversationId: "conv-1",
370
+ label: "Digest",
371
+ trustContext: TRUST,
372
+ });
373
+
374
+ await h.resolveLatest({
375
+ status: "completed",
376
+ result: { summary: "all good" },
377
+ agentsSpawned: 4,
378
+ inputTokens: 100,
379
+ outputTokens: 50,
380
+ });
381
+
382
+ const completed = h.broadcasts.find((b) => b.type === "workflow_completed");
383
+ expect(completed).toMatchObject({
384
+ type: "workflow_completed",
385
+ runId,
386
+ status: "completed",
387
+ agentsSpawned: 4,
388
+ inputTokens: 100,
389
+ outputTokens: 50,
390
+ });
391
+ expect(typeof completed?.summary).toBe("string");
392
+
393
+ // Completion summary surfaced to the originating conversation via a wake.
394
+ expect(h.wakes).toHaveLength(1);
395
+ expect(h.wakes[0]).toMatchObject({
396
+ conversationId: "conv-1",
397
+ source: "workflow_completed",
398
+ });
399
+ expect(h.wakes[0]!.hint).toContain("completed");
400
+
401
+ // Final stats persisted to the run row.
402
+ const run = h.manager.status(runId);
403
+ expect(run?.status).toBe("completed");
404
+ expect(run?.agentsSpawned).toBe(4);
405
+ expect(run?.inputTokens).toBe(100);
406
+ expect(run?.outputTokens).toBe(50);
407
+ });
408
+
409
+ test("a large result is truncated in the summary but kept whole on the run row", async () => {
410
+ const h = makeHarness();
411
+ const big = "x".repeat(50_000);
412
+ const { runId } = h.manager.start({
413
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
414
+ args: {},
415
+ manifest: { tools: [], hostFunctions: [], persona: false },
416
+ conversationId: "conv-big",
417
+ label: "Digest",
418
+ trustContext: TRUST,
419
+ });
420
+
421
+ await h.resolveLatest({
422
+ status: "completed",
423
+ result: big,
424
+ agentsSpawned: 1,
425
+ inputTokens: 1,
426
+ outputTokens: 1,
427
+ });
428
+
429
+ const completed = h.broadcasts.find((b) => b.type === "workflow_completed");
430
+ // The summary (event + wake) is bounded well under the raw result size.
431
+ const summary = String(completed?.summary ?? "");
432
+ expect(summary.length).toBeLessThan(big.length);
433
+ expect(summary).toContain("[truncated");
434
+ expect(h.wakes[0]!.hint).toContain("[truncated");
435
+
436
+ // The durable run row keeps the FULL, untruncated result.
437
+ expect(h.manager.status(runId)?.result).toBe(big);
438
+ });
439
+
440
+ test("no conversationId → completion events fire but no wake", async () => {
441
+ const h = makeHarness();
442
+ h.manager.start({
443
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
444
+ args: {},
445
+ manifest: { tools: [], hostFunctions: [], persona: false },
446
+ trustContext: TRUST,
447
+ });
448
+
449
+ await h.resolveLatest({
450
+ status: "completed",
451
+ result: "x",
452
+ agentsSpawned: 1,
453
+ inputTokens: 1,
454
+ outputTokens: 1,
455
+ });
456
+
457
+ expect(h.broadcasts.some((b) => b.type === "workflow_completed")).toBe(
458
+ true,
459
+ );
460
+ expect(h.wakes).toHaveLength(0);
461
+ });
462
+ });
463
+
464
+ describe("WorkflowRunManager.start — saved-workflow name resolution", () => {
465
+ const SAVED_SOURCE =
466
+ "export const meta = { name: 'saved-flow', description: 'd' }";
467
+
468
+ test("start({ name }) resolves the library source and runs it", () => {
469
+ const h = makeHarness({
470
+ getWorkflow: (name) =>
471
+ name === "saved-flow"
472
+ ? { source: SAVED_SOURCE, path: "/ws/workflows/saved.workflow.ts" }
473
+ : null,
474
+ });
475
+
476
+ const { runId } = h.manager.start({
477
+ name: "saved-flow",
478
+ args: { x: 1 },
479
+ manifest: { tools: [], hostFunctions: [], persona: false },
480
+ trustContext: TRUST,
481
+ });
482
+
483
+ // The run row + engine call carry the RESOLVED source, not the name.
484
+ expect(h.fake.rows.get(runId)?.scriptSource).toBe(SAVED_SOURCE);
485
+ expect(h.executeCalls).toHaveLength(1);
486
+ expect(h.executeCalls[0]!.scriptSource).toBe(SAVED_SOURCE);
487
+ // meta.name is extracted from the resolved source.
488
+ expect(h.fake.rows.get(runId)?.name).toBe("saved-flow");
489
+ });
490
+
491
+ test("start({ name }) with an unknown name throws and creates no run row", () => {
492
+ const h = makeHarness({ getWorkflow: () => null });
493
+ expect(() =>
494
+ h.manager.start({
495
+ name: "ghost",
496
+ args: {},
497
+ manifest: { tools: [], hostFunctions: [], persona: false },
498
+ trustContext: TRUST,
499
+ }),
500
+ ).toThrow(WorkflowNotFoundError);
501
+ expect(h.executeCalls).toHaveLength(0);
502
+ expect(h.fake.rows.size).toBe(0);
503
+ });
504
+ });
505
+
506
+ // Seed a run row directly into the fake journal (bypassing `start`), so resume
507
+ // tests can stand up a pre-existing `interrupted`/other-status row.
508
+ function seedRun(
509
+ h: ManagerHarness,
510
+ overrides: Partial<WorkflowRun> & { id: string },
511
+ ): WorkflowRun {
512
+ const run: WorkflowRun = {
513
+ name: "seeded",
514
+ scriptSource: "export const meta = { name: 'seeded', description: 'd' }",
515
+ scriptHash: "hash",
516
+ args: { k: "v" },
517
+ capabilities: { tools: [], hostFunctions: [], persona: false },
518
+ status: "interrupted",
519
+ conversationId: null,
520
+ trust: null,
521
+ agentsSpawned: 3,
522
+ inputTokens: 100,
523
+ outputTokens: 50,
524
+ result: null,
525
+ error: null,
526
+ createdAt: Date.now(),
527
+ updatedAt: Date.now(),
528
+ finishedAt: null,
529
+ ...overrides,
530
+ };
531
+ h.fake.rows.set(run.id, run);
532
+ return run;
533
+ }
534
+
535
+ describe("WorkflowRunManager.reconcileOrphanedRuns", () => {
536
+ test("flips running rows to interrupted, leaving counters untouched", () => {
537
+ const h = makeHarness({});
538
+ seedRun(h, { id: "r-run", status: "running", agentsSpawned: 7 });
539
+ seedRun(h, { id: "r-done", status: "completed", agentsSpawned: 2 });
540
+
541
+ const n = h.manager.reconcileOrphanedRuns();
542
+
543
+ expect(n).toBe(1);
544
+ const reconciled = h.fake.rows.get("r-run")!;
545
+ expect(reconciled.status).toBe("interrupted");
546
+ // Accounting preserved — reconciliation is status-only.
547
+ expect(reconciled.agentsSpawned).toBe(7);
548
+ expect(reconciled.inputTokens).toBe(100);
549
+ expect(reconciled.outputTokens).toBe(50);
550
+ // A completed run is untouched.
551
+ expect(h.fake.rows.get("r-done")!.status).toBe("completed");
552
+ });
553
+ });
554
+
555
+ describe("WorkflowRunManager.resume", () => {
556
+ test("an interrupted run re-invokes the engine with the SAME runId and carries accounting", async () => {
557
+ const h = makeHarness({});
558
+ seedRun(h, {
559
+ id: "run-x",
560
+ status: "interrupted",
561
+ args: { foo: 1 },
562
+ agentsSpawned: 4,
563
+ });
564
+
565
+ const { runId } = h.manager.resume("run-x");
566
+ expect(runId).toBe("run-x");
567
+
568
+ // Engine invoked with the same runId + reconstructed args; no NEW row.
569
+ expect(h.executeCalls).toHaveLength(1);
570
+ expect(h.executeCalls[0]!.runId).toBe("run-x");
571
+ expect(h.executeCalls[0]!.args).toEqual({ foo: 1 });
572
+
573
+ // Status transitions interrupted → running before launch.
574
+ expect(h.fake.rows.get("run-x")!.status).toBe("running");
575
+
576
+ // The engine reports the carried-forward accounting (seed: 4 agents) plus
577
+ // newly spawned work — the count carries, it does NOT reset.
578
+ await h.resolveLatest({
579
+ status: "completed",
580
+ result: "done",
581
+ agentsSpawned: 6,
582
+ inputTokens: 200,
583
+ outputTokens: 90,
584
+ });
585
+
586
+ const run = h.manager.status("run-x");
587
+ expect(run?.status).toBe("completed");
588
+ expect(run?.agentsSpawned).toBe(6);
589
+ expect(h.manager.inflightCount()).toBe(0);
590
+ });
591
+
592
+ test("resume replays the SAME (non-guardian) trust the run started under — never escalates", () => {
593
+ const h = makeHarness({});
594
+ // A run started by a low-trust actor: persisted trust is NOT guardian.
595
+ const lowTrust: TrustContext = {
596
+ sourceChannel: "slack",
597
+ trustClass: "unknown",
598
+ requesterIdentifier: "@someone",
599
+ };
600
+ seedRun(h, {
601
+ id: "run-x",
602
+ status: "interrupted",
603
+ conversationId: null,
604
+ trust: lowTrust,
605
+ });
606
+
607
+ h.manager.resume("run-x");
608
+
609
+ const replayed = h.executeCalls[0]!.trustContext;
610
+ // SECURITY: resume must reconstruct the exact trust the run started under,
611
+ // never elevate to guardian (which would clear the side-effect gate).
612
+ expect(replayed.trustClass).toBe("unknown");
613
+ expect(replayed.trustClass).not.toBe("guardian");
614
+ expect(replayed.sourceChannel).toBe("slack");
615
+ });
616
+
617
+ test("resume of a trusted_contact run keeps trusted_contact (no escalation)", () => {
618
+ const h = makeHarness({});
619
+ seedRun(h, {
620
+ id: "run-tc",
621
+ status: "interrupted",
622
+ trust: { sourceChannel: "slack", trustClass: "trusted_contact" },
623
+ });
624
+
625
+ h.manager.resume("run-tc");
626
+
627
+ expect(h.executeCalls[0]!.trustContext.trustClass).toBe("trusted_contact");
628
+ });
629
+
630
+ test("SECURITY: a legacy run row with no persisted trust resumes at LOW trust, never guardian", () => {
631
+ const h = makeHarness({});
632
+ // Legacy row: written before the trust_json column existed → trust is null.
633
+ seedRun(h, { id: "run-legacy", status: "interrupted", trust: null });
634
+
635
+ h.manager.resume("run-legacy");
636
+
637
+ const replayed = h.executeCalls[0]!.trustContext;
638
+ expect(replayed.trustClass).toBe("unknown");
639
+ expect(replayed.trustClass).not.toBe("guardian");
640
+ });
641
+
642
+ test("a persisted trust with an unrecognized trustClass falls back to LOW trust", () => {
643
+ const h = makeHarness({});
644
+ seedRun(h, {
645
+ id: "run-bad",
646
+ status: "interrupted",
647
+ // Corrupt/forward-incompatible snapshot: not a known trust class.
648
+ trust: { sourceChannel: "slack", trustClass: "superuser" },
649
+ });
650
+
651
+ h.manager.resume("run-bad");
652
+
653
+ expect(h.executeCalls[0]!.trustContext.trustClass).toBe("unknown");
654
+ });
655
+
656
+ test("start persists the originating trust context on the run row", () => {
657
+ const h = makeHarness({});
658
+ const startTrust: TrustContext = {
659
+ sourceChannel: "slack",
660
+ trustClass: "unknown",
661
+ };
662
+ const { runId } = h.manager.start({
663
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
664
+ args: {},
665
+ manifest: { tools: [], hostFunctions: [], persona: false },
666
+ trustContext: startTrust,
667
+ });
668
+
669
+ expect(h.fake.rows.get(runId)?.trust).toEqual(startTrust);
670
+ });
671
+
672
+ test("a non-interrupted run throws WorkflowResumeNotPossibleError and does not invoke the engine", () => {
673
+ const h = makeHarness({});
674
+ seedRun(h, { id: "run-done", status: "completed" });
675
+
676
+ expect(() => h.manager.resume("run-done")).toThrow(
677
+ WorkflowResumeNotPossibleError,
678
+ );
679
+ expect(h.executeCalls).toHaveLength(0);
680
+ // The row is untouched (still completed).
681
+ expect(h.fake.rows.get("run-done")!.status).toBe("completed");
682
+ });
683
+
684
+ test("an unknown run throws WorkflowResumeNotPossibleError", () => {
685
+ const h = makeHarness({});
686
+ expect(() => h.manager.resume("ghost")).toThrow(
687
+ WorkflowResumeNotPossibleError,
688
+ );
689
+ expect(h.executeCalls).toHaveLength(0);
690
+ });
691
+
692
+ test("resume respects the concurrent-run cap", () => {
693
+ const h = makeHarness({ maxConcurrentRuns: 1 });
694
+ // Occupy the single slot with a live run.
695
+ h.manager.start({
696
+ scriptSource: "export const meta = { name: 'x', description: 'y' }",
697
+ args: {},
698
+ manifest: { tools: [], hostFunctions: [], persona: false },
699
+ trustContext: TRUST,
700
+ });
701
+ seedRun(h, { id: "run-x", status: "interrupted" });
702
+
703
+ expect(() => h.manager.resume("run-x")).toThrow(WorkflowRunCapError);
704
+ });
705
+
706
+ test("resume is rejected when the flag is off", () => {
707
+ const h = makeHarness({ flagEnabled: false });
708
+ seedRun(h, { id: "run-x", status: "interrupted" });
709
+ expect(() => h.manager.resume("run-x")).toThrow(WorkflowsDisabledError);
710
+ });
711
+ });