@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
@@ -0,0 +1,147 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type { Message, ToolDefinition } from "../../types.js";
4
+ import {
5
+ mapNeutralToolChoice,
6
+ OpenAIChatCompletionsProvider,
7
+ } from "../chat-completions-provider.js";
8
+ import { mapNeutralToolChoiceForResponses } from "../responses-provider.js";
9
+
10
+ const TOOLS: ToolDefinition[] = [
11
+ {
12
+ name: "bash",
13
+ description: "Run a shell command",
14
+ input_schema: { type: "object", properties: {} },
15
+ },
16
+ ];
17
+
18
+ const USER_MESSAGE: Message[] = [
19
+ { role: "user", content: [{ type: "text", text: "hi" }] },
20
+ ];
21
+
22
+ /**
23
+ * Stub the SDK client so we can capture the outgoing chat.completions.create
24
+ * params without hitting the network.
25
+ */
26
+ function stubChatProvider(): {
27
+ provider: OpenAIChatCompletionsProvider;
28
+ requests: Array<Record<string, unknown>>;
29
+ } {
30
+ const provider = new OpenAIChatCompletionsProvider("test-key", "test-model");
31
+ const requests: Array<Record<string, unknown>> = [];
32
+ (provider as unknown as { client: unknown }).client = {
33
+ chat: {
34
+ completions: {
35
+ create: async (params: Record<string, unknown>) => {
36
+ requests.push(params);
37
+ return {
38
+ async *[Symbol.asyncIterator]() {
39
+ yield {
40
+ choices: [{ delta: {}, finish_reason: "stop" }],
41
+ usage: { prompt_tokens: 1, completion_tokens: 1 },
42
+ };
43
+ },
44
+ };
45
+ },
46
+ },
47
+ },
48
+ };
49
+ return { provider, requests };
50
+ }
51
+
52
+ describe("mapNeutralToolChoice (chat-completions wire format)", () => {
53
+ // Each neutral tool_choice variant maps to its OpenAI-compatible form.
54
+ test("maps the Anthropic-shaped union to OpenAI's tool_choice values", () => {
55
+ // GIVEN the neutral (Anthropic-shaped) tool_choice union
56
+ // WHEN each variant is mapped to the chat-completions wire format
57
+ // THEN it produces the OpenAI-compatible value
58
+ expect(mapNeutralToolChoice({ type: "auto" })).toBe("auto");
59
+ expect(mapNeutralToolChoice({ type: "any" })).toBe("required");
60
+ expect(mapNeutralToolChoice({ type: "none" })).toBe("none");
61
+ expect(mapNeutralToolChoice({ type: "tool", name: "bash" })).toEqual({
62
+ type: "function",
63
+ function: { name: "bash" },
64
+ });
65
+ });
66
+
67
+ // Unmappable input yields undefined so the request omits tool_choice.
68
+ test("returns undefined for absent or unrecognized values", () => {
69
+ // GIVEN values that don't describe a valid tool_choice
70
+ // WHEN they are mapped
71
+ // THEN the mapper returns undefined (request falls back to the API default)
72
+ expect(mapNeutralToolChoice(undefined)).toBeUndefined();
73
+ expect(mapNeutralToolChoice(null)).toBeUndefined();
74
+ expect(mapNeutralToolChoice("none")).toBeUndefined();
75
+ expect(mapNeutralToolChoice({ type: "bogus" })).toBeUndefined();
76
+ // AND a forced tool with no name can't be expressed
77
+ expect(mapNeutralToolChoice({ type: "tool" })).toBeUndefined();
78
+ });
79
+ });
80
+
81
+ describe("mapNeutralToolChoiceForResponses (Responses API wire format)", () => {
82
+ // The Responses API named shape omits the chat-completions function wrapper.
83
+ test("uses the un-nested named-function shape", () => {
84
+ // GIVEN the neutral tool_choice union
85
+ // WHEN mapped to the Responses API format
86
+ // THEN string variants match and the named shape has no `function` wrapper
87
+ expect(mapNeutralToolChoiceForResponses({ type: "auto" })).toBe("auto");
88
+ expect(mapNeutralToolChoiceForResponses({ type: "any" })).toBe("required");
89
+ expect(mapNeutralToolChoiceForResponses({ type: "none" })).toBe("none");
90
+ expect(
91
+ mapNeutralToolChoiceForResponses({ type: "tool", name: "bash" }),
92
+ ).toEqual({ type: "function", name: "bash" });
93
+ expect(mapNeutralToolChoiceForResponses({ type: "bogus" })).toBeUndefined();
94
+ });
95
+ });
96
+
97
+ describe("OpenAIChatCompletionsProvider tool_choice wiring", () => {
98
+ // A `{ type: "none" }` config forces a text-only answer on the wire.
99
+ test("forwards config.tool_choice on the wire when tools are present", async () => {
100
+ // GIVEN a provider with a stubbed SDK client
101
+ const { provider, requests } = stubChatProvider();
102
+
103
+ // WHEN sendMessage is called with tools and tool_choice `{ type: "none" }`
104
+ await provider.sendMessage(USER_MESSAGE, {
105
+ tools: TOOLS,
106
+ config: { tool_choice: { type: "none" } },
107
+ });
108
+
109
+ // THEN the outgoing request carries `tool_choice: "none"`
110
+ expect(requests).toHaveLength(1);
111
+ expect(requests[0].tool_choice).toBe("none");
112
+ expect(requests[0].tools).toBeDefined();
113
+ });
114
+
115
+ // A `{ type: "tool", name }` config forces that specific tool call.
116
+ test("maps a forced tool choice to the named-function wire shape", async () => {
117
+ // GIVEN a provider with a stubbed SDK client
118
+ const { provider, requests } = stubChatProvider();
119
+
120
+ // WHEN sendMessage is called with a forced tool choice
121
+ await provider.sendMessage(USER_MESSAGE, {
122
+ tools: TOOLS,
123
+ config: { tool_choice: { type: "tool", name: "bash" } },
124
+ });
125
+
126
+ // THEN the request carries the OpenAI named-function shape
127
+ expect(requests[0].tool_choice).toEqual({
128
+ type: "function",
129
+ function: { name: "bash" },
130
+ });
131
+ });
132
+
133
+ // tool_choice is only meaningful alongside tools, so it is dropped.
134
+ test("omits tool_choice when no tools are supplied", async () => {
135
+ // GIVEN a provider with a stubbed SDK client
136
+ const { provider, requests } = stubChatProvider();
137
+
138
+ // WHEN sendMessage is called with a tool_choice but no tools
139
+ await provider.sendMessage(USER_MESSAGE, {
140
+ config: { tool_choice: { type: "none" } },
141
+ });
142
+
143
+ // THEN the request omits both tools and tool_choice
144
+ expect(requests[0].tools).toBeUndefined();
145
+ expect(requests[0].tool_choice).toBeUndefined();
146
+ });
147
+ });
@@ -237,6 +237,43 @@ export function clampReasoningEffort(
237
237
  : value;
238
238
  }
