@vellumai/assistant 0.8.10 → 0.8.11-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 (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -15,6 +15,7 @@ import {
15
15
  } from "../../config/env.js";
16
16
  import { loadRawConfig, saveRawConfig } from "../../config/loader.js";
17
17
  import { loadSkillCatalog } from "../../config/skills.js";
18
+ import { findConversation } from "../../daemon/conversation-registry.js";
18
19
  import {
19
20
  computeGatewayTarget,
20
21
  getIngressConfigResult,
@@ -41,7 +42,7 @@ import {
41
42
  type ManifestOverride,
42
43
  resolveExecutionTarget,
43
44
  } from "../../tools/execution-target.js";
44
- import { getAllTools, getTool } from "../../tools/registry.js";
45
+ import { getAllTools, getTool, getToolOwner } from "../../tools/registry.js";
45
46
  import {
46
47
  ACTIVITY_SKIP_SET,
47
48
  injectActivityField,
@@ -80,10 +81,7 @@ function handleVoiceConfigUpdate({ body = {} }: RouteHandlerArgs) {
80
81
  // Avatar generation
81
82
  // ---------------------------------------------------------------------------
82
83
 
83
- async function handleGenerateAvatar({
84
- body = {},
85
- headers,
86
- }: RouteHandlerArgs) {
84
+ async function handleGenerateAvatar({ body = {}, headers }: RouteHandlerArgs) {
87
85
  const { description } = body as { description?: string };
88
86
  if (!description?.trim()) {
89
87
  throw new BadRequestError("Description is required.");
@@ -380,14 +378,32 @@ function resolveManifestOverride(
380
378
  return undefined;
381
379
  }
382
380
 
383
- function handleToolNamesList() {
381
+ type SchemaShape = {
382
+ type: string;
383
+ properties?: Record<string, unknown>;
384
+ required?: string[];
385
+ };
386
+
387
+ interface ToolNamesListResponse {
388
+ names: string[];
389
+ schemas: Record<string, SchemaShape>;
390
+ tools: ToolListEntry[];
391
+ }
392
+
393
+ /**
394
+ * Tool inventory. With no `conversationId`, reports every tool in the
395
+ * global registry (plus skill-manifest tools not yet loaded, for the
396
+ * permission-simulator catalog). With a `conversationId`, scopes the
397
+ * inventory to the tools available to that conversation as of its most
398
+ * recent turn — see {@link handleConversationToolList}.
399
+ */
400
+ function handleToolNamesList(conversationId?: string): ToolNamesListResponse {
401
+ if (conversationId) {
402
+ return handleConversationToolList(conversationId);
403
+ }
404
+
384
405
  const tools = getAllTools();
385
406
  const nameSet = new Set(tools.map((t) => t.name));
386
- type SchemaShape = {
387
- type: string;
388
- properties?: Record<string, unknown>;
389
- required?: string[];
390
- };
391
407
  const schemas: Record<string, SchemaShape> = {};
392
408
 
393
409
  const rawDefs: ToolDefinition[] = tools;
@@ -419,7 +435,89 @@ function handleToolNamesList() {
419
435
  }
420
436
 
421
437
  const names = Array.from(nameSet).sort((a, b) => a.localeCompare(b));
422
- return { names, schemas };
438
+ return { names, schemas, tools: buildRegisteredToolEntries() };
439
+ }
440
+
441
+ /**
442
+ * Scope the tool inventory to a single conversation. Conversations gain
443
+ * tools over their lifecycle (skill loads, MCP reloads), so the global
444
+ * registry over-reports what a given conversation can actually call. We
445
+ * read the conversation's turn snapshot (`getRegisteredToolNames()`) — a
446
+ * pure read that does not re-run the side-effecting `resolveTools`
447
+ * callback — and resolve each name's metadata/schema from the registry.
448
+ */
449
+ function handleConversationToolList(
450
+ conversationId: string,
451
+ ): ToolNamesListResponse {
452
+ const conversation = findConversation(conversationId);
453
+ if (!conversation) {
454
+ throw new NotFoundError(
455
+ `No active conversation "${conversationId}". Run 'assistant conversations list' to see active conversations.`,
456
+ );
457
+ }
458
+
459
+ const names = Array.from(conversation.getRegisteredToolNames()).sort((a, b) =>
460
+ a.localeCompare(b),
461
+ );
462
+
463
+ const schemaByName = new Map<string, SchemaShape>(
464
+ injectActivityField(getAllTools(), ACTIVITY_SKIP_SET).map((d) => [
465
+ d.name,
466
+ d.input_schema as SchemaShape,
467
+ ]),
468
+ );
469
+
470
+ const schemas: Record<string, SchemaShape> = {};
471
+ const tools: ToolListEntry[] = [];
472
+ for (const name of names) {
473
+ const schema = schemaByName.get(name);
474
+ if (schema) schemas[name] = schema;
475
+ tools.push(toolEntryForName(name));
476
+ }
477
+
478
+ return { names, schemas, tools };
479
+ }
480
+
481
+ interface ToolListEntry {
482
+ name: string;
483
+ description: string;
484
+ riskLevel: string;
485
+ category: string;
486
+ /** Tool origin: "core" for built-ins, otherwise "<kind>:<id>" (e.g. "plugin:echo"). */
487
+ source: string;
488
+ }
489
+
490
+ /**
491
+ * Build a catalog entry for one tool name, reading metadata and ownership
492
+ * from the registry (the single source of truth) rather than off any
493
+ * caller-supplied object, so `source` cannot be spoofed by a manifest
494
+ * field. `source` is `core` for built-ins, `<kind>:<id>` for an owned tool
495
+ * (e.g. `plugin:echo`), and `unknown` for a name no longer in the registry
496
+ * (e.g. a conversation snapshot referencing a since-unloaded skill tool).
497
+ */
498
+ function toolEntryForName(name: string): ToolListEntry {
499
+ const tool = getTool(name);
500
+ const owner = getToolOwner(name);
501
+ return {
502
+ name,
503
+ description: tool?.description ?? "",
504
+ riskLevel: tool?.defaultRiskLevel ?? "unknown",
505
+ category: tool?.category ?? "",
506
+ source: owner ? `${owner.kind}:${owner.id}` : tool ? "core" : "unknown",
507
+ };
508
+ }
509
+
510
+ /**
511
+ * Build the registered-tool inventory with the metadata a catalog view
512
+ * needs: description, author-asserted risk band, category, and the
513
+ * extension that contributed the tool. Sorted by name for stable output.
514
+ * Covers only tools loaded into the registry; skill tools whose manifests
515
+ * are present but not yet loaded appear in `names`/`schemas` but not here.
516
+ */
517
+ function buildRegisteredToolEntries(): ToolListEntry[] {
518
+ return getAllTools()
519
+ .map((tool) => toolEntryForName(tool.name))
520
+ .sort((a, b) => a.name.localeCompare(b.name));
423
521
  }
424
522
 
425
523
  async function handleToolPermissionSimulate({ body = {} }: RouteHandlerArgs) {
@@ -766,11 +864,38 @@ export const ROUTES: RouteDefinition[] = [
766
864
  requiredScopes: ["settings.read"],
767
865
  allowedPrincipalTypes: ACTOR_PRINCIPALS,
768
866
  },
769
- summary: "List tools",
867
+ summary: "List registered tools with metadata and schemas",
770
868
  description:
771
- "Return available tool names with their descriptions, risk levels, and categories.",
869
+ "Return registered tools. Without `conversationId`, returns every tool in the global registry; `tools` carries per-tool metadata (description, author-asserted risk level, category, and contributing source: core, skill, plugin, or MCP server) and `names`/`schemas` additionally cover skill tools whose manifests are present but not yet loaded, for the permission-simulator catalog. With `conversationId`, scopes the result to the tools available to that conversation as of its most recent turn (including skill/MCP tools registered over its lifecycle); 404 if no such conversation is active.",
772
870
  tags: ["tools"],
773
- handler: () => handleToolNamesList(),
871
+ queryParams: [
872
+ {
873
+ name: "conversationId",
874
+ type: "string",
875
+ required: false,
876
+ description:
877
+ "When set, scope the tool inventory to this conversation's most recent turn instead of the global registry.",
878
+ },
879
+ ],
880
+ responseBody: z.object({
881
+ names: z.array(z.string()),
882
+ schemas: z.record(z.string(), z.record(z.string(), z.unknown())),
883
+ tools: z.array(
884
+ z.object({
885
+ name: z.string(),
886
+ description: z.string(),
887
+ riskLevel: z.string(),
888
+ category: z.string(),
889
+ source: z
890
+ .string()
891
+ .describe(
892
+ 'Tool origin: "core" for built-ins, otherwise "<kind>:<id>" (e.g. "plugin:echo", "skill:my-skill", "mcp:server").',
893
+ ),
894
+ }),
895
+ ),
896
+ }),
897
+ handler: ({ queryParams }) =>
898
+ handleToolNamesList(queryParams?.conversationId),
774
899
  },
775
900
  {
776
901
  operationId: "tools_simulate_permission_post",
@@ -49,10 +49,22 @@ const slimSkillBase = {
49
49
  category: z.string(),
50
50
  };
51
51
 
52
+ // Extension that ships a skill, mirroring the tool registry's OwnerInfo.
53
+ // Plugin-resident skills carry `{ kind: "plugin", id: <plugin dir name> }` so
54
+ // clients can attribute them instead of collapsing to their kind/origin.
55
+ const skillOwnerSchema = z
56
+ .object({
57
+ kind: z.enum(["skill", "mcp", "plugin", "workspace"]),
58
+ id: z.string(),
59
+ })
60
+ .optional();
61
+
62
+ const slimSkillBaseWithOwner = { ...slimSkillBase, owner: skillOwnerSchema };
63
+
52
64
  const slimSkillSchema = z.discriminatedUnion("origin", [
53
- z.object({ ...slimSkillBase, origin: z.literal("vellum") }),
65
+ z.object({ ...slimSkillBaseWithOwner, origin: z.literal("vellum") }),
54
66
  z.object({
55
- ...slimSkillBase,
67
+ ...slimSkillBaseWithOwner,
56
68
  origin: z.literal("clawhub"),
57
69
  slug: z.string(),
58
70
  author: z.string(),
@@ -63,18 +75,18 @@ const slimSkillSchema = z.discriminatedUnion("origin", [
63
75
  version: z.string(),
64
76
  }),
65
77
  z.object({
66
- ...slimSkillBase,
78
+ ...slimSkillBaseWithOwner,
67
79
  origin: z.literal("skillssh"),
68
80
  slug: z.string(),
69
81
  sourceRepo: z.string(),
70
82
  installs: z.number(),
71
83
  audit: z.record(z.string(), partnerAuditSchema).optional(),
72
84
  }),
73
- z.object({ ...slimSkillBase, origin: z.literal("custom") }),
85
+ z.object({ ...slimSkillBaseWithOwner, origin: z.literal("custom") }),
74
86
  ]);
75
87
 
76
88
  const skillDetailSchema = z.discriminatedUnion("origin", [
77
- z.object({ ...slimSkillBase, origin: z.literal("vellum") }),
89
+ z.object({ ...slimSkillBaseWithOwner, origin: z.literal("vellum") }),
78
90
  z.object({
79
91
  ...slimSkillBase,
80
92
  origin: z.literal("clawhub"),
@@ -120,7 +132,7 @@ const skillDetailSchema = z.discriminatedUnion("origin", [
120
132
  installs: z.number(),
121
133
  audit: z.record(z.string(), partnerAuditSchema).optional(),
122
134
  }),
123
- z.object({ ...slimSkillBase, origin: z.literal("custom") }),
135
+ z.object({ ...slimSkillBaseWithOwner, origin: z.literal("custom") }),
124
136
  ]);
125
137
 
126
138
  // ---------------------------------------------------------------------------
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Unit tests for `serializeConversationSummary` — specifically the wire-level
3
+ * `groupId` normalization for surfaced conversations.
4
+ *
5
+ * Surfaced rows (`surfaced_at IS NOT NULL`) must serialize with
6
+ * `groupId: "system:all"` when their persisted group is the system
7
+ * Background/Scheduled bucket (or null), so legacy clients that bucket purely
8
+ * by `groupId` (macOS Swift app) render them in Recents without decoding
9
+ * `surfacedAt`. The persisted DB value is untouched — clearing `surfaced_at`
10
+ * (demotion) makes serialization return the original group again.
11
+ */
12
+
13
+ import { describe, expect, test } from "bun:test";
14
+
15
+ import type { ConversationRow } from "../../../memory/conversation-crud.js";
16
+ import { serializeConversationSummary } from "../conversation-serializer.js";
17
+
18
+ function makeConversationRow(
19
+ overrides: Partial<ConversationRow> = {},
20
+ ): ConversationRow {
21
+ return {
22
+ id: "conv-123",
23
+ title: "Background run",
24
+ createdAt: 1000,
25
+ updatedAt: 2000,
26
+ totalInputTokens: 0,
27
+ totalOutputTokens: 0,
28
+ totalEstimatedCost: 0,
29
+ contextSummary: null,
30
+ contextCompactedMessageCount: 0,
31
+ contextCompactedAt: null,
32
+ historyStrippedAt: null,
33
+ slackContextCompactionWatermarkTs: null,
34
+ slackContextCompactionWatermarkAt: null,
35
+ conversationType: "background",
36
+ source: "task",
37
+ memoryScopeId: "default",
38
+ originChannel: null,
39
+ originInterface: null,
40
+ forkParentConversationId: null,
41
+ forkParentMessageId: null,
42
+ isAutoTitle: 0,
43
+ scheduleJobId: null,
44
+ lastMessageAt: 3000,
45
+ archivedAt: null,
46
+ surfacedAt: null,
47
+ inferenceProfile: null,
48
+ inferenceProfileSessionId: null,
49
+ inferenceProfileExpiresAt: null,
50
+ lastNotifiedInferenceProfile: null,
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ function serialize(
56
+ conversation: ConversationRow,
57
+ groupId: string | null = null,
58
+ isPinned = false,
59
+ ) {
60
+ return serializeConversationSummary({
61
+ conversation,
62
+ displayMeta: { displayOrder: null, isPinned, groupId },
63
+ parentCache: new Map(),
64
+ isProcessing: false,
65
+ });
66
+ }
67
+
68
+ describe("serializeConversationSummary · surfaced groupId normalization", () => {
69
+ test("surfaced row with persisted system:background serializes as system:all", () => {
70
+ const summary = serialize(
71
+ makeConversationRow({ surfacedAt: 1704067200000 }),
72
+ "system:background",
73
+ );
74
+ expect(summary.groupId).toBe("system:all");
75
+ expect(summary.surfacedAt).toBe(1704067200000);
76
+ });
77
+
78
+ test("surfaced row with persisted system:scheduled serializes as system:all", () => {
79
+ const summary = serialize(
80
+ makeConversationRow({
81
+ conversationType: "scheduled",
82
+ source: "schedule",
83
+ surfacedAt: 1704067200000,
84
+ }),
85
+ "system:scheduled",
86
+ );
87
+ expect(summary.groupId).toBe("system:all");
88
+ expect(summary.surfacedAt).toBe(1704067200000);
89
+ });
90
+
91
+ test("surfaced row with null persisted group serializes as system:all", () => {
92
+ // Legacy clients re-derive a null groupId into Background/Scheduled from
93
+ // `source`, so the surfaced normalization must cover the null case too.
94
+ const summary = serialize(
95
+ makeConversationRow({ surfacedAt: 1704067200000 }),
96
+ null,
97
+ );
98
+ expect(summary.groupId).toBe("system:all");
99
+ });
100
+
101
+ test("after demotion (surfaced_at cleared) the original group comes back", () => {
102
+ const summary = serialize(
103
+ makeConversationRow({ surfacedAt: null }),
104
+ "system:background",
105
+ );
106
+ expect(summary.groupId).toBe("system:background");
107
+ expect(summary.surfacedAt).toBeUndefined();
108
+ });
109
+
110
+ test("a custom group wins over surfacing", () => {
111
+ // Mirrors web's getEffectiveGroupId precedence: user-created groups
112
+ // take priority over the surfaced promotion.
113
+ const summary = serialize(
114
+ makeConversationRow({ surfacedAt: 1704067200000 }),
115
+ "group-custom-1",
116
+ );
117
+ expect(summary.groupId).toBe("group-custom-1");
118
+ expect(summary.surfacedAt).toBe(1704067200000);
119
+ });
120
+
121
+ test("a pin wins over surfacing", () => {
122
+ const summary = serialize(
123
+ makeConversationRow({ surfacedAt: 1704067200000 }),
124
+ "system:pinned",
125
+ true,
126
+ );
127
+ expect(summary.groupId).toBe("system:pinned");
128
+ expect("isPinned" in summary && summary.isPinned).toBe(true);
129
+ });
130
+
131
+ test("non-surfaced rows pass the persisted group through untouched", () => {
132
+ expect(serialize(makeConversationRow(), null).groupId).toBeNull();
133
+ expect(serialize(makeConversationRow(), "system:scheduled").groupId).toBe(
134
+ "system:scheduled",
135
+ );
136
+ expect(serialize(makeConversationRow(), "group-custom-1").groupId).toBe(
137
+ "group-custom-1",
138
+ );
139
+ });
140
+ });
@@ -94,6 +94,37 @@ function buildForkParent(
94
94
  };
95
95
  }
96
96
 
97
+ /**
98
+ * Resolve the wire-level `groupId` for a conversation summary.
99
+ *
100
+ * Surfaced conversations (`surfaced_at IS NOT NULL`) render in the Recents
101
+ * grouping on every client, but legacy clients (the macOS Swift app) bucket
102
+ * purely by `groupId` and do not decode `surfacedAt`. Normalize the
103
+ * *serialized* `groupId` to `"system:all"` for surfaced rows so those
104
+ * clients render them in Recents without code changes — the persisted
105
+ * `group_id` is untouched, so clearing `surfaced_at` (demotion) makes
106
+ * serialization return the original group again.
107
+ *
108
+ * Mirrors web's `getEffectiveGroupId` precedence: an explicit pin
109
+ * (`system:pinned`) or a user-created custom group wins over surfacing, so
110
+ * only the system Background/Scheduled groups (and the null fallback, which
111
+ * legacy clients re-derive into those buckets from `source`) are rewritten.
112
+ */
113
+ function resolveSerializedGroupId(
114
+ conversation: ConversationRow,
115
+ persistedGroupId: string | null,
116
+ ): string | null {
117
+ if (conversation.surfacedAt == null) return persistedGroupId;
118
+ if (
119
+ persistedGroupId == null ||
120
+ persistedGroupId === "system:background" ||
121
+ persistedGroupId === "system:scheduled"
122
+ ) {
123
+ return "system:all";
124
+ }
125
+ return persistedGroupId;
126
+ }
127
+
97
128
  function buildChannelBinding(binding: ExternalConversationBinding) {
98
129
  const externalChatName =
99
130
  binding.externalChatName?.trim() ||
@@ -213,11 +244,17 @@ export function serializeConversationSummary(params: {
213
244
  displayOrder: displayMeta.displayOrder,
214
245
  }
215
246
  : {}),
216
- groupId: displayMeta?.groupId ?? null,
247
+ groupId: resolveSerializedGroupId(
248
+ conversation,
249
+ displayMeta?.groupId ?? null,
250
+ ),
217
251
  ...(forkParent ? { forkParent } : {}),
218
252
  ...(conversation.archivedAt != null
219
253
  ? { archivedAt: conversation.archivedAt }
220
254
  : {}),
255
+ ...(conversation.surfacedAt != null
256
+ ? { surfacedAt: conversation.surfacedAt }
257
+ : {}),
221
258
  ...(conversation.inferenceProfile != null
222
259
  ? { inferenceProfile: conversation.inferenceProfile }
223
260
  : {}),
@@ -2,7 +2,7 @@
2
2
  * Shared outbound verification action logic.
3
3
  *
4
4
  * These pure functions encapsulate the business logic for starting, resending,
5
- * and cancelling outbound verification flows (Telegram, voice, Slack).
5
+ * and cancelling outbound verification flows (Telegram, voice, Slack, email).
6
6
  * They return transport-agnostic result objects and are consumed by both the
7
7
  * message handler (config-channels.ts) and the HTTP route layer (channel-verification-routes.ts).
8
8
  */
@@ -26,6 +26,7 @@ import {
26
26
  updateSessionStatus,
27
27
  } from "./channel-verification-service.js";
28
28
  import {
29
+ composeVerificationEmail,
29
30
  composeVerificationSlack,
30
31
  composeVerificationTelegram,
31
32
  GUARDIAN_VERIFY_TEMPLATE_KEYS,
@@ -52,6 +53,8 @@ export const DESTINATION_RATE_WINDOW_MS = 3_600_000;
52
53
  /** Session TTL in seconds (matches challenge TTL of 10 minutes). */
53
54
  const SESSION_TTL_SECONDS = 600;
54
55
 
56
+ const EMAIL_VERIFICATION_SUBJECT = "Vellum Assistant Guardian Verification";
57
+
55
58
  // ---------------------------------------------------------------------------
56
59
  // Telegram destination classification
57
60
  // ---------------------------------------------------------------------------
@@ -122,6 +125,8 @@ interface OutboundActionResult {
122
125
  * reach the gateway. The daemon HTTP route handler calls
123
126
  * deliverVerificationSlack() after receiving this payload. */
124
127
  _pendingSlackDm?: { userId: string; text: string; assistantId: string };
128
+ /** Internal: email delivery payload for the caller to dispatch (same pattern as Slack). */
129
+ _pendingEmail?: { to: string; text: string; subject: string; assistantId: string };
125
130
  }
126
131
 
127
132
  // ---------------------------------------------------------------------------
@@ -512,6 +517,80 @@ export function deliverVerificationSlack(
512
517
  deliverVerificationSlackAsync(userId, text, assistantId);
513
518
  }
514
519
 
520
+ // ---------------------------------------------------------------------------
521
+ // Email delivery helper
522
+ // ---------------------------------------------------------------------------
523
+
524
+ /**
525
+ * Deliver a verification email via the platform email send API.
526
+ * Fire-and-forget wrapper for use in the daemon process (HTTP route handlers).
527
+ */
528
+ export function deliverVerificationEmail(
529
+ to: string,
530
+ text: string,
531
+ subject: string,
532
+ _assistantId: string,
533
+ ): void {
534
+ (async () => {
535
+ try {
536
+ const { VellumPlatformClient } = await import(
537
+ "../platform/client.js"
538
+ );
539
+ const client = await VellumPlatformClient.create();
540
+ if (!client?.platformAssistantId) {
541
+ log.error("Cannot deliver verification email: platform client not configured");
542
+ return;
543
+ }
544
+
545
+ const listResponse = await client.fetch(
546
+ `/v1/assistants/${client.platformAssistantId}/email-addresses/`,
547
+ );
548
+ if (!listResponse.ok) {
549
+ log.error({ status: listResponse.status }, "Failed to list email addresses for verification");
550
+ return;
551
+ }
552
+ const listData = (await listResponse.json()) as {
553
+ results: { address: string }[];
554
+ };
555
+ const addresses = listData.results ?? [];
556
+ if (addresses.length === 0) {
557
+ log.error("No email address registered — cannot deliver verification email");
558
+ return;
559
+ }
560
+ const fromAddress = addresses[0].address;
561
+
562
+ const { markdownToEmailHtml } = await import(
563
+ "../email/html-renderer.js"
564
+ );
565
+ const html = markdownToEmailHtml(text);
566
+
567
+ const response = await client.fetch("/v1/runtime-proxy/email/send/", {
568
+ method: "POST",
569
+ headers: { "Content-Type": "application/json" },
570
+ body: JSON.stringify({
571
+ to: [to],
572
+ from_address: fromAddress,
573
+ text,
574
+ subject,
575
+ html,
576
+ }),
577
+ });
578
+
579
+ if (response.ok) {
580
+ log.info({ to }, "Verification email delivered");
581
+ } else {
582
+ const respBody = await response.json().catch(() => ({}));
583
+ log.error(
584
+ { to, status: response.status, respBody },
585
+ "Failed to deliver verification email",
586
+ );
587
+ }
588
+ } catch (err) {
589
+ log.error({ err, to }, "Failed to deliver verification email");
590
+ }
591
+ })();
592
+ }
593
+
515
594
  function startOutboundSlack(
516
595
  destination: string | undefined,
517
596
  assistantId: string,
@@ -644,13 +723,35 @@ function startOutboundEmail(
644
723
  verificationPurpose: "guardian",
645
724
  });
646
725
 
726
+ const emailBody = composeVerificationEmail(
727
+ GUARDIAN_VERIFY_TEMPLATE_KEYS.EMAIL_CHALLENGE_REQUEST,
728
+ {
729
+ code: sessionResult.secret,
730
+ expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
731
+ },
732
+ );
733
+
734
+ const now = Date.now();
735
+ const nextResendAt = now + RESEND_COOLDOWN_MS;
736
+ const sendCount = 1;
737
+
738
+ updateSessionDelivery(sessionResult.sessionId, now, sendCount, nextResendAt);
739
+
647
740
  return {
648
741
  success: true,
649
742
  verificationSessionId: sessionResult.sessionId,
650
743
  secret: sessionResult.secret,
651
744
  expiresAt: sessionResult.expiresAt,
745
+ nextResendAt,
746
+ sendCount,
652
747
  channel,
653
748
  originConversationId,
749
+ _pendingEmail: {
750
+ to: normalizedEmail,
751
+ text: emailBody,
752
+ subject: EMAIL_VERIFICATION_SUBJECT,
753
+ assistantId,
754
+ },
654
755
  };
655
756
  }
656
757
 
@@ -851,12 +952,56 @@ export function resendOutbound(
851
952
  originConversationId,
852
953
  _pendingSlackDm: { userId: destination, text: slackBody, assistantId },
853
954
  };
955
+ } else if (channel === "email") {
956
+ const newSession = createOutboundSession({
957
+ channel,
958
+ expectedExternalUserId: destination,
959
+ expectedChatId: destination,
960
+ identityBindingStatus: "bound",
961
+ destinationAddress: destination,
962
+ verificationPurpose: "guardian",
963
+ });
964
+
965
+ const emailBody = composeVerificationEmail(
966
+ GUARDIAN_VERIFY_TEMPLATE_KEYS.EMAIL_RESEND,
967
+ {
968
+ code: newSession.secret,
969
+ expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
970
+ },
971
+ );
972
+
973
+ const now = Date.now();
974
+ const newSendCount = currentSendCount + 1;
975
+ const nextResendAt = now + RESEND_COOLDOWN_MS;
976
+
977
+ updateSessionDelivery(
978
+ newSession.sessionId,
979
+ now,
980
+ newSendCount,
981
+ nextResendAt,
982
+ );
983
+
984
+ return {
985
+ success: true,
986
+ verificationSessionId: newSession.sessionId,
987
+ secret: newSession.secret,
988
+ nextResendAt,
989
+ sendCount: newSendCount,
990
+ channel,
991
+ originConversationId,
992
+ _pendingEmail: {
993
+ to: destination,
994
+ text: emailBody,
995
+ subject: EMAIL_VERIFICATION_SUBJECT,
996
+ assistantId,
997
+ },
998
+ };
854
999
  }
855
1000
 
856
1001
  return {
857
1002
  success: false,
858
1003
  error: "unsupported_channel",
859
- message: `Resend is only supported for Telegram, phone, and Slack. Got: ${channel}`,
1004
+ message: `Resend is only supported for Telegram, phone, Slack, and email. Got: ${channel}`,
860
1005
  channel,
861
1006
  };
862
1007
  }