@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,372 @@
1
+ /**
2
+ * Tests for the default `task-progress-nudge` plugin's `post-tool-use` hook.
3
+ *
4
+ * Covers:
5
+ * - Stays silent below the round threshold; nudges once a multi-step turn
6
+ * reaches it with no task_progress card shown.
7
+ * - Never nudges when a task_progress card was already shown this turn.
8
+ * - Fires at most once per turn (dedupes parallel results / further rounds).
9
+ * - Resets across turns so a later multi-step turn can nudge again.
10
+ * - Gating: fires only for weak models (Kimi/DeepSeek/MiniMax) and skips
11
+ * capable models, non-mainAgent call sites, surface-incapable clients, and
12
+ * subagent conversations.
13
+ * - Appends to (not overwrites) `additionalContext` set by an earlier hook.
14
+ * - The nudge leaves the tool result's `content` untouched.
15
+ */
16
+
17
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
18
+
19
+ import * as realConversationRegistry from "../daemon/conversation-registry.js";
20
+
21
+ // The hook lazily imports the conversation registry and subagent manager on
22
+ // the would-nudge path; mock both before importing the hook. The registry mock
23
+ // spreads the real module so other consumers in the shared test process keep
24
+ // their exports — only `findConversation` is overridden.
25
+ interface MockConversation {
26
+ currentCallSite?: string;
27
+ currentTurnChannelCapabilities?: {
28
+ channel: string;
29
+ supportsDynamicUi: boolean;
30
+ };
31
+ channelCapabilities?: { channel: string; supportsDynamicUi: boolean };
32
+ }
33
+ let mockConversation: (
34
+ conversationId: string,
35
+ ) => MockConversation | undefined = () => ({
36
+ currentCallSite: "mainAgent",
37
+ currentTurnChannelCapabilities: { channel: "web", supportsDynamicUi: true },
38
+ });
39
+ mock.module("../daemon/conversation-registry.js", () => ({
40
+ ...realConversationRegistry,
41
+ findConversation: (conversationId: string) =>
42
+ mockConversation(conversationId),
43
+ }));
44
+
45
+ let mockParentInfo: (conversationId: string) => unknown = () => undefined;
46
+ mock.module("../subagent/index.js", () => ({
47
+ getSubagentManager: () => ({
48
+ getParentInfo: (conversationId: string) => mockParentInfo(conversationId),
49
+ }),
50
+ }));
51
+
52
+ import type { PluginLogger, PostToolUseContext } from "../plugin-api/types.js";
53
+ import postToolUse, {
54
+ resetTaskProgressNudgeStateForTests,
55
+ TASK_PROGRESS_NUDGE_ROUND_THRESHOLD,
56
+ TASK_PROGRESS_NUDGE_TEXT,
57
+ } from "../plugins/defaults/task-progress-nudge/hooks/post-tool-use.js";
58
+ import type { Message, ToolResultContent } from "../providers/types.js";
59
+
60
+ const noopLogger: PluginLogger = {
61
+ info: () => {},
62
+ warn: () => {},
63
+ error: () => {},
64
+ debug: () => {},
65
+ };
66
+
67
+ let conversationCounter = 0;
68
+ function freshConversationId(): string {
69
+ conversationCounter++;
70
+ return `conv-nudge-test-${conversationCounter}`;
71
+ }
72
+
73
+ function toolUseTurn(
74
+ id: string,
75
+ name: string,
76
+ input: Record<string, unknown> = {},
77
+ ): Message {
78
+ return {
79
+ role: "assistant",
80
+ content: [{ type: "tool_use", id, name, input }],
81
+ };
82
+ }
83
+
84
+ function toolResultTurn(toolUseId: string): Message {
85
+ return {
86
+ role: "user",
87
+ content: [
88
+ {
89
+ type: "tool_result",
90
+ tool_use_id: toolUseId,
91
+ content: "ok",
92
+ is_error: false,
93
+ },
94
+ ],
95
+ };
96
+ }
97
+
98
+ function currentResponse(toolUseId: string): ToolResultContent {
99
+ return {
100
+ type: "tool_result",
101
+ tool_use_id: toolUseId,
102
+ content: "result body",
103
+ is_error: false,
104
+ };
105
+ }
106
+
107
+ /** A weak-model id that matches WEAK_MODEL_PATTERN (the gated population). */
108
+ const WEAK_MODEL = "minimax/minimax-m3";
109
+
110
+ function makeCtx(
111
+ conversationId: string,
112
+ toolResponse: ToolResultContent,
113
+ messages: Message[],
114
+ model: string = WEAK_MODEL,
115
+ ): PostToolUseContext {
116
+ return {
117
+ conversationId,
118
+ toolResponse,
119
+ messages,
120
+ additionalContext: null,
121
+ model,
122
+ maxInputTokens: 10_000,
123
+ logger: noopLogger,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Build a turn of `rounds` tool-use rounds. The last round's tool_use has no
129
+ * result in history (it arrives via `ctx.toolResponse`). When `showCardAtRound`
130
+ * is set, that round issues a `ui_show` task_progress card instead of a plain
131
+ * tool.
132
+ */
133
+ function turnWithRounds(
134
+ rounds: number,
135
+ opts: { showCardAtRound?: number } = {},
136
+ ): { messages: Message[]; currentToolUseId: string } {
137
+ const messages: Message[] = [
138
+ { role: "user", content: [{ type: "text", text: "build the thing" }] },
139
+ ];
140
+ for (let r = 1; r <= rounds; r++) {
141
+ const id = `tool-${r}`;
142
+ if (opts.showCardAtRound === r) {
143
+ messages.push(
144
+ toolUseTurn(id, "ui_show", {
145
+ surface_type: "card",
146
+ template: "task_progress",
147
+ templateData: { status: "in_progress", steps: [] },
148
+ }),
149
+ );
150
+ } else {
151
+ messages.push(toolUseTurn(id, "bash", { command: `cmd-${r}` }));
152
+ }
153
+ if (r < rounds) messages.push(toolResultTurn(id));
154
+ }
155
+ return { messages, currentToolUseId: `tool-${rounds}` };
156
+ }
157
+
158
+ describe("task-progress-nudge post-tool-use hook", () => {
159
+ beforeEach(() => {
160
+ resetTaskProgressNudgeStateForTests();
161
+ mockConversation = () => ({
162
+ currentCallSite: "mainAgent",
163
+ currentTurnChannelCapabilities: {
164
+ channel: "web",
165
+ supportsDynamicUi: true,
166
+ },
167
+ });
168
+ mockParentInfo = () => undefined;
169
+ });
170
+
171
+ test("stays silent below the round threshold", async () => {
172
+ const { messages, currentToolUseId } = turnWithRounds(
173
+ TASK_PROGRESS_NUDGE_ROUND_THRESHOLD - 1,
174
+ );
175
+ const ctx = makeCtx(
176
+ freshConversationId(),
177
+ currentResponse(currentToolUseId),
178
+ messages,
179
+ );
180
+ await postToolUse(ctx);
181
+ expect(ctx.additionalContext).toBeNull();
182
+ });
183
+
184
+ test("nudges once the turn reaches the threshold with no card shown", async () => {
185
+ const { messages, currentToolUseId } = turnWithRounds(
186
+ TASK_PROGRESS_NUDGE_ROUND_THRESHOLD,
187
+ );
188
+ const ctx = makeCtx(
189
+ freshConversationId(),
190
+ currentResponse(currentToolUseId),
191
+ messages,
192
+ );
193
+ await postToolUse(ctx);
194
+ expect(ctx.additionalContext).toBe(TASK_PROGRESS_NUDGE_TEXT);
195
+ // tool result content is untouched
196
+ expect(ctx.toolResponse.content).toBe("result body");
197
+ });
198
+
199
+ test("never nudges when a task_progress card was already shown this turn", async () => {
200
+ const { messages, currentToolUseId } = turnWithRounds(
201
+ TASK_PROGRESS_NUDGE_ROUND_THRESHOLD + 2,
202
+ { showCardAtRound: 1 },
203
+ );
204
+ const ctx = makeCtx(
205
+ freshConversationId(),
206
+ currentResponse(currentToolUseId),
207
+ messages,
208
+ );
209
+ await postToolUse(ctx);
210
+ expect(ctx.additionalContext).toBeNull();
211
+ });
212
+
213
+ test("fires at most once per turn", async () => {
214
+ const conversationId = freshConversationId();
215
+ const first = turnWithRounds(TASK_PROGRESS_NUDGE_ROUND_THRESHOLD);
216
+ const ctx1 = makeCtx(
217
+ conversationId,
218
+ currentResponse(first.currentToolUseId),
219
+ first.messages,
220
+ );
221
+ await postToolUse(ctx1);
222
+ expect(ctx1.additionalContext).toBe(TASK_PROGRESS_NUDGE_TEXT);
223
+
224
+ // One more round, still no card — must not nudge again this turn.
225
+ const second = turnWithRounds(TASK_PROGRESS_NUDGE_ROUND_THRESHOLD + 1);
226
+ const ctx2 = makeCtx(
227
+ conversationId,
228
+ currentResponse(second.currentToolUseId),
229
+ second.messages,
230
+ );
231
+ await postToolUse(ctx2);
232
+ expect(ctx2.additionalContext).toBeNull();
233
+ });
234
+
235
+ test("resets across turns so a later multi-step turn nudges again", async () => {
236
+ const conversationId = freshConversationId();
237
+ const first = turnWithRounds(TASK_PROGRESS_NUDGE_ROUND_THRESHOLD);
238
+ const ctx1 = makeCtx(
239
+ conversationId,
240
+ currentResponse(first.currentToolUseId),
241
+ first.messages,
242
+ );
243
+ await postToolUse(ctx1);
244
+ expect(ctx1.additionalContext).toBe(TASK_PROGRESS_NUDGE_TEXT);
245
+
246
+ // A brand-new turn (counting restarts low) below threshold — resets state.
247
+ const lull = turnWithRounds(1);
248
+ const ctxLull = makeCtx(
249
+ conversationId,
250
+ currentResponse(lull.currentToolUseId),
251
+ lull.messages,
252
+ );
253
+ await postToolUse(ctxLull);
254
+ expect(ctxLull.additionalContext).toBeNull();
255
+
256
+ // Another multi-step turn — should nudge again.
257
+ const third = turnWithRounds(TASK_PROGRESS_NUDGE_ROUND_THRESHOLD);
258
+ const ctx3 = makeCtx(
259
+ conversationId,
260
+ currentResponse(third.currentToolUseId),
261
+ third.messages,
262
+ );
263
+ await postToolUse(ctx3);
264
+ expect(ctx3.additionalContext).toBe(TASK_PROGRESS_NUDGE_TEXT);
265
+ });
266
+
267
+ test("fires for weak models (Kimi, DeepSeek, MiniMax)", async () => {
268
+ for (const model of [
269
+ "moonshotai/kimi-k2.6",
270
+ "deepseek/deepseek-chat",
271
+ "accounts/fireworks/models/minimax-m3",
272
+ ]) {
273
+ const { messages, currentToolUseId } = turnWithRounds(
274
+ TASK_PROGRESS_NUDGE_ROUND_THRESHOLD,
275
+ );
276
+ const ctx = makeCtx(
277
+ freshConversationId(),
278
+ currentResponse(currentToolUseId),
279
+ messages,
280
+ model,
281
+ );
282
+ await postToolUse(ctx);
283
+ expect(ctx.additionalContext).toBe(TASK_PROGRESS_NUDGE_TEXT);
284
+ }
285
+ });
286
+
287
+ test("skips capable models (not in the weak-model set)", async () => {
288
+ for (const model of ["claude-opus-4-8", "gpt-5.5"]) {
289
+ const { messages, currentToolUseId } = turnWithRounds(
290
+ TASK_PROGRESS_NUDGE_ROUND_THRESHOLD + 2,
291
+ );
292
+ const ctx = makeCtx(
293
+ freshConversationId(),
294
+ currentResponse(currentToolUseId),
295
+ messages,
296
+ model,
297
+ );
298
+ await postToolUse(ctx);
299
+ expect(ctx.additionalContext).toBeNull();
300
+ }
301
+ });
302
+
303
+ test("skips non-mainAgent call sites", async () => {
304
+ mockConversation = () => ({
305
+ currentCallSite: "wake",
306
+ currentTurnChannelCapabilities: {
307
+ channel: "web",
308
+ supportsDynamicUi: true,
309
+ },
310
+ });
311
+ const { messages, currentToolUseId } = turnWithRounds(
312
+ TASK_PROGRESS_NUDGE_ROUND_THRESHOLD,
313
+ );
314
+ const ctx = makeCtx(
315
+ freshConversationId(),
316
+ currentResponse(currentToolUseId),
317
+ messages,
318
+ );
319
+ await postToolUse(ctx);
320
+ expect(ctx.additionalContext).toBeNull();
321
+ });
322
+
323
+ test("skips clients without dynamic UI support", async () => {
324
+ mockConversation = () => ({
325
+ currentCallSite: "mainAgent",
326
+ currentTurnChannelCapabilities: {
327
+ channel: "telegram",
328
+ supportsDynamicUi: false,
329
+ },
330
+ });
331
+ const { messages, currentToolUseId } = turnWithRounds(
332
+ TASK_PROGRESS_NUDGE_ROUND_THRESHOLD,
333
+ );
334
+ const ctx = makeCtx(
335
+ freshConversationId(),
336
+ currentResponse(currentToolUseId),
337
+ messages,
338
+ );
339
+ await postToolUse(ctx);
340
+ expect(ctx.additionalContext).toBeNull();
341
+ });
342
+
343
+ test("skips subagent conversations", async () => {
344
+ mockParentInfo = () => ({ parentConversationId: "parent-1" });
345
+ const { messages, currentToolUseId } = turnWithRounds(
346
+ TASK_PROGRESS_NUDGE_ROUND_THRESHOLD,
347
+ );
348
+ const ctx = makeCtx(
349
+ freshConversationId(),
350
+ currentResponse(currentToolUseId),
351
+ messages,
352
+ );
353
+ await postToolUse(ctx);
354
+ expect(ctx.additionalContext).toBeNull();
355
+ });
356
+
357
+ test("appends to existing additionalContext rather than overwriting", async () => {
358
+ const { messages, currentToolUseId } = turnWithRounds(
359
+ TASK_PROGRESS_NUDGE_ROUND_THRESHOLD,
360
+ );
361
+ const ctx = makeCtx(
362
+ freshConversationId(),
363
+ currentResponse(currentToolUseId),
364
+ messages,
365
+ );
366
+ ctx.additionalContext = "<system_notice>prior coaching</system_notice>";
367
+ await postToolUse(ctx);
368
+ expect(ctx.additionalContext).toBe(
369
+ `<system_notice>prior coaching</system_notice>\n${TASK_PROGRESS_NUDGE_TEXT}`,
370
+ );
371
+ });
372
+ });
@@ -44,6 +44,22 @@ mock.module("../runtime/background-job-runner.js", () => ({
44
44
  },
45
45
  }));
46
46
 
47
+ // Capture workflow-mode dispatch. The scheduler's `workflow` branch calls
48
+ // `getWorkflowRunManager().start(...)`; we stub the singleton so each trigger
49
+ // is observable without spinning up the real engine.
50
+ const workflowStartCalls: Array<Record<string, unknown>> = [];
51
+ let workflowStartImpl: (opts: Record<string, unknown>) => { runId: string } = (
52
+ opts,
53
+ ) => {
54
+ workflowStartCalls.push(opts);
55
+ return { runId: "wf-run-1" };
56
+ };
57
+ mock.module("../workflows/run-manager.js", () => ({
58
+ getWorkflowRunManager: () => ({
59
+ start: (opts: Record<string, unknown>) => workflowStartImpl(opts),
60
+ }),
61
+ }));
62
+
47
63
  // Capture `emitNotificationSignal` calls so tests can assert that scheduled
48
64
  // task failures surface via the notification pipeline (home feed + native).
49
65
  const emitNotificationCalls: Array<Record<string, unknown>> = [];
@@ -60,11 +76,21 @@ mock.module("../notifications/emit-signal.js", () => ({
60
76
  },
61
77
  }));
