@vellumai/assistant 0.8.12 → 0.9.0-staging.1

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 (414) 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/openapi.yaml +6935 -5545
  15. package/package.json +8 -4
  16. package/scripts/generate-openapi.ts +66 -114
  17. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  18. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  19. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  20. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  21. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  22. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  23. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  24. package/src/__tests__/approval-cascade.test.ts +0 -5
  25. package/src/__tests__/approval-routes-http.test.ts +91 -0
  26. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  27. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  28. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  29. package/src/__tests__/compaction-events.test.ts +63 -7
  30. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  32. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  33. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  35. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  36. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  37. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  39. package/src/__tests__/conversation-queue.test.ts +0 -5
  40. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  41. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  42. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  43. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  44. package/src/__tests__/conversation-usage.test.ts +2 -0
  45. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  46. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  47. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  48. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  49. package/src/__tests__/credential-broker.test.ts +1 -1
  50. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  52. package/src/__tests__/credential-vault.test.ts +37 -0
  53. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  54. package/src/__tests__/dynamic-page-surface.test.ts +125 -0
  55. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  56. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  57. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  58. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  59. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  60. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  61. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  62. package/src/__tests__/llm-resolver.test.ts +115 -0
  63. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  64. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  65. package/src/__tests__/media-generate-image.test.ts +20 -9
  66. package/src/__tests__/model-intents.test.ts +1 -1
  67. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  68. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  69. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  70. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  71. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  72. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  73. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  74. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  75. package/src/__tests__/schedule-routes.test.ts +257 -4
  76. package/src/__tests__/schedule-store.test.ts +60 -0
  77. package/src/__tests__/schedule-tools.test.ts +247 -2
  78. package/src/__tests__/skill-execute-input.test.ts +85 -0
  79. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  80. package/src/__tests__/skills.test.ts +3 -3
  81. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  82. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  83. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  84. package/src/__tests__/system-prompt.test.ts +1 -1
  85. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  86. package/src/__tests__/task-scheduler.test.ts +299 -0
  87. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  88. package/src/__tests__/tool-result-spool.test.ts +3 -1
  89. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  90. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  91. package/src/agent/compaction-circuit.ts +11 -0
  92. package/src/agent/loop.ts +181 -12
  93. package/src/api/constants/call-sites.ts +12 -0
  94. package/src/api/events/assistant-thinking-delta.ts +10 -0
  95. package/src/api/events/usage-update.ts +7 -0
  96. package/src/api/index.ts +4 -1
  97. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  98. package/src/approvals/approval-primitive.ts +2 -2
  99. package/src/background-wake/background-wake-routes.test.ts +5 -2
  100. package/src/bundler/compiler-tools.ts +1 -1
  101. package/src/calls/call-domain.ts +1 -1
  102. package/src/calls/guardian-action-sweep.ts +16 -93
  103. package/src/cli/AGENTS.md +4 -0
  104. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  105. package/src/cli/commands/credentials.ts +28 -24
  106. package/src/cli/commands/image-generation.ts +23 -9
  107. package/src/cli/commands/notifications.ts +1 -1
  108. package/src/cli/commands/plugins.ts +89 -46
  109. package/src/cli/commands/schedules.ts +384 -11
  110. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  111. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  112. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  113. package/src/cli/lib/inspect-plugin.ts +62 -1
  114. package/src/cli/lib/install-from-github.ts +52 -5
  115. package/src/cli/lib/upgrade-plugin.ts +18 -0
  116. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  117. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  118. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  119. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  120. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  121. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  122. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  123. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  124. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  125. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  126. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  127. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  128. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  129. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  130. package/src/config/bundled-tool-registry.ts +14 -2
  131. package/src/config/call-site-defaults.ts +5 -0
  132. package/src/config/feature-flag-registry.json +12 -4
  133. package/src/config/llm-context-resolution.ts +8 -0
  134. package/src/config/llm-resolver.ts +30 -0
  135. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  136. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  137. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  138. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  139. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  140. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  141. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  142. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  143. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  144. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  145. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  146. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  147. package/src/config/schema.ts +2 -0
  148. package/src/config/schemas/call-site-catalog.ts +7 -0
  149. package/src/config/schemas/heartbeat.ts +4 -1
  150. package/src/config/schemas/llm.ts +33 -26
  151. package/src/config/schemas/memory-retrospective.ts +19 -0
  152. package/src/config/schemas/platform.ts +8 -0
  153. package/src/config/schemas/services.ts +5 -2
  154. package/src/config/schemas/workflows.ts +42 -0
  155. package/src/config/skills.ts +3 -3
  156. package/src/context/compactor.ts +273 -39
  157. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  158. package/src/context/tool-result-spool.ts +12 -17
  159. package/src/credential-execution/executable-discovery.ts +1 -1
  160. package/src/credential-execution/process-manager.ts +37 -3
  161. package/src/credential-execution/prompted-credential.ts +205 -0
  162. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  163. package/src/daemon/conversation-process.ts +11 -2
  164. package/src/daemon/conversation-surfaces.ts +75 -0
  165. package/src/daemon/conversation-tool-setup.ts +103 -26
  166. package/src/daemon/conversation-usage.ts +2 -0
  167. package/src/daemon/conversation.ts +115 -11
  168. package/src/daemon/handlers/shared.ts +26 -14
  169. package/src/daemon/host-cu-proxy.ts +15 -12
  170. package/src/daemon/host-file-proxy.ts +15 -12
  171. package/src/daemon/host-transfer-proxy.ts +30 -24
  172. package/src/daemon/lifecycle.ts +40 -3
  173. package/src/daemon/message-protocol.ts +3 -0
  174. package/src/daemon/message-types/messages.ts +2 -10
  175. package/src/daemon/message-types/workflows.ts +49 -0
  176. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  177. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  178. package/src/daemon/process-message.ts +6 -0
  179. package/src/daemon/tool-setup-types.ts +57 -0
  180. package/src/daemon/wake-conversation-ops.ts +18 -0
  181. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  182. package/src/home/feed-types.ts +1 -1
  183. package/src/ipc/gateway-flag-listener.ts +28 -6
  184. package/src/mcp/mcp-auth-state.ts +8 -20
  185. package/src/media/__tests__/image-models.test.ts +57 -0
  186. package/src/media/image-models.ts +66 -0
  187. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  188. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  189. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  190. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  191. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  192. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  193. package/src/memory/auto-analysis-enqueue.ts +14 -1
  194. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  195. package/src/memory/conversation-crud.ts +9 -2
  196. package/src/memory/conversation-disk-view.ts +1 -1
  197. package/src/memory/conversation-queries.ts +22 -7
  198. package/src/memory/db-init.ts +20 -0
  199. package/src/memory/db-maintenance.ts +16 -0
  200. package/src/memory/embedding-runtime-manager.ts +1 -1
  201. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  202. package/src/memory/llm-request-log-source-local.ts +19 -1
  203. package/src/memory/llm-request-log-source.ts +35 -6
  204. package/src/memory/llm-request-log-store.ts +90 -2
  205. package/src/memory/memory-retrospective-constants.ts +9 -0
  206. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  207. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  208. package/src/memory/memory-retrospective-job.ts +500 -208
  209. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  210. package/src/memory/memory-retrospective-state.ts +85 -2
  211. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  212. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  213. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  214. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  215. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  216. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  217. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  218. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  219. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  220. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  221. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  222. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  223. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  224. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  225. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  226. package/src/memory/migrations/index.ts +10 -0
  227. package/src/memory/preloaded-apps.ts +116 -0
  228. package/src/memory/schema/infrastructure.ts +4 -0
  229. package/src/memory/schema/memory-core.ts +4 -0
  230. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  231. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  232. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  233. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  234. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  235. package/src/memory/v2/page-store.ts +4 -3
  236. package/src/memory/v2/qdrant.ts +42 -3
  237. package/src/memory/v2/types.ts +16 -10
  238. package/src/messaging/draft-store.ts +1 -1
  239. package/src/notifications/access-request-copy.ts +200 -113
  240. package/src/notifications/adapters/slack.ts +250 -111
  241. package/src/notifications/adapters/telegram.ts +7 -44
  242. package/src/notifications/approval-card-builder.ts +93 -0
  243. package/src/notifications/broadcaster.ts +74 -0
  244. package/src/notifications/conversation-pairing.ts +8 -6
  245. package/src/notifications/copy-composer.ts +32 -26
  246. package/src/notifications/decision-engine.ts +59 -7
  247. package/src/notifications/guardian-question-mode.ts +145 -155
  248. package/src/notifications/home-feed-side-effect.ts +1 -1
  249. package/src/notifications/notification-utils.ts +66 -0
  250. package/src/notifications/signal.ts +6 -0
  251. package/src/notifications/tool-approval-copy.ts +142 -0
  252. package/src/notifications/types.ts +19 -0
  253. package/src/permissions/threshold.ts +11 -0
  254. package/src/plugin-api/types.ts +16 -4
  255. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  256. package/src/plugins/defaults/index.ts +46 -0
  257. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  258. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  259. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  260. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  261. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  262. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  263. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  264. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  265. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  266. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  267. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  268. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  269. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  270. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  271. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  272. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  273. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  274. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  275. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  276. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  277. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  278. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  279. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  280. package/src/prompts/normalize-onboarding.ts +2 -0
  281. package/src/prompts/persona-resolver.ts +3 -0
  282. package/src/prompts/system-prompt.ts +51 -2
  283. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  284. package/src/prompts/templates/system-sections.ts +8 -3
  285. package/src/providers/call-site-routing.ts +6 -3
  286. package/src/providers/fireworks/client.ts +3 -0
  287. package/src/providers/inference/auth.ts +52 -46
  288. package/src/providers/model-intents.ts +2 -2
  289. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  290. package/src/providers/openai/chat-completions-provider.ts +47 -9
  291. package/src/providers/openai/coerce-object-args.ts +104 -0
  292. package/src/providers/retry.ts +8 -5
  293. package/src/providers/types.ts +10 -0
  294. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  295. package/src/runtime/access-request-helper.ts +16 -9
  296. package/src/runtime/agent-wake.ts +302 -51
  297. package/src/runtime/assistant-stream-state.ts +141 -8
  298. package/src/runtime/background-job-runner.ts +9 -0
  299. package/src/runtime/channel-approval-types.ts +1 -0
  300. package/src/runtime/guardian-action-message-composer.ts +0 -54
  301. package/src/runtime/http-server.ts +6 -14
  302. package/src/runtime/http-types.ts +0 -1
  303. package/src/runtime/message-composer-types.ts +0 -9
  304. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  305. package/src/runtime/middleware/auth.ts +27 -3
  306. package/src/runtime/middleware/rate-limiter.ts +28 -1
  307. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  308. package/src/runtime/pending-interactions.ts +20 -1
  309. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  310. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  311. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  312. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  313. package/src/runtime/routes/approval-routes.ts +35 -8
  314. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  315. package/src/runtime/routes/btw-routes.ts +37 -1
  316. package/src/runtime/routes/channel-route-shared.ts +3 -1
  317. package/src/runtime/routes/consolidation-routes.ts +17 -13
  318. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  319. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  321. package/src/runtime/routes/conversation-routes.ts +15 -1
  322. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  323. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  324. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  325. package/src/runtime/routes/image-generation-routes.ts +20 -2
  326. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +26 -1
  327. package/src/runtime/routes/index.ts +6 -0
  328. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  329. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  330. package/src/runtime/routes/migration-routes.ts +5 -9
  331. package/src/runtime/routes/plugins-routes.ts +26 -0
  332. package/src/runtime/routes/ps-routes.ts +10 -8
  333. package/src/runtime/routes/retrospective-routes.ts +235 -0
  334. package/src/runtime/routes/runs-pagination.ts +75 -0
  335. package/src/runtime/routes/schedule-routes.ts +367 -59
  336. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  337. package/src/runtime/routes/surface-action-routes.ts +84 -4
  338. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  339. package/src/runtime/routes/workflow-routes.ts +363 -0
  340. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  341. package/src/runtime/routes/workspace-routes.ts +26 -1
  342. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  343. package/src/runtime/services/analyze-conversation.ts +26 -13
  344. package/src/schedule/inference-profile.ts +28 -0
  345. package/src/schedule/schedule-store.ts +122 -4
  346. package/src/schedule/scheduler-types.ts +6 -0
  347. package/src/schedule/scheduler.ts +96 -0
  348. package/src/security/secret-allowlist.ts +1 -1
  349. package/src/skills/path-classifier.ts +1 -1
  350. package/src/tools/browser/browser-execution.ts +9 -11
  351. package/src/tools/credentials/broker.ts +4 -4
  352. package/src/tools/credentials/vault.ts +26 -137
  353. package/src/tools/executor.ts +69 -0
  354. package/src/tools/flag-gated-tools.test.ts +76 -0
  355. package/src/tools/permission-checker.ts +8 -1
  356. package/src/tools/registry.ts +51 -0
  357. package/src/tools/schedule/create.ts +77 -1
  358. package/src/tools/schedule/list.ts +1 -0
  359. package/src/tools/schedule/update.ts +73 -1
  360. package/src/tools/skills/execute.ts +56 -0
  361. package/src/tools/terminal/shell.ts +1 -1
  362. package/src/tools/ui-surface/definitions.ts +48 -2
  363. package/src/tools/workflows/manage-workflows.ts +183 -0
  364. package/src/tools/workflows/run-workflow.test.ts +442 -0
  365. package/src/tools/workflows/run-workflow.ts +88 -0
  366. package/src/types/onboarding-context.ts +2 -0
  367. package/src/usage/attribution.ts +24 -0
  368. package/src/util/canonicalize-identity.ts +12 -3
  369. package/src/util/platform.ts +17 -17
  370. package/src/watcher/__tests__/engine.test.ts +24 -0
  371. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  372. package/src/watcher/engine.ts +7 -0
  373. package/src/watcher/telemetry.ts +74 -0
  374. package/src/workflows/capabilities.test.ts +365 -0
  375. package/src/workflows/capabilities.ts +359 -0
  376. package/src/workflows/deterministic-stringify.ts +27 -0
  377. package/src/workflows/engine-integration.test.ts +656 -0
  378. package/src/workflows/engine.test.ts +1144 -0
  379. package/src/workflows/engine.ts +1078 -0
  380. package/src/workflows/fanout-load.test.ts +168 -0
  381. package/src/workflows/journal-store.test.ts +369 -0
  382. package/src/workflows/journal-store.ts +470 -0
  383. package/src/workflows/leaf-runner.test.ts +704 -0
  384. package/src/workflows/leaf-runner.ts +589 -0
  385. package/src/workflows/library.test.ts +134 -0
  386. package/src/workflows/library.ts +124 -0
  387. package/src/workflows/run-manager.test.ts +711 -0
  388. package/src/workflows/run-manager.ts +593 -0
  389. package/src/workflows/sandbox-escape.test.ts +339 -0
  390. package/src/workflows/sandbox.test.ts +251 -0
  391. package/src/workflows/sandbox.ts +447 -0
  392. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  393. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  394. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  395. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  396. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  397. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  398. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  399. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  400. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  401. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  402. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  403. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  404. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  405. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  406. package/src/workspace/migrations/registry.ts +6 -0
  407. package/src/workspace/migrations/runner.ts +1 -1
  408. package/tsconfig.json +1 -1
  409. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  410. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  411. package/src/__tests__/guardian-action-store.test.ts +0 -106
  412. package/src/daemon/guardian-action-generators.ts +0 -71
  413. package/src/memory/guardian-action-store.ts +0 -484
  414. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -50,7 +50,11 @@ describe("schedule_syntax column migration", () => {
50
50
  reuse_conversation INTEGER NOT NULL DEFAULT 0,
51
51
  script TEXT,
52
52
  wake_conversation_id TEXT,
53
+ workflow_name TEXT,
54
+ workflow_args_json TEXT,
53
55
  timeout_ms INTEGER,
56
+ inference_profile TEXT,
57
+ capabilities_json TEXT,
54
58
  description TEXT NOT NULL DEFAULT '',
55
59
  created_at INTEGER NOT NULL,
56
60
  updated_at INTEGER NOT NULL
@@ -138,6 +142,16 @@ describe("schedule_syntax column migration", () => {
138
142
  } catch {
139
143
  /* already exists */
140
144
  }
145
+ try {
146
+ raw.exec(`ALTER TABLE cron_jobs ADD COLUMN workflow_name TEXT`);
147
+ } catch {
148
+ /* already exists */
149
+ }
150
+ try {
151
+ raw.exec(`ALTER TABLE cron_jobs ADD COLUMN workflow_args_json TEXT`);
152
+ } catch {
153
+ /* already exists */
154
+ }
141
155
  migrateScheduleSourceConversation(db);
142
156
  migrateScheduleDescription(db);
143
157
 
@@ -203,6 +217,16 @@ describe("schedule_syntax column migration", () => {
203
217
  } catch {
204
218
  /* ok */
205
219
  }
220
+ try {
221
+ raw.exec(`ALTER TABLE cron_jobs ADD COLUMN workflow_name TEXT`);
222
+ } catch {
223
+ /* ok */
224
+ }
225
+ try {
226
+ raw.exec(`ALTER TABLE cron_jobs ADD COLUMN workflow_args_json TEXT`);
227
+ } catch {
228
+ /* ok */
229
+ }
206
230
  migrateScheduleSourceConversation(db);
207
231
  migrateScheduleSourceConversation(db);
208
232
  migrateScheduleDescription(db);
@@ -125,6 +125,56 @@ describe("ui_show dynamic_page app substitute guard", () => {
125
125
  expect(proxied).toBe(false);
126
126
  });
127
127
 
128
+ test("rejects dynamic_page with empty data and does not proxy", async () => {
129
+ let proxied = false;
130
+
131
+ const result = await uiShowTool.execute(
132
+ {
133
+ surface_type: "dynamic_page",
134
+ title: "SKILL.md — elevenlabs-tts",
135
+ activity: "Reopening SKILL.md with rendered content",
136
+ data: {},
137
+ },
138
+ {
139
+ conversationId: "conversation-123",
140
+ workingDir: "/tmp",
141
+ trustClass: "guardian",
142
+ proxyToolResolver: async () => {
143
+ proxied = true;
144
+ return { content: "proxied", isError: false };
145
+ },
146
+ },
147
+ );
148
+
149
+ expect(result.isError).toBe(true);
150
+ expect(result.content).toContain("data.html");
151
+ expect(proxied).toBe(false);
152
+ });
153
+
154
+ test("rejects dynamic_page with whitespace-only html and does not proxy", async () => {
155
+ let proxied = false;
156
+
157
+ const result = await uiShowTool.execute(
158
+ {
159
+ surface_type: "dynamic_page",
160
+ title: "Blank",
161
+ data: { html: " \n " },
162
+ },
163
+ {
164
+ conversationId: "conversation-123",
165
+ workingDir: "/tmp",
166
+ trustClass: "guardian",
167
+ proxyToolResolver: async () => {
168
+ proxied = true;
169
+ return { content: "proxied", isError: false };
170
+ },
171
+ },
172
+ );
173
+
174
+ expect(result.isError).toBe(true);
175
+ expect(proxied).toBe(false);
176
+ });
177
+
128
178
  test("allows transient non-app dynamic_page surfaces", async () => {
129
179
  let proxied = false;
130
180
 
@@ -156,6 +206,81 @@ describe("ui_show dynamic_page app substitute guard", () => {
156
206
  });
157
207
  });
158
208
 
209
+ // ---------------------------------------------------------------------------
210
+ // task_progress ui_show appends the update hint to its return value
211
+ // ---------------------------------------------------------------------------
212
+
213
+ describe("ui_show task_progress update hint", () => {
214
+ const ctx = {
215
+ conversationId: "conversation-123",
216
+ workingDir: "/tmp",
217
+ trustClass: "guardian" as const,
218
+ proxyToolResolver: async () => ({
219
+ content: "Surface displayed (surface_id: surf-1).",
220
+ isError: false,
221
+ }),
222
+ };
223
+
224
+ test("appends the ui_update hint after a task_progress card is shown", async () => {
225
+ const result = await uiShowTool.execute(
226
+ {
227
+ surface_type: "card",
228
+ template: "task_progress",
229
+ templateData: { status: "in_progress", steps: [] },
230
+ },
231
+ ctx,
232
+ );
233
+
234
+ expect(result.isError).toBe(false);
235
+ expect(result.content).toContain("Surface displayed (surface_id: surf-1).");
236
+ expect(result.content).toContain("call ui_update with this surface_id");
237
+ });
238
+
239
+ test("recognizes task_progress nested under data", async () => {
240
+ const result = await uiShowTool.execute(
241
+ {
242
+ surface_type: "card",
243
+ data: {
244
+ template: "task_progress",
245
+ templateData: { status: "in_progress" },
246
+ },
247
+ },
248
+ ctx,
249
+ );
250
+
251
+ expect(result.content).toContain("call ui_update with this surface_id");
252
+ });
253
+
254
+ test("does not append the hint for a non-task_progress card", async () => {
255
+ const result = await uiShowTool.execute(
256
+ { surface_type: "card", data: { title: "Plain", body: "hi" } },
257
+ ctx,
258
+ );
259
+
260
+ expect(result.content).toBe("Surface displayed (surface_id: surf-1).");
261
+ });
262
+
263
+ test("does not append the hint when the surface call errors", async () => {
264
+ const result = await uiShowTool.execute(
265
+ {
266
+ surface_type: "card",
267
+ template: "task_progress",
268
+ templateData: { status: "in_progress" },
269
+ },
270
+ {
271
+ ...ctx,
272
+ proxyToolResolver: async () => ({
273
+ content: "blocked on this channel",
274
+ isError: true,
275
+ }),
276
+ },
277
+ );
278
+
279
+ expect(result.isError).toBe(true);
280
+ expect(result.content).toBe("blocked on this channel");
281
+ });
282
+ });
283
+
159
284
  // ---------------------------------------------------------------------------
160
285
  // UiSurfaceShowDynamicPage structure
161
286
  // ---------------------------------------------------------------------------
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Unit tests for the empty-state greeting cache
3
+ * (runtime/routes/empty-state-greeting-cache.ts).
4
+ *
5
+ * Validates TTL round-tripping, expiry, and the TTL=0 "always regenerate"
6
+ * behavior that disables caching entirely.
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Mocks — defined before importing the module under test
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const checkpointStore = new Map<string, string>();
16
+
17
+ mock.module("../memory/checkpoints.js", () => ({
18
+ getMemoryCheckpoint: (key: string) => checkpointStore.get(key) ?? null,
19
+ setMemoryCheckpoint: (key: string, value: string) => {
20
+ checkpointStore.set(key, value);
21
+ },
22
+ }));
23
+
24
+ let cacheTtlMs = 4 * 60 * 60 * 1000;
25
+
26
+ mock.module("../config/loader.js", () => ({
27
+ getConfig: () => ({ ui: { emptyStateGreetingCacheTtlMs: cacheTtlMs } }),
28
+ }));
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Imports (after mocks)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ import {
35
+ getCachedEmptyStateGreeting,
36
+ setCachedEmptyStateGreeting,
37
+ } from "../runtime/routes/empty-state-greeting-cache.js";
38
+
39
+ const TIMESTAMP_KEY = "empty_state:greeting:cached_at";
40
+
41
+ beforeEach(() => {
42
+ cacheTtlMs = 4 * 60 * 60 * 1000;
43
+ });
44
+
45
+ afterEach(() => {
46
+ checkpointStore.clear();
47
+ });
48
+
49
+ describe("empty-state greeting cache", () => {
50
+ test("returns null when the cache is empty", () => {
51
+ expect(getCachedEmptyStateGreeting()).toBeNull();
52
+ });
53
+
54
+ test("round-trips set then get within the TTL", () => {
55
+ setCachedEmptyStateGreeting("hey there");
56
+ expect(getCachedEmptyStateGreeting()).toBe("hey there");
57
+ });
58
+
59
+ test("returns null once the TTL is exceeded", () => {
60
+ setCachedEmptyStateGreeting("stale");
61
+ checkpointStore.set(
62
+ TIMESTAMP_KEY,
63
+ String(Date.now() - (4 * 60 + 1) * 60 * 1000),
64
+ );
65
+ expect(getCachedEmptyStateGreeting()).toBeNull();
66
+ });
67
+
68
+ test("returns the cached value just within the TTL", () => {
69
+ setCachedEmptyStateGreeting("fresh enough");
70
+ checkpointStore.set(
71
+ TIMESTAMP_KEY,
72
+ String(Date.now() - (3 * 60 + 59) * 60 * 1000),
73
+ );
74
+ expect(getCachedEmptyStateGreeting()).toBe("fresh enough");
75
+ });
76
+
77
+ test("TTL of 0 disables caching: writes are skipped and reads miss", () => {
78
+ cacheTtlMs = 0;
79
+ setCachedEmptyStateGreeting("should not persist");
80
+ expect(checkpointStore.size).toBe(0);
81
+ expect(getCachedEmptyStateGreeting()).toBeNull();
82
+ });
83
+
84
+ test("TTL of 0 ignores a value cached while caching was enabled", () => {
85
+ setCachedEmptyStateGreeting("cached while on");
86
+ cacheTtlMs = 0;
87
+ expect(getCachedEmptyStateGreeting()).toBeNull();
88
+ });
89
+
90
+ test("returns null when the timestamp checkpoint is missing", () => {
91
+ checkpointStore.set("empty_state:greeting:text", "orphaned");
92
+ expect(getCachedEmptyStateGreeting()).toBeNull();
93
+ });
94
+ });
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
2
  import { createServer, type Server, type Socket } from "node:net";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { afterEach,beforeEach, describe, expect, mock, test } from "bun:test";
5
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
6
6
 
7
7
  // ---------------------------------------------------------------------------
8
8
  // Isolated temp directory for the IPC socket
@@ -48,12 +48,23 @@ mock.module("../runtime/sync/sync-publisher.js", () => ({
48
48
  },
49
49
  }));
50
50
 
51
+ // ---------------------------------------------------------------------------
52
+ // Track calls to syncFlagGatedTools so we can assert the listener registers
53
+ // newly-enabled flag-gated tools after a runtime refresh (not just the cache).
54
+ // ---------------------------------------------------------------------------
55
+ let syncToolsCallCount = 0;
56
+
57
+ mock.module("../tools/registry.js", () => ({
58
+ syncFlagGatedTools: async () => {
59
+ syncToolsCallCount++;
60
+ },
61
+ }));
62
+
51
63
  // ---------------------------------------------------------------------------
52
64
  // Dynamic imports (after mock.module)
53
65
  // ---------------------------------------------------------------------------
54
- const { startGatewayFlagListener, stopGatewayFlagListener } = await import(
55
- "../ipc/gateway-flag-listener.js"
56
- );
66
+ const { startGatewayFlagListener, stopGatewayFlagListener } =
67
+ await import("../ipc/gateway-flag-listener.js");
57
68
 
58
69
  // ---------------------------------------------------------------------------
59
70
  // Helpers
@@ -104,6 +115,7 @@ describe("gateway-flag-listener", () => {
104
115
  beforeEach(() => {
105
116
  mkdirSync(testRoot, { recursive: true });
106
117
  refreshCallCount = 0;
118
+ syncToolsCallCount = 0;
107
119
  publishedTagSets = [];
108
120
  testServer = createTestServer();
109
121
  });
@@ -123,7 +135,7 @@ describe("gateway-flag-listener", () => {
123
135
  }
124
136
  });
125
137
 
126
- test("refreshes flag cache on connect and on feature_flags_changed event", async () => {
138
+ test("refreshes flag cache AND syncs gated tools on connect and on feature_flags_changed event", async () => {
127
139
  await new Promise<void>((resolve) => {
128
140
  testServer.server.listen(socketPath, resolve);
129
141
  });
@@ -133,11 +145,17 @@ describe("gateway-flag-listener", () => {
133
145
  await new Promise((r) => setTimeout(r, 100));
134
146
 
135
147
  expect(refreshCallCount).toBe(1);
148
+ // Connect must also sync gated tools, so a flag flipped while disconnected
149
+ // registers its tools without waiting for a restart.
150
+ expect(syncToolsCallCount).toBe(1);
136
151
 
137
152
  testServer.emit("feature_flags_changed");
138
153
  await new Promise((r) => setTimeout(r, 100));
139
154
 
140
155
  expect(refreshCallCount).toBe(2);
156
+ // The runtime flag change must register newly-enabled tools too — not just
157
+ // refresh the cache (the bug: routes pass the gate but tools stay absent).
158
+ expect(syncToolsCallCount).toBe(2);
141
159
  });
142
160
 
143
161
  test("broadcasts feature-flags sync_changed when flags change", async () => {
@@ -207,8 +225,7 @@ describe("gateway-flag-listener", () => {
207
225
  const countAfterReconnect = refreshCallCount;
208
226
  expect(countAfterReconnect).toBeGreaterThan(0);
209
227
 
210
- const payload =
211
- JSON.stringify({ event: "feature_flags_changed" }) + "\n";
228
+ const payload = JSON.stringify({ event: "feature_flags_changed" }) + "\n";
212
229
  secondClient!.write(payload);
213
230
  await new Promise((r) => setTimeout(r, 200));
214
231
 
@@ -1,4 +1,4 @@
1
- import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  mock.module("../util/logger.js", () => ({
4
4
  getLogger: () =>
@@ -18,27 +18,11 @@ mock.module("../runtime/gateway-client.js", () => ({
18
18
  },
19
19
  }));
20
20
 
21
- import {
22
- createCallSession,
23
- createPendingQuestion,
24
- getPendingQuestion,
25
- } from "../calls/call-store.js";
26
- import {
27
- startGuardianActionSweep,
28
- stopGuardianActionSweep,
29
- sweepExpiredGuardianActions,
30
- } from "../calls/guardian-action-sweep.js";
21
+ import type { ExpiryDeliveryInfo } from "../calls/guardian-action-sweep.js";
22
+ import { sendGuardianExpiryNotices } from "../calls/guardian-action-sweep.js";
31
23
  import { getDb } from "../memory/db-connection.js";
32
24
  import { initializeDb } from "../memory/db-init.js";
33
- import {
34
- createGuardianActionDelivery,
35
- createGuardianActionRequest,
36
- getDeliveriesByRequestId,
37
- getGuardianActionRequest,
38
- updateDeliveryStatus,
39
- } from "../memory/guardian-action-store.js";
40
- import { conversations } from "../memory/schema.js";
41
- import { resetDbForTesting } from "./db-test-helpers.js";
25
+ import { conversations, messages } from "../memory/schema.js";
42
26
 
43
27
  initializeDb();
44
28
 
@@ -55,218 +39,71 @@ function ensureConversation(id: string): void {
55
39
  .run();
56
40
  }
57
41
 
58
- function resetTables(): void {
59
- const db = getDb();
60
- db.run("DELETE FROM guardian_action_deliveries");
61
- db.run("DELETE FROM guardian_action_requests");
62
- db.run("DELETE FROM call_pending_questions");
63
- db.run("DELETE FROM call_events");
64
- db.run("DELETE FROM call_sessions");
65
- db.run("DELETE FROM messages");
66
- db.run("DELETE FROM conversations");
67
- deliveredMessages.length = 0;
42
+ function makeDelivery(
43
+ overrides: Partial<ExpiryDeliveryInfo> = {},
44
+ ): ExpiryDeliveryInfo {
45
+ return {
46
+ id: "delivery-1",
47
+ status: "sent",
48
+ destinationChannel: "telegram",
49
+ destinationConversationId: null,
50
+ destinationChatId: "chat-123",
51
+ ...overrides,
52
+ };
68
53
  }
69
54
 
70
- describe("guardian-action-sweep", () => {
55
+ describe("sendGuardianExpiryNotices", () => {
71
56
  beforeEach(() => {
72
- resetTables();
73
- });
74
-
75
- afterAll(() => {
76
- stopGuardianActionSweep();
77
- resetDbForTesting();
57
+ const db = getDb();
58
+ db.run("DELETE FROM messages");
59
+ db.run("DELETE FROM conversations");
60
+ deliveredMessages.length = 0;
78
61
  });
79
62
 
80
- test("sweepExpiredGuardianActions expires requests past their expiresAt", async () => {
81
- const convId = "conv-sweep-1";
82
- ensureConversation(convId);
83
-
84
- const session = createCallSession({
85
- conversationId: convId,
86
- provider: "twilio",
87
- fromNumber: "+15550001111",
88
- toNumber: "+15550002222",
89
- });
90
- const pq = createPendingQuestion(session.id, "What is the code?");
91
-
92
- // Create request that has already expired
93
- const request = createGuardianActionRequest({
94
- kind: "ask_guardian",
95
- sourceChannel: "phone",
96
- sourceConversationId: convId,
97
- callSessionId: session.id,
98
- pendingQuestionId: pq.id,
99
- questionText: pq.questionText,
100
- expiresAt: Date.now() - 10_000, // expired 10s ago
101
- });
102
-
103
- ensureConversation("conv-mac-1");
104
- createGuardianActionDelivery({
105
- requestId: request.id,
106
- destinationChannel: "vellum",
107
- destinationConversationId: "conv-mac-1",
108
- });
109
- updateDeliveryStatus(
110
- getDeliveriesByRequestId(request.id).find(
111
- (d) => d.destinationChannel === "vellum",
112
- )!.id,
113
- "sent",
63
+ test("sends external channel expiry notices for sent deliveries", async () => {
64
+ await sendGuardianExpiryNotices(
65
+ [makeDelivery({ status: "sent" }), makeDelivery({ id: "d2", status: "pending", destinationChatId: "chat-456" })],
66
+ "assistant-1",
114
67
  );
115
68
 
116
- await sweepExpiredGuardianActions();
117
-
118
- const updatedRequest = getGuardianActionRequest(request.id);
119
- expect(updatedRequest).not.toBeNull();
120
- expect(updatedRequest!.status).toBe("expired");
121
-
122
- const deliveries = getDeliveriesByRequestId(request.id);
123
- expect(deliveries).toHaveLength(1);
124
- expect(deliveries[0].status).toBe("expired");
125
- });
126
-
127
- test("sweepExpiredGuardianActions expires pending questions", async () => {
128
- const convId = "conv-sweep-2";
129
- ensureConversation(convId);
130
-
131
- const session = createCallSession({
132
- conversationId: convId,
133
- provider: "twilio",
134
- fromNumber: "+15550001111",
135
- toNumber: "+15550002222",
136
- });
137
- const pq = createPendingQuestion(session.id, "What is the gate code?");
138
-
139
- createGuardianActionRequest({
140
- kind: "ask_guardian",
141
- sourceChannel: "phone",
142
- sourceConversationId: convId,
143
- callSessionId: session.id,
144
- pendingQuestionId: pq.id,
145
- questionText: pq.questionText,
146
- expiresAt: Date.now() - 5_000,
147
- });
148
-
149
- // Verify the question is still pending before sweep
150
- expect(getPendingQuestion(session.id)).not.toBeNull();
151
-
152
- await sweepExpiredGuardianActions();
153
-
154
- // Pending question should be expired
155
- expect(getPendingQuestion(session.id)).toBeNull();
69
+ expect(deliveredMessages).toHaveLength(2);
70
+ expect(deliveredMessages[0].url).toBe("/deliver/telegram");
71
+ expect(deliveredMessages[0].body.chatId).toBe("chat-123");
72
+ expect(deliveredMessages[0].body.assistantId).toBe("assistant-1");
73
+ expect(deliveredMessages[1].body.chatId).toBe("chat-456");
156
74
  });
157
75
 
158
- test("sweepExpiredGuardianActions does nothing when no expired requests exist", async () => {
159
- const convId = "conv-sweep-3";
160
- ensureConversation(convId);
161
-
162
- const session = createCallSession({
163
- conversationId: convId,
164
- provider: "twilio",
165
- fromNumber: "+15550001111",
166
- toNumber: "+15550002222",
167
- });
168
- const pq = createPendingQuestion(session.id, "Still valid?");
169
-
170
- // Request that expires in the future
171
- const request = createGuardianActionRequest({
172
- kind: "ask_guardian",
173
- sourceChannel: "phone",
174
- sourceConversationId: convId,
175
- callSessionId: session.id,
176
- pendingQuestionId: pq.id,
177
- questionText: pq.questionText,
178
- expiresAt: Date.now() + 60_000, // expires in 60s
179
- });
180
-
181
- await sweepExpiredGuardianActions();
182
-
183
- const updatedRequest = getGuardianActionRequest(request.id);
184
- expect(updatedRequest!.status).toBe("pending");
185
- expect(getPendingQuestion(session.id)).not.toBeNull();
186
- });
187
-
188
- test("sweepExpiredGuardianActions sends external channel expiry notices for sent deliveries", async () => {
189
- const convId = "conv-sweep-4";
190
- ensureConversation(convId);
191
-
192
- const session = createCallSession({
193
- conversationId: convId,
194
- provider: "twilio",
195
- fromNumber: "+15550001111",
196
- toNumber: "+15550002222",
197
- });
198
- const pq = createPendingQuestion(session.id, "External question?");
199
-
200
- const request = createGuardianActionRequest({
201
- kind: "ask_guardian",
202
- sourceChannel: "phone",
203
- sourceConversationId: convId,
204
- callSessionId: session.id,
205
- pendingQuestionId: pq.id,
206
- questionText: pq.questionText,
207
- expiresAt: Date.now() - 5_000,
208
- });
209
-
210
- const delivery = createGuardianActionDelivery({
211
- requestId: request.id,
212
- destinationChannel: "telegram",
213
- destinationChatId: "chat-123",
214
- });
215
- updateDeliveryStatus(delivery.id, "sent");
216
-
217
- await sweepExpiredGuardianActions();
218
-
219
- // Wait for the fire-and-forget async delivery to complete
220
- await new Promise((resolve) => setTimeout(resolve, 50));
221
-
222
- // The external delivery should trigger an HTTP POST to the gateway
223
- expect(deliveredMessages.length).toBeGreaterThanOrEqual(1);
224
- });
225
-
226
- test("sweepExpiredGuardianActions skips failed deliveries", async () => {
227
- const convId = "conv-sweep-5";
228
- ensureConversation(convId);
229
-
230
- const session = createCallSession({
231
- conversationId: convId,
232
- provider: "twilio",
233
- fromNumber: "+15550001111",
234
- toNumber: "+15550002222",
235
- });
236
- const pq = createPendingQuestion(session.id, "Skip this?");
237
-
238
- const request = createGuardianActionRequest({
239
- kind: "ask_guardian",
240
- sourceChannel: "phone",
241
- sourceConversationId: convId,
242
- callSessionId: session.id,
243
- pendingQuestionId: pq.id,
244
- questionText: pq.questionText,
245
- expiresAt: Date.now() - 5_000,
246
- });
247
-
248
- const delivery = createGuardianActionDelivery({
249
- requestId: request.id,
250
- destinationChannel: "telegram",
251
- destinationChatId: "chat-456",
252
- });
253
- updateDeliveryStatus(delivery.id, "failed", "Network error");
254
-
255
- deliveredMessages.length = 0;
256
-
257
- await sweepExpiredGuardianActions();
76
+ test("skips deliveries that are not sent or pending", async () => {
77
+ await sendGuardianExpiryNotices(
78
+ [
79
+ makeDelivery({ status: "failed" }),
80
+ makeDelivery({ id: "d2", status: "expired" }),
81
+ ],
82
+ "assistant-1",
83
+ );
258
84
 
259
- // Should NOT have sent an expiry notice for a failed delivery
260
85
  expect(deliveredMessages).toHaveLength(0);
261
86
  });
262
87
 
263
- test("startGuardianActionSweep and stopGuardianActionSweep manage timer", () => {
264
- startGuardianActionSweep();
265
- // Calling start again should be a no-op (idempotent)
266
- startGuardianActionSweep();
88
+ test("adds an expiry message to vellum guardian conversations", async () => {
89
+ ensureConversation("guardian-conv");
90
+
91
+ await sendGuardianExpiryNotices(
92
+ [
93
+ makeDelivery({
94
+ destinationChannel: "vellum",
95
+ destinationConversationId: "guardian-conv",
96
+ destinationChatId: null,
97
+ }),
98
+ ],
99
+ "assistant-1",
100
+ );
267
101
 
268
- stopGuardianActionSweep();
269
- // Calling stop again should be safe
270
- stopGuardianActionSweep();
102
+ expect(deliveredMessages).toHaveLength(0);
103
+ const db = getDb();
104
+ const rows = db.select().from(messages).all();
105
+ expect(rows).toHaveLength(1);
106
+ expect(rows[0].conversationId).toBe("guardian-conv");
107
+ expect(rows[0].content).toContain("expired");
271
108
  });
272
109
  });