239
239
 
240
+ /**
241
+ * Translate the neutral (Anthropic-shaped) `tool_choice` carried on the call
242
+ * config into the OpenAI chat-completions wire format. Callers express
243
+ * `tool_choice` once in the Anthropic union — `{ type: "auto" | "any" | "none"
244
+ * | "tool", name? }` — and each provider maps it to its own dialect (the
245
+ * Anthropic client forwards the union verbatim). For OpenAI-compatible APIs:
246
+ * - `{ type: "auto" }` -> `"auto"`
247
+ * - `{ type: "any" }` -> `"required"`
248
+ * - `{ type: "none" }` -> `"none"`
249
+ * - `{ type: "tool", name }` -> `{ type: "function", function: { name } }`
250
+ * Returns `undefined` for an absent or unrecognized value so the request omits
251
+ * `tool_choice` and falls back to the API default.
252
+ *
253
+ * See OpenAI's tool_choice spec:
254
+ * https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice
255
+ */
256
+ export function mapNeutralToolChoice(
257
+ toolChoice: unknown,
258
+ ): OpenAI.Chat.Completions.ChatCompletionToolChoiceOption | undefined {
259
+ if (toolChoice == null || typeof toolChoice !== "object") return undefined;
260
+ const tc = toolChoice as { type?: unknown; name?: unknown };
261
+ switch (tc.type) {
262
+ case "auto":
263
+ return "auto";
264
+ case "any":
265
+ return "required";
266
+ case "none":
267
+ return "none";
268
+ case "tool":
269
+ return typeof tc.name === "string"
270
+ ? { type: "function", function: { name: tc.name } }
271
+ : undefined;
272
+ default:
273
+ return undefined;
274
+ }
275
+ }
276
+
240
277
  const OPENAI_SUPPORTED_IMAGE_TYPES = new Set([
241
278
  "image/jpeg",
242
279
  "image/png",
@@ -367,6 +404,15 @@ export class OpenAIChatCompletionsProvider implements Provider {
367
404
  parameters: t.input_schema as OpenAI.FunctionParameters,
368
405
  },
369
406
  }));
