@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
@@ -122,6 +122,136 @@ describe("MCP AbortSignal threading", () => {
122
122
  });
123
123
 
124
124
  describe("createMcpTool execute", () => {
125
+ test("keeps safe MCP tool names unchanged", () => {
126
+ const fakeManager = { callTool: jest.fn() } as any;
127
+
128
+ const tool = createMcpTool(
129
+ {
130
+ name: "my-tool",
131
+ description: "A test tool",
132
+ inputSchema: { type: "object", properties: {} },
133
+ },
134
+ "test-server",
135
+ {
136
+ transport: { type: "stdio", command: "echo", args: [] },
137
+ enabled: true,
138
+ defaultRiskLevel: "high",
139
+ maxTools: 100,
140
+ },
141
+ fakeManager,
142
+ );
143
+
144
+ expect(tool.name).toBe("mcp__test-server__my-tool");
145
+ expect(tool.getDefinition().name).toBe("mcp__test-server__my-tool");
146
+ });
147
+
148
+ test("keeps MCP tool names with trailing whitespace distinct", () => {
149
+ const fakeManager = { callTool: jest.fn() } as any;
150
+
151
+ const plain = createMcpTool(
152
+ {
153
+ name: "deploy",
154
+ description: "Deploy",
155
+ inputSchema: { type: "object", properties: {} },
156
+ },
157
+ "test-server",
158
+ {
159
+ transport: { type: "stdio", command: "echo", args: [] },
160
+ enabled: true,
161
+ defaultRiskLevel: "high",
162
+ maxTools: 100,
163
+ },
164
+ fakeManager,
165
+ );
166
+ const padded = createMcpTool(
167
+ {
168
+ name: "deploy ",
169
+ description: "Deploy padded",
170
+ inputSchema: { type: "object", properties: {} },
171
+ },
172
+ "test-server",
173
+ {
174
+ transport: { type: "stdio", command: "echo", args: [] },
175
+ enabled: true,
176
+ defaultRiskLevel: "high",
177
+ maxTools: 100,
178
+ },
179
+ fakeManager,
180
+ );
181
+
182
+ expect(plain.name).toBe("mcp__test-server__deploy");
183
+ expect(padded.name).toMatch(/^mcp__test-server__deploy__[a-f0-9]{12}$/);
184
+ expect(padded.name).not.toBe(plain.name);
185
+ });
186
+
187
+ test("exposes provider-safe MCP names while preserving raw execution names", async () => {
188
+ const callToolSpy = jest.fn().mockResolvedValue({
189
+ content: "tool result",
190
+ isError: false,
191
+ });
192
+ const fakeManager = { callTool: callToolSpy } as any;
193
+
194
+ const tool = createMcpTool(
195
+ {
196
+ name: "create link",
197
+ description: "Create a Stripe Link CLI resource",
198
+ inputSchema: { type: "object", properties: {} },
199
+ },
200
+ "stripe.link-cli",
201
+ {
202
+ transport: { type: "stdio", command: "echo", args: [] },
203
+ enabled: true,
204
+ defaultRiskLevel: "high",
205
+ maxTools: 100,
206
+ },
207
+ fakeManager,
208
+ );
209
+
210
+ expect(tool.name).toMatch(/^[a-zA-Z0-9_-]{1,64}$/);
211
+ expect(tool.name.startsWith("mcp__stripe_link-cli__create_link__")).toBe(
212
+ true,
213
+ );
214
+ expect(tool.getDefinition().name).toBe(tool.name);
215
+
216
+ await tool.execute(
217
+ { someArg: "value" },
218
+ {
219
+ workingDir: "/tmp",
220
+ conversationId: "conv-1",
221
+ trustClass: "guardian",
222
+ },
223
+ );
224
+
225
+ expect(callToolSpy).toHaveBeenCalledWith(
226
+ "stripe.link-cli",
227
+ "create link",
228
+ { someArg: "value" },
229
+ undefined,
230
+ );
231
+ });
232
+
233
+ test("caps long MCP names at the provider limit", () => {
234
+ const fakeManager = { callTool: jest.fn() } as any;
235
+ const tool = createMcpTool(
236
+ {
237
+ name: "x".repeat(180),
238
+ description: "A test tool",
239
+ inputSchema: { type: "object", properties: {} },
240
+ },
241
+ "server",
242
+ {
243
+ transport: { type: "stdio", command: "echo", args: [] },
244
+ enabled: true,
245
+ defaultRiskLevel: "high",
246
+ maxTools: 100,
247
+ },
248
+ fakeManager,
249
+ );
250
+
251
+ expect(tool.name).toHaveLength(64);
252
+ expect(tool.name).toMatch(/^[a-zA-Z0-9_-]{1,64}$/);
253
+ });
254
+
125
255
  test("threads context.signal through manager.callTool", async () => {
126
256
  const callToolSpy = jest.fn().mockResolvedValue({
127
257
  content: "tool result",
@@ -15,7 +15,6 @@ interface CapturedSearch {
15
15
 
16
16
  const capturedSearches: CapturedSearch[] = [];
17
17
  const getConfiguredProviderCalls: string[] = [];
18
- const scopeByConversation = new Map<string, string | undefined>();
19
18
  const testConfig = {} as AssistantConfig;
20
19
 
21
20
  mock.module("../config/loader.js", () => ({
@@ -56,13 +55,11 @@ mock.module("../memory/embedding-backend.js", () => ({
56
55
 
57
56
  mock.module("../memory/conversation-crud.js", () => ({
58
57
  addMessage: () => ({ id: "msg-1" }),
59
- createConversation: () => ({ id: "conv-1", memoryScopeId: "default" }),
58
+ createConversation: () => ({ id: "conv-1" }),
60
59
  deleteConversation: () => true,
61
60
  getAssistantMessageIdsInTurn: () => [],
62
61
  getConversation: () => null,
63
62
  getConversationHostAccess: () => false,
64
- getConversationMemoryScopeId: (conversationId: string) =>
65
- scopeByConversation.get(conversationId),
66
63
  getConversationOverrideProfile: () => undefined,
67
64
  getConversationSource: () => null,
68
65
  getMessageById: () => null,
@@ -165,12 +162,9 @@ describe("memory admin recall", () => {
165
162
  beforeEach(() => {
166
163
  capturedSearches.length = 0;
167
164
  getConfiguredProviderCalls.length = 0;
168
- scopeByConversation.clear();
169
165
  });
170
166
 
171
- test("uses the conversation memory scope and safe admin sources", async () => {
172
- scopeByConversation.set("conv-admin", "scope-admin");
173
-
167
+ test("uses safe admin sources", async () => {
174
168
  const result = await queryMemory("launch notes", "conv-admin");
175
169
 
176
170
  expect(capturedSearches).toHaveLength(1);
@@ -180,7 +174,6 @@ describe("memory admin recall", () => {
180
174
  });
181
175
  expect(capturedSearches[0].context).toMatchObject({
182
176
  workingDir: getWorkspaceDir(),
183
- memoryScopeId: "scope-admin",
184
177
  conversationId: "conv-admin",
185
178
  config: testConfig,
186
179
  });
@@ -202,13 +195,12 @@ describe("memory admin recall", () => {
202
195
  });
203
196
  });
204
197
 
205
- test("falls back to default scope without invoking a provider", async () => {
198
+ test("does not invoke a provider for deterministic recall", async () => {
206
199
  await queryMemory("offline recall", "missing-conversation");
207
200
 
208
201
  expect(capturedSearches).toHaveLength(1);
209
202
  expect(capturedSearches[0].context).toMatchObject({
210
203
  workingDir: getWorkspaceDir(),
211
- memoryScopeId: "default",
212
204
  conversationId: "missing-conversation",
213
205
  });
214
206
  expect(capturedSearches[0].input.sources).toEqual([
@@ -158,6 +158,27 @@ describe("runDefaultMemoryRetrieval", () => {
158
158
  expect(result.nowContent).toBe("now-default");
159
159
  });
160
160
 
161
+ test("propagates errors from prepareMemory rather than swallowing them", async () => {
162
+ // Memory is critical — failures must surface to the caller (the agent
163
+ // loop) rather than silently degrading to an empty memory block.
164
+ const failingPrepare = mock(
165
+ (
166
+ _msgs: Message[],
167
+ _cfg: AssistantConfig,
168
+ _signal: AbortSignal,
169
+ _onEvent: (msg: ServerMessage) => void,
170
+ ) => Promise.reject(new Error("retrieval failed")),
171
+ );
172
+ const graphMemory = {
173
+ prepareMemory: failingPrepare,
174
+ } as unknown as ConversationGraphMemory;
175
+ const deps = makeDeps({ graphMemory, isTrustedActor: true });
176
+
177
+ await expect(
178
+ runDefaultMemoryRetrieval(makeMemoryArgs(), deps),
179
+ ).rejects.toThrow("retrieval failed");
180
+ });
181
+
161
182
  test("passes through null PKB and NOW when the files are absent", async () => {
162
183
  readPkbContextMock.mockImplementation(() => null);
163
184
  readNowContextMock.mockImplementation(() => null);
@@ -314,7 +335,7 @@ describe("memoryRetrieval pipeline — default vs custom plugin", () => {
314
335
  (innerArgs: MemoryArgs) => runDefaultMemoryRetrieval(innerArgs, deps),
315
336
  args,
316
337
  makeTurnCtx(),
317
- 30, // tiny budget real production path uses 5_000ms
338
+ 30, // tiny pipeline budget to keep the test fast
318
339
  );
319
340
  } catch (err) {
320
341
  caught = err;
@@ -0,0 +1,180 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ normalizeOnboardingContext,
5
+ normalizeTasks,
6
+ normalizeTools,
7
+ TASK_DISPLAY_LABELS,
8
+ TOOL_DISPLAY_NAMES,
9
+ } from "../prompts/normalize-onboarding.js";
10
+ import type { OnboardingContext } from "../types/onboarding-context.js";
11
+
12
+ describe("normalizeTools", () => {
13
+ test("known tool IDs produce display labels", () => {
14
+ expect(normalizeTools(["github"])).toEqual(["GitHub"]);
15
+ expect(normalizeTools(["google-calendar"])).toEqual(["Google Calendar"]);
16
+ expect(normalizeTools(["slack"])).toEqual(["Slack"]);
17
+ expect(normalizeTools(["notion"])).toEqual(["Notion"]);
18
+ expect(normalizeTools(["linear"])).toEqual(["Linear"]);
19
+ expect(normalizeTools(["gmail"])).toEqual(["Gmail"]);
20
+ expect(normalizeTools(["google-drive"])).toEqual(["Google Drive"]);
21
+ expect(normalizeTools(["figma"])).toEqual(["Figma"]);
22
+ expect(normalizeTools(["jira"])).toEqual(["Jira"]);
23
+ expect(normalizeTools(["outlook"])).toEqual(["Outlook"]);
24
+ expect(normalizeTools(["excel"])).toEqual(["Excel"]);
25
+ expect(normalizeTools(["apple-notes"])).toEqual(["Apple Notes"]);
26
+ });
27
+
28
+ test("all known tool IDs from the client onboarding UI are mapped", () => {
29
+ const clientToolIds = [
30
+ "gmail",
31
+ "outlook",
32
+ "google-calendar",
33
+ "slack",
34
+ "notion",
35
+ "linear",
36
+ "jira",
37
+ "github",
38
+ "figma",
39
+ "google-drive",
40
+ "excel",
41
+ "apple-notes",
42
+ ];
43
+ expect(Object.keys(TOOL_DISPLAY_NAMES)).toEqual(
44
+ expect.arrayContaining(clientToolIds),
45
+ );
46
+ expect(Object.keys(TOOL_DISPLAY_NAMES)).toHaveLength(clientToolIds.length);
47
+ });
48
+
49
+ test("unknown/custom tool IDs pass through with first-letter capitalization", () => {
50
+ expect(normalizeTools(["trello"])).toEqual(["Trello"]);
51
+ expect(normalizeTools(["asana"])).toEqual(["Asana"]);
52
+ });
53
+
54
+ test("mixed known and unknown IDs normalize correctly", () => {
55
+ expect(normalizeTools(["github", "trello", "slack"])).toEqual([
56
+ "GitHub",
57
+ "Trello",
58
+ "Slack",
59
+ ]);
60
+ });
61
+
62
+ test("empty array produces empty array", () => {
63
+ expect(normalizeTools([])).toEqual([]);
64
+ });
65
+ });
66
+
67
+ describe("normalizeTasks", () => {
68
+ test("known task IDs produce plain-language labels", () => {
69
+ expect(normalizeTasks(["code-building"])).toEqual([
70
+ "builds code, apps, or tools",
71
+ ]);
72
+ expect(normalizeTasks(["writing"])).toEqual([
73
+ "writes docs, emails, or content",
74
+ ]);
75
+ expect(normalizeTasks(["research"])).toEqual([
76
+ "does research and analysis",
77
+ ]);
78
+ expect(normalizeTasks(["project-management"])).toEqual([
79
+ "plans and coordinates work",
80
+ ]);
81
+ expect(normalizeTasks(["scheduling"])).toEqual([
82
+ "handles meetings, calendar, and logistics",
83
+ ]);
84
+ expect(normalizeTasks(["personal"])).toEqual(["handles life admin"]);
85
+ });
86
+
87
+ test("all six known task IDs are mapped", () => {
88
+ const knownIds = [
89
+ "code-building",
90
+ "writing",
91
+ "research",
92
+ "project-management",
93
+ "scheduling",
94
+ "personal",
95
+ ];
96
+ expect(Object.keys(TASK_DISPLAY_LABELS)).toEqual(
97
+ expect.arrayContaining(knownIds),
98
+ );
99
+ expect(Object.keys(TASK_DISPLAY_LABELS)).toHaveLength(knownIds.length);
100
+ });
101
+
102
+ test("unknown/custom task IDs pass through unchanged", () => {
103
+ expect(normalizeTasks(["data-entry"])).toEqual(["data-entry"]);
104
+ expect(normalizeTasks(["custom-workflow"])).toEqual(["custom-workflow"]);
105
+ });
106
+
107
+ test("mixed known and unknown IDs normalize correctly", () => {
108
+ expect(normalizeTasks(["writing", "data-entry", "research"])).toEqual([
109
+ "writes docs, emails, or content",
110
+ "data-entry",
111
+ "does research and analysis",
112
+ ]);
113
+ });
114
+
115
+ test("empty array produces empty array", () => {
116
+ expect(normalizeTasks([])).toEqual([]);
117
+ });
118
+ });
119
+
120
+ describe("normalizeOnboardingContext", () => {
121
+ test("maps userName to preferredName", () => {
122
+ const ctx: OnboardingContext = {
123
+ tools: [],
124
+ tasks: [],
125
+ tone: "friendly",
126
+ userName: "Alice",
127
+ };
128
+ const result = normalizeOnboardingContext(ctx);
129
+ expect(result.preferredName).toBe("Alice");
130
+ });
131
+
132
+ test("absent userName yields undefined preferredName", () => {
133
+ const ctx: OnboardingContext = {
134
+ tools: [],
135
+ tasks: [],
136
+ tone: "professional",
137
+ };
138
+ const result = normalizeOnboardingContext(ctx);
139
+ expect(result.preferredName).toBeUndefined();
140
+ });
141
+
142
+ test("tone passes through", () => {
143
+ const ctx: OnboardingContext = {
144
+ tools: [],
145
+ tasks: [],
146
+ tone: "casual",
147
+ };
148
+ const result = normalizeOnboardingContext(ctx);
149
+ expect(result.tone).toBe("casual");
150
+ });
151
+
152
+ test("assistantName passes through", () => {
153
+ const ctx: OnboardingContext = {
154
+ tools: [],
155
+ tasks: [],
156
+ tone: "friendly",
157
+ assistantName: "Jarvis",
158
+ };
159
+ const result = normalizeOnboardingContext(ctx);
160
+ expect(result.assistantName).toBe("Jarvis");
161
+ });
162
+
163
+ test("normalizes tools and tasks together", () => {
164
+ const ctx: OnboardingContext = {
165
+ tools: ["github", "trello"],
166
+ tasks: ["code-building", "data-entry"],
167
+ tone: "professional",
168
+ userName: "Bob",
169
+ assistantName: "Friday",
170
+ };
171
+ const result = normalizeOnboardingContext(ctx);
172
+ expect(result).toEqual({
173
+ preferredName: "Bob",
174
+ commonWork: ["builds code, apps, or tools", "data-entry"],
175
+ dailyTools: ["GitHub", "Trello"],
176
+ tone: "professional",
177
+ assistantName: "Friday",
178
+ });
179
+ });
180
+ });
@@ -95,6 +95,97 @@ describe("notification decision fallback copy", () => {
95
95
  );
96
96
  });
97
97
 
98
+ test("enforces guardian-facing popup copy for heartbeat alerts", async () => {
99
+ configuredProvider = { sendMessage: async () => ({}) };
100
+ extractedToolUse = {
101
+ input: {
102
+ shouldNotify: true,
103
+ selectedChannels: ["vellum"],
104
+ reasoningSummary: "Heartbeat found a useful follow-up.",
105
+ renderedCopy: {
106
+ vellum: {
107
+ title: "Heartbeat Follow-up",
108
+ body: "The daily tracker is ready; consider reminding the guardian to review it before the next check-in.",
109
+ },
110
+ },
111
+ dedupeKey: "heartbeat:test",
112
+ confidence: 0.9,
113
+ },
114
+ };
115
+
116
+ const decision = await evaluateSignal(
117
+ makeSignal({
118
+ sourceEventName: "heartbeat.alert",
119
+ sourceChannel: "watcher",
120
+ contextPayload: {
121
+ summary:
122
+ "The daily tracker is ready; consider reminding the guardian to review it before the next check-in.",
123
+ conversationTitle: "Running Habit Tracking",
124
+ },
125
+ attentionHints: {
126
+ requiresAction: true,
127
+ urgency: "medium",
128
+ isAsyncBackground: true,
129
+ visibleInSourceNow: false,
130
+ },
131
+ }),
132
+ ["vellum"] as NotificationChannel[],
133
+ );
134
+
135
+ expect(decision.fallbackUsed).toBe(false);
136
+ expect(decision.renderedCopy.vellum?.title).toBe("Heartbeat Alert");
137
+ expect(decision.renderedCopy.vellum?.body).toBe(
138
+ "I found something worth your attention in a heartbeat check. Open the conversation for details.",
139
+ );
140
+ expect(decision.renderedCopy.vellum?.body).not.toContain(
141
+ "reminding the guardian",
142
+ );
143
+ });
144
+
145
+ test("keeps direct guardian-facing heartbeat copy", async () => {
146
+ configuredProvider = { sendMessage: async () => ({}) };
147
+ extractedToolUse = {
148
+ input: {
149
+ shouldNotify: true,
150
+ selectedChannels: ["vellum"],
151
+ reasoningSummary: "Heartbeat found a useful follow-up.",
152
+ renderedCopy: {
153
+ vellum: {
154
+ title: "Tracker Ready",
155
+ body: "Your daily tracker is ready. Review it before the next check-in.",
156
+ },
157
+ },
158
+ dedupeKey: "heartbeat:direct-copy-test",
159
+ confidence: 0.9,
160
+ },
161
+ };
162
+
163
+ const decision = await evaluateSignal(
164
+ makeSignal({
165
+ sourceEventName: "heartbeat.alert",
166
+ sourceChannel: "watcher",
167
+ contextPayload: {
168
+ summary:
169
+ "The daily tracker is ready; consider reminding the guardian to review it before the next check-in.",
170
+ conversationTitle: "Running Habit Tracking",
171
+ },
172
+ attentionHints: {
173
+ requiresAction: true,
174
+ urgency: "medium",
175
+ isAsyncBackground: true,
176
+ visibleInSourceNow: false,
177
+ },
178
+ }),
179
+ ["vellum"] as NotificationChannel[],
180
+ );
181
+
182
+ expect(decision.fallbackUsed).toBe(false);
183
+ expect(decision.renderedCopy.vellum?.title).toBe("Tracker Ready");
184
+ expect(decision.renderedCopy.vellum?.body).toBe(
185
+ "Your daily tracker is ready. Review it before the next check-in.",
186
+ );
187
+ });
188
+
98
189
  test("enforces free-text answer instructions for guardian.question when requestCode exists", async () => {
99
190
  const signal = makeSignal({
100
191
  contextPayload: {
@@ -373,6 +373,28 @@ describe("notification decision strategy", () => {
373
373
  "A guardian question needs your attention",
374
374
  );
375
375
  });
376
+
377
+ test("heartbeat.alert fallback avoids intermediary-instruction popup copy", () => {
378
+ const signal = makeSignal({
379
+ sourceEventName: "heartbeat.alert",
380
+ contextPayload: {
381
+ summary:
382
+ "The daily tracker is ready; consider reminding the guardian to review it before the next check-in.",
383
+ conversationTitle: "Running Habit Tracking",
384
+ },
385
+ });
386
+
387
+ const copy = composeFallbackCopy(signal, channels);
388
+ expect(copy.vellum).toBeDefined();
389
+ expect(copy.vellum!.title).toBe("Heartbeat Alert");
390
+ expect(copy.vellum!.body).toBe(
391
+ "I found something worth your attention in a heartbeat check. Open the conversation for details.",
392
+ );
393
+ expect(copy.vellum!.conversationSeedMessage).toContain(
394
+ "consider reminding the guardian",
395
+ );
396
+ expect(copy.telegram!.deliveryText).toBe(copy.vellum!.body);
397
+ });
376
398
  });
377
399
 
378
400
  // -- NotificationChannel type correctness ----------------------------------
@@ -62,6 +62,13 @@ let mockUpsertAppImpl:
62
62
  let mockOrchestrateOAuthConnect: (
63
63
  opts: Record<string, unknown>,
64
64
  ) => Promise<Record<string, unknown>>;
65
+ let mockCliIpcCall: (
66
+ operationId: string,
67
+ params?: Record<string, unknown>,
68
+ ) => Promise<Record<string, unknown>> = async () => ({
69
+ ok: false,
70
+ error: "Could not connect to assistant daemon (test default)",
71
+ });
65
72
  let mockGetAppByProviderAndClientId: (
66
73
  provider: string,
67
74
  clientId: string,
@@ -335,6 +342,15 @@ mock.module("../oauth/connect-orchestrator.js", () => ({
335
342
  mockOrchestrateOAuthConnect(opts),
336
343
  }));
337
344
 
345
+ // ---------------------------------------------------------------------------
346
+ // Mock cli-client (IPC) — used by `oauth connect` for daemon-orchestrated flow
347
+ // ---------------------------------------------------------------------------
348
+
349
+ mock.module("../ipc/cli-client.js", () => ({
350
+ cliIpcCall: (operationId: string, params?: Record<string, unknown>) =>
351
+ mockCliIpcCall(operationId, params),
352
+ }));
353
+
338
354
  mock.module("../oauth/seed-providers.js", () => ({
339
355
  SEEDED_PROVIDER_KEYS: new Set([
340
356
  "google",
@@ -1220,6 +1236,111 @@ describe("assistant oauth connect managed mode — platform 401/403 errors", ()
1220
1236
  });
1221
1237
  });
1222
1238
 
1239
+ // ---------------------------------------------------------------------------
1240
+ // `assistant oauth connect <provider>` BYO mode — daemon-unreachable behavior.
1241
+ //
1242
+ // We deleted the in-process `orchestrateOAuthConnect` fallback (the same
1243
+ // pattern as the MCP CLI consolidation in #29484). When the daemon is
1244
+ // unreachable, the CLI must surface a clear error and exit 1 — never
1245
+ // silently fall back to in-process flow.
1246
+ // ---------------------------------------------------------------------------
1247
+
1248
+ describe("assistant oauth connect <provider> — daemon unreachable (BYO mode)", () => {
1249
+ beforeEach(() => {
1250
+ // BYO provider with a registered app and no managed-mode wiring.
1251
+ mockGetProvider = () => ({
1252
+ provider: "github",
1253
+ authorizeUrl: "https://github.com/login/oauth/authorize",
1254
+ tokenExchangeUrl: "https://github.com/login/oauth/access_token",
1255
+ defaultScopes: "[]",
1256
+ availableScopes: null,
1257
+ authorizeParams: null,
1258
+ managedServiceConfigKey: null,
1259
+ requiresClientSecret: false,
1260
+ createdAt: Date.now(),
1261
+ updatedAt: Date.now(),
1262
+ });
1263
+ mockGetMostRecentAppByProvider = () => ({
1264
+ provider: "github",
1265
+ clientId: "test-client-id",
1266
+ clientSecretCredentialPath: "oauth_app/github/test/client_secret",
1267
+ });
1268
+ mockGetSecureKey = () => "test-secret";
1269
+ mockGetConfig = () => ({ services: {} });
1270
+ });
1271
+
1272
+ test("daemon connect-refused → exit 1 with 'Is the assistant running?'", async () => {
1273
+ mockCliIpcCall = async () => ({
1274
+ ok: false,
1275
+ error: "Could not connect to assistant daemon: ECONNREFUSED",
1276
+ });
1277
+
1278
+ const { exitCode, stdout } = await runCli([
1279
+ "connect",
1280
+ "github",
1281
+ "--no-browser",
1282
+ "--json",
1283
+ ]);
1284
+
1285
+ expect(exitCode).toBe(1);
1286
+ const parsed = JSON.parse(stdout);
1287
+ expect(parsed.ok).toBe(false);
1288
+ expect(parsed.error).toContain("Could not reach the assistant");
1289
+ expect(parsed.error).toContain("Is the assistant running?");
1290
+ });
1291
+
1292
+ test("daemon route missing (Unknown method) → exit 1, never falls through to in-process", async () => {
1293
+ let orchestratorCalls = 0;
1294
+ mockOrchestrateOAuthConnect = async () => {
1295
+ orchestratorCalls++;
1296
+ return { success: true, deferred: false, grantedScopes: [] };
1297
+ };
1298
+ mockCliIpcCall = async () => ({
1299
+ ok: false,
1300
+ error: "Unknown method: internal_oauth_connect_start",
1301
+ });
1302
+
1303
+ const { exitCode } = await runCli([
1304
+ "connect",
1305
+ "github",
1306
+ "--no-browser",
1307
+ "--json",
1308
+ ]);
1309
+
1310
+ expect(exitCode).toBe(1);
1311
+ // Critical regression guard: the in-process `orchestrateOAuthConnect`
1312
+ // must NOT be invoked from the CLI. The daemon-orchestrated path is
1313
+ // the sole code path; this is the same invariant #29484 established
1314
+ // for the MCP CLI.
1315
+ expect(orchestratorCalls).toBe(0);
1316
+ });
1317
+
1318
+ test("daemon HTTP error (statusCode set) → surfaces error verbatim, no fallback", async () => {
1319
+ let orchestratorCalls = 0;
1320
+ mockOrchestrateOAuthConnect = async () => {
1321
+ orchestratorCalls++;
1322
+ return { success: true, deferred: false, grantedScopes: [] };
1323
+ };
1324
+ mockCliIpcCall = async () => ({
1325
+ ok: false,
1326
+ statusCode: 400,
1327
+ error: "service must be registered before connecting",
1328
+ });
1329
+
1330
+ const { exitCode, stdout } = await runCli([
1331
+ "connect",
1332
+ "github",
1333
+ "--no-browser",
1334
+ "--json",
1335
+ ]);
1336
+
1337
+ expect(exitCode).toBe(1);
1338
+ const parsed = JSON.parse(stdout);
1339
+ expect(parsed.error).toContain("service must be registered");
1340
+ expect(orchestratorCalls).toBe(0);
1341
+ });
1342
+ });
1343
+
1223
1344
  // ---------------------------------------------------------------------------
1224
1345
  // requirePlatformClient — improved error messages
1225
1346
  // ---------------------------------------------------------------------------