@vellumai/assistant 0.12.1 → 0.12.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -10,13 +10,26 @@ import {
10
10
  discardLastAssistantDisplayTurn,
11
11
  extractUserPromptText,
12
12
  } from "../daemon/conversation-history.js";
13
+ import { registerPluginHooks } from "../hooks/registry.js";
13
14
  import { addMessage, getMessages } from "../persistence/conversation-crud.js";
14
- import { getDb } from "../persistence/db-connection.js";
15
+ import { getDb, getMemorySqlite } from "../persistence/db-connection.js";
15
16
  import { initializeDb } from "../persistence/db-init.js";
16
17
  import { conversations, messages } from "../persistence/schema/index.js";
18
+ import memoryMessageDeleted from "../plugins/defaults/memory/hooks/message-deleted.js";
19
+ import {
20
+ ensureRetrospectiveCursorColumn,
21
+ getRetrospectiveState,
22
+ } from "../plugins/defaults/memory/memory-retrospective-state.js";
23
+ import type { PluginHooks } from "../plugins/types.js";
17
24
 
18
25
  await initializeDb();
19
26
 
27
+ // Register the memory plugin's `message-deleted` hook the way boot does, so
28
+ // the delete primitive's dispatch reaches the cursor bookkeeping.
29
+ registerPluginHooks("default-memory", {
30
+ "message-deleted": memoryMessageDeleted,
31
+ } as PluginHooks);
32
+
20
33
  const CONV_ID = "conv-retry-discard-test";
21
34
 
22
35
  function seedConversation(): void {
@@ -211,3 +224,43 @@ describe("extractUserPromptText", () => {
211
224
  expect(extractUserPromptText([])).toBe("");
212
225
  });
213
226
  });