407
+
408
+ // Honor a caller-supplied tool_choice (e.g. `{ type: "none" }` to force
409
+ // a text-only answer, or `{ type: "tool", name }` for a forced call).
410
+ // Only meaningful when tools are present — OpenAI rejects a named or
411
+ // "required" choice with no tools.
412
+ const toolChoice = mapNeutralToolChoice(configObj?.tool_choice);
413
+ if (toolChoice !== undefined) {
414
+ params.tool_choice = toolChoice;
415
+ }
370
416
  }
371
417
 
372
418
  const { signal: timeoutSignal, cleanup: cleanupTimeout } =
@@ -49,6 +49,41 @@ const EFFORT_TO_REASONING_EFFORT: Record<
49
49
  /** Values accepted by the Responses API `text.verbosity` parameter. */
50
50
  const VALID_VERBOSITIES = new Set<string>(["low", "medium", "high"]);
51
51
 
52
+ /**
53
+ * Translate the neutral (Anthropic-shaped) `tool_choice` carried on the call
54
+ * config into the OpenAI Responses API wire format. Callers express
55
+ * `tool_choice` once in the Anthropic union — `{ type: "auto" | "any" | "none"
56
+ * | "tool", name? }`. For the Responses API:
57
+ * - `{ type: "auto" }` -> `"auto"`
58
+ * - `{ type: "any" }` -> `"required"`
59
+ * - `{ type: "none" }` -> `"none"`
60
+ * - `{ type: "tool", name }` -> `{ type: "function", name }`
61
+ * Note the named shape differs from chat-completions (no nested `function`
62
+ * wrapper). Returns `undefined` for an absent or unrecognized value.
63
+ *
64
+ * https://platform.openai.com/docs/api-reference/responses/create#responses-create-tool_choice
65
+ */
66
+ export function mapNeutralToolChoiceForResponses(
67
+ toolChoice: unknown,
68
+ ): string | { type: "function"; name: string } | undefined {
69
+ if (toolChoice == null || typeof toolChoice !== "object") return undefined;
70
+ const tc = toolChoice as { type?: unknown; name?: unknown };
71
+ switch (tc.type) {
72
+ case "auto":
73
+ return "auto";
74
+ case "any":
75
+ return "required";
76
+ case "none":
77
+ return "none";
78
+ case "tool":
79
+ return typeof tc.name === "string"
80
+ ? { type: "function", name: tc.name }
81
+ : undefined;
82
+ default:
83
+ return undefined;
84
+ }
85
+ }
86
+
52
87
  /** `text.verbosity` is a GPT-5-series-only parameter. Older models on the
53
88
  * Responses API (o-series, etc.) reject unknown wire fields with HTTP 400, so
54
89
  * gate forwarding by model name here. The retry layer can't make this call
@@ -229,6 +264,16 @@ export class OpenAIResponsesProvider implements Provider {
229
264
  strict: null,
230
265
  }));
231
266
  }
267
+
268
+ // Honor a caller-supplied tool_choice (e.g. `{ type: "none" }` to force
269
+ // a text-only answer, or `{ type: "tool", name }` for a forced call).
270
+ // Only meaningful when tools are present.
271
+ const toolChoice = mapNeutralToolChoiceForResponses(
272
+ configObj?.tool_choice,
273
+ );
274
+ if (toolChoice !== undefined) {
275
+ params.tool_choice = toolChoice;
276
+ }
232
277
  }
233
278
 
234
279
  const { signal: timeoutSignal, cleanup: cleanupTimeout } =
@@ -28,7 +28,6 @@ const log = getLogger("provider-registry");
28
28
 
29
29
  const providers = new Map<string, Provider>();
30
30
  const routingSources = new Map<string, "user-key" | "managed-proxy">();
31
- const OPENAI_COMPATIBLE_ENDPOINTS_FLAG = "openai-compatible-endpoints";
32
31
  const NATIVE_WEB_SEARCH_PROVIDER_IDS = new Set(["anthropic", "openai"]);
33
32
 
34
33
  /** Per-connection provider cache, keyed by connection name and model. */