62
78
 
79
+ // Control the tool-registry readiness gate the scheduler checks before launching
80
+ // workflow schedules. Default ready=true so the existing workflow-mode tests are
81
+ // unaffected; one test flips it to exercise the boot-time deferral path. Only the
82
+ // scheduler consumes registry in this test's graph, so a minimal mock suffices.
83
+ let coreToolsReady = true;
84
+ mock.module("../tools/registry.js", () => ({
85
+ areCoreToolsInitialized: () => coreToolsReady,
86
+ }));
87
+
63
88
  import { getDb } from "../memory/db-connection.js";
64
89
  import { initializeDb } from "../memory/db-init.js";
65
90
  import { recordUsageEvent } from "../memory/llm-usage-store.js";
66
91
  import {
67
92
  createSchedule,
93
+ deferClaimedSchedule,
68
94
  getSchedule,
69
95
  getScheduleRuns,
70
96
  } from "../schedule/schedule-store.js";
@@ -460,3 +486,276 @@ describe("scheduler run_task detection", () => {
460
486
  });
461
487
  });
462
488
  });
489
+
490
+ // ── Workflow mode dispatch ──────────────────────────────────────────
491
+
492
+ describe("scheduler workflow mode", () => {
493
+ beforeEach(() => {
494
+ const db = getDb();
495
+ db.run("DELETE FROM cron_runs");
496
+ db.run("DELETE FROM cron_jobs");
497
+ workflowStartCalls.length = 0;
498
+ workflowStartImpl = (opts) => {
499
+ workflowStartCalls.push(opts);
500
+ return { runId: "wf-run-1" };
501
+ };
502
+ coreToolsReady = true;
503
+ });
504
+
505
+ test("a due workflow-mode job triggers the run manager with name/args", async () => {
506
+ const schedule = createSchedule({
507
+ name: "Triage inbox",
508
+ cronExpression: "0 9 * * *",
509
+ message: "triage",
510
+ syntax: "cron",
511
+ mode: "workflow",
512
+ workflowName: "triage-inbox",
513
+ workflowArgs: { folder: "primary" },
514
+ wakeConversationId: "conv-origin",
515
+ });
516
+ forceScheduleDue(schedule.id);
517
+
518
+ const result = await runScheduleDueWorkOnce(
519
+ async () => {},
520
+ () => {},
521
+ );
522
+
523
+ expect(result.completed).toBe(1);
524
+ expect(result.failed).toBe(0);
525
+ expect(workflowStartCalls).toHaveLength(1);
526
+ expect(workflowStartCalls[0]).toMatchObject({
527
+ name: "triage-inbox",
528
+ args: { folder: "primary" },
529
+ conversationId: "conv-origin",
530
+ manifest: { tools: [], hostFunctions: [], persona: false },
531
+ });
532
+
533
+ const runs = getScheduleRuns(schedule.id);
534
+ expect(runs).toHaveLength(1);
535
+ expect(runs[0].status).toBe("ok");
536
+ });
537
+
538
+ test("fires with the schedule's stored side-effecting manifest", async () => {
539
+ const schedule = createSchedule({
540
+ name: "Side-effecting workflow",
541
+ cronExpression: "0 9 * * *",
542
+ message: "go",
543
+ syntax: "cron",
544
+ mode: "workflow",
545
+ workflowName: "send-report",
546
+ capabilities: {
547
+ tools: ["file_write"],
548
+ hostFunctions: ["notify"],
549
+ persona: true,
550
+ },
551
+ });
552
+ forceScheduleDue(schedule.id);
553
+
554
+ await runScheduleDueWorkOnce(
555
+ async () => {},
556
+ () => {},
557
+ );
558
+
559
+ expect(workflowStartCalls).toHaveLength(1);
560
+ expect(workflowStartCalls[0]).toMatchObject({
561
+ manifest: {
562
+ tools: ["file_write"],
563
+ hostFunctions: ["notify"],
564
+ persona: true,
565
+ },
566
+ });
567
+ });
568
+
569
+ test("a legacy schedule (null capabilities) fires with the read-only baseline", async () => {
570
+ const schedule = createSchedule({
571
+ name: "Legacy workflow",
572
+ cronExpression: "0 9 * * *",
573
+ message: "go",
574
+ syntax: "cron",
575
+ mode: "workflow",
576
+ workflowName: "digest",
577
+ capabilities: null,
578
+ });
579
+ forceScheduleDue(schedule.id);
580
+
581
+ await runScheduleDueWorkOnce(
582
+ async () => {},
583
+ () => {},
584
+ );
585
+
586
+ expect(workflowStartCalls).toHaveLength(1);
587
+ expect(workflowStartCalls[0]).toMatchObject({
588
+ manifest: { tools: [], hostFunctions: [], persona: false },
589
+ });
590
+ });
591
+
592
+ test("falls back to createdFromConversationId for the completion wake", async () => {
593
+ // Workflow schedules created via schedule_create store the originating
594
+ // conversation as createdFromConversationId and leave wakeConversationId
595
+ // unset; without the fallback the completion summary lands nowhere.
596
+ const schedule = createSchedule({
597
+ name: "Morning digest",
598
+ cronExpression: "0 9 * * *",
599
+ message: "",
600
+ syntax: "cron",
601
+ mode: "workflow",
602
+ workflowName: "digest",
603
+ createdFromConversationId: "conv-creator",
604
+ });
605
+ forceScheduleDue(schedule.id);
606
+
607
+ await runScheduleDueWorkOnce(
608
+ async () => {},
609
+ () => {},
610
+ );
611
+
612
+ expect(workflowStartCalls).toHaveLength(1);
613
+ expect(workflowStartCalls[0]).toMatchObject({
614
+ conversationId: "conv-creator",
615
+ });
616
+ });
617
+
618
+ test("prefers an explicit wakeConversationId over createdFromConversationId", async () => {
619
+ const schedule = createSchedule({
620
+ name: "Both set",
621
+ cronExpression: "0 9 * * *",
622
+ message: "",
623
+ syntax: "cron",
624
+ mode: "workflow",
625
+ workflowName: "digest",
626
+ wakeConversationId: "conv-wake",
627
+ createdFromConversationId: "conv-creator",
628
+ });
629
+ forceScheduleDue(schedule.id);
630
+
631
+ await runScheduleDueWorkOnce(
632
+ async () => {},
633
+ () => {},
634
+ );
635
+
636
+ expect(workflowStartCalls[0]).toMatchObject({
637
+ conversationId: "conv-wake",
638
+ });
639
+ });
640
+
641
+ test("defaults workflowArgs to {} when unset", async () => {
642
+ const schedule = createSchedule({
643
+ name: "No-args workflow",
644
+ cronExpression: "0 9 * * *",
645
+ message: "go",
646
+ syntax: "cron",
647
+ mode: "workflow",
648
+ workflowName: "nightly",
649
+ });
650
+ forceScheduleDue(schedule.id);
651
+
652
+ await runScheduleDueWorkOnce(
653
+ async () => {},
654
+ () => {},
655
+ );
656
+
657
+ expect(workflowStartCalls).toHaveLength(1);
658
+ expect(workflowStartCalls[0].args).toEqual({});
659
+ });
660
+
661
+ test("records an error and skips when start() throws", async () => {
662
+ workflowStartImpl = () => {
663
+ throw new Error("workflows disabled");
664
+ };
665
+ const schedule = createSchedule({
666
+ name: "Failing workflow",
667
+ cronExpression: "0 9 * * *",
668
+ message: "go",
669
+ syntax: "cron",
670
+ mode: "workflow",
671
+ workflowName: "broken",
672
+ });
673
+ forceScheduleDue(schedule.id);
674
+
675
+ const result = await runScheduleDueWorkOnce(
676
+ async () => {},
677
+ () => {},
678
+ );
679
+
680
+ expect(result.failed).toBe(1);
681
+ const runs = getScheduleRuns(schedule.id);
682
+ expect(runs[0].status).toBe("error");
683
+ expect(runs[0].error).toContain("workflows disabled");
684
+ });
685
+
686
+ test("skips a workflow-mode job with no workflowName", async () => {
687
+ const schedule = createSchedule({
688
+ name: "Nameless workflow",
689
+ cronExpression: "0 9 * * *",
690
+ message: "go",
691
+ syntax: "cron",
692
+ mode: "workflow",
693
+ });
694
+ forceScheduleDue(schedule.id);
695
+
696
+ const result = await runScheduleDueWorkOnce(
697
+ async () => {},
698
+ () => {},
699
+ );
700
+
701
+ expect(result.skipped).toBe(1);
702
+ expect(workflowStartCalls).toHaveLength(0);
703
+ });
704
+
705
+ test("defers a due workflow job until tools are registered, then fires it", async () => {
706
+ coreToolsReady = false;
707
+ const schedule = createSchedule({
708
+ name: "Boot workflow",
709
+ cronExpression: "0 9 * * *",
710
+ message: "go",
711
+ syntax: "cron",
712
+ mode: "workflow",
713
+ workflowName: "triage-inbox",
714
+ });
715
+ forceScheduleDue(schedule.id);
716
+
717
+ // Tools not yet registered: the run must be deferred, not launched with an
718
+ // empty baseline. No run-manager start, no run record, marked skipped.
719
+ const result = await runScheduleDueWorkOnce(
720
+ async () => {},
721
+ () => {},
722
+ );
723
+ expect(workflowStartCalls).toHaveLength(0);
724
+ expect(result.skipped).toBe(1);
725
+ expect(getScheduleRuns(schedule.id)).toHaveLength(0);
726
+
727
+ // Re-armed to due (not consumed): once tools are ready, a later tick fires
728
+ // it with the full baseline.
729
+ coreToolsReady = true;
730
+ await runScheduleDueWorkOnce(
731
+ async () => {},
732
+ () => {},
733
+ );
734
+ expect(workflowStartCalls).toHaveLength(1);
735
+ expect(workflowStartCalls[0]).toMatchObject({ name: "triage-inbox" });
736
+ });
737
+
738
+ test("deferring a final/disabled occurrence re-enables it so it isn't lost", async () => {
739
+ // claimDueSchedules disables a row whose claimed occurrence was its last
740
+ // (one-shot / exhausted finite RRULE). The due-claim query requires
741
+ // enabled=true, so a deferred final occurrence must be re-enabled or it is
742
+ // never re-claimed. Simulate that disabled-on-claim state, then defer.
743
+ const schedule = createSchedule({
744
+ name: "Final workflow occurrence",
745
+ cronExpression: "0 9 * * *",
746
+ message: "",
747
+ syntax: "cron",
748
+ mode: "workflow",
749
+ workflowName: "digest",
750
+ enabled: false, // as claimDueSchedules leaves a last occurrence
751
+ });
752
+ expect(getSchedule(schedule.id)?.enabled).toBe(false);
753
+
754
+ deferClaimedSchedule(schedule.id);
755
+
756
+ const after = getSchedule(schedule.id);
757
+ expect(after?.enabled).toBe(true);
758
+ expect(after?.status).toBe("active");
759
+ expect(after?.nextRunAt).toBeLessThanOrEqual(Date.now());
760
+ });
761
+ });