227
+
228
+ describe("discardLastAssistantDisplayTurn keeps the memory-retrospective cursor placeable", () => {
229
+ beforeEach(() => {
230
+ ensureRetrospectiveCursorColumn("test");
231
+ getMemorySqlite()!.exec(`DELETE FROM memory_retrospective_state`);
232
+ });
233
+
234
+ test("a cursor stored without its timestamp gets it from the reply being discarded", async () => {
235
+ const [, assistantId] = await seed([
236
+ { role: "user", content: text("hello") },
237
+ { role: "assistant", content: text("first attempt") },
238
+ ]);
239
+ const replyCreatedAt = getMessages(CONV_ID).find(
240
+ (m) => m.id === assistantId,
241
+ )!.createdAt;
242
+ // A state row as a build without the timestamp column wrote it: the
243
+ // retrospective reviewed through the reply, so the cursor sits on it.
244
+ getMemorySqlite()!
245
+ .query(
246
+ `INSERT INTO memory_retrospective_state
247
+ (conversation_id, last_processed_message_id, last_run_at, remembered_log, last_processed_created_at)
248
+ VALUES (?, ?, 1, NULL, NULL)`,
249
+ )
250
+ .run(CONV_ID, assistantId);
251
+
252
+ discardLastAssistantDisplayTurn(CONV_ID);
253
+
254
+ expect(getMessages(CONV_ID).some((m) => m.id === assistantId)).toBe(false);
255
+ // The hook chain is fire-and-forget from the delete primitive, so the
256
+ // stamp lands shortly after the discard returns.
257
+ const deadline = Date.now() + 2_000;
258
+ let stamped =
259
+ getRetrospectiveState(CONV_ID)?.lastProcessedCreatedAt ?? null;
260
+ while (stamped === null && Date.now() < deadline) {
261
+ await Bun.sleep(10);
262
+ stamped = getRetrospectiveState(CONV_ID)?.lastProcessedCreatedAt ?? null;
263
+ }
264
+ expect(stamped).toBe(replyCreatedAt);
265
+ });
266
+ });
@@ -119,6 +119,7 @@ import {
119
119
  applyRuntimeInjections,
120
120
  assembleSlackActiveThreadFocusBlock,
121
121
  assembleSlackChronologicalMessages,
122
+ buildChannelCapabilityBlock,
122
123
  buildSubagentStatusBlock,
123
124
  getSlackCompactionWatermarkForPrefix,
124
125
  getSlackWatermarkAdvanceForRowPrefix,
@@ -586,6 +587,33 @@ describe("injectChannelCapabilityContext", () => {
586
587
  expect(text).not.toContain("Do NOT use ui_show, ui_update, or app_create");
587
588
  });
588
589
 
590
+ test("guides non-interactive turns to persist UI for a later capable client", () => {
591
+ const slack: ChannelCapabilities = {
592
+ channel: "slack",
593
+ dashboardCapable: false,
594
+ supportsDynamicUi: false,
595
+ supportsVoiceInput: false,
596
+ };
597
+ const phone: ChannelCapabilities = {
598
+ channel: "phone",
599
+ dashboardCapable: false,
600
+ supportsDynamicUi: false,
601
+ supportsVoiceInput: false,
602
+ };
603
+
604
+ for (const caps of [slack, phone]) {
605
+ const text = buildChannelCapabilityBlock(caps, undefined, true)!;
606
+ expect(text).toContain(
607
+ "ui_show, ui_update, and ui_dismiss persist conversation content",
608
+ );
609
+ expect(text).not.toContain("Only use ui_show/ui_update");
610
+ expect(text).not.toContain("Do NOT use ui_show, ui_update, or app_create");
611
+ expect(text).not.toContain(
612
+ "Present information as well-formatted text instead of dynamic UI.",
613
+ );
614
+ }
615
+ });
616
+
589
617
  test("keeps blanket ui_show/ui_update prohibition for other non-dynamic channels", () => {
590
618
  const caps: ChannelCapabilities = {
591
619
  channel: "phone",
@@ -545,6 +545,33 @@ describe("projectSkillTools", () => {
545
545
  expect(mockRegisteredTools.has("deploy")).toBe(true);
546
546
  });
547
547
 
548
+ test("catalog membership changes are visible on the next projection with a persistent cache", () => {
549
+ const cache = {};
550
+ mockCatalog = [];
551
+ mockManifests = { deploy: makeManifest(["deploy_run"]) };
552
+
553
+ const history: Message[] = [
554
+ ...skillLoadMessages('<loaded_skill id="deploy" />'),
555
+ ];
556
+
557
+ const result1 = projectSkillTools(history, {
558
+ previouslyActiveSkillIds: sessionState,
559
+ cache,
560
+ });
561
+ expect(result1.allowedToolNames.size).toBe(0);
562
+ expect(sessionState.has("deploy")).toBe(false);
563
+
564
+ mockCatalog = [makeSkill("deploy")];
565
+
566
+ const result2 = projectSkillTools(history, {
567
+ previouslyActiveSkillIds: sessionState,
568
+ cache,
569
+ });
570
+ expect(result2.allowedToolNames).toEqual(new Set(["deploy_run"]));
571
+ expect(sessionState.has("deploy")).toBe(true);
572
+ expect(mockRegisteredTools.has("deploy")).toBe(true);
573
+ });
574
+
548
575
  test("skill with manifest failure on turn 1 is registered when manifest is available on turn 2", () => {
549
576
  mockCatalog = [makeSkill("deploy")];
550
577
  // No manifest — will fail to load
@@ -146,8 +146,10 @@ describe("surfaceProxyResolver — app-control tool routing", () => {
146
146
  });
147
147
 
148
148
  expect(result.isError).toBe(true);
149
- expect(result.content).toContain("not available");
150
- expect(result.content).toContain("app-control");
149
+ expect(result.content).toContain(
150
+ "The Vellum desktop app is needed to control apps on your computer",
151
+ );
152
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
151
153
  // No envelope dispatched.
152
154
  expect(sentMessages).toHaveLength(0);
153
155
  });
@@ -163,7 +165,10 @@ describe("surfaceProxyResolver — app-control tool routing", () => {
163
165
  });
164
166
 
165
167
  expect(result.isError).toBe(true);
166
- expect(result.content).toContain("not available");
168
+ expect(result.content).toContain(
169
+ "The Vellum desktop app is needed to control apps on your computer",
170
+ );
171
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
167
172
  expect(sentMessages).toHaveLength(0);
168
173
 
169
174
  proxy.dispose();
@@ -24,6 +24,7 @@ function proxyDouble() {
24
24
  recordAction,
25
25
  request,
26
26
  reset: () => {},
27
+ endTask: () => {},
27
28
  stepCount: 0,
28
29
  },
29
30
  };
@@ -30,6 +30,7 @@ function proxyDouble() {
30
30
  recordAction: () => {},
31
31
  request,
32
32
  reset: () => {},
33
+ endTask: () => {},
33
34
  stepCount: 0,
34
35
  },
35
36
  };
@@ -21,6 +21,7 @@ import {
21
21
  function makeContext(
22
22
  sent: AssistantEvent[] = [],
23
23
  channelCapabilities?: { channel: string; supportsDynamicUi: boolean },
24
+ opts?: { hasNoClient?: boolean },
24
25
  ): Conversation {
25
26
  return asConversation({
26
27
  conversationId: "session-1",
@@ -37,6 +38,7 @@ function makeContext(
37
38
  accumulatedSurfaceState: new Map<string, Record<string, unknown>>(),
38
39
  surfaceActionRequestIds: new Set<string>(),
39
40
  currentTurnSurfaces: [],
41
+ hasNoClient: opts?.hasNoClient ?? false,
40
42
  isProcessing: () => false,
41
43
  enqueueMessage: () => ({ queued: false, requestId: "req-1" }),
42
44
  getQueueDepth: () => 0,
@@ -65,6 +67,71 @@ describe("task_progress surface compatibility", () => {
65
67
  expect(sent).toHaveLength(0);
66
68
  });
67
69
 
70
+ test("persists a clientless choice from a non-rendering channel without waiting", async () => {
71
+ const sent: AssistantEvent[] = [];
72
+ const ctx = makeContext(
73
+ sent,
74
+ {
75
+ channel: "phone",
76
+ supportsDynamicUi: false,
77
+ },
78
+ { hasNoClient: true },
79
+ );
80
+
81
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
82
+ surface_type: "choice",
83
+ title: "Choose a focus",
84
+ data: {
85
+ options: [
86
+ { id: "inbox", title: "Inbox" },
87
+ { id: "calendar", title: "Calendar" },
88
+ ],
89
+ },
90
+ });
91
+
92
+ expect(result.isError).toBe(false);
93
+ expect(result.yieldToUser).toBeUndefined();
94
+ const { surfaceId } = JSON.parse(result.content) as { surfaceId: string };
95
+ expect(ctx.currentTurnSurfaces.some((s) => s.surfaceId === surfaceId)).toBe(
96
+ true,
97
+ );
98
+ expect(ctx.pendingSurfaceActions.has(surfaceId)).toBe(false);
99
+ expect(sent.some((msg) => msg.type === "ui_surface_show")).toBe(true);
100
+ });
101
+
102
+ test("persists a clientless update in the current-turn surface snapshot", async () => {
103
+ const sent: AssistantEvent[] = [];
104
+ const ctx = makeContext(
105
+ sent,
106
+ {
107
+ channel: "phone",
108
+ supportsDynamicUi: false,
109
+ },
110
+ { hasNoClient: true },
111
+ );
112
+ const shown = await surfaceProxyResolver(ctx, "ui_show", {
113
+ surface_type: "card",
114
+ title: "Background work",
115
+ data: {
116
+ template: "task_progress",
117
+ templateData: { status: "in_progress", steps: [] },
118
+ },
119
+ });
120
+ const { surfaceId } = JSON.parse(shown.content) as { surfaceId: string };
121
+
122
+ const result = await surfaceProxyResolver(ctx, "ui_update", {
123
+ surface_id: surfaceId,
124
+ data: { templateData: { status: "completed" } },
125
+ });
126
+
127
+ expect(result.isError).toBe(false);
128
+ const data = ctx.currentTurnSurfaces.find((s) => s.surfaceId === surfaceId)
129
+ ?.data as CardSurfaceData;
130
+ expect((data.templateData as Record<string, unknown>).status).toBe(
131
+ "completed",
132
+ );
133
+ });
134
+
68
135
  test("blocks ui_update when channel lacks dynamic UI support", async () => {
69
136
  const sent: AssistantEvent[] = [];
70
137
  const ctx = makeContext(sent, {
@@ -104,6 +104,7 @@ mock.module("../runtime/sync/resource-sync-events.js", () => ({
104
104
  publishConversationTitleChanged: mockPublishConversationTitleChanged,
105
105
  }));
106
106
 
107
+ import { MESSAGE_KEYS } from "../i18n/index.js";
107
108
  import {
108
109
  AUTO_TITLE_DETERMINISTIC,
109
110
  AUTO_TITLE_LLM,
@@ -309,10 +310,10 @@ describe("conversation-title-service", () => {
309
310
  userMessage: "so about that t-shirt...",
310
311
  });
311
312
 
312
- expect(result.title).toBe("Untitled Conversation");
313
+ expect(result.title).toBe(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED);
313
314
  expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
314
315
  "conv-1",
315
- "Untitled Conversation",
316
+ MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED,
316
317
  AUTO_TITLE_DETERMINISTIC,
317
318
  );
318
319
  });
@@ -333,7 +334,7 @@ describe("conversation-title-service", () => {
333
334
  userMessage: "something",
334
335
  });
335
336
 
336
- expect(result.title).toBe("Untitled Conversation");
337
+ expect(result.title).toBe(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED);
337
338
  });
338
339
 
339
340
  // The core bug this PR fixes: weak title models emit their reasoning or
@@ -361,10 +362,10 @@ describe("conversation-title-service", () => {
361
362
  userMessage: "hey baby",
362
363
  });
363
364
 
364
- expect(result.title).toBe("Untitled Conversation");
365
+ expect(result.title).toBe(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED);
365
366
  expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
366
367
  "conv-1",
367
- "Untitled Conversation",
368
+ MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED,
368
369
  AUTO_TITLE_DETERMINISTIC,
369
370
  );
370
371
  });
@@ -423,7 +424,7 @@ describe("conversation-title-service", () => {
423
424
  userMessage: "x",
424
425
  });
425
426
 
426
- expect(result.title).toBe("Untitled Conversation");
427
+ expect(result.title).toBe(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED);
427
428
  });