@@ -258,13 +257,6 @@ export async function resolveProviderFromConnection(
258
257
  config: ProvidersConfig,
259
258
  opts: { model?: string } = {},
260
259
  ): Promise<Provider | null> {
261
- if (
262
- connection.provider === "openai-compatible" &&
263
- !isProviderFeatureFlagEnabled(OPENAI_COMPATIBLE_ENDPOINTS_FLAG, config)
264
- ) {
265
- return null;
266
- }
267
-
268
260
  const model = opts.model ?? resolveModel(config, connection.provider);
269
261
  const cacheKey = getConnectionProviderCacheKey(connection, model);
270
262
  const cached = connectionProviders.get(cacheKey);
@@ -249,7 +249,6 @@ import {
249
249
  const runResult = (history: Message[]): AgentLoopRunResult => ({
250
250
  history,
251
251
  exitReason: null,
252
- appendedNewMessages: true,
253
252
  // The wake path slices its own new-message boundary off the returned
254
253
  // history (it never destructures `newMessages`), so this is type-only.
255
254
  newMessages: [],
@@ -63,6 +63,10 @@ import {
63
63
  persistWakeTailMessage,
64
64
  scopeWakeAllowedTools,
65
65
  } from "../daemon/wake-conversation-ops.js";
66
+ import {
67
+ recordCompactionEndBestEffort,
68
+ recordCompactionStartBestEffort,
69
+ } from "../memory/compaction-log-writer-clickhouse.js";
66
70
  import { getConversationOverrideProfile } from "../memory/conversation-crud.js";
67
71
  import {
68
72
  buildProviderErrorResponsePayload,
@@ -598,6 +602,15 @@ export async function wakeAgentForOpportunity(
598
602
  }
599
603
  };
600
604
  const onEvent = (event: AgentEvent): void => {
605
+ // Compaction logging is observability, not turn state, so it writes
606
+ // immediately even while buffering — a compaction during a silent
607
+ // (never-goes-live) wake still happened and should be recorded.
608
+ if (event.type === "context_compacting") {
609
+ recordCompactionStartBestEffort(conversationId, event);
610
+ }
611
+ if (event.type === "compaction_completed") {
612
+ recordCompactionEndBestEffort(conversationId, event);
613
+ }
601
614
  // Replicates the recordRequestLog side-effect in `handleUsage` because
602
615
  // wakes own their own onEvent and never reach `dispatchAgentEvent`.
603
616
  // Defer persistence while buffering — see `pendingLogs` above.
@@ -75,6 +75,7 @@ const steerOrResumeMock = mock(
75
75
  mock.module("../../../acp/index.js", () => ({
76
76
  getAcpSessionManager: () => ({
77
77
  getStatus: () => fakeInMemorySessions,
78
+ getActiveAndPendingIds: () => fakeInMemorySessions.map((s) => s.id),
78
79
  spawn: spawnMock,
79
80
  steerOrResume: steerOrResumeMock,
80
81
  }),
@@ -86,6 +87,34 @@ mock.module("../../../acp/prepare-agent-env.js", () => ({
86
87
  prepareAgentEnv: async (agentConfig: unknown) => agentConfig,
87
88
  }));
88
89
 
90
+ // The spawn route and steer's resume branch gate on a high-risk approval
91
+ // (ATL-822) before starting the host agent. These tests pin the
92
+ // resolve/install/resume flow, so the hub mock auto-resolves the freshly
93
+ // registered confirmation the same way `POST /v1/confirm` would (resolve +
94
+ // directResolve). `approvalBehavior` flips allow/deny; `confirmationRequests`
95
+ // captures the prompts. Other event types are ignored.
96
+ import * as pendingInteractions from "../../../runtime/pending-interactions.js";
97
+
98
+ let approvalBehavior: "allow" | "deny" = "allow";
99
+ const confirmationRequests: Array<Record<string, unknown>> = [];
100
+ const broadcasts: Array<Record<string, unknown>> = [];
101
+
102
+ mock.module("../../../runtime/assistant-event-hub.js", () => ({
103
+ broadcastMessage: (msg: { type?: string; requestId?: string }) => {
104
+ broadcasts.push(msg as Record<string, unknown>);
105
+ if (msg?.type !== "confirmation_request") return;
106
+ confirmationRequests.push(msg as Record<string, unknown>);
107
+ const interaction = pendingInteractions.resolve(
108
+ msg.requestId as string,
109
+ approvalBehavior === "allow" ? "approved" : "rejected",
110
+ );
111
+ interaction?.directResolve?.(approvalBehavior);
112
+ },
113
+ }));
114
+
115
+ /** Drain pending micro/macrotasks so background resume work settles. */
116
+ const flushAsync = () => new Promise((resolve) => setTimeout(resolve, 0));
117
+
89
118
  const config = await installAcpConfigStub();
90
119
  const which = installWhichStub();
91
120
 
@@ -143,6 +172,9 @@ beforeEach(() => {
143
172
  _resetAdapterInstallCacheForTests();
144
173
  config.setConfig({});
145
174
  which.setWhich((cmd) => `/usr/local/bin/${cmd}`);
175
+ approvalBehavior = "allow";
176
+ confirmationRequests.length = 0;
177
+ broadcasts.length = 0;
146
178
  });
147
179
 
148
180
  describe("GET /v1/acp/sessions — merged in-memory + history", () => {
@@ -652,3 +684,122 @@ describe("POST /v1/acp/:id/steer: resume fallback", () => {
652
684
  await expect(promise).rejects.toBeInstanceOf(NotFoundError);
653
685
  });
654
686
  });
687
+
688
+ // ---------------------------------------------------------------------------
689
+ // POST /v1/acp/:id/steer: resume crosses the host-spawn boundary, so it is
690
+ // gated by the same high-risk guardian approval as spawn (ATL-822). Steering
691
+ // a session still active in memory only redirects an already-approved live
692
+ // process and is NOT prompted.
693
+ // ---------------------------------------------------------------------------
694
+
695
+ describe("POST /v1/acp/:id/steer: resume approval gate", () => {
696
+ test("acks immediately with approvalPending, then resumes once granted", async () => {
697
+ insertHistoryRow({
698
+ id: "gone-1",
699
+ agentId: "claude",
700
+ parentConversationId: "conv-z",
701
+ status: "completed",
702
+ cwd: "/work/repo",
703
+ });
704
+ steerOrResumeImpl = async () => ({ resumed: true });
705
+
706
+ const handler = getSteerHandler();
707
+ // The ack returns before the (async) resume completes, so a slow guardian
708
+ // approval can't trip the client's short ack timeout (ATL-822 / Codex P2).
709
+ const body = await handler({
710
+ pathParams: { id: "gone-1" },
711
+ body: { instruction: "keep going" },
712
+ });
713
+ expect(body).toEqual({
714
+ acpSessionId: "gone-1",
715
+ steered: false,
716
+ approvalPending: true,
717
+ });
718
+
719
+ // The high-risk prompt is surfaced for the resume.
720
+ expect(confirmationRequests).toHaveLength(1);
721
+ const prompt = confirmationRequests[0];
722
+ expect(prompt.toolName).toBe("acp_steer");
723
+ expect(prompt.riskLevel).toBe("high");
724
+ expect(prompt.executionTarget).toBe("host");
725
+ expect(prompt.conversationId).toBe("conv-z");
726
+ expect((prompt.input as { cwd?: string }).cwd).toBe("/work/repo");
727
+
728
+ // After approval settles, the background worker performs the resume.
729
+ await flushAsync();
730
+ expect(steerOrResumeMock).toHaveBeenCalledTimes(1);
731
+ expect(steerOrResumeMock.mock.calls[0][0]).toBe("gone-1");
732
+ });
733
+
734
+ test("denied resume never reaches the session manager and reports an error event", async () => {
735
+ approvalBehavior = "deny";
736
+ insertHistoryRow({
737
+ id: "gone-2",
738
+ acpSessionId: "proto-gone-2",
739
+ parentConversationId: "conv-z",
740
+ status: "completed",
741
+ cwd: "/work/repo",
742
+ });
743
+
744
+ const handler = getSteerHandler();
745
+ const body = await handler({
746
+ pathParams: { id: "gone-2" },
747
+ body: { instruction: "do evil" },
748
+ });
749
+ expect(body).toEqual({
750
+ acpSessionId: "gone-2",
751
+ steered: false,
752
+ approvalPending: true,
753
+ });
754
+
755
+ await flushAsync();
756
+ // Denied before any host re-spawn, and the denial surfaces over SSE.
757
+ expect(steerOrResumeMock).not.toHaveBeenCalled();
758
+ expect(pendingInteractions.getAll()).toHaveLength(0);
759
+ // Keyed by the daemon/route id (what SSE consumers index by), not the
760
+ // persisted protocol id.
761
+ const errEvent = broadcasts.find((m) => m.type === "acp_session_error");
762
+ expect(errEvent?.acpSessionId).toBe("gone-2");
763
+ });
764
+
765
+ test("a legacy row without a persisted cwd is not resumable, so no prompt", async () => {
766
+ insertHistoryRow({ id: "legacy-2", status: "completed", cwd: null });
767
+ steerOrResumeImpl = async (id) => {
768
+ throw new AcpResumeError(
769
+ new Error(`ACP session "${id}" cannot be resumed.`),
770
+ );
771
+ };
772
+
773
+ const handler = getSteerHandler();
774
+ await expect(
775
+ handler({ pathParams: { id: "legacy-2" }, body: { instruction: "go" } }),
776
+ ).rejects.toBeInstanceOf(FailedDependencyError);
777
+ // No spawn would occur, so the gate stays out of the way.
778
+ expect(confirmationRequests).toHaveLength(0);
779
+ });
780
+
781
+ test("steering a session active in memory is not prompted", async () => {
782
+ fakeInMemorySessions = [
783
+ {
784
+ id: "live-2",
785
+ agentId: "claude",
786
+ acpSessionId: "proto-live-2",
787
+ parentConversationId: "conv-1",
788
+ status: "running",
789
+ startedAt: 1000,
790
+ },
791
+ ];
792
+ // A resumable row also exists, but the in-memory session wins → steer.
793
+ insertHistoryRow({ id: "live-2", status: "completed", cwd: "/work/repo" });
794
+
795
+ const handler = getSteerHandler();
796
+ const body = await handler({
797
+ pathParams: { id: "live-2" },
798
+ body: { instruction: "redirect" },
799
+ });
800
+
801
+ expect(confirmationRequests).toHaveLength(0);
802
+ expect(body).toEqual({ acpSessionId: "live-2", steered: true });
803
+ expect(steerOrResumeMock).toHaveBeenCalledTimes(1);
804
+ });
805
+ });
@@ -24,7 +24,7 @@
24
24
  * once assistant output exists.
25
25
  */
26
26
 
27
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
27
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
28
28
  import { tmpdir } from "node:os";
29
29
  import { join } from "node:path";
30
30
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
@@ -40,7 +40,6 @@ import { invalidateConfigCache } from "../../../config/loader.js";
40
40
  import { createConversation } from "../../../memory/conversation-crud.js";
41
41
  import { getDb } from "../../../memory/db-connection.js";
42
42
  import { initializeDb } from "../../../memory/db-init.js";
43
- import { enqueueMemoryJob } from "../../../memory/jobs-store.js";
44
43
  import { recordUsageEvent } from "../../../memory/llm-usage-store.js";
45
44
  import { rawAll, rawRun } from "../../../memory/raw-query.js";
46
45
  import { ROUTES } from "../consolidation-routes.js";
@@ -112,10 +111,6 @@ function recordUsageCostAt(
112
111
  );
113
112
  }
114
113
 
115
- function readConfig(): Record<string, unknown> {
116
- return JSON.parse(readFileSync(configPath, "utf-8"));
117
- }
118
-
119
114
  function readMemoryJobRows(): Array<{
120
115
  id: string;
121
116
  status: string;
@@ -379,7 +374,7 @@ describe("listConsolidationRuns handler", () => {
379
374
  });
380
375
  });
381
376
 
382
- describe("updateConsolidationConfig handler", () => {
377
+ describe("consolidation config and run-now handlers", () => {
383
378
  beforeEach(() => {
384
379
  workspaceDir = mkdtempSync(join(tmpdir(), "vellum-consolidation-routes-"));
385
380
  origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
@@ -399,12 +394,13 @@ describe("updateConsolidationConfig handler", () => {
399
394
  rmSync(workspaceDir, { recursive: true, force: true });
400
395
  });
401
396
 
402
- test("persists only the consolidation enabled override without disabling memory v2", async () => {
397
+ test("reports consolidation unavailable when global memory is disabled", async () => {
403
398
  writeFileSync(
404
399
  configPath,
405
400
  JSON.stringify(
406
401
  {
407
402
  memory: {
403
+ enabled: false,
408
404
  v2: {
409
405
  enabled: true,
410
406
  consolidation_interval_hours: 4,
@@ -416,8 +412,8 @@ describe("updateConsolidationConfig handler", () => {
416
412
  ) + "\n",
417
413
  );
418
414
 
419
- const handler = findHandler("updateConsolidationConfig");
420
- const result = (await handler({ body: { enabled: false } })) as {
415
+ const handler = findHandler("getConsolidationConfig");
416
+ const result = (await handler({})) as {
421
417
  available: boolean;
422
418
  enabled: boolean;
423
419
  intervalMs: number;
@@ -425,17 +421,8 @@ describe("updateConsolidationConfig handler", () => {
425
421
  success: boolean;
426
422
  };
427
423
 
428
- expect(readConfig()).toEqual({
429
- memory: {
430
- v2: {
431
- enabled: true,
432
- consolidation_interval_hours: 4,
433
- consolidation_enabled: false,
434
- },
435
- },
436
- });
437
424
  expect(result).toMatchObject({
438
- available: true,
425
+ available: false,
439
426
  enabled: false,
440
427
  intervalMs: 4 * 60 * 60 * 1000,
441
428
  nextRunAt: null,
@@ -443,15 +430,15 @@ describe("updateConsolidationConfig handler", () => {
443
430
  });
444
431
  });
445
432
 
446
- test("disabling automatic consolidation cancels pending automatic jobs but preserves manual run-now jobs", async () => {
433
+ test("run-now is unavailable when global memory is disabled", async () => {
447
434
  writeFileSync(
448
435
  configPath,
449
436
  JSON.stringify(
450
437
  {
451
438
  memory: {
439
+ enabled: false,
452
440
  v2: {
453
441
  enabled: true,
454
- consolidation_enabled: true,
455
442
  },
456
443
  },
457
444
  },
@@ -459,36 +446,12 @@ describe("updateConsolidationConfig handler", () => {
459
446
  2,
460
447
  ) + "\n",
461
448
  );
462
- const automaticJobId = enqueueMemoryJob("memory_v2_consolidate", {
463
- trigger: "automatic",
464
- });
465
- const legacyJobId = enqueueMemoryJob("memory_v2_consolidate", {});
466
- const manualJobId = enqueueMemoryJob("memory_v2_consolidate", {
467
- trigger: "manual",
468
- });
469
449
 
470
- const handler = findHandler("updateConsolidationConfig");
471
- await handler({ body: { enabled: false } });
472
-
473
- const rowsById = new Map(
474
- readMemoryJobRows().map((row) => [row.id, row] as const),
475
- );
476
- expect(rowsById.get(automaticJobId)).toMatchObject({
477
- status: "failed",
478
- lastError: "automatic_consolidation_disabled",
479
- });
480
- expect(rowsById.get(legacyJobId)).toMatchObject({
481
- status: "failed",
482
- lastError: "automatic_consolidation_disabled",
483
- });
484
- expect(rowsById.get(manualJobId)).toMatchObject({
485
- status: "pending",
486
- lastError: null,
487
- payload: JSON.stringify({ trigger: "manual" }),
488
- });
450
+ const handler = findHandler("runConsolidationNow");
451
+ await expect(handler({})).rejects.toThrow("Consolidation is not available");
489
452
  });
490
453
 
491
- test("run-now remains available when automatic consolidation is disabled", async () => {
454
+ test("run-now remains available when memory v2 is enabled", async () => {
492
455
  writeFileSync(
493
456
  configPath,
494
457
  JSON.stringify(
@@ -496,7 +459,6 @@ describe("updateConsolidationConfig handler", () => {
496
459
  memory: {
497
460
  v2: {
498
461
  enabled: true,
499
- consolidation_enabled: false,
500
462
  },
501
463
  },
502
464
  },