@vellumai/assistant 0.7.2 → 0.8.0

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 (424) hide show
  1. package/ARCHITECTURE.md +45 -29
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/docs/architecture/memory.md +5 -2
  6. package/knip.json +1 -0
  7. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  8. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  13. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  16. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  17. package/openapi.yaml +470 -25
  18. package/package.json +3 -1
  19. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  20. package/src/__tests__/app-control-flow.test.ts +21 -11
  21. package/src/__tests__/approval-cascade.test.ts +8 -16
  22. package/src/__tests__/approval-routes-http.test.ts +6 -0
  23. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  24. package/src/__tests__/assistant-event.test.ts +0 -10
  25. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  26. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  29. package/src/__tests__/call-constants.test.ts +10 -1
  30. package/src/__tests__/call-controller.test.ts +127 -0
  31. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  32. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  33. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  34. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  35. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  36. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  37. package/src/__tests__/config-schema.test.ts +1 -0
  38. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  39. package/src/__tests__/config-watcher.test.ts +140 -69
  40. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  41. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  42. package/src/__tests__/context-search-fanout.test.ts +0 -1
  43. package/src/__tests__/context-search-memory-source.test.ts +6 -33
  44. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  45. package/src/__tests__/context-search-pkb-source.test.ts +12 -7
  46. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  47. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  48. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  51. package/src/__tests__/conversation-agent-loop.test.ts +457 -8
  52. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  53. package/src/__tests__/conversation-error.test.ts +150 -3
  54. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  55. package/src/__tests__/conversation-process-callsite.test.ts +38 -0
  56. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
  58. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  59. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  60. package/src/__tests__/conversation-store.test.ts +0 -18
  61. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  62. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  63. package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
  65. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  66. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  67. package/src/__tests__/credentials-cli.test.ts +7 -0
  68. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  69. package/src/__tests__/date-context.test.ts +164 -2
  70. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  71. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  72. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  73. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  74. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  75. package/src/__tests__/disk-usage.test.ts +150 -0
  76. package/src/__tests__/events-client-registration.test.ts +52 -0
  77. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  78. package/src/__tests__/file-write-tool.test.ts +4 -10
  79. package/src/__tests__/filing-service.test.ts +2 -20
  80. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  82. package/src/__tests__/heartbeat-service.test.ts +260 -11
  83. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  84. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  85. package/src/__tests__/host-bash-routes.test.ts +178 -13
  86. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  87. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  88. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  89. package/src/__tests__/host-file-proxy.test.ts +268 -6
  90. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  91. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  92. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  93. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  94. package/src/__tests__/injector-chain.test.ts +36 -16
  95. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  96. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  98. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  99. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  100. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  101. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  102. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  103. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  104. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  105. package/src/__tests__/oauth-cli.test.ts +121 -0
  106. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  107. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  108. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  109. package/src/__tests__/openai-provider.test.ts +45 -8
  110. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  111. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  112. package/src/__tests__/platform.test.ts +2 -1
  113. package/src/__tests__/playbook-execution.test.ts +0 -43
  114. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  115. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  116. package/src/__tests__/provider-tool-name.test.ts +23 -0
  117. package/src/__tests__/relay-server.test.ts +60 -5
  118. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  119. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  120. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  121. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  122. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  123. package/src/__tests__/secret-response-routing.test.ts +7 -5
  124. package/src/__tests__/server-history-render.test.ts +82 -0
  125. package/src/__tests__/skill-include-graph.test.ts +31 -0
  126. package/src/__tests__/skill-load-tool.test.ts +44 -16
  127. package/src/__tests__/skills.test.ts +39 -0
  128. package/src/__tests__/suggestion-routes.test.ts +46 -0
  129. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  130. package/src/__tests__/tool-executor.test.ts +155 -0
  131. package/src/__tests__/twilio-validation.test.ts +2 -2
  132. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  133. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  134. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  136. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  137. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  138. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
  139. package/src/agent/loop.ts +11 -0
  140. package/src/approvals/guardian-request-resolvers.ts +3 -32
  141. package/src/backup/snapshot-lock.ts +2 -27
  142. package/src/bundler/compiler-tools.ts +3 -2
  143. package/src/calls/call-constants.ts +5 -8
  144. package/src/calls/call-controller.ts +130 -67
  145. package/src/calls/call-conversation-messages.ts +46 -10
  146. package/src/calls/relay-server.ts +7 -1
  147. package/src/calls/voice-session-bridge.ts +1 -1
  148. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  149. package/src/cli/commands/bash.ts +35 -108
  150. package/src/cli/commands/contacts.ts +64 -25
  151. package/src/cli/commands/credentials.ts +56 -0
  152. package/src/cli/commands/memory-v2.ts +11 -10
  153. package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
  154. package/src/cli/commands/oauth/connect.ts +124 -40
  155. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  156. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  157. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  158. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  159. package/src/cli/commands/platform/index.ts +16 -7
  160. package/src/cli/commands/status.ts +57 -0
  161. package/src/cli/program.ts +4 -2
  162. package/src/config/assistant-feature-flags.ts +13 -3
  163. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  164. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  165. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  166. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  167. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  168. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  169. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  170. package/src/config/env.ts +0 -8
  171. package/src/config/feature-flag-registry.json +13 -5
  172. package/src/config/loader.ts +199 -27
  173. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  174. package/src/config/schemas/call-site-catalog.ts +14 -0
  175. package/src/config/schemas/channels.ts +0 -5
  176. package/src/config/schemas/heartbeat.ts +1 -1
  177. package/src/config/schemas/llm.ts +2 -0
  178. package/src/config/schemas/memory-lifecycle.ts +13 -0
  179. package/src/config/schemas/memory-v2.ts +76 -12
  180. package/src/config/schemas/platform.ts +43 -3
  181. package/src/config/schemas/services.ts +28 -0
  182. package/src/config/seed-inference-profiles.ts +230 -33
  183. package/src/contacts/contact-store.ts +0 -25
  184. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  185. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  186. package/src/daemon/assistant-attachments.ts +4 -4
  187. package/src/daemon/config-watcher.ts +85 -57
  188. package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
  189. package/src/daemon/conversation-agent-loop.ts +183 -43
  190. package/src/daemon/conversation-error.ts +87 -15
  191. package/src/daemon/conversation-lifecycle.ts +22 -10
  192. package/src/daemon/conversation-process.ts +8 -0
  193. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  194. package/src/daemon/conversation-store.ts +2 -2
  195. package/src/daemon/conversation-surfaces.ts +211 -29
  196. package/src/daemon/conversation-tool-setup.ts +66 -19
  197. package/src/daemon/conversation.ts +18 -23
  198. package/src/daemon/date-context.ts +71 -22
  199. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  200. package/src/daemon/disk-pressure-guard.ts +343 -0
  201. package/src/daemon/disk-pressure-policy.ts +163 -0
  202. package/src/daemon/handlers/shared.ts +26 -1
  203. package/src/daemon/handlers/skills.ts +3 -4
  204. package/src/daemon/host-app-control-proxy.ts +137 -41
  205. package/src/daemon/host-bash-proxy.ts +47 -22
  206. package/src/daemon/host-browser-proxy.ts +1 -1
  207. package/src/daemon/host-cu-proxy.ts +50 -4
  208. package/src/daemon/host-file-proxy.ts +44 -8
  209. package/src/daemon/host-transfer-proxy.ts +97 -6
  210. package/src/daemon/lifecycle.ts +167 -101
  211. package/src/daemon/meet-host-supervisor.ts +4 -4
  212. package/src/daemon/meet-manifest-loader.ts +0 -1
  213. package/src/daemon/memory-v2-startup.ts +66 -15
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/conversations.ts +4 -0
  216. package/src/daemon/message-types/disk-pressure.ts +9 -0
  217. package/src/daemon/message-types/messages.ts +22 -1
  218. package/src/daemon/profiler-run-store.ts +5 -5
  219. package/src/daemon/tool-setup-types.ts +2 -2
  220. package/src/documents/document-store.ts +119 -0
  221. package/src/filing/filing-service.ts +29 -5
  222. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  223. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  224. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  225. package/src/heartbeat/heartbeat-service.ts +205 -31
  226. package/src/home/feed-scheduler.ts +18 -0
  227. package/src/inbound/platform-callback-registration.ts +8 -15
  228. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  229. package/src/ipc/assistant-server.ts +149 -38
  230. package/src/ipc/gateway-client.ts +37 -3
  231. package/src/ipc/skill-server.ts +99 -42
  232. package/src/live-voice/live-voice-archive.ts +4 -4
  233. package/src/live-voice/protocol.ts +5 -7
  234. package/src/media/image-service.ts +1 -7
  235. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  236. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
  237. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  238. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  239. package/src/memory/admin.ts +5 -9
  240. package/src/memory/context-search/agent-runner.ts +19 -2
  241. package/src/memory/context-search/sources/conversations.ts +2 -11
  242. package/src/memory/context-search/sources/memory-v2.ts +1 -16
  243. package/src/memory/context-search/sources/memory.ts +2 -3
  244. package/src/memory/context-search/sources/pkb.ts +2 -3
  245. package/src/memory/context-search/types.ts +0 -1
  246. package/src/memory/conversation-crud.ts +4 -12
  247. package/src/memory/db-init.ts +2 -0
  248. package/src/memory/embedding-runtime-manager.ts +119 -5
  249. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
  250. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  251. package/src/memory/graph/conversation-graph-memory.ts +72 -61
  252. package/src/memory/graph/extraction.ts +1 -3
  253. package/src/memory/graph/graph-search.test.ts +11 -67
  254. package/src/memory/graph/graph-search.ts +4 -24
  255. package/src/memory/graph/retriever.test.ts +12 -1
  256. package/src/memory/graph/retriever.ts +10 -15
  257. package/src/memory/graph/tool-handlers.ts +3 -4
  258. package/src/memory/graph/tools.ts +4 -4
  259. package/src/memory/indexer.ts +53 -45
  260. package/src/memory/job-handlers/backfill.ts +2 -11
  261. package/src/memory/job-handlers/cleanup.ts +43 -0
  262. package/src/memory/job-handlers/embedding.ts +6 -8
  263. package/src/memory/job-handlers/summarization.ts +2 -7
  264. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  265. package/src/memory/jobs/embed-concept-page.ts +223 -87
  266. package/src/memory/jobs-store.ts +48 -0
  267. package/src/memory/jobs-worker.ts +85 -43
  268. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  269. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  270. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  271. package/src/memory/migrations/index.ts +1 -0
  272. package/src/memory/pkb/pkb-search.test.ts +7 -0
  273. package/src/memory/pkb/pkb-search.ts +4 -5
  274. package/src/memory/qdrant-client.ts +3 -13
  275. package/src/memory/rerank-local.ts +374 -0
  276. package/src/memory/search/semantic.ts +10 -72
  277. package/src/memory/trace-event-store.ts +1 -17
  278. package/src/memory/v2/__tests__/activation.test.ts +346 -255
  279. package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
  280. package/src/memory/v2/__tests__/injection.test.ts +297 -190
  281. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  282. package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
  283. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  284. package/src/memory/v2/__tests__/sim.test.ts +113 -196
  285. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  286. package/src/memory/v2/__tests__/static-context.test.ts +77 -14
  287. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  288. package/src/memory/v2/activation.ts +149 -156
  289. package/src/memory/v2/consolidation-job.ts +69 -20
  290. package/src/memory/v2/injection.ts +75 -68
  291. package/src/memory/v2/page-store.ts +39 -0
  292. package/src/memory/v2/prompts/consolidation.ts +41 -1
  293. package/src/memory/v2/qdrant.ts +306 -46
  294. package/src/memory/v2/reranker.ts +177 -0
  295. package/src/memory/v2/sim.ts +77 -110
  296. package/src/memory/v2/skill-content.ts +4 -3
  297. package/src/memory/v2/skill-store.ts +82 -59
  298. package/src/memory/v2/static-context.ts +26 -8
  299. package/src/memory/v2/sweep-job.ts +5 -6
  300. package/src/memory/v2/types.ts +17 -10
  301. package/src/notifications/copy-composer.ts +47 -0
  302. package/src/notifications/decision-engine.ts +46 -0
  303. package/src/notifications/signal.ts +4 -0
  304. package/src/oauth/AGENTS.md +3 -1
  305. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  306. package/src/oauth/connect-orchestrator.ts +2 -0
  307. package/src/oauth/connection-resolver.test.ts +66 -1
  308. package/src/oauth/connection-resolver.ts +55 -1
  309. package/src/oauth/oauth-connect-state.ts +77 -0
  310. package/src/oauth/seed-providers.ts +58 -1
  311. package/src/permissions/gateway-threshold-reader.ts +116 -8
  312. package/src/permissions/prompter.ts +86 -96
  313. package/src/permissions/secret-prompter.ts +31 -31
  314. package/src/plugins/defaults/injectors.ts +36 -4
  315. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  316. package/src/plugins/types.ts +7 -0
  317. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  318. package/src/proactive-artifact/decision.test.ts +226 -0
  319. package/src/proactive-artifact/decision.ts +165 -0
  320. package/src/proactive-artifact/index.ts +7 -0
  321. package/src/proactive-artifact/job.test.ts +914 -0
  322. package/src/proactive-artifact/job.ts +366 -0
  323. package/src/proactive-artifact/message-copy.ts +58 -0
  324. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  325. package/src/proactive-artifact/trigger-state.ts +119 -0
  326. package/src/prompts/normalize-onboarding.ts +80 -0
  327. package/src/prompts/persona-resolver.ts +101 -9
  328. package/src/prompts/system-prompt.ts +21 -7
  329. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  330. package/src/prompts/templates/SOUL.md +13 -28
  331. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  332. package/src/providers/model-intents.ts +7 -0
  333. package/src/providers/openrouter/client.ts +8 -0
  334. package/src/providers/retry.ts +50 -0
  335. package/src/providers/types.ts +1 -0
  336. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  337. package/src/runtime/agent-wake.ts +238 -100
  338. package/src/runtime/assistant-event-hub.ts +36 -6
  339. package/src/runtime/assistant-event.ts +0 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  341. package/src/runtime/auth/route-policy.ts +15 -1
  342. package/src/runtime/auth/same-actor.ts +216 -0
  343. package/src/runtime/channel-approvals.ts +3 -2
  344. package/src/runtime/channel-retry-sweep.ts +65 -1
  345. package/src/runtime/local-actor-identity.ts +52 -11
  346. package/src/runtime/pending-interactions.ts +27 -15
  347. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  348. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  349. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  350. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  351. package/src/runtime/routes/approval-routes.ts +7 -3
  352. package/src/runtime/routes/client-routes.ts +20 -2
  353. package/src/runtime/routes/consolidation-routes.ts +8 -9
  354. package/src/runtime/routes/contact-routes.ts +0 -25
  355. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  356. package/src/runtime/routes/conversation-routes.ts +35 -26
  357. package/src/runtime/routes/debug-bash-routes.ts +165 -0
  358. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  359. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  360. package/src/runtime/routes/documents-routes.ts +2 -75
  361. package/src/runtime/routes/events-routes.ts +41 -9
  362. package/src/runtime/routes/filing-routes.ts +2 -3
  363. package/src/runtime/routes/host-bash-routes.ts +23 -3
  364. package/src/runtime/routes/host-cu-routes.ts +33 -6
  365. package/src/runtime/routes/host-file-routes.ts +32 -6
  366. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  367. package/src/runtime/routes/identity-routes.ts +7 -138
  368. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  369. package/src/runtime/routes/index.ts +6 -0
  370. package/src/runtime/routes/memory-item-routes.test.ts +37 -17
  371. package/src/runtime/routes/memory-item-routes.ts +5 -6
  372. package/src/runtime/routes/memory-v2-routes.ts +136 -17
  373. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  374. package/src/runtime/verification-outbound-actions.ts +4 -4
  375. package/src/schedule/run-script.ts +37 -5
  376. package/src/schedule/scheduler.ts +20 -1
  377. package/src/security/encrypted-store.ts +2 -0
  378. package/src/security/secure-keys.ts +55 -0
  379. package/src/skills/include-graph.ts +35 -13
  380. package/src/skills/remote-skill-policy.ts +4 -10
  381. package/src/subagent/index.ts +1 -7
  382. package/src/subagent/manager.ts +1 -15
  383. package/src/tasks/task-runner.ts +0 -1
  384. package/src/tasks/task-store.ts +0 -3
  385. package/src/tools/background-tool-registry.ts +17 -3
  386. package/src/tools/document/document-tool.ts +20 -0
  387. package/src/tools/executor.ts +18 -2
  388. package/src/tools/host-filesystem/edit.test.ts +151 -0
  389. package/src/tools/host-filesystem/edit.ts +43 -1
  390. package/src/tools/host-filesystem/read.test.ts +129 -0
  391. package/src/tools/host-filesystem/read.ts +43 -1
  392. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  393. package/src/tools/host-filesystem/transfer.ts +56 -11
  394. package/src/tools/host-filesystem/write.test.ts +134 -0
  395. package/src/tools/host-filesystem/write.ts +43 -1
  396. package/src/tools/host-terminal/host-shell.ts +13 -6
  397. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  398. package/src/tools/memory/register.test.ts +14 -9
  399. package/src/tools/memory/register.ts +1 -2
  400. package/src/tools/permission-checker.ts +15 -0
  401. package/src/tools/provider-tool-name.ts +28 -0
  402. package/src/tools/registry.ts +30 -9
  403. package/src/tools/skills/load.ts +24 -20
  404. package/src/tools/terminal/shell.ts +9 -1
  405. package/src/tools/tool-approval-handler.ts +31 -6
  406. package/src/tools/tool-name-aliases.ts +19 -0
  407. package/src/tools/types.ts +43 -3
  408. package/src/tts/provider-catalog.ts +3 -5
  409. package/src/util/disk-usage.ts +138 -0
  410. package/src/util/platform.ts +21 -11
  411. package/src/util/process-liveness.ts +26 -0
  412. package/src/workspace/heartbeat-service.ts +19 -0
  413. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  414. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  415. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
  416. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  417. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  418. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  419. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  420. package/src/workspace/migrations/registry.ts +14 -0
  421. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  422. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  423. package/src/memory/v2/skill-qdrant.ts +0 -404
  424. package/src/signals/bash.ts +0 -198