428
429
 
429
430
  test("regeneration skips LLM call when recent messages have no extractable text", async () => {
@@ -515,17 +516,17 @@ describe("conversation-title-service", () => {
515
516
  });
516
517
 
517
518
  expect(provider.sendMessage).not.toHaveBeenCalled();
518
- expect(result).toEqual({ title: "Untitled Conversation", updated: true });
519
+ expect(result).toEqual({ title: MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED, updated: true });
519
520
  expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
520
521
  "conv-1",
521
- "Untitled Conversation",
522
+ MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED,
522
523
  AUTO_TITLE_DETERMINISTIC,
523
524
  );
524
525
  });
525
526
 
526
527
  test("keeps a background job's deterministic title when the model declines", async () => {
527
528
  // The model answered with prose the normalizer rejects, so there is no
528
- // generated title. Settling for "Untitled Conversation" would name the
529
+ // generated title. Settling for the untitled fallback would name the
529
530
  // job's work less well than the bootstrap title already does.
530
531
  mockGetConversation.mockImplementation(() => ({
531
532
  title: "Memory consolidation",
@@ -584,6 +585,9 @@ describe("conversation-title-service", () => {
584
585
  expect(content).not.toContain("Generate a very short title");
585
586
  expect(content).not.toContain("do NOT respond");
586
587
  expect(options.systemPrompt).toContain("Do NOT respond");
588
+ expect(options.systemPrompt).toContain(
589
+ "same language as the conversation content",
590
+ );
587
591
  });
588
592
 
589
593
  test("queueGenerateConversationTitle serializes concurrent calls", async () => {
@@ -661,7 +665,7 @@ describe("conversation-title-service", () => {
661
665
  ).find((c) => c[0] === "conv-1");
662
666
  expect(firstUpdate).toEqual([
663
667
  "conv-1",
664
- "Untitled Conversation",
668
+ MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED,
665
669
  AUTO_TITLE_DETERMINISTIC,
666
670
  ]);
667
671
  const secondUpdate = (
@@ -7,8 +7,8 @@
7
7
  * app_id from the executor's typed `resolvedAppId` side channel — and that
8
8
  * non-hooked tools (app_file_edit, app_file_write) do not trigger side effects.
9
9
  *
10
- * File-change detection for file_write/file_edit is handled by
11
- * AppSourceWatcher (see app-source-watcher.test.ts).
10
+ * File-change detection for file_write/file_edit is not hooked: compile with
11
+ * `app_refresh` or `assistant apps refresh` after a batch of edits.
12
12
  */
13
13
 
14
14
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -50,9 +50,7 @@ mock.module("../tools/browser/browser-screencast.js", () => ({
50
50
  registerConversationSender: mock(() => {}),
51
51
  }));
52
52
 
53
- // Stub app-store functions used by other modules (e.g. app-source-watcher,
54
- // conversation-surfaces) so tool-side-effects' hooks can run without touching
55
- // the real app store during tests.
53
+ // Stub app-store functions used by other modules (e.g. conversation-surfaces)
56
54
  mock.module("../apps/app-store.js", () => ({
57
55
  getApp: mock(() => null),
58
56
  getAppDirPath: mock(() => "/tmp/test-apps/dummy"),
@@ -354,6 +354,57 @@ describe("createToolExecutor isInteractive threading", () => {
354
354
  });
355
355
  });
356
356
 
357
+ describe("createToolExecutor unattended host-tool gate", () => {
358
+ test("rejects a direct host tool in a background turn before dispatch", async () => {
359
+ const { executor, calls } = makeCapturingExecutor();
360
+ const toolFn = makeToolFn(
361
+ executor,
362
+ makeCtx({ currentTurnIsNonInteractive: true, transportInterface: "web" }),
363
+ );
364
+
365
+ const result = await toolFn("host_bash", { command: "pwd" });
366
+
367
+ expect(result).toMatchObject({ isError: true });
368
+ expect(result.content).toContain("requires an interactive user turn");
369
+ expect(calls).toHaveLength(0);
370
+ });
371
+
372
+ test("rejects a skill-dispatched host tool in a background turn before dispatch", async () => {
373
+ const { executor, calls } = makeCapturingExecutor();
374
+ const toolFn = makeToolFn(
375
+ executor,
376
+ makeCtx({ currentTurnIsNonInteractive: true, transportInterface: "web" }),
377
+ );
378
+
379
+ const result = await toolFn("skill_execute", {
380
+ tool: "host_file_read",
381
+ input: { path: "/tmp/a" },
382
+ activity: "testing",
383
+ });
384
+
385
+ expect(result).toMatchObject({ isError: true });
386
+ expect(result.content).toContain("requires an interactive user turn");
387
+ expect(calls).toHaveLength(0);
388
+ });
389
+
390
+ test("dispatches a host tool during an interactive turn", async () => {
391
+ const { executor, calls } = makeCapturingExecutor();
392
+ const toolFn = makeToolFn(
393
+ executor,
394
+ makeCtx({
395
+ currentTurnIsNonInteractive: false,
396
+ transportInterface: "web",
397
+ }),
398
+ );
399
+
400
+ const result = await toolFn("host_bash", { command: "pwd" });
401
+
402
+ expect(result).toMatchObject({ content: "ok", isError: false });
403
+ expect(calls).toHaveLength(1);
404
+ expect(calls[0].name).toBe("host_bash");
405
+ });
406
+ });
407
+
357
408
  describe("createToolExecutor channel-permission coordinate threading", () => {
358
409
  beforeEach(() => {
359
410
  mockBindingExternalChatId = null;
@@ -8,8 +8,10 @@ import type { CredentialRecord } from "@vellumai/service-contracts/credential-rp
8
8
  import type { CredentialRecordBackend } from "../security/ces-rpc-record-backend.js";
9
9
  import {
10
10
  _ensureCesRecordsLoaded,
11
+ _setHttpRecordBackendForTests,
11
12
  _setMetadataPath,
12
13
  getCredentialMetadata,
14
+ listCredentialRecordsLive,
13
15
  setCredentialRecordBackend,
14
16
  upsertCredentialMetadata,
15
17
  } from "../tools/credentials/metadata-store.js";
@@ -35,7 +37,9 @@ function leftoverContents(): string {
35
37
  return readFileSync(leftoverPath(), "utf-8");
36
38
  }
37
39
 
38
- function makeBackend(): CredentialRecordBackend & {
40
+ function makeBackend(
41
+ name?: string,
42
+ ): CredentialRecordBackend & {
39
43
  store: Map<string, CredentialRecord>;
40
44
  bulkSetCalls: number;
41
45
  listCalls: number;
@@ -46,6 +50,7 @@ function makeBackend(): CredentialRecordBackend & {
46
50
  bulkSetCalls: number;
47
51
  listCalls: number;
48
52
  } = {
53
+ name,
49
54
  store,
50
55
  bulkSetCalls: 0,
51
56
  listCalls: 0,
@@ -83,6 +88,7 @@ function makeBackend(): CredentialRecordBackend & {
83
88
  describe("CES credential record cache", () => {
84
89
  beforeEach(() => {
85
90
  setCredentialRecordBackend(undefined);
91
+ _setHttpRecordBackendForTests(null);
86
92
  _setMetadataPath(null);
87
93
  const leftover = leftoverPath();
88
94
  if (existsSync(leftover)) {
@@ -92,6 +98,7 @@ describe("CES credential record cache", () => {
92
98
 
93
99
  afterEach(() => {
94
100
  setCredentialRecordBackend(undefined);
101
+ _setHttpRecordBackendForTests(null);
95
102
  _setMetadataPath(null);
96
103
  const leftover = leftoverPath();
97
104
  if (existsSync(leftover)) {
@@ -218,6 +225,169 @@ describe("CES credential record cache", () => {
218
225
  await _ensureCesRecordsLoaded();
219
226
  });
220
227
 
228
+ test("live catalog list returns CES records without the in-process cache", async () => {
229
+ const backend = makeBackend();
230
+ const record: CredentialRecord = {
231
+ credentialId: "cred-live",
232
+ service: "github",
233
+ field: "token",
234
+ allowedTools: ["bash"],
235
+ allowedDomains: [],
236
+ createdAt: 1,
237
+ updatedAt: 2,
238
+ };
239
+ backend.store.set(credentialKey("github", "token"), record);
240
+ setCredentialRecordBackend(backend);
241
+
242
+ const live = await listCredentialRecordsLive();
243
+ expect(live.unreachable).toBe(false);
244
+ expect(live.records).toHaveLength(1);
245
+ expect(live.records[0]?.credentialId).toBe("cred-live");
246
+ expect(getCredentialMetadata("github", "token")).toBeUndefined();
247
+ });
248
+
249
+ test("live catalog list reports unreachable when CES list fails", async () => {
250
+ const backend = makeBackend();
251
+ backend.list = async () => null;
252
+ setCredentialRecordBackend(backend);
253
+
254
+ const live = await listCredentialRecordsLive();
255
+ expect(live.unreachable).toBe(true);
256
+ expect(live.records).toEqual([]);
257
+ });
258
+
259
+ test("live catalog list reports unreachable when no record backend is attached", async () => {
260
+ const live = await listCredentialRecordsLive();
261
+ expect(live.unreachable).toBe(true);
262
+ expect(live.records).toEqual([]);
263
+ });
264
+
265
+ test("live catalog list fails over to CES HTTP when CES RPC list fails", async () => {
266
+ const rpc = makeBackend("ces-rpc");
267
+ rpc.list = async () => null;
268
+ setCredentialRecordBackend(rpc);
269
+
270
+ const http = makeBackend("ces-http");
271
+ const record: CredentialRecord = {
272
+ credentialId: "cred-http",
273
+ service: "github",
274
+ field: "token",
275
+ allowedTools: ["bash"],
276
+ allowedDomains: [],
277
+ createdAt: 1,
278
+ updatedAt: 2,
279
+ };
280
+ http.store.set(credentialKey("github", "token"), record);
281
+ _setHttpRecordBackendForTests(http);
282
+
283
+ const live = await listCredentialRecordsLive();
284
+ expect(live.unreachable).toBe(false);
285
+ expect(live.records).toHaveLength(1);
286
+ expect(live.records[0]?.credentialId).toBe("cred-http");
287
+ expect(http.listCalls).toBe(1);
288
+ });
289
+
290
+ test("live catalog list fails over to CES HTTP when CES RPC is unavailable", async () => {
291
+ const rpc = makeBackend("ces-rpc");
292
+ rpc.isAvailable = () => false;
293
+ setCredentialRecordBackend(rpc);
294
+
295
+ const http = makeBackend("ces-http");
296
+ const record: CredentialRecord = {
297
+ credentialId: "cred-http-down-rpc",
298
+ service: "github",
299
+ field: "token",
300
+ allowedTools: ["bash"],
301
+ allowedDomains: [],
302
+ createdAt: 1,
303
+ updatedAt: 2,
304
+ };
305
+ http.store.set(credentialKey("github", "token"), record);
306
+ _setHttpRecordBackendForTests(http);
307
+
308
+ const live = await listCredentialRecordsLive();
309
+ expect(live.unreachable).toBe(false);
310
+ expect(live.records[0]?.credentialId).toBe("cred-http-down-rpc");
311
+ });
312
+
313
+ test("live catalog list uses CES HTTP when no record backend is attached", async () => {
314
+ const http = makeBackend("ces-http");
315
+ const record: CredentialRecord = {
316
+ credentialId: "cred-http-only",
317
+ service: "github",
318
+ field: "token",
319
+ allowedTools: ["bash"],
320
+ allowedDomains: [],
321
+ createdAt: 1,
322
+ updatedAt: 2,
323
+ };
324
+ http.store.set(credentialKey("github", "token"), record);
325
+ _setHttpRecordBackendForTests(http);
326
+
327
+ const live = await listCredentialRecordsLive();
328
+ expect(live.unreachable).toBe(false);
329
+ expect(live.records[0]?.credentialId).toBe("cred-http-only");
330
+ });
331
+
332
+ test("live catalog list does not fail over when CES RPC returns an empty catalog", async () => {
333
+ const rpc = makeBackend("ces-rpc");
334
+ setCredentialRecordBackend(rpc);
335
+
336
+ const http = makeBackend("ces-http");
337
+ http.store.set(credentialKey("github", "token"), {
338
+ credentialId: "cred-http-ignored",
339
+ service: "github",
340
+ field: "token",
341
+ allowedTools: ["bash"],
342
+ allowedDomains: [],
343
+ createdAt: 1,
344
+ updatedAt: 2,
345
+ });
346
+ _setHttpRecordBackendForTests(http);
347
+
348
+ const live = await listCredentialRecordsLive();
349
+ expect(live.unreachable).toBe(false);
350
+ expect(live.records).toEqual([]);
351
+ expect(http.listCalls).toBe(0);
352
+ });
353
+
354
+ test("live catalog list does not fail over for a non-RPC record backend", async () => {
355
+ const backend = makeBackend();
356
+ backend.list = async () => null;
357
+ setCredentialRecordBackend(backend);
358
+
359
+ const http = makeBackend("ces-http");
360
+ http.store.set(credentialKey("github", "token"), {
361
+ credentialId: "cred-http-ignored",
362
+ service: "github",
363
+ field: "token",
364
+ allowedTools: ["bash"],
365
+ allowedDomains: [],
366
+ createdAt: 1,
367
+ updatedAt: 2,
368
+ });
369
+ _setHttpRecordBackendForTests(http);
370
+
371
+ const live = await listCredentialRecordsLive();
372
+ expect(live.unreachable).toBe(true);
373
+ expect(live.records).toEqual([]);
374
+ expect(http.listCalls).toBe(0);
375
+ });
376
+
377
+ test("live catalog list reports unreachable when CES RPC and HTTP both fail", async () => {
378
+ const rpc = makeBackend("ces-rpc");
379
+ rpc.list = async () => null;
380
+ setCredentialRecordBackend(rpc);
381
+
382
+ const http = makeBackend("ces-http");
383
+ http.list = async () => null;
384
+ _setHttpRecordBackendForTests(http);
385
+
386
+ const live = await listCredentialRecordsLive();
387
+ expect(live.unreachable).toBe(true);
388
+ expect(live.records).toEqual([]);
389
+ });
390
+
221
391
  test("upsert write-throughs to CES without updating leftover metadata.json", async () => {
222
392
  const leftoverRecord: CredentialRecord = {
223
393
  credentialId: "cred-leftover",