@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
@@ -4,6 +4,8 @@
4
4
  * Exposes spawn, steer, cancel, close, sessions, and permission operations
5
5
  * over HTTP and IPC.
6
6
  */
7
+ import { randomUUID } from "node:crypto";
8
+
7
9
  import { and, desc, eq, inArray, notInArray } from "drizzle-orm";
8
10
  import { z } from "zod";
9
11
 
@@ -13,16 +15,20 @@ import { prepareAgentEnv } from "../../acp/prepare-agent-env.js";
13
15
  import { formatResolveFailure } from "../../acp/resolve-agent.js";
14
16
  import { AcpResumeError } from "../../acp/session-manager.js";
15
17
  import type { AcpSessionState } from "../../acp/types.js";
18
+ import { getConfig } from "../../config/loader.js";
16
19
  import { getDb } from "../../memory/db-connection.js";
17
20
  import { rawChanges } from "../../memory/raw-query.js";
18
21
  import { acpSessionHistory } from "../../memory/schema.js";
22
+ import type { UserDecision } from "../../permissions/types.js";
19
23
  import { broadcastMessage } from "../../runtime/assistant-event-hub.js";
20
24
  import { getLogger } from "../../util/logger.js";
21
25
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
26
+ import * as pendingInteractions from "../pending-interactions.js";
22
27
  import {
23
28
  BadRequestError,
24
29
  ConflictError,
25
30
  FailedDependencyError,
31
+ ForbiddenError,
26
32
  NotFoundError,
27
33
  } from "./errors.js";
28
34
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
@@ -53,7 +59,109 @@ type SessionEntry = z.infer<typeof sessionEntrySchema>;
53
59
  // Handlers
54
60
  // ---------------------------------------------------------------------------
55
61
 