@@ -1,4 +1,11 @@
1
- import { describe, expect, it } from "bun:test";
1
+ import { beforeEach, describe, expect, it, mock } from "bun:test";
2
+
3
+ let providerRoutingSources: Record<string, "user-key" | "managed-proxy"> = {};
4
+
5
+ mock.module("../providers/registry.js", () => ({
6
+ getProviderRoutingSource: (provider: string) =>
7
+ providerRoutingSources[provider],
8
+ }));
2
9
 
3
10
  import type { ErrorContext } from "../daemon/conversation-error.js";
4
11
  import {
@@ -63,6 +70,10 @@ describe("isUserCancellation", () => {
63
70
  describe("classifyConversationError", () => {
64
71
  const baseCtx: ErrorContext = { phase: "agent_loop" };
65
72
 
73
+ beforeEach(() => {
74
+ providerRoutingSources = {};
75
+ });
76
+
66
77
  describe("network errors", () => {
67
78
  const cases = [
68
79
  "ECONNREFUSED",
@@ -105,6 +116,52 @@ describe("classifyConversationError", () => {
105
116
  expect(result.errorCategory).toBe("rate_limit");
106
117
  });
107
118
  }
119
+
120
+ it("classifies managed-proxy daily quota responses as MANAGED_USAGE_LIMIT", () => {
121
+ const err = new ProviderError(
122
+ 'Anthropic API error (429): 429 {"code":"daily_quota_exceeded","detail":"You\'ve reached your usage limit for today. You\'ve made 1000 requests, but your current plan allows 1000 per day.","provider":"anthropic"}',
123
+ "anthropic",
124
+ 429,
125
+ );
126
+
127
+ const result = classifyConversationError(err, baseCtx);
128
+
129
+ expect(result.code).toBe("MANAGED_USAGE_LIMIT");
130
+ expect(result.retryable).toBe(true);
131
+ expect(result.userMessage).toContain("Vellum managed inference");
132
+ expect(result.userMessage).toContain("not an AI provider outage");
133
+ expect(result.errorCategory).toBe("managed_usage_limit");
134
+ });
135
+
136
+ it("classifies managed-proxy routed 429s as MANAGED_USAGE_LIMIT", () => {
137
+ providerRoutingSources.anthropic = "managed-proxy";
138
+ const err = new ProviderError(
139
+ "Anthropic API error (429): Too many requests",
140
+ "anthropic",
141
+ 429,
142
+ );
143
+
144
+ const result = classifyConversationError(err, baseCtx);
145
+
146
+ expect(result.code).toBe("MANAGED_USAGE_LIMIT");
147
+ expect(result.userMessage).toContain("Vellum managed inference");
148
+ expect(result.errorCategory).toBe("managed_usage_limit");
149
+ });
150
+
151
+ it("keeps provider copy for direct provider 429s", () => {
152
+ providerRoutingSources.anthropic = "user-key";
153
+ const err = new ProviderError(
154
+ "Anthropic API error (429): Too many requests",
155
+ "anthropic",
156
+ 429,
157
+ );
158
+
159
+ const result = classifyConversationError(err, baseCtx);
160
+
161
+ expect(result.code).toBe("PROVIDER_RATE_LIMIT");
162
+ expect(result.userMessage).toContain("AI provider");
163
+ expect(result.errorCategory).toBe("rate_limit");
164
+ });
108
165
  });
109
166
 
110
167
  describe("provider overloaded errors", () => {
@@ -509,11 +566,11 @@ describe("classifyConversationError", () => {
509
566
  expect(result.errorCategory).toBe("provider_not_configured");
510
567
  });
511
568
 
512
- it("classifies ProviderError with 402 as credits_exhausted (non-retryable)", () => {
569
+ it("classifies direct ProviderError with 402 as provider_billing (non-retryable)", () => {
513
570
  const err = new ProviderError("Payment Required", "anthropic", 402);
514
571
  const result = classifyConversationError(err, baseCtx);
515
572
  expect(result.code).toBe("PROVIDER_BILLING");
516
- expect(result.errorCategory).toBe("credits_exhausted");
573
+ expect(result.errorCategory).toBe("provider_billing");
517
574
  expect(result.retryable).toBe(false);
518
575
  });
519
576
 
@@ -560,6 +617,96 @@ describe("classifyConversationError", () => {
560
617
  });
561
618
  });
562
619
 
620
+ describe("OpenRouter billing classification", () => {
621
+ it("keeps managed-proxy OpenRouter 402 responses as credits_exhausted", () => {
622
+ providerRoutingSources.openrouter = "managed-proxy";
623
+ const err = new ProviderError(
624
+ "OpenRouter API error (402): Payment Required",
625
+ "openrouter",
626
+ 402,
627
+ );
628
+
629
+ const result = classifyConversationError(err, baseCtx);
630
+
631
+ expect(result.code).toBe("PROVIDER_BILLING");
632
+ expect(result.errorCategory).toBe("credits_exhausted");
633
+ expect(result.retryable).toBe(false);
634
+ expect(result.userMessage).toContain("Add funds");
635
+ expect(result.userMessage).toContain("assistant");
636
+ });
637
+
638
+ it("classifies direct Anthropic, OpenAI, and OpenRouter 402 responses as provider_billing", () => {
639
+ providerRoutingSources.anthropic = "user-key";
640
+ providerRoutingSources.openai = "user-key";
641
+ providerRoutingSources.openrouter = "user-key";
642
+
643
+ for (const provider of ["anthropic", "openai", "openrouter"]) {
644
+ const err = new ProviderError(
645
+ `${provider} API error (402): Payment Required`,
646
+ provider,
647
+ 402,
648
+ );
649
+
650
+ const result = classifyConversationError(err, baseCtx);
651
+
652
+ expect(result.code).toBe("PROVIDER_BILLING");
653
+ expect(result.errorCategory).toBe("provider_billing");
654
+ expect(result.retryable).toBe(false);
655
+ expect(result.userMessage).toContain("provider");
656
+ expect(result.userMessage).toContain("Settings");
657
+ }
658
+ });
659
+
660
+ it("classifies OpenRouter 400 credit-limit messages as provider_billing", () => {
661
+ const cases = [
662
+ "OpenRouter API error (400): This request requires more credits",
663
+ "OpenRouter API error (400): You can only afford 1000 tokens",
664
+ ];
665
+
666
+ for (const message of cases) {
667
+ const err = new ProviderError(message, "openrouter", 400);
668
+
669
+ const result = classifyConversationError(err, baseCtx);
670
+
671
+ expect(result.code).toBe("PROVIDER_BILLING");
672
+ expect(result.errorCategory).toBe("provider_billing");
673
+ expect(result.retryable).toBe(false);
674
+ }
675
+ });
676
+
677
+ it("classifies managed-proxy OpenRouter insufficient_balance bodies as credits_exhausted", () => {
678
+ providerRoutingSources.openrouter = "managed-proxy";
679
+ const err = new ProviderError(
680
+ 'OpenRouter API error (402): {"code":"insufficient_balance","detail":"Managed balance exhausted"}',
681
+ "openrouter",
682
+ 402,
683
+ );
684
+
685
+ const result = classifyConversationError(err, baseCtx);
686
+
687
+ expect(result.code).toBe("PROVIDER_BILLING");
688
+ expect(result.errorCategory).toBe("credits_exhausted");
689
+ expect(result.retryable).toBe(false);
690
+ });
691
+
692
+ it("classifies direct OpenRouter insufficient_balance bodies as provider_billing", () => {
693
+ providerRoutingSources.openrouter = "user-key";
694
+ const err = new ProviderError(
695
+ 'OpenRouter API error (402): {"code":"insufficient_balance","detail":"Provider account balance exhausted"}',
696
+ "openrouter",
697
+ 402,
698
+ );
699
+
700
+ const result = classifyConversationError(err, baseCtx);
701
+
702
+ expect(result.code).toBe("PROVIDER_BILLING");
703
+ expect(result.errorCategory).toBe("provider_billing");
704
+ expect(result.retryable).toBe(false);
705
+ expect(result.userMessage).toContain("provider");
706
+ expect(result.userMessage).toContain("Settings");
707
+ });
708
+ });
709
+
563
710
  describe("debug detail truncation", () => {
564
711
  it("truncates debugDetails longer than 4000 chars", () => {
565
712
  const longMsg = "x".repeat(5000);
@@ -465,7 +465,7 @@ describe("End-to-end session creation benchmark", () => {
465
465
  timings.push(performance.now() - start);
466
466
 
467
467
  if (i === 0) {
468
- expect(session.eventBus.listenerCount()).toBeGreaterThan(0);
468
+ expect(session.eventBus.anyListenerCount()).toBeGreaterThan(0);
469
469
  }
470
470
  session.dispose();
471
471
  }
@@ -77,6 +77,7 @@ mock.module("../config/loader.js", () => ({
77
77
  pricingOverrides: [],
78
78
  },
79
79
  rateLimit: { maxRequestsPerMinute: 0 },
80
+ memory: { v2: { enabled: false } },
80
81
  daemon: {
81
82
  startupSocketWaitMs: 5000,
82
83
  stopTimeoutMs: 5000,
@@ -382,4 +383,41 @@ describe("processMessage callSite threading", () => {
382
383
  expect(captured.resolvedMaxTokens).toBe(1234);
383
384
  expect(captured.resolvedHasMaxTokens).toBe(true);
384
385
  });
386
+
387
+ test("applies clientTimezone in the create and reuse transport metadata path", async () => {
388
+ mockConversation = {
389
+ id: "conv-store-client-timezone",
390
+ contextSummary: null,
391
+ contextCompactedMessageCount: 0,
392
+ totalInputTokens: 0,
393
+ totalOutputTokens: 0,
394
+ totalEstimatedCost: 0,
395
+ };
396
+ mockDbMessages = [];
397
+ clearCaptured();
398
+ clearAllActiveConversations();
399
+
400
+ const conversation = await getOrCreateConversation(
401
+ "conv-store-client-timezone",
402
+ {
403
+ transport: {
404
+ channelId: "vellum",
405
+ interfaceId: "macos",
406
+ clientTimezone: "america/new_york",
407
+ },
408
+ },
409
+ );
410
+
411
+ expect(conversation.clientTimezone).toBe("America/New_York");
412
+
413
+ await getOrCreateConversation("conv-store-client-timezone", {
414
+ transport: {
415
+ channelId: "vellum",
416
+ interfaceId: "ios",
417
+ clientTimezone: "europe/london",
418
+ },
419
+ });
420
+
421
+ expect(conversation.clientTimezone).toBe("Europe/London");
422
+ });
385
423
  });
@@ -57,6 +57,7 @@ mock.module("../config/loader.js", () => ({
57
57
  pricingOverrides: [],
58
58
  },
59
59
  rateLimit: { maxRequestsPerMinute: 0 },
60
+ memory: { v2: { enabled: false } },
60
61
  services: {
61
62
  inference: {
62
63
  mode: "your-own",
@@ -1,5 +1,21 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
+ // This test exercises v1 PKB injection. `config.memory.v2.enabled` (default
4
+ // `true`) makes the PKB injector go silent — force it off here so the v1
5
+ // injection chain assertions stay meaningful.
6
+ const realLoaderForAssemblyTest = await import("../config/loader.js");
7
+ const realGetConfigForAssemblyTest = realLoaderForAssemblyTest.getConfig;
8
+ mock.module("../config/loader.js", () => ({
9
+ ...realLoaderForAssemblyTest,
10
+ getConfig: () => {
11
+ const real = realGetConfigForAssemblyTest();
12
+ return {
13
+ ...real,
14
+ memory: { ...real.memory, v2: { ...real.memory.v2, enabled: false } },
15
+ };
16
+ },
17
+ }));
18
+
3
19
  // PKB search is mocked so the reminder-hints tests can assert behavior
4
20
  // without standing up Qdrant. The mock returns whatever is staged in
5
21
  // `pkbSearchResults` / `pkbSearchThrows` for the enclosing test.
@@ -1430,6 +1446,64 @@ describe("buildUnifiedTurnContextBlock", () => {
1430
1446
  expect(text).toContain("time_since_last_message: yesterday");
1431
1447
  expect(text).toContain("canonical_actor_identity: user-1");
1432
1448
  });
1449
+
1450
+ test("timezone mismatch: omits extra lines when there is no manual override", () => {
1451
+ const text = buildUnifiedTurnContextBlock({
1452
+ timestamp: "2026-04-02 (Thursday) 08:00:00 -04:00 (America/New_York)",
1453
+ clientTimezone: "America/New_York",
1454
+ detectedTimezone: "America/New_York",
1455
+ });
1456
+
1457
+ expect(text).toContain(
1458
+ "current_time: 2026-04-02 (Thursday) 08:00:00 -04:00 (America/New_York)",
1459
+ );
1460
+ expect(text).not.toContain("configured_user_timezone:");
1461
+ expect(text).not.toContain("client_device_timezone:");
1462
+ expect(text).not.toContain("timezone_update_available:");
1463
+ });
1464
+
1465
+ test("timezone mismatch: omits extra lines when configured and client timezone match", () => {
1466
+ const text = buildUnifiedTurnContextBlock({
1467
+ timestamp: "2026-04-02 (Thursday) 08:00:00 -04:00 (America/New_York)",
1468
+ configuredUserTimezone: "America/New_York",
1469
+ clientTimezone: "America/New_York",
1470
+ detectedTimezone: "America/New_York",
1471
+ });
1472
+
1473
+ expect(text).not.toContain("configured_user_timezone:");
1474
+ expect(text).not.toContain("client_device_timezone:");
1475
+ expect(text).not.toContain("timezone_update_available:");
1476
+ });
1477
+
1478
+ test("timezone mismatch: emits configured and client device timezone when they differ", () => {
1479
+ const text = buildUnifiedTurnContextBlock({
1480
+ timestamp: "2026-04-02 (Thursday) 08:00:00 -04:00 (America/New_York)",
1481
+ configuredUserTimezone: "America/New_York",
1482
+ clientTimezone: "America/Los_Angeles",
1483
+ detectedTimezone: "America/Los_Angeles",
1484
+ });
1485
+
1486
+ expect(text).toContain("configured_user_timezone: America/New_York");
1487
+ expect(text).toContain("client_device_timezone: America/Los_Angeles");
1488
+ });
1489
+
1490
+ test("timezone mismatch: emits CLI affordance only in mismatch case", () => {
1491
+ const mismatchText = buildUnifiedTurnContextBlock({
1492
+ timestamp: "2026-04-02 (Thursday) 08:00:00 -04:00 (America/New_York)",
1493
+ configuredUserTimezone: "America/New_York",
1494
+ clientTimezone: "America/Los_Angeles",
1495
+ });
1496
+ const matchingText = buildUnifiedTurnContextBlock({
1497
+ timestamp: "2026-04-02 (Thursday) 08:00:00 -04:00 (America/New_York)",
1498
+ configuredUserTimezone: "America/New_York",
1499
+ clientTimezone: "America/New_York",
1500
+ });
1501
+
1502
+ expect(mismatchText).toContain(
1503
+ 'timezone_update_available: after explicit user confirmation, persist client_device_timezone with `assistant config set ui.userTimezone "America/Los_Angeles"`',
1504
+ );
1505
+ expect(matchingText).not.toContain("timezone_update_available:");
1506
+ });
1433
1507
  });
1434
1508
 
1435
1509
  // ---------------------------------------------------------------------------
@@ -60,6 +60,7 @@ mock.module("../config/loader.js", () => ({
60
60
  pricingOverrides: [],
61
61
  },
62
62
  rateLimit: { maxRequestsPerMinute: 0 },
63
+ memory: { v2: { enabled: false } },
63
64
  daemon: {
64
65
  startupSocketWaitMs: 5000,
65
66
  stopTimeoutMs: 5000,
@@ -294,7 +294,6 @@ describe("per-conversation speed override", () => {
294
294
  4096,
295
295
  makeSendToClient(),
296
296
  "/tmp",
297
- undefined, // memoryPolicy
298
297
  undefined, // sharedCesClient
299
298
  "standard", // speedOverride
300
299
  );
@@ -315,7 +314,6 @@ describe("per-conversation speed override", () => {
315
314
  4096,
316
315
  makeSendToClient(),
317
316
  "/tmp",
318
- undefined, // memoryPolicy
319
317
  undefined, // sharedCesClient
320
318
  // no speedOverride — should fall back to global config "fast"
321
319
  );
@@ -335,7 +333,6 @@ describe("per-conversation speed override", () => {
335
333
  4096,
336
334
  makeSendToClient(),
337
335
  "/tmp",
338
- undefined, // memoryPolicy
339
336
  undefined, // sharedCesClient
340
337
  "fast", // speedOverride
341
338
  );
@@ -19,7 +19,6 @@ import {
19
19
  createConversation,
20
20
  deleteLastExchange,
21
21
  getConversation,
22
- getConversationMemoryScopeId,
23
22
  getMessages,
24
23
  } from "../memory/conversation-crud.js";
25
24
  import { isLastUserMessageToolResult } from "../memory/conversation-queries.js";
@@ -454,23 +453,6 @@ describe("createConversation with conversation type option", () => {
454
453
  });
455
454
  });
456
455
 
457
- describe("conversation metadata read helpers", () => {
458
- beforeEach(() => {
459
- const db = getDb();
460
- db.run(`DELETE FROM messages`);
461
- db.run(`DELETE FROM conversations`);
462
- });
463
-
464
- test("getConversationMemoryScopeId returns default for standard conversation", () => {
465
- const conv = createConversation("test");
466
- expect(getConversationMemoryScopeId(conv.id)).toBe("default");
467
- });
468
-
469
- test("getConversationMemoryScopeId returns default for missing conversation", () => {
470
- expect(getConversationMemoryScopeId("nonexistent-id")).toBe("default");
471
- });
472
- });
473
-
474
456
  // ---------------------------------------------------------------------------
475
457
  // Baseline: attachment reuse across conversations
476
458
  // ---------------------------------------------------------------------------
@@ -1,13 +1,19 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import {
4
- createSurfaceMutex,
5
- handleSurfaceAction,
6
- type SurfaceConversationContext,
7
- surfaceProxyResolver,
8
- } from "../daemon/conversation-surfaces.js";
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { ServerMessage } from "../daemon/message-protocol.js";
4
+
5
+ let broadcastedMessages: ServerMessage[] = [];
6
+ const realEventHub = await import("../runtime/assistant-event-hub.js");
7
+ mock.module("../runtime/assistant-event-hub.js", () => ({
8
+ ...realEventHub,
9
+ broadcastMessage: (msg: ServerMessage) => broadcastedMessages.push(msg),
10
+ }));
11
+
12
+ const { createSurfaceMutex, handleSurfaceAction, surfaceProxyResolver } =
13
+ await import("../daemon/conversation-surfaces.js");
14
+
15
+ import type { SurfaceConversationContext } from "../daemon/conversation-surfaces.js";
9
16
  import type {
10
- ServerMessage,
11
17
  SurfaceData,
12
18
  SurfaceType,
13
19
  UiSurfaceShow,
@@ -81,6 +87,10 @@ function makeContext(sent: ServerMessage[] = []): SurfaceConversationContext & {
81
87
  }
82
88
 
83
89
  describe("surface action delivery to assistant", () => {
90
+ beforeEach(() => {
91
+ broadcastedMessages = [];
92
+ });
93
+
84
94
  test("table action button click triggers processMessage with action content", async () => {
85
95
  const sent: ServerMessage[] = [];
86
96
  const ctx = makeContext(sent);
@@ -199,4 +209,155 @@ describe("surface action delivery to assistant", () => {
199
209
  "[User action on app:",
200
210
  );
201
211
  });
212
+
213
+ test("confirmation surface broadcasts ui_surface_complete on action", async () => {
214
+ const sent: ServerMessage[] = [];
215
+ const ctx = makeContext(sent);
216
+
217
+ const showResult = await surfaceProxyResolver(ctx, "ui_show", {
218
+ surface_type: "confirmation",
219
+ title: "Delete files?",
220
+ data: {
221
+ message: "This will permanently delete 3 files.",
222
+ confirmLabel: "Delete",
223
+ cancelLabel: "Keep",
224
+ },
225
+ });
226
+
227
+ expect(showResult.isError).toBe(false);
228
+ expect(showResult.yieldToUser).toBe(true);
229
+
230
+ const showMessage = sent.find(
231
+ (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show",
232
+ ) as UiSurfaceShow;
233
+ const surfaceId = showMessage.surfaceId;
234
+ expect(ctx.pendingSurfaceActions.has(surfaceId)).toBe(true);
235
+
236
+ await handleSurfaceAction(ctx, surfaceId, "confirm", {});
237
+
238
+ const completeMsg = broadcastedMessages.find(
239
+ (m) =>
240
+ (m as unknown as Record<string, unknown>).type ===
241
+ "ui_surface_complete" &&
242
+ (m as unknown as Record<string, unknown>).surfaceId === surfaceId,
243
+ ) as unknown as Record<string, unknown> | undefined;
244
+ expect(completeMsg).toBeDefined();
245
+ expect(completeMsg?.conversationId).toBe("conv-1");
246
+ expect(completeMsg?.summary).toContain("Delete");
247
+ });
248
+
249
+ test("file_upload surface broadcasts ui_surface_complete on action", async () => {
250
+ const sent: ServerMessage[] = [];
251
+ const ctx = makeContext(sent);
252
+
253
+ const showResult = await surfaceProxyResolver(ctx, "ui_show", {
254
+ surface_type: "file_upload",
255
+ title: "Upload documents",
256
+ data: { accept: ".pdf,.docx", maxFiles: 5 },
257
+ });
258
+
259
+ expect(showResult.isError).toBe(false);
260
+ expect(showResult.yieldToUser).toBe(true);
261
+
262
+ const showMessage = sent.find(
263
+ (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show",
264
+ ) as UiSurfaceShow;
265
+ const surfaceId = showMessage.surfaceId;
266
+ expect(ctx.pendingSurfaceActions.has(surfaceId)).toBe(true);
267
+
268
+ await handleSurfaceAction(ctx, surfaceId, "submit", {
269
+ files: [
270
+ {
271
+ filename: "doc.pdf",
272
+ mimeType: "application/pdf",
273
+ data: "base64encodedcontent",
274
+ },
275
+ ],
276
+ });
277
+
278
+ const completeMsg = broadcastedMessages.find(
279
+ (m) =>
280
+ (m as unknown as Record<string, unknown>).type ===
281
+ "ui_surface_complete" &&
282
+ (m as unknown as Record<string, unknown>).surfaceId === surfaceId,
283
+ ) as unknown as Record<string, unknown> | undefined;
284
+ expect(completeMsg).toBeDefined();
285
+ expect(completeMsg?.conversationId).toBe("conv-1");
286
+ });
287
+
288
+ test("file_upload completion event does not include base64 file blobs", async () => {
289
+ const sent: ServerMessage[] = [];
290
+ const ctx = makeContext(sent);
291
+
292
+ await surfaceProxyResolver(ctx, "ui_show", {
293
+ surface_type: "file_upload",
294
+ title: "Upload",
295
+ data: { accept: "*" },
296
+ });
297
+
298
+ const showMessage = sent.find(
299
+ (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show",
300
+ ) as UiSurfaceShow;
301
+ const surfaceId = showMessage.surfaceId;
302
+
303
+ const largeBase64 = "A".repeat(10_000);
304
+ await handleSurfaceAction(ctx, surfaceId, "submit", {
305
+ files: [
306
+ {
307
+ filename: "big.pdf",
308
+ mimeType: "application/pdf",
309
+ data: largeBase64,
310
+ },
311
+ ],
312
+ });
313
+
314
+ const completeMsg = broadcastedMessages.find(
315
+ (m) =>
316
+ (m as unknown as Record<string, unknown>).type ===
317
+ "ui_surface_complete" &&
318
+ (m as unknown as Record<string, unknown>).surfaceId === surfaceId,
319
+ ) as unknown as Record<string, unknown> | undefined;
320
+ expect(completeMsg).toBeDefined();
321
+
322
+ const submittedData = completeMsg?.submittedData as
323
+ | Record<string, unknown>
324
+ | undefined;
325
+ // The files array with base64 blobs should be stripped from the
326
+ // completion event — only the sanitized payload (without files) is sent.
327
+ expect(submittedData?.files).toBeUndefined();
328
+ // The raw base64 content should not appear anywhere in the event
329
+ expect(JSON.stringify(completeMsg)).not.toContain(largeBase64);
330
+ });
331
+
332
+ test("table surface does NOT broadcast ui_surface_complete (not one-shot)", async () => {
333
+ const sent: ServerMessage[] = [];
334
+ const ctx = makeContext(sent);
335
+
336
+ await surfaceProxyResolver(ctx, "ui_show", {
337
+ surface_type: "table",
338
+ title: "Items",
339
+ data: {
340
+ columns: [{ id: "name", label: "Name" }],
341
+ rows: [{ id: "r1", cells: { name: "Item 1" } }],
342
+ },
343
+ actions: [{ id: "select", label: "Select" }],
344
+ });
345
+
346
+ const showMessage = sent.find(
347
+ (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show",
348
+ ) as UiSurfaceShow;
349
+ const surfaceId = showMessage.surfaceId;
350
+
351
+ broadcastedMessages = [];
352
+ await handleSurfaceAction(ctx, surfaceId, "select", {
353
+ selectedIds: ["r1"],
354
+ });
355
+
356
+ const completeMsg = broadcastedMessages.find(
357
+ (m) =>
358
+ (m as unknown as Record<string, unknown>).type ===
359
+ "ui_surface_complete",
360
+ );
361
+ expect(completeMsg).toBeUndefined();
362
+ });
202
363
  });
@@ -33,8 +33,11 @@ mock.module("../runtime/pending-interactions.js", () => ({
33
33
 
34
34
  const { surfaceProxyResolver } =
35
35
  await import("../daemon/conversation-surfaces.js");
36
- const { HostAppControlProxy, _resetActiveAppControlConversationId } =
37
- await import("../daemon/host-app-control-proxy.js");
36
+ const {
37
+ HostAppControlProxy,
38
+ _resetActiveAppControlSession,
39
+ _setActiveAppControlSession,
40
+ } = await import("../daemon/host-app-control-proxy.js");
38
41
  type SurfaceConversationContext =
39
42
  import("../daemon/conversation-surfaces.js").SurfaceConversationContext;
40
43
 
@@ -83,11 +86,11 @@ describe("surfaceProxyResolver — app-control tool routing", () => {
83
86
  beforeEach(() => {
84
87
  sentMessages.length = 0;
85
88
  mockHasClient = true;
86
- _resetActiveAppControlConversationId();
89
+ _resetActiveAppControlSession();
87
90
  });
88
91
 
89
92
  afterEach(() => {
90
- _resetActiveAppControlConversationId();
93
+ _resetActiveAppControlSession();
91
94
  });
92
95
 
93
96
  // -------------------------------------------------------------------------
@@ -148,6 +151,10 @@ describe("surfaceProxyResolver — app-control tool routing", () => {
148
151
  test("app_control_observe routes through proxy and returns observation", async () => {
149
152
  const proxy = new HostAppControlProxy("conv-1");
150
153
  const ctx = buildMockContext(proxy, "conv-1");
154
+ _setActiveAppControlSession({
155
+ conversationId: "conv-1",
156
+ app: "com.example.editor",
157
+ });
151
158
 
152
159
  const resultPromise = surfaceProxyResolver(ctx, "app_control_observe", {
153
160
  tool: "observe",
@@ -254,6 +261,10 @@ describe("surfaceProxyResolver — app-control tool routing", () => {
254
261
  test("injects `tool` derived from toolName when the agent input omits it", async () => {
255
262
  const proxy = new HostAppControlProxy("conv-1");
256
263
  const ctx = buildMockContext(proxy, "conv-1");
264
+ _setActiveAppControlSession({
265
+ conversationId: "conv-1",
266
+ app: "com.example.editor",
267
+ });
257
268
 
258
269
  // Agent inputs do not carry the discriminator — the resolver has to
259
270
  // synthesize it from `toolName` ("app_control_observe" → "observe")