@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
@@ -811,6 +811,7 @@ describe("POST /v1/plugins/install", () => {
811
811
  fileCount: 7,
812
812
  ref: opts.ref ?? "main",
813
813
  commit: null,
814
+ committedAt: null,
814
815
  }));
815
816
 
816
817
  const result = await invokeInstall({
@@ -842,6 +843,7 @@ describe("POST /v1/plugins/install", () => {
842
843
  fileCount: 7,
843
844
  ref: opts.ref ?? "main",
844
845
  commit: null,
846
+ committedAt: null,
845
847
  }));
846
848
 
847
849
  const result = await invokeInstall({
@@ -940,6 +942,7 @@ function inspection(
940
942
  ? {
941
943
  target: "/workspace/.vellum/plugins/level-up",
942
944
  commit: "60a392b0000000000000000000000000000000aa",
945
+ committedAt: "2026-06-01T12:34:56.000Z",
943
946
  version: "0.1.0",
944
947
  description: "Surfaces a Level Up diff card.",
945
948
  installedAt: "2026-06-08T00:00:00.000Z",
@@ -964,6 +967,7 @@ function inspection(
964
967
  repo: "vellum-ai/level-up",
965
968
  path: "",
966
969
  commit: "3eae1820000000000000000000000000000000bb",
970
+ committedAt: "2026-06-05T08:12:24.000Z",
967
971
  description: "Surfaces a Level Up diff card.",
968
972
  homepage: "https://github.com/vellum-ai/level-up",
969
973
  license: "MIT",
@@ -1064,7 +1068,15 @@ function upgradeResult(
1064
1068
  overrides.fromCommit === undefined
1065
1069
  ? "60a392b0000000000000000000000000000000aa"
1066
1070
  : overrides.fromCommit,
1071
+ fromTimestamp:
1072
+ overrides.fromTimestamp === undefined
1073
+ ? "2026-06-01T12:34:56.000Z"
1074
+ : overrides.fromTimestamp,
1067
1075
  toCommit: overrides.toCommit ?? "3eae1820000000000000000000000000000000bb",
1076
+ toTimestamp:
1077
+ overrides.toTimestamp === undefined
1078
+ ? "2026-06-05T08:12:24.000Z"
1079
+ : overrides.toTimestamp,
1068
1080
  target: overrides.target ?? "/workspace/.vellum/plugins/level-up",
1069
1081
  fileCount: overrides.fileCount === undefined ? 12 : overrides.fileCount,
1070
1082
  dryRun: overrides.dryRun ?? false,
@@ -1076,7 +1088,9 @@ async function invokeUpgrade(args: RouteHandlerArgs = {}): Promise<{
1076
1088
  name: string;
1077
1089
  outcome: string;
1078
1090
  fromCommit: string | null;
1091
+ fromTimestamp: string | null;
1079
1092
  toCommit: string;
1093
+ toTimestamp: string | null;
1080
1094
  target: string;
1081
1095
  fileCount: number | null;
1082
1096
  dryRun: boolean;
@@ -1086,7 +1100,9 @@ async function invokeUpgrade(args: RouteHandlerArgs = {}): Promise<{
1086
1100
  name: string;
1087
1101
  outcome: string;
1088
1102
  fromCommit: string | null;
1103
+ fromTimestamp: string | null;
1089
1104
  toCommit: string;
1105
+ toTimestamp: string | null;
1090
1106
  target: string;
1091
1107
  fileCount: number | null;
1092
1108
  dryRun: boolean;
@@ -1114,7 +1130,9 @@ describe("POST /v1/plugins/:name/upgrade", () => {
1114
1130
  name: "level-up",
1115
1131
  outcome: "upgraded",
1116
1132
  fromCommit: "60a392b0000000000000000000000000000000aa",
1133
+ fromTimestamp: "2026-06-01T12:34:56.000Z",
1117
1134
  toCommit: "3eae1820000000000000000000000000000000bb",
1135
+ toTimestamp: "2026-06-05T08:12:24.000Z",
1118
1136
  target: "/workspace/.vellum/plugins/level-up",
1119
1137
  fileCount: 12,
1120
1138
  dryRun: false,
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Asserts `listRetrospectiveRuns` merges background-conversation rows from
3
+ * BOTH retrospective sources (`memory-retrospective` → kind `legacy`,
4
+ * `memory-retrospective-fork` → kind `fork`) into the heartbeat-runs
5
+ * response shape (plus `kind` and `title`), derives `status` / `finishedAt`
6
+ * from assistant-message presence **at or after the conversation's
7
+ * createdAt** (fork runs copy source messages with their original
8
+ * timestamps), and clamps the `limit` query param.
9
+ *
10
+ * Also asserts `getRetrospectiveConfig` gates `available` on
11
+ * `memory.enabled` alone (NOT `memory.v2.enabled`) and always reports
12
+ * `nextRunAt: null` — retrospectives are event-driven per conversation,
13
+ * never globally scheduled.
14
+ */
15
+
16
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
20
+
21
+ mock.module("../../../util/logger.js", () => ({
22
+ getLogger: () =>
23
+ new Proxy({} as Record<string, unknown>, {
24
+ get: () => () => {},
25
+ }),
26
+ }));
27
+
28
+ import { invalidateConfigCache } from "../../../config/loader.js";
29
+ import { createConversation } from "../../../memory/conversation-crud.js";
30
+ import { getDb } from "../../../memory/db-connection.js";
31
+ import { initializeDb } from "../../../memory/db-init.js";
32
+ import { recordUsageEvent } from "../../../memory/llm-usage-store.js";
33
+ import {
34
+ MEMORY_RETROSPECTIVE_FORK_SOURCE,
35
+ MEMORY_RETROSPECTIVE_SOURCE,
36
+ } from "../../../memory/memory-retrospective-constants.js";
37
+ import { rawRun } from "../../../memory/raw-query.js";
38
+ import { ROUTES } from "../retrospective-routes.js";
39
+ import type { RouteDefinition } from "../types.js";
40
+
41
+ initializeDb();
42
+
43
+ let workspaceDir: string;
44
+ let origWorkspaceDir: string | undefined;
45
+ let configPath: string;
46
+
47
+ function resetTables(): void {
48
+ const db = getDb();
49
+ db.run(`DELETE FROM llm_usage_events`);
50
+ db.run(`DELETE FROM messages`);
51
+ db.run(`DELETE FROM conversations`);
52
+ }
53
+
54
+ function findHandler(operationId: string): RouteDefinition["handler"] {
55
+ const route = ROUTES.find((r) => r.operationId === operationId);
56
+ if (!route) throw new Error(`Route ${operationId} not found`);
57
+ return route.handler;
58
+ }
59
+
60
+ function insertMessage(
61
+ conversationId: string,
62
+ role: string,
63
+ createdAt: number,
64
+ ): void {
65
+ rawRun(
66
+ "INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)",
67
+ `msg-${conversationId}-${role}-${createdAt}`,
68
+ conversationId,
69
+ role,
70
+ "x",
71
+ createdAt,
72
+ );
73
+ }
74
+
75
+ function setCreatedAt(conversationId: string, createdAt: number): void {
76
+ rawRun(
77
+ "UPDATE conversations SET created_at = ? WHERE id = ?",
78
+ createdAt,
79
+ conversationId,
80
+ );
81
+ }
82
+
83
+ function recordUsageCostAt(
84
+ conversationId: string,
85
+ requestId: string,
86
+ createdAt: number,
87
+ estimatedCostUsd: number,
88
+ ): void {
89
+ const event = recordUsageEvent(
90
+ {
91
+ conversationId,
92
+ runId: null,
93
+ requestId,
94
+ actor: "main_agent",
95
+ callSite: "mainAgent",
96
+ inferenceProfile: "balanced",
97
+ provider: "anthropic",
98
+ model: "claude-sonnet-4-20250514",
99
+ inputTokens: 100,
100
+ outputTokens: 50,
101
+ cacheCreationInputTokens: 0,
102
+ cacheReadInputTokens: 0,
103
+ rawUsage: null,
104
+ },
105
+ { estimatedCostUsd, pricingStatus: "priced" },
106
+ );
107
+ rawRun(
108
+ "UPDATE llm_usage_events SET created_at = ? WHERE id = ?",
109
+ createdAt,
110
+ event.id,
111
+ );
112
+ }
113
+
114
+ interface RunRecord {
115
+ id: string;
116
+ scheduledFor: number;
117
+ startedAt: number | null;
118
+ finishedAt: number | null;
119
+ durationMs: number | null;
120
+ status: "ok" | "running";
121
+ skipReason: string | null;
122
+ error: string | null;
123
+ conversationId: string | null;
124
+ estimatedCostUsd: number;
125
+ createdAt: number;
126
+ kind: "legacy" | "fork";
127
+ title: string | null;
128
+ }
129
+
130
+ interface ListRunsResponse {
131
+ runs: RunRecord[];
132
+ }
133
+
134
+ interface ConfigResponse {
135
+ available: boolean;
136
+ enabled: boolean;
137
+ intervalMs: number;
138
+ nextRunAt: number | null;
139
+ lastRunAt: number | null;
140
+ success: boolean;
141
+ }
142
+
143
+ beforeEach(() => {
144
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-retrospective-routes-"));
145
+ origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
146
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
147
+ configPath = join(workspaceDir, "config.json");
148
+ invalidateConfigCache();
149
+ resetTables();
150
+ });
151
+
152
+ afterEach(() => {
153
+ if (origWorkspaceDir === undefined) {
154
+ delete process.env.VELLUM_WORKSPACE_DIR;
155
+ } else {
156
+ process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
157
+ }
158
+ invalidateConfigCache();
159
+ rmSync(workspaceDir, { recursive: true, force: true });
160
+ });
161
+
162
+ describe("listRetrospectiveRuns handler", () => {
163
+ test("merges both retrospective sources with the kind field and excludes others", async () => {
164
+ const legacy = createConversation({
165
+ title: "legacy run",
166
+ source: MEMORY_RETROSPECTIVE_SOURCE,
167
+ });
168
+ const fork = createConversation({
169
+ title: "Planning chat (Retrospective)",
170
+ source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
171
+ });
172
+ createConversation({ title: "h1", source: "heartbeat" });
173
+ createConversation({ title: "c1", source: "memory_v2_consolidation" });
174
+ createConversation({ title: "u1", source: "user" });
175
+ setCreatedAt(legacy.id, 1000);
176
+ setCreatedAt(fork.id, 2000);
177
+
178
+ const handler = findHandler("listRetrospectiveRuns");
179
+ const result = (await handler({})) as ListRunsResponse;
180
+
181
+ expect(result.runs).toHaveLength(2);
182
+ expect(result.runs.map((r) => [r.id, r.kind, r.title])).toEqual([
183
+ [fork.id, "fork", "Planning chat (Retrospective)"],
184
+ [legacy.id, "legacy", "legacy run"],
185
+ ]);
186
+ });
187
+
188
+ test("orders merged runs by createdAt descending across sources", async () => {
189
+ const a = createConversation({
190
+ title: "a",
191
+ source: MEMORY_RETROSPECTIVE_SOURCE,
192
+ });
193
+ const b = createConversation({
194
+ title: "b",
195
+ source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
196
+ });
197
+ const c = createConversation({
198
+ title: "c",
199
+ source: MEMORY_RETROSPECTIVE_SOURCE,
200
+ });
201
+ setCreatedAt(a.id, 1000);
202
+ setCreatedAt(b.id, 3000);
203
+ setCreatedAt(c.id, 2000);
204
+
205
+ const handler = findHandler("listRetrospectiveRuns");
206
+ const result = (await handler({})) as ListRunsResponse;
207
+
208
+ expect(result.runs.map((r) => r.id)).toEqual([b.id, c.id, a.id]);
209
+ });
210
+
211
+ test("synthesizes status='ok' with finishedAt from latest assistant message", async () => {
212
+ const conv = createConversation({
213
+ title: "r1",
214
+ source: MEMORY_RETROSPECTIVE_SOURCE,
215
+ });
216
+ setCreatedAt(conv.id, 1000);
217
+ insertMessage(conv.id, "user", 1100);
218
+ insertMessage(conv.id, "assistant", 2000);
219
+ insertMessage(conv.id, "assistant", 2500);
220
+
221
+ const handler = findHandler("listRetrospectiveRuns");
222
+ const result = (await handler({})) as ListRunsResponse;
223
+
224
+ const run = result.runs[0]!;
225
+ expect(run.id).toBe(conv.id);
226
+ expect(run.conversationId).toBe(conv.id);
227
+ expect(run.status).toBe("ok");
228
+ expect(run.scheduledFor).toBe(1000);
229
+ expect(run.startedAt).toBe(1000);
230
+ expect(run.finishedAt).toBe(2500);
231
+ expect(run.durationMs).toBe(1500);
232
+ expect(run.skipReason).toBeNull();
233
+ expect(run.error).toBeNull();
234
+ });
235
+
236
+ test("fork run with only the copied source prefix stays 'running' (no negative duration)", async () => {
237
+ // forkConversation copies source messages with their ORIGINAL
238
+ // timestamps, so a fresh fork contains assistant messages that predate
239
+ // the fork conversation's createdAt. Those must not count as agent
240
+ // output — status stays 'running' until the retrospective agent itself
241
+ // replies (at-or-after the fork's creation).
242
+ const conv = createConversation({
243
+ title: "Planning chat (Retrospective)",
244
+ source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
245
+ });
246
+ setCreatedAt(conv.id, 5000);
247
+ // Copied prefix from the source conversation (pre-fork timestamps).
248
+ insertMessage(conv.id, "user", 2000);
249
+ insertMessage(conv.id, "assistant", 3000);
250
+
251
+ const handler = findHandler("listRetrospectiveRuns");
252
+ const running = (await handler({})) as ListRunsResponse;
253
+ expect(running.runs[0]!.status).toBe("running");
254
+ expect(running.runs[0]!.finishedAt).toBeNull();
255
+ expect(running.runs[0]!.durationMs).toBeNull();
256
+
257
+ // Retrospective agent replies after the fork was created.
258
+ insertMessage(conv.id, "assistant", 6000);
259
+ const done = (await handler({})) as ListRunsResponse;
260
+ expect(done.runs[0]!.status).toBe("ok");
261
+ expect(done.runs[0]!.finishedAt).toBe(6000);
262
+ expect(done.runs[0]!.durationMs).toBe(1000);
263
+ });
264
+
265
+ test("exposes estimatedCostUsd from the conversation total, falling back to window usage", async () => {
266
+ const withTotal = createConversation({
267
+ title: "r1",
268
+ source: MEMORY_RETROSPECTIVE_SOURCE,
269
+ });
270
+ rawRun(
271
+ "UPDATE conversations SET created_at = ?, total_estimated_cost = ? WHERE id = ?",
272
+ 1000,
273
+ 0.42,
274
+ withTotal.id,
275
+ );
276
+ insertMessage(withTotal.id, "assistant", 2000);
277
+
278
+ const withWindow = createConversation({
279
+ title: "r2",
280
+ source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
281
+ });
282
+ setCreatedAt(withWindow.id, 1000);
283
+ insertMessage(withWindow.id, "assistant", 2000);
284
+ recordUsageCostAt(withWindow.id, "retro-before", 999, 0.4);
285
+ recordUsageCostAt(withWindow.id, "retro-inside", 1500, 0.07);
286
+ recordUsageCostAt(withWindow.id, "retro-after", 2001, 0.5);
287
+
288
+ const handler = findHandler("listRetrospectiveRuns");
289
+ const result = (await handler({})) as ListRunsResponse;
290
+
291
+ const byId = new Map(result.runs.map((r) => [r.id, r]));
292
+ expect(byId.get(withTotal.id)!.estimatedCostUsd).toBeCloseTo(0.42);
293
+ expect(byId.get(withWindow.id)!.estimatedCostUsd).toBeCloseTo(0.07);
294
+ });
295
+
296
+ test("limit defaults to 20, clamps to [1, 100], and merges correctly under the cap", async () => {
297
+ // 3 legacy (newest) + 3 fork (older) — a merged limit of 4 must take
298
+ // all 3 legacy rows plus the newest fork row, which requires fetching
299
+ // the limit from EACH source before merging.
300
+ const ids: Array<{ id: string; createdAt: number }> = [];
301
+ for (let i = 0; i < 3; i++) {
302
+ const conv = createConversation({
303
+ title: `legacy-${i}`,
304
+ source: MEMORY_RETROSPECTIVE_SOURCE,
305
+ });
306
+ setCreatedAt(conv.id, 10_000 + i);
307
+ ids.push({ id: conv.id, createdAt: 10_000 + i });
308
+ }
309
+ for (let i = 0; i < 3; i++) {
310
+ const conv = createConversation({
311
+ title: `fork-${i}`,
312
+ source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
313
+ });
314
+ setCreatedAt(conv.id, 5_000 + i);
315
+ ids.push({ id: conv.id, createdAt: 5_000 + i });
316
+ }
317
+ const expectedTop4 = ids
318
+ .sort((a, b) => b.createdAt - a.createdAt)
319
+ .slice(0, 4)
320
+ .map((entry) => entry.id);
321
+
322
+ const handler = findHandler("listRetrospectiveRuns");
323
+
324
+ const def = (await handler({})) as ListRunsResponse;
325
+ expect(def.runs).toHaveLength(6);
326
+
327
+ const lim4 = (await handler({
328
+ queryParams: { limit: "4" },
329
+ })) as ListRunsResponse;
330
+ expect(lim4.runs.map((r) => r.id)).toEqual(expectedTop4);
331
+
332
+ const neg = (await handler({
333
+ queryParams: { limit: "-5" },
334
+ })) as ListRunsResponse;
335
+ expect(neg.runs).toHaveLength(1);
336
+
337
+ const huge = (await handler({
338
+ queryParams: { limit: "5000" },
339
+ })) as ListRunsResponse;
340
+ expect(huge.runs).toHaveLength(6);
341
+
342
+ const bad = (await handler({
343
+ queryParams: { limit: "garbage" },
344
+ })) as ListRunsResponse;
345
+ expect(bad.runs).toHaveLength(6);
346
+ });
347
+ });
348
+
349
+ describe("getRetrospectiveConfig handler", () => {
350
+ test("reports availability and threshold interval with nextRunAt always null", async () => {
351
+ writeFileSync(
352
+ configPath,
353
+ JSON.stringify(
354
+ {
355
+ memory: {
356
+ retrospective: { timeThresholdMs: 45 * 60 * 1000 },
357
+ },
358
+ },
359
+ null,
360
+ 2,
361
+ ) + "\n",
362
+ );
363
+ const conv = createConversation({
364
+ title: "r1",
365
+ source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
366
+ });
367
+ setCreatedAt(conv.id, 123_456);
368
+
369
+ const handler = findHandler("getRetrospectiveConfig");
370
+ const result = (await handler({})) as ConfigResponse;
371
+
372
+ expect(result).toEqual({
373
+ available: true,
374
+ enabled: true,
375
+ intervalMs: 45 * 60 * 1000,
376
+ nextRunAt: null,
377
+ lastRunAt: 123_456,
378
+ success: true,
379
+ });
380
+ });
381
+
382
+ test("lastRunAt is the newest run across both sources, null when none exist", async () => {
383
+ const handler = findHandler("getRetrospectiveConfig");
384
+ const empty = (await handler({})) as ConfigResponse;
385
+ expect(empty.lastRunAt).toBeNull();
386
+
387
+ const legacy = createConversation({
388
+ title: "legacy",
389
+ source: MEMORY_RETROSPECTIVE_SOURCE,
390
+ });
391
+ const fork = createConversation({
392
+ title: "fork",
393
+ source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
394
+ });
395
+ setCreatedAt(legacy.id, 9000);
396
+ setCreatedAt(fork.id, 4000);
397
+
398
+ const result = (await handler({})) as ConfigResponse;
399
+ expect(result.lastRunAt).toBe(9000);
400
+ });
401
+
402
+ test("reports unavailable when global memory is disabled", async () => {
403
+ writeFileSync(
404
+ configPath,
405
+ JSON.stringify({ memory: { enabled: false } }, null, 2) + "\n",
406
+ );
407
+
408
+ const handler = findHandler("getRetrospectiveConfig");
409
+ const result = (await handler({})) as ConfigResponse;
410
+
411
+ expect(result.available).toBe(false);
412
+ expect(result.enabled).toBe(false);
413
+ expect(result.nextRunAt).toBeNull();
414
+ expect(result.success).toBe(true);
415
+ });
416
+
417
+ test("stays available when memory v2 is disabled (retrospectives do not gate on v2)", async () => {
418
+ writeFileSync(
419
+ configPath,
420
+ JSON.stringify({ memory: { v2: { enabled: false } } }, null, 2) + "\n",
421
+ );
422
+
423
+ const handler = findHandler("getRetrospectiveConfig");
424
+ const result = (await handler({})) as ConfigResponse;
425
+
426
+ expect(result.available).toBe(true);
427
+ expect(result.enabled).toBe(true);
428
+ });
429
+
430
+ test("no run-now route is exposed (retrospectives are event-driven)", () => {
431
+ expect(ROUTES.some((route) => route.method === "POST")).toBe(false);
432
+ expect(
433
+ ROUTES.find((route) => route.endpoint.includes("run-now")),
434
+ ).toBeUndefined();
435
+ });
436
+ });
@@ -72,6 +72,17 @@ mock.module("../../../memory/raw-query.js", () => ({
72
72
  },
73
73
  }));
74
74
 
75
+ // Mock guardian-action-service to cut off its deep transitive dependency chain.
76
+ // The apr:* routing is tested in guardian-routing-invariants.test.ts; this file
77
+ // focuses on the surface→conversation rehydration path.
78
+ mock.module("../../guardian-action-service.js", () => ({
79
+ processGuardianDecision: async () => ({ ok: true, applied: true }),
80
+ }));
81
+
82
+ mock.module("../channel-route-shared.js", () => ({
83
+ parseCallbackData: () => null,
84
+ }));
85
+
75
86
  // Defer route import until after mocks are installed.
76
87
  const { ROUTES } = await import("../surface-action-routes.js");
77
88
  const { NotFoundError } = await import("../errors.js");
@@ -11,6 +11,10 @@ import { z } from "zod";
11
11
 
12
12
  import { findConversation } from "../../daemon/conversation-registry.js";
13
13
  import { getConversationByKey } from "../../memory/conversation-key-store.js";
14
+ import type {
15
+ SecretDelivery,
16
+ SecretPromptResult,
17
+ } from "../../permissions/secret-prompter.js";
14
18
  import type { UserDecision } from "../../permissions/types.js";
15
19
  import { getLogger } from "../../util/logger.js";
16
20
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
@@ -127,24 +131,47 @@ function handleSecret({ body }: RouteHandlerArgs) {
127
131
  throw new BadRequestError('delivery must be "store" or "transient_send"');
128
132
  }
129
133
 
130
- // Use get() — SecretPrompter.resolveSecret() owns deregistration.
131
134
  const interaction = pendingInteractions.get(requestId);
132
135
  if (!interaction) {
133
136
  throw new NotFoundError("No pending interaction found for this requestId");
134
137
  }
135
138
 
136
- const conversation = findConversation(interaction.conversationId);
137
- if (!conversation) {
139
+ // /v1/secret only settles secret prompts. A requestId belonging to another
140
+ // interaction kind (a confirmation or host-proxy request posted here from
141
+ // stale or mismatched client state) must not be consumed or resolved with a
142
+ // SecretPromptResult, which would strand its real approval/result endpoint.
143
+ if (interaction.kind !== "secret") {
138
144
  throw new NotFoundError(
139
- "Conversation not found for this pending secret request",
145
+ "No pending secret request found for this requestId",
140
146
  );
141
147
  }
142
148
 
143
- conversation.handleSecretResponse(
149
+ // When a live conversation owns the request, route through it so the
150
+ // SecretPrompter's ownership tracking and dispose path stay consistent (this
151
+ // also drives the voice auto-resolve path). The prompter owns deregistration.
152
+ const conversation = interaction.conversationId
153
+ ? findConversation(interaction.conversationId)
154
+ : undefined;
155
+ if (conversation?.hasPendingSecret(requestId)) {
156
+ conversation.handleSecretResponse(
157
+ requestId,
158
+ value,
159
+ delivery as "store" | "transient_send" | undefined,
160
+ );
161
+ return { accepted: true };
162
+ }
163
+
164
+ // Conversation-less requests (e.g. the CLI `credentials prompt` command) and
165
+ // any request no live conversation owns resolve generically via the resolver
166
+ // stored on the interaction, with no Conversation in the loop.
167
+ const resolved = pendingInteractions.resolve(
144
168
  requestId,
145
- value,
146
- delivery as "store" | "transient_send" | undefined,
169
+ value === undefined ? "cancelled" : "answered",
147
170
  );
171
+ (resolved?.rpcResolve as ((r: SecretPromptResult) => void) | undefined)?.({
172
+ value: value ?? null,
173
+ delivery: (delivery as SecretDelivery) ?? "store",
174
+ });
148
175
  return { accepted: true };
149
176
  }
150
177
 
@@ -321,7 +348,7 @@ export const ROUTES: RouteDefinition[] = [
321
348
  .array(
322
349
  z.object({
323
350
  requestId: z.string(),
324
- conversationId: z.string(),
351
+ conversationId: z.string().optional(),
325
352
  kind: z.string(),
326
353
  toolName: z.string().optional(),
327
354
  riskLevel: z.string().optional(),
@@ -831,7 +831,8 @@ async function handleAccessRequestApproval(
831
831
  }
832
832
 
833
833
  // Approved: deliver the verification code to the guardian and notify the requester.
834
- const requesterIdentifier = approval.requesterExternalUserId;
834
+ const requesterIdentifier =
835
+ requesterDisplayName || approval.requesterExternalUserId;
835
836
 
836
837
  let codeDelivered = true;
837
838
  if (decisionResult.verificationCode) {
@@ -22,8 +22,16 @@ import { getAllToolDefinitions } from "../../tools/registry.js";
22
22
  import { getLogger } from "../../util/logger.js";
23
23
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
24
24
  import { runBtwSidechain } from "../btw-sidechain.js";
25
+ import {
26
+ getCachedEmptyStateGreeting,
27
+ setCachedEmptyStateGreeting,
28
+ } from "./empty-state-greeting-cache.js";
25
29
  import { BadRequestError, ServiceUnavailableError } from "./errors.js";
26
- import { getCachedIntro, setCachedIntro } from "./identity-intro-cache.js";
30
+ import {
31
+ getCachedIntro,
32
+ readWorkspaceGreetings,
33
+ setCachedIntro,
34
+ } from "./identity-intro-cache.js";
27
35
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
28
36
 
29
37
  const log = getLogger("btw-routes");
@@ -73,6 +81,24 @@ async function handleBtw({
73
81
  }
74
82
  }
75
83
 
84
+ // ----- Empty-state greeting fast-path -----
85
+ // User-authored `## Greetings` win; otherwise replay a cached greeting when
86
+ // one is fresh within the configurable TTL (`ui.emptyStateGreetingCacheTtlMs`,
87
+ // 0 = always regenerate). Either path avoids an LLM call and streams the
88
+ // text straight back.
89
+ if (conversationKey === GREETING_KEY) {
90
+ const authored = readWorkspaceGreetings();
91
+ if (authored && authored.length > 0) {
92
+ log.debug("Returning authored empty-state greeting");
93
+ return streamText(pickRandom(authored));
94
+ }
95
+ const cached = getCachedEmptyStateGreeting();
96
+ if (cached) {
97
+ log.debug("Returning cached empty-state greeting");
98
+ return streamText(cached);
99
+ }
100
+ }
101
+
76
102
  // ----- Greeting context enrichment -----
77
103
  let effectiveContent = trimmedContent;
78
104
  if (
@@ -136,6 +162,12 @@ async function handleBtw({
136
162
  }
137
163
  }
138
164
 
165
+ if (isGreeting && result.text) {
166
+ // setCachedEmptyStateGreeting is a no-op when the TTL is 0.
167
+ setCachedEmptyStateGreeting(result.text);
168
+ log.debug("Cached empty-state greeting text");
169
+ }
170
+
139
171
  controller.enqueue(sseEvent("btw_complete", {}));
140
172
  controller.close();
141
173
  } catch (err: unknown) {
@@ -153,6 +185,10 @@ async function handleBtw({
153
185
  });
154
186
  }
155
187
 
188
+ function pickRandom<T>(items: readonly T[]): T {
189
+ return items[Math.floor(Math.random() * items.length)]!;
190
+ }
191
+
156
192
  function streamText(text: string): ReadableStream<Uint8Array> {
157
193
  return new ReadableStream({
158
194
  start(controller) {