56
- async function spawnSession({ body }: RouteHandlerArgs) {
62
+ /**
63
+ * Surface a high-risk confirmation for an ACP route that starts a host agent
64
+ * subprocess (`POST /v1/acp/spawn`, and the resume branch of
65
+ * `POST /v1/acp/:id/steer`) and resolve to the guardian's decision.
66
+ *
67
+ * Unlike the `acp_spawn` / `acp_steer` skill tools (which are dispatched
68
+ * through `ToolExecutor`/`PermissionChecker` and so inherit the descriptors'
69
+ * `risk: "high"` approval prompt), these HTTP/IPC routes reach the ACP
70
+ * session manager directly. A spawned ACP agent is a host subprocess with
71
+ * advertised filesystem + terminal access whose in-session permission
72
+ * requests are auto-allowed, so starting one is exactly the high-risk action
73
+ * the descriptors gate. Without this prompt a caller holding only
74
+ * `chat.write` could start one with no human in the loop — directly via
75
+ * spawn (arbitrary `cwd`), or by resuming a terminal session via steer (its
76
+ * persisted `cwd`, enumerable through `GET /v1/acp/sessions`) (ATL-822).
77
+ *
78
+ * The confirmation is registered with `directResolve` so `POST /v1/confirm`
79
+ * resolves it without a live `Conversation` object. That route requires the
80
+ * bound guardian (`approval.write` + `requireGuardian`) — a strictly higher
81
+ * bar than the `chat.write` these routes demand — so the `chat.write` caller
82
+ * cannot self-approve. Timeout and client disconnect both deny, matching the
83
+ * tool path's "no interactive client → do not run" posture.
84
+ */
85
+ function awaitRouteApproval(args: {
86
+ toolName: string;
87
+ input: Record<string, unknown>;
88
+ conversationId: string;
89
+ signal?: AbortSignal;
90
+ }): Promise<UserDecision> {
91
+ const { toolName, input, conversationId, signal } = args;
92
+ const requestId = randomUUID();
93
+ // Mirror the interactive prompt timeout; fall back to the schema default
94
+ // (300s) if the timeouts block is somehow absent so the gate never wedges.
95
+ const timeoutMs = (getConfig().timeouts?.permissionTimeoutSec ?? 300) * 1000;
96
+
97
+ return new Promise<UserDecision>((resolve) => {
98
+ let settled = false;
99
+ const settle = (
100
+ decision: UserDecision,
101
+ state: "approved" | "rejected" | "cancelled",
102
+ ) => {
103
+ if (settled) return;
104
+ settled = true;
105
+ clearTimeout(timer);
106
+ signal?.removeEventListener("abort", onAbort);
107
+ // Idempotent: the `/v1/confirm` directResolve path already removed the
108
+ // entry before invoking us, so this is a no-op there and only fires
109
+ // `interaction_resolved` for the timeout/abort paths.
110
+ pendingInteractions.resolve(requestId, state);
111
+ resolve(decision);
112
+ };
113
+
114
+ const timer = setTimeout(() => {
115
+ log.warn(
116
+ { requestId, toolName, conversationId },
117
+ "ACP route approval timed out — denying",
118
+ );
119
+ settle("deny", "cancelled");
120
+ }, timeoutMs);
121
+
122
+ const onAbort = () => settle("deny", "cancelled");
123
+ if (signal?.aborted) {
124
+ clearTimeout(timer);
125
+ resolve("deny");
126
+ return;
127
+ }
128
+ signal?.addEventListener("abort", onAbort, { once: true });
129
+
130
+ pendingInteractions.register(requestId, {
131
+ conversationId,
132
+ kind: "confirmation",
133
+ confirmationDetails: {
134
+ toolName,
135
+ input,
136
+ riskLevel: "high",
137
+ executionTarget: "host",
138
+ allowlistOptions: [],
139
+ scopeOptions: [],
140
+ persistentDecisionsAllowed: false,
141
+ },
142
+ directResolve: (decision) =>
143
+ settle(decision, decision === "allow" ? "approved" : "rejected"),
144
+ });
145
+
146
+ broadcastMessage(
147
+ {
148
+ type: "confirmation_request",
149
+ requestId,
150
+ toolName,
151
+ input,
152
+ riskLevel: "high",
153
+ executionTarget: "host",
154
+ allowlistOptions: [],
155
+ scopeOptions: [],
156
+ conversationId,
157
+ persistentDecisionsAllowed: false,
158
+ },
159
+ conversationId,
160
+ );
161
+ });
162
+ }
163
+
164
+ async function spawnSession({ body, abortSignal }: RouteHandlerArgs) {
57
165
  const agent = body?.agent as string | undefined;
58
166
  const task = body?.task as string | undefined;
59
167
  const conversationId = body?.conversationId as string | undefined;
@@ -63,6 +171,22 @@ async function spawnSession({ body }: RouteHandlerArgs) {
63
171
  throw new BadRequestError("agent, task, and conversationId are required");
64
172
  }
65
173
 
174
+ // High-risk approval gate. Block BEFORE any side effects — resolution can
175
+ // trigger a `bun` global install (auto-install.ts) and `manager.spawn`
176
+ // launches the host subprocess — so an unapproved request mutates nothing.
177
+ const decision = await awaitRouteApproval({
178
+ toolName: "acp_spawn",
179
+ input: { agent, task, cwd },
180
+ conversationId,
181
+ signal: abortSignal,
182
+ });
183
+ if (decision !== "allow") {
184
+ throw new ForbiddenError(
185
+ "Spawning an ACP coding agent requires guardian approval, which was " +
186
+ "not granted.",
187
+ );
188
+ }
189
+
66
190
  // Resolve the agent, silently auto-installing a missing allowlisted
67
191
  // adapter binary (see acp/auto-install.ts). Shared with the skill-tool
68
192
  // path in tools/acp/spawn.ts; only the transport mapping differs.
@@ -117,6 +241,43 @@ async function steerSession({ pathParams, body }: RouteHandlerArgs) {
117
241
  // broadcastMessage plays the sender role spawnSession gives it, so
118
242
  // connected clients render the session.
119
243
  const manager = getAcpSessionManager();
244
+
245
+ // Resume re-spawns the host agent subprocess, so it crosses the same
246
+ // high-risk boundary as spawn and needs guardian approval (ATL-822). A
247
+ // steer of a session still active in memory only redirects an
248
+ // already-running, already-approved process, so it is left unprompted —
249
+ // matching "gate before spawning". A resume only happens when the id is
250
+ // absent from memory AND a resumable history row (non-null cwd) exists.
251
+ if (!manager.getActiveAndPendingIds().includes(id)) {
252
+ const resumable = getDb()
253
+ .select({
254
+ cwd: acpSessionHistory.cwd,
255
+ agentId: acpSessionHistory.agentId,
256
+ parentConversationId: acpSessionHistory.parentConversationId,
257
+ })
258
+ .from(acpSessionHistory)
259
+ .where(eq(acpSessionHistory.id, id))
260
+ .get();
261
+ if (resumable?.cwd != null) {
262
+ // Guardian approval is an out-of-band human action that can take far
263
+ // longer than a client's ack timeout (the macOS client uses 10s; the
264
+ // approval window is permissionTimeoutSec, default 300s). Blocking the
265
+ // HTTP response here would surface as a client transport failure even
266
+ // on an eventual approval. Acknowledge immediately and run approval +
267
+ // resume in the background, streaming via SSE like any other ACP
268
+ // activity. The request abortSignal is deliberately NOT threaded into
269
+ // the wait — the request is already complete.
270
+ void approveThenResume({
271
+ id,
272
+ instruction,
273
+ agentId: resumable.agentId,
274
+ cwd: resumable.cwd,
275
+ conversationId: resumable.parentConversationId,
276
+ });
277
+ return { acpSessionId: id, steered: false, approvalPending: true };
278
+ }
279
+ }
280
+
120
281
  try {
121
282
  const { resumed } = await manager.steerOrResume(
122
283
  id,
@@ -138,6 +299,81 @@ async function steerSession({ pathParams, body }: RouteHandlerArgs) {
138
299
  }
139
300
  }
140
301
 
302
+ /**
303
+ * Background worker for an approval-gated ACP resume initiated over the steer
304
+ * route. Awaits the guardian decision (see `awaitRouteApproval`), then resumes
305
+ * via the session manager so the agent's output streams back over SSE. A
306
+ * denial, timeout, or resume failure is surfaced as an `acp_session_error` on
307
+ * the parent conversation so the client isn't left with an optimistic steer
308
+ * row and no follow-up. Never throws — it owns the request lifecycle after the
309
+ * route has already acked.
310
+ */
311
+ async function approveThenResume(args: {
312
+ id: string;
313
+ instruction: string;
314
+ agentId: string;
315
+ cwd: string;
316
+ conversationId: string;
317
+ }): Promise<void> {
318
+ const decision = await awaitRouteApproval({
319
+ toolName: "acp_steer",
320
+ input: {
321
+ acp_session_id: args.id,
322
+ instruction: args.instruction,
323
+ agent: args.agentId,
324
+ cwd: args.cwd,
325
+ },
326
+ conversationId: args.conversationId,
327
+ });
328
+
329
+ if (decision !== "allow") {
330
+ log.info(
331
+ { acpSessionId: args.id, conversationId: args.conversationId },
332
+ "ACP resume approval not granted — skipping resume",
333
+ );
334
+ broadcastMessage(
335
+ {
336
+ type: "acp_session_error",
337
+ // Key the error by the daemon session id (the value the steer route
338
+ // accepts and ACP SSE consumers index by — see registerSession, where
339
+ // state.id is the broadcast acpSessionId), NOT the persisted protocol
340
+ // id. A denied resume never emits acp_session_spawned to map the
341
+ // protocol id, so a protocol-keyed error would be dropped.
342
+ acpSessionId: args.id,
343
+ error: "Resume was not approved.",
344
+ },
345
+ args.conversationId,
346
+ );
347
+ return;
348
+ }
349
+
350
+ try {
351
+ await getAcpSessionManager().steerOrResume(
352
+ args.id,
353
+ args.instruction,
354
+ broadcastMessage,
355
+ );
356
+ } catch (err) {
357
+ const message =
358
+ err instanceof AcpResumeError || err instanceof Error
359
+ ? err.message
360
+ : String(err);
361
+ log.warn(
362
+ { acpSessionId: args.id, conversationId: args.conversationId, err },
363
+ "Approved ACP resume failed",
364
+ );
365
+ broadcastMessage(
366
+ {
367
+ type: "acp_session_error",
368
+ // Daemon session id (route + SSE key), not the protocol id — see above.
369
+ acpSessionId: args.id,
370
+ error: message,
371
+ },
372
+ args.conversationId,
373
+ );
374
+ }
375
+ }
376
+
141
377
  async function cancelSession({ pathParams }: RouteHandlerArgs) {
142
378
  const id = pathParams?.id as string;
143
379
  const manager = getAcpSessionManager();
@@ -276,7 +512,10 @@ export const ROUTES: RouteDefinition[] = [
276
512
  "Send a steering instruction to an ACP session. Sessions no longer " +
277
513
  "in memory (completed, or lost to a daemon restart) are " +
278
514
  "transparently resumed from persisted history first, when the agent " +
279
- "supports ACP session loading.",
515
+ "supports ACP session loading. Resuming a terminal session re-spawns " +
516
+ "the host agent, so it requires guardian approval: the route acks " +
517
+ "immediately with approvalPending=true and performs the resume in the " +
518
+ "background once approved, streaming results over SSE.",
280
519
  tags: ["acp"],
281
520
  requestBody: z.object({
282
521
  instruction: z.string(),
@@ -290,6 +529,13 @@ export const ROUTES: RouteDefinition[] = [
290
529
  .describe(
291
530
  "True when the session was resumed from persisted history before steering.",
292
531
  ),
532
+ approvalPending: z
533
+ .boolean()
534
+ .optional()
535
+ .describe(
536
+ "True when the steer triggered a guardian-approval-gated resume " +
537
+ "that will run asynchronously; watch SSE for the outcome.",
538
+ ),
293
539
  }),
294
540
  },
295
541
  {
@@ -114,7 +114,7 @@ async function handleBrowserTabs({ body = {} }: RouteHandlerArgs) {
114
114
  }
115
115
  } else if (targetClientId) {
116
116
  // Only scope-clear the targeted client's pin. With the per-(conversationId,
117
- // clientId) pin store (#31361), passing no clientId to clearPinnedTab
117
+ // clientId) pin store, passing no clientId to clearPinnedTab
118
118
  // would wipe pins for *every* connected client on this conversation
119
119
  // and break their routing — so we skip the clear entirely when the
120
120
  // caller didn't explicitly target a client. The stale pin (if any)
@@ -23,6 +23,7 @@ import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
23
23
  import { revokePendingSessions } from "../channel-verification-service.js";
24
24
  import {
25
25
  cancelOutbound,
26
+ deliverVerificationEmail,
26
27
  deliverVerificationSlack,
27
28
  normalizeTelegramDestination,
28
29
  resendOutbound,
@@ -139,11 +140,15 @@ export async function handleCreateVerificationSession({
139
140
  verificationRateLimiter.recordFailure(rateLimitKey);
140
141
  }
141
142
 
142
- // Dispatch Slack DM delivery from the daemon process (not sandboxed).
143
+ // Dispatch delivery from the daemon process (not sandboxed).
143
144
  if (result._pendingSlackDm) {
144
145
  const { userId, text, assistantId: aid } = result._pendingSlackDm;
145
146
  deliverVerificationSlack(userId, text, aid);
146
147
  }
148
+ if (result._pendingEmail) {
149
+ const { to, text, subject, assistantId: aid } = result._pendingEmail;
150
+ deliverVerificationEmail(to, text, subject, aid);
151
+ }
147
152
 
148
153
  if (!result.success) {
149
154
  if (result.error === "rate_limited") {
@@ -157,8 +162,8 @@ export async function handleCreateVerificationSession({
157
162
  );
158
163
  }
159
164
 
160
- // Strip internal field from the response
161
- const { _pendingSlackDm: _, ...publicResult } = result;
165
+ // Strip internal fields from the response
166
+ const { _pendingSlackDm: _, _pendingEmail: __, ...publicResult } = result;
162
167
  return publicResult;
163
168
  }
164
169
 
@@ -204,11 +209,15 @@ export async function handleResendVerificationSession({
204
209
 
205
210
  const result = resendOutbound({ channel, originConversationId });
206
211
 
207
- // Dispatch Slack DM delivery from the daemon process (not sandboxed).
212
+ // Dispatch delivery from the daemon process (not sandboxed).
208
213
  if (result._pendingSlackDm) {
209
214
  const { userId, text, assistantId: aid } = result._pendingSlackDm;
210
215
  deliverVerificationSlack(userId, text, aid);
211
216
  }
217
+ if (result._pendingEmail) {
218
+ const { to, text, subject, assistantId: aid } = result._pendingEmail;
219
+ deliverVerificationEmail(to, text, subject, aid);
220
+ }
212
221
 
213
222
  if (!result.success) {
214
223
  if (result.error === "rate_limited") {
@@ -221,7 +230,7 @@ export async function handleResendVerificationSession({
221
230
  );
222
231
  }
223
232
 
224
- const { _pendingSlackDm: _, ...publicResult } = result;
233
+ const { _pendingSlackDm: _, _pendingEmail: __, ...publicResult } = result;
225
234
  return publicResult;
226
235
  }
227
236
 
@@ -11,8 +11,6 @@
11
11
 
12
12
  import { z } from "zod";
13
13
 
14
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
15
- import { getConfigReadOnly } from "../../config/loader.js";
16
14
  import { getDb } from "../../memory/db-connection.js";
17
15
  import {
18
16
  createConnection,
@@ -29,18 +27,8 @@ import {
29
27
  import { setSecureKeyAsync } from "../../security/secure-keys.js";
30
28
  import { getLogger } from "../../util/logger.js";
31
29
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
32
- import { BadRequestError } from "./errors.js";
33
30
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
34
31
 
35
- function requireFeatureFlag() {
36
- const config = getConfigReadOnly();
37
- if (!isAssistantFeatureFlagEnabled("chatgpt-subscription-auth", config)) {
38
- throw new BadRequestError(
39
- "ChatGPT subscription auth is not enabled for this assistant.",
40
- );
41
- }
42
- }
43
-
44
32
  const log = getLogger("chatgpt-subscription-auth");
45
33
 
46
34
  // ---------------------------------------------------------------------------
@@ -87,7 +75,6 @@ function cleanupExpiredEntries(): void {
87
75
  // ---------------------------------------------------------------------------
88
76
 
89
77
  async function handleStartAuth(_args: RouteHandlerArgs) {
90
- requireFeatureFlag();
91
78
  cleanupExpiredEntries();
92
79
 
93
80
  const codeVerifier = generateCodeVerifier();
@@ -115,7 +102,6 @@ async function handleStartAuth(_args: RouteHandlerArgs) {
115
102
  }
116
103
 
117
104
  async function handleExchange(args: RouteHandlerArgs) {
118
- requireFeatureFlag();
119
105
  const { code, state } = args.body as { code: string; state: string };
120
106
 
121
107
  const pending = pendingAuths.get(state);
@@ -15,20 +15,13 @@
15
15
 
16
16
  import { z } from "zod";
17
17
 
18
- import {
19
- getConfig,
20
- invalidateConfigCache,
21
- loadRawConfig,
22
- saveRawConfig,
23
- } from "../../config/loader.js";
24
- import type { AssistantConfig } from "../../config/schema.js";
18
+ import { getConfig } from "../../config/loader.js";
25
19
  import { getMemoryCheckpoint } from "../../memory/checkpoints.js";
26
20
  import {
27
21
  getMessageRoleStatsByConversation,
28
22
  listConversationsBySource,
29
23
  } from "../../memory/conversation-queries.js";
30
24
  import {
31
- cancelPendingAutomaticConsolidationJobs,
32
25
  enqueueMemoryJob,
33
26
  hasActiveJobOfType,
34
27
  MEMORY_V2_CONSOLIDATION_JOB_TRIGGERS,
@@ -37,15 +30,12 @@ import { GRAPH_MAINTENANCE_CHECKPOINTS } from "../../memory/jobs-worker.js";
37
30
  import { getUsageCostForConversationWindow } from "../../memory/llm-usage-store.js";
38
31
  import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../memory/v2/constants.js";
39
32
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
40
- import { BadRequestError, InternalError } from "./errors.js";
33
+ import { BadRequestError } from "./errors.js";
41
34
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
42
35
 
43
36
  function isConsolidationAvailable(): boolean {
44
- return getConfig().memory.v2.enabled;
45
- }
46
-
47
- function isAutomaticConsolidationEnabled(config: AssistantConfig): boolean {
48
- return config.memory.v2.enabled && config.memory.v2.consolidation_enabled;
37
+ const config = getConfig();
38
+ return config.memory.enabled !== false && config.memory.v2.enabled;
49
39
  }
50
40
 
51
41
  function consolidationIntervalMs(): number {
@@ -63,8 +53,8 @@ function readLastRunAt(): number | null {
63
53
 
64
54
  function readConsolidationConfigResponse() {
65
55
  const config = getConfig();
66
- const available = config.memory.v2.enabled;
67
- const enabled = isAutomaticConsolidationEnabled(config);
56
+ const available = config.memory.enabled !== false && config.memory.v2.enabled;
57
+ const enabled = available;
68
58
  const intervalMs = consolidationIntervalMs();
69
59
  const lastRunAt = readLastRunAt();
70
60
  const nextRunAt =
@@ -79,43 +69,6 @@ function readConsolidationConfigResponse() {
79
69
  };
80
70
  }
81
71
 
82
- function handleUpdateConfig(body: Record<string, unknown>) {
83
- const consolidationPatch: Record<string, unknown> = {};
84
- const disablingAutomaticConsolidation = body.enabled === false;
85
- if ("enabled" in body && typeof body.enabled === "boolean") {
86
- consolidationPatch.consolidation_enabled = body.enabled;
87
- }
88
-
89
- try {
90
- const raw = loadRawConfig() as Record<string, unknown>;
91
- const memory =
92
- raw.memory != null && typeof raw.memory === "object"
93
- ? (raw.memory as Record<string, unknown>)
94
- : {};
95
- const v2 =
96
- memory.v2 != null && typeof memory.v2 === "object"
97
- ? (memory.v2 as Record<string, unknown>)
98
- : {};
99
-
100
- raw.memory = {
101
- ...memory,
102
- v2: {
103
- ...v2,
104
- ...consolidationPatch,
105
- },
106
- };
107
- saveRawConfig(raw);
108
- invalidateConfigCache();
109
- if (disablingAutomaticConsolidation) {
110
- cancelPendingAutomaticConsolidationJobs();
111
- }
112
- } catch {
113
- throw new InternalError("Failed to save consolidation config");
114
- }
115
-
116
- return readConsolidationConfigResponse();
117
- }
118
-
119
72
  // ---------------------------------------------------------------------------
120
73
  // Shared ROUTES
121
74
  // ---------------------------------------------------------------------------
@@ -145,35 +98,6 @@ export const ROUTES: RouteDefinition[] = [
145
98
  return readConsolidationConfigResponse();
146
99
  },
147
100
  },
148
- {
149
- operationId: "updateConsolidationConfig",
150
- endpoint: "consolidation/config",
151
- method: "PUT",
152
- policy: {
153
- requiredScopes: ["settings.write"],
154
- allowedPrincipalTypes: ACTOR_PRINCIPALS,
155
- },
156
- summary: "Update consolidation config",
157
- description: "Update the memory v2 consolidation schedule configuration.",
158
- tags: ["consolidation"],
159
- requestBody: z.object({
160
- enabled: z
161
- .boolean()
162
- .optional()
163
- .describe("Enable or disable automatic consolidation"),
164
- }),
165
- responseBody: z.object({
166
- available: z.boolean(),
167
- enabled: z.boolean(),
168
- intervalMs: z.number(),
169
- nextRunAt: z.number().nullable(),
170
- lastRunAt: z.number().nullable(),
171
- success: z.boolean(),
172
- }),
173
- handler: async ({ body = {} }: RouteHandlerArgs) => {
174
- return handleUpdateConfig(body);
175
- },
176
- },
177
101
  {
178
102
  operationId: "runConsolidationNow",
179
103
  endpoint: "consolidation/run-now",
@@ -133,6 +133,12 @@ export const conversationSummarySchema = z.object({
133
133
  groupId: z.string().nullable(),
134
134
  forkParent: forkParentSchema.optional(),
135
135
  archivedAt: z.number().optional(),
136
+ /**
137
+ * Epoch-ms timestamp set when a background/scheduled conversation was
138
+ * explicitly promoted ("surfaced") into the Recents sidebar grouping via
139
+ * `POST /v1/conversations/:id/surface`. Absent when not surfaced.
140
+ */
141
+ surfacedAt: z.number().optional(),
136
142
  inferenceProfile: z.string().optional(),
137
143
  /**
138
144
  * True when the agent loop is currently mid-turn for this conversation.
@@ -11,6 +11,7 @@
11
11
  * DELETE /v1/conversations/:id — delete a single conversation
12
12
  * POST /v1/conversations/:id/archive — archive a conversation
13
13
  * POST /v1/conversations/:id/unarchive — restore an archived conversation
14
+ * POST /v1/conversations/:id/surface — promote to / demote from Recents
14
15
  * POST /v1/conversations/:id/cancel — cancel generation
15
16
  * POST /v1/conversations/:id/undo — undo last message
16
17
  * POST /v1/conversations/:id/regenerate — regenerate last assistant response
@@ -36,6 +37,7 @@ import {
36
37
  deleteConversation,
37
38
  forkConversation as forkConversationInStore,
38
39
  getConversation,
40
+ setConversationSurfaced,
39
41
  unarchiveConversation,
40
42
  updateConversationTitle,
41
43
  wipeConversation,
@@ -360,6 +362,43 @@ function handleUnarchiveConversation({
360
362
  return { ok: true, conversationId: resolvedId };
361
363
  }
362
364
 
365
+ /**
366
+ * Set or clear the `surfacedAt` promotion marker on a conversation.
367
+ *
368
+ * Surfacing is the explicit opt-in that makes a background/scheduled
369
+ * conversation appear in the default conversation listing (and therefore the
370
+ * Recents sidebar grouping). It is never set automatically — product flows
371
+ * call this when a background conversation deserves foreground visibility
372
+ * (e.g. the user sent a follow-up message in it).
373
+ */
374
+ function handleSurfaceConversation({
375
+ pathParams = {},
376
+ body = {},
377
+ headers,
378
+ }: RouteHandlerArgs) {
379
+ if (typeof body.surfaced !== "boolean") {
380
+ throw new BadRequestError("Missing surfaced boolean");
381
+ }
382
+ const resolvedId = resolveOrThrow(pathParams.id!);
383
+ const result = setConversationSurfaced(resolvedId, body.surfaced);
384
+ if (!result) {
385
+ throw new NotFoundError(`Conversation ${pathParams.id} not found`);
386
+ }
387
+ // Shape-changing for the default list (row appears in / disappears from
388
+ // the standard listing), so publish with a shape-changing reason — web
389
+ // refetches the paginated list, macOS gets the legacy typed broadcast.
390
+ publishConversationListAndMetadataChanged(
391
+ "reordered",
392
+ resolvedId,
393
+ headers?.["x-vellum-client-id"]?.trim() || undefined,
394
+ );
395
+ return {
396
+ ok: true,
397
+ conversationId: resolvedId,
398
+ surfacedAt: result.surfacedAt,
399
+ };
400
+ }
401
+
363
402
  function handleCancelGeneration({ pathParams = {} }: RouteHandlerArgs) {
364
403
  const resolvedId = resolveConversationId(pathParams.id!) ?? pathParams.id!;
365
404
  const cancelled = cancelGeneration(resolvedId);
@@ -656,6 +695,38 @@ export const ROUTES: RouteDefinition[] = [
656
695
  }),
657
696
  handler: handleUnarchiveConversation,
658
697
  },
698
+ {
699
+ operationId: "surfaceConversation",
700
+ endpoint: "conversations/:id/surface",
701
+ method: "POST",
702
+ policy: {
703
+ requiredScopes: ["chat.write"],
704
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
705
+ },
706
+ summary: "Surface a conversation into Recents",
707
+ description:
708
+ "Explicitly promote a background or scheduled conversation into the " +
709
+ "default conversation listing (the Recents sidebar grouping), or demote " +
710
+ "it with surfaced=false. Conversations are never surfaced automatically.",
711
+ tags: ["conversations"],
712
+ pathParams: [{ name: "id", type: "uuid" }],
713
+ requestBody: z.object({
714
+ surfaced: z
715
+ .boolean()
716
+ .describe(
717
+ "true to surface the conversation into Recents, false to clear the promotion.",
718
+ ),
719
+ }),
720
+ responseBody: z.object({
721
+ ok: z.boolean(),
722
+ conversationId: z.string(),
723
+ surfacedAt: z
724
+ .number()
725
+ .nullable()
726
+ .describe("Epoch-ms timestamp of the promotion, or null when cleared."),
727
+ }),
728
+ handler: handleSurfaceConversation,
729
+ },
659
730
  {
660
731
  operationId: "cancelConversationGeneration",
661
732
  endpoint: "conversations/:id/cancel",