@vellumai/assistant 0.12.1-dev.202609141913.5e16607 → 0.12.1-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/AGENTS.md +0 -4
  2. package/ARCHITECTURE.md +12 -11
  3. package/docs/architecture/turn-actor.md +8 -15
  4. package/docs/guardian-request-flow.md +0 -16
  5. package/openapi.yaml +49 -291
  6. package/package.json +1 -1
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +10 -18
  8. package/src/__tests__/app-compiler.test.ts +0 -6
  9. package/src/__tests__/app-control-tool-schemas.test.ts +2 -5
  10. package/src/__tests__/app-dir-path-guard.test.ts +3 -1
  11. package/src/__tests__/app-source-watcher.test.ts +218 -0
  12. package/src/{calls/__tests__ → __tests__}/call-controller.test.ts +82 -503
  13. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +2 -5
  14. package/src/__tests__/computer-use-tools.test.ts +5 -17
  15. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  16. package/src/__tests__/config-schema.test.ts +29 -28
  17. package/src/__tests__/config-watcher-skill-reseed.test.ts +359 -0
  18. package/src/__tests__/config-watcher.test.ts +8 -23
  19. package/src/__tests__/connection-model-compat.test.ts +2 -2
  20. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  21. package/src/__tests__/conversation-pairing.test.ts +0 -26
  22. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -4
  23. package/src/__tests__/conversation-queue.test.ts +207 -48
  24. package/src/__tests__/conversation-retry-discard.test.ts +1 -54
  25. package/src/__tests__/conversation-skill-tools.test.ts +0 -27
  26. package/src/__tests__/conversation-surfaces-app-control.test.ts +3 -8
  27. package/src/__tests__/conversation-surfaces-task-progress.test.ts +0 -67
  28. package/src/__tests__/conversation-title-service.test.ts +10 -14
  29. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +5 -3
  30. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -51
  31. package/src/__tests__/credential-record-retire.test.ts +1 -171
  32. package/src/__tests__/credential-routes.test.ts +3 -212
  33. package/src/__tests__/credential-security-invariants.test.ts +2 -4
  34. package/src/__tests__/cu-unified-flow.test.ts +2 -8
  35. package/src/__tests__/document-sync-tags.test.ts +0 -25
  36. package/src/__tests__/drain-kick-guard.test.ts +2 -0
  37. package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
  38. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  39. package/src/__tests__/host-bash-proxy.test.ts +45 -26
  40. package/src/__tests__/host-browser-proxy.test.ts +17 -6
  41. package/src/__tests__/host-file-proxy-targeted.test.ts +0 -23
  42. package/src/__tests__/host-file-proxy.test.ts +23 -11
  43. package/src/__tests__/host-transfer-proxy-targeted.test.ts +9 -30
  44. package/src/__tests__/llm-catalog-parity.test.ts +0 -25
  45. package/src/__tests__/mcp-auth-routes.test.ts +9 -6
  46. package/src/__tests__/mcp-config-secret-boundary.test.ts +136 -78
  47. package/src/__tests__/mcp-health-check.test.ts +11 -8
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +10 -8
  49. package/src/{calls/__tests__ → __tests__}/media-stream-audio-transcode.test.ts +1 -1
  50. package/src/{calls/__tests__ → __tests__}/media-stream-output.test.ts +8 -37
  51. package/src/{calls/__tests__ → __tests__}/media-stream-parser.test.ts +1 -1
  52. package/src/{calls/__tests__ → __tests__}/media-stream-server-integration.test.ts +57 -150
  53. package/src/{calls/__tests__ → __tests__}/media-stream-stt-session.test.ts +5 -5
  54. package/src/{calls/__tests__ → __tests__}/media-turn-detector.test.ts +1 -1
  55. package/src/__tests__/messages-after-tiebreaker.test.ts +0 -91
  56. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  57. package/src/__tests__/messaging-send-tool.test.ts +548 -355
  58. package/src/__tests__/model-intents.test.ts +0 -3
  59. package/src/__tests__/notification-vellum-adapter.test.ts +2 -85
  60. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  61. package/src/__tests__/platform-callback-registration.test.ts +16 -8
  62. package/src/__tests__/platform-client-verify-credential.test.ts +0 -2
  63. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -13
  64. package/src/__tests__/script-proxy-certs.test.ts +0 -35
  65. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  66. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  67. package/src/__tests__/speaker-identification.test.ts +56 -0
  68. package/src/__tests__/subagent-tool-gate-mode.test.ts +3 -19
  69. package/src/__tests__/subagent-tools.test.ts +2 -3
  70. package/src/__tests__/telephony-tts-guard.test.ts +3 -38
  71. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +0 -4
  72. package/src/{calls/__tests__/voice-session-bridge-turn-flow.test.ts → __tests__/voice-session-bridge.test.ts} +12 -20
  73. package/src/__tests__/watch-retro-tool-availability.test.ts +17 -8
  74. package/src/__tests__/workspace-git-service.test.ts +0 -26
  75. package/src/acp/__tests__/agent-process.test.ts +4 -258
  76. package/src/acp/__tests__/auth-required.test.ts +0 -96
  77. package/src/acp/__tests__/client-handler.test.ts +2 -90
  78. package/src/acp/__tests__/session-manager-persistence.test.ts +1 -5
  79. package/src/acp/__tests__/session-manager-resume.test.ts +6 -270
  80. package/src/acp/__tests__/session-manager-usage.test.ts +2 -4
  81. package/src/acp/__tests__/session-manager.test.ts +4 -895
  82. package/src/acp/agent-process.test.ts +1 -4
  83. package/src/acp/agent-process.ts +9 -94
  84. package/src/acp/auth-required.ts +1 -50
  85. package/src/acp/auto-install.test.ts +7 -9
  86. package/src/acp/client-handler.ts +3 -16
  87. package/src/acp/resolve-agent.test.ts +6 -220
  88. package/src/acp/resolve-agent.ts +23 -88
  89. package/src/acp/session-manager.test.ts +3 -196
  90. package/src/acp/session-manager.ts +18 -441
  91. package/src/acp/types.ts +5 -50
  92. package/src/api/events/app-files-changed.ts +3 -4
  93. package/src/api/events/notification-intent.ts +0 -2
  94. package/src/api/index.ts +0 -6
  95. package/src/apps/app-store.ts +1 -1
  96. package/src/bundler/app-compiler.ts +0 -15
  97. package/src/calls/__tests__/voice-control-protocol.test.ts +25 -58
  98. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -37
  99. package/src/calls/__tests__/voice-triage-escalate.test.ts +0 -105
  100. package/src/calls/call-constants.ts +4 -24
  101. package/src/calls/call-controller.ts +199 -499
  102. package/src/calls/call-transport.ts +0 -16
  103. package/src/calls/media-stream-output.ts +0 -32
  104. package/src/calls/media-stream-server.ts +30 -82
  105. package/src/calls/media-stream-stt-session.ts +0 -15
  106. package/src/calls/resolve-call-tts-provider.ts +2 -33
  107. package/src/calls/speaker-identification.ts +233 -0
  108. package/src/calls/tts-call-strategy.ts +5 -9
  109. package/src/calls/types.ts +0 -2
  110. package/src/calls/voice-control-protocol.ts +21 -53
  111. package/src/calls/voice-session-bridge.ts +46 -59
  112. package/src/calls/voice-triage-escalate.ts +56 -175
  113. package/src/cli/commands/apps.help.ts +13 -67
  114. package/src/cli/commands/apps.ts +0 -183
  115. package/src/cli/commands/bash.help.ts +13 -8
  116. package/src/cli/commands/bash.ts +25 -7
  117. package/src/cli/commands/channels/index.help.ts +2 -70
  118. package/src/cli/commands/channels/index.ts +3 -16
  119. package/src/cli/commands/credentials.help.ts +2 -6
  120. package/src/cli/commands/credentials.ts +2 -2
  121. package/src/cli/commands/mcp.help.ts +8 -8
  122. package/src/cli/commands/mcp.ts +1 -1
  123. package/src/cli/commands/roadmap.help.ts +1 -5
  124. package/src/cli/commands/roadmap.ts +1 -1
  125. package/src/cli/lib/bundled-marketplace.json +90 -103
  126. package/src/config/__tests__/default-profile-catalog.test.ts +0 -22
  127. package/src/config/__tests__/memory-retrospective-schema.test.ts +1 -15
  128. package/src/config/__tests__/platform-rehydration.test.ts +9 -99
  129. package/src/config/acp-defaults.test.ts +3 -13
  130. package/src/config/acp-defaults.ts +6 -23
  131. package/src/config/acp-schema.ts +2 -30
  132. package/src/config/bundled-skills/acp/SKILL.md +14 -22
  133. package/src/config/bundled-skills/acp/TOOLS.json +1 -5
  134. package/src/config/bundled-skills/app-builder/SKILL.md +9 -12
  135. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  136. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  137. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  138. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +139 -67
  139. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  140. package/src/config/bundled-skills/schedule/SKILL.md +4 -3
  141. package/src/config/bundled-skills/screen-annotation/SKILL.md +1 -8
  142. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +1 -4
  143. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  144. package/src/config/default-profile-catalog.ts +1 -1
  145. package/src/config/env.ts +2 -2
  146. package/src/config/feature-flag-registry.json +12 -24
  147. package/src/config/loader.ts +0 -1
  148. package/src/config/memory-v3-gate.test.ts +0 -31
  149. package/src/config/memory-v3-gate.ts +16 -20
  150. package/src/config/platform-rehydration.ts +27 -11
  151. package/src/config/schema.ts +2 -2
  152. package/src/config/schemas/__tests__/live-voice.test.ts +158 -8
  153. package/src/config/schemas/calls.ts +28 -1
  154. package/src/config/schemas/live-voice.ts +173 -1
  155. package/src/config/schemas/mcp.ts +1 -1
  156. package/src/config/schemas/memory-retrospective.ts +0 -10
  157. package/src/config/schemas/services.ts +0 -8
  158. package/src/config/schemas/skills.ts +14 -0
  159. package/src/credential-execution/process-manager.test.ts +19 -18
  160. package/src/daemon/__tests__/conversation-tool-setup.test.ts +121 -66
  161. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -4
  162. package/src/daemon/app-source-watcher.ts +218 -0
  163. package/src/daemon/config-watcher.ts +194 -13
  164. package/src/daemon/conversation-process.ts +64 -35
  165. package/src/daemon/conversation-skill-tools.ts +31 -13
  166. package/src/daemon/conversation-store.ts +1 -1
  167. package/src/daemon/conversation-surfaces.ts +8 -10
  168. package/src/daemon/conversation-tool-setup.ts +43 -37
  169. package/src/daemon/conversation.ts +3 -0
  170. package/src/daemon/host-bash-proxy.ts +12 -8
  171. package/src/daemon/host-browser-proxy.ts +1 -15
  172. package/src/daemon/host-file-proxy.ts +10 -8
  173. package/src/daemon/host-transfer-proxy.ts +18 -14
  174. package/src/daemon/lifecycle.ts +5 -0
  175. package/src/daemon/mcp-reload-service.ts +5 -5
  176. package/src/daemon/message-types/sync.ts +0 -1
  177. package/src/daemon/orphan-reaper.ts +1 -1
  178. package/src/daemon/providers-setup.ts +1 -1
  179. package/src/daemon/shutdown-handlers.ts +9 -1
  180. package/src/daemon/skill-memory-refresh.ts +0 -64
  181. package/src/daemon/tool-side-effects.ts +18 -7
  182. package/src/home/__tests__/feed-writer.test.ts +0 -22
  183. package/src/home/feed-writer.ts +2 -11
  184. package/src/hooks/types.ts +0 -33
  185. package/src/inbound/platform-callback-registration.ts +5 -4
  186. package/src/live-voice/__tests__/live-voice-metrics.test.ts +3 -0
  187. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  188. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  189. package/src/{calls → live-voice}/__tests__/progress-narration.test.ts +4 -4
  190. package/src/{calls → live-voice}/__tests__/progress-phrases.test.ts +1 -1
  191. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +69 -13
  192. package/src/live-voice/live-voice-session.ts +693 -213
  193. package/src/{calls → live-voice}/progress-narration.ts +2 -2
  194. package/src/live-voice/protocol.ts +2 -0
  195. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  196. package/src/mcp/effective-config.ts +3 -3
  197. package/src/mcp/mcp-header-store.ts +45 -29
  198. package/src/mcp/startup.ts +4 -8
  199. package/src/messaging/provider.ts +3 -10
  200. package/src/messaging/providers/callback-routing.ts +0 -18
  201. package/src/messaging/providers/channel-transport.ts +0 -56
  202. package/src/messaging/providers/discord/transport.ts +0 -17
  203. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +17 -6
  204. package/src/messaging/providers/slack/adapter.ts +44 -7
  205. package/src/messaging/providers/slack/transport.ts +0 -16
  206. package/src/messaging/providers/telegram-bot/adapter.ts +48 -1
  207. package/src/messaging/providers/telegram-bot/client.ts +26 -0
  208. package/src/messaging/providers/telegram-bot/transport.ts +0 -20
  209. package/src/messaging/providers/whatsapp/adapter.ts +48 -1
  210. package/src/messaging/providers/whatsapp/client.ts +29 -0
  211. package/src/messaging/providers/whatsapp/transport.ts +0 -18
  212. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +0 -8
  213. package/src/notifications/__tests__/assistant-reply-producer.test.ts +0 -55
  214. package/src/notifications/__tests__/guardian-feed-projection.test.ts +2 -158
  215. package/src/notifications/__tests__/home-feed-side-effect.test.ts +3 -153
  216. package/src/notifications/__tests__/schedule-result-producer.test.ts +3 -99
  217. package/src/notifications/adapters/macos.ts +0 -3
  218. package/src/notifications/assistant-reply-producer.ts +3 -8
  219. package/src/notifications/conversation-pairing.ts +16 -36
  220. package/src/notifications/delivered-post-record.ts +1 -11
  221. package/src/notifications/guardian-feed-projection.ts +5 -125
  222. package/src/notifications/home-feed-side-effect.ts +7 -55
  223. package/src/notifications/schedule-result-producer.ts +22 -66
  224. package/src/oauth/__tests__/seed-providers-managed.test.ts +0 -54
  225. package/src/oauth/byo-connection.test.ts +0 -45
  226. package/src/oauth/byo-connection.ts +6 -36
  227. package/src/oauth/connection-resolver.test.ts +0 -73
  228. package/src/oauth/connection-resolver.ts +2 -95
  229. package/src/oauth/seed-providers.ts +0 -165
  230. package/src/outbound-proxy/certs.ts +5 -3
  231. package/src/permissions/checker.test.ts +6 -11
  232. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  233. package/src/persistence/conversation-crud.ts +63 -71
  234. package/src/persistence/conversation-lineage.ts +4 -12
  235. package/src/persistence/conversation-title-service.ts +26 -9
  236. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  237. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  238. package/src/persistence/message-reads.ts +0 -30
  239. package/src/persistence/migrations/157-invite-contact-id.ts +6 -18
  240. package/src/persistence/schema/memory-core.ts +0 -5
  241. package/src/platform/client.test.ts +2 -4
  242. package/src/platform/client.ts +10 -2
  243. package/src/plugin-api/constants.ts +0 -2
  244. package/src/plugin-api/index.ts +0 -1
  245. package/src/plugin-api/types.ts +0 -2
  246. package/src/plugins/defaults/index.ts +0 -2
  247. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +0 -46
  248. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +5 -9
  249. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +10 -20
  250. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +0 -219
  251. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +0 -31
  252. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +0 -63
  253. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +0 -4
  254. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -4
  255. package/src/plugins/defaults/memory/hooks/init.ts +0 -10
  256. package/src/plugins/defaults/memory/hooks/shutdown.ts +0 -2
  257. package/src/plugins/defaults/memory/jobs-worker.ts +0 -13
  258. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +60 -34
  259. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +5 -3
  260. package/src/plugins/defaults/memory/memory-retrospective-job.ts +3 -12
  261. package/src/plugins/defaults/memory/memory-retrospective-state.ts +71 -312
  262. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +1 -2
  263. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +3 -5
  264. package/src/plugins/defaults/memory/startup.ts +1 -6
  265. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +9 -7
  266. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  267. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +0 -89
  268. package/src/plugins/defaults/memory/worker.ts +0 -6
  269. package/src/plugins/mcp-servers.ts +75 -15
  270. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  271. package/src/providers/gemini/client.ts +23 -9
  272. package/src/providers/inference/auth.ts +3 -2
  273. package/src/providers/inference/connection-availability.ts +8 -27
  274. package/src/providers/inference/resolve-auth.ts +4 -22
  275. package/src/providers/model-catalog.ts +0 -34
  276. package/src/providers/model-intents.ts +5 -10
  277. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +18 -146
  278. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +1 -83
  279. package/src/providers/openai/chat-completions-provider.ts +78 -192
  280. package/src/providers/openai/codex-models.ts +4 -0
  281. package/src/runtime/AGENTS.md +0 -2
  282. package/src/runtime/auth/same-actor.ts +3 -38
  283. package/src/runtime/http-server.ts +151 -160
  284. package/src/runtime/routes/__tests__/acp-routes.test.ts +4 -159
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +1 -108
  286. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  287. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +12 -16
  288. package/src/runtime/routes/acp-routes.test.ts +0 -15
  289. package/src/runtime/routes/acp-routes.ts +9 -72
  290. package/src/runtime/routes/app-management-routes.ts +0 -73
  291. package/src/runtime/routes/app-routes.ts +1 -1
  292. package/src/runtime/routes/conversation-list-routes.ts +3 -14
  293. package/src/runtime/routes/conversation-query-routes.ts +0 -40
  294. package/src/runtime/routes/credential-routes.ts +10 -19
  295. package/src/runtime/routes/debug-bash-routes.ts +173 -0
  296. package/src/runtime/routes/diagnostics-routes.ts +3 -55
  297. package/src/runtime/routes/index.ts +2 -4
  298. package/src/runtime/routes/log-export-routes.ts +0 -2
  299. package/src/runtime/routes/mcp-auth-routes.ts +47 -39
  300. package/src/runtime/routes/migration-routes.ts +20 -6
  301. package/src/runtime/routes/platform-routes.ts +15 -18
  302. package/src/runtime/services/__tests__/conversation-serializer.test.ts +0 -28
  303. package/src/runtime/services/conversation-serializer.ts +3 -16
  304. package/src/runtime/sync/resource-sync-events.ts +1 -2
  305. package/src/schedule/worker.ts +1 -1
  306. package/src/security/ces-rpc-record-backend.ts +0 -7
  307. package/src/subagent/consult-context.ts +25 -12
  308. package/src/tools/acp/list-agents.test.ts +1 -3
  309. package/src/tools/acp/spawn.test.ts +11 -146
  310. package/src/tools/acp/spawn.ts +3 -21
  311. package/src/tools/acp/status.ts +3 -40
  312. package/src/tools/app-control/skill-proxy-bridge.ts +1 -2
  313. package/src/tools/capability-offer.test.ts +2 -22
  314. package/src/tools/capability-offer.ts +1 -16
  315. package/src/tools/computer-use/definitions.ts +2 -4
  316. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -2
  317. package/src/tools/credentials/metadata-store.ts +0 -100
  318. package/src/tools/host-filesystem/edit.test.ts +1 -2
  319. package/src/tools/host-filesystem/edit.ts +2 -2
  320. package/src/tools/host-filesystem/read.test.ts +1 -2
  321. package/src/tools/host-filesystem/read.ts +2 -2
  322. package/src/tools/host-filesystem/transfer.test.ts +1 -2
  323. package/src/tools/host-filesystem/transfer.ts +2 -2
  324. package/src/tools/host-filesystem/write.test.ts +1 -2
  325. package/src/tools/host-filesystem/write.ts +2 -2
  326. package/src/tools/host-terminal/host-shell.test.ts +0 -16
  327. package/src/tools/host-terminal/host-shell.ts +20 -4
  328. package/src/tools/policy-context.ts +7 -6
  329. package/src/tools/subagent/spawn.ts +8 -5
  330. package/src/tools/watch/watch-retro-report.ts +8 -0
  331. package/src/util/__tests__/language-subtag.test.ts +1 -23
  332. package/src/util/fs-watcher-error.ts +36 -0
  333. package/src/util/language-subtag.ts +0 -16
  334. package/src/util/platform.ts +0 -8
  335. package/src/workspace/git-service.ts +54 -5
  336. package/src/workspace/migrations/registry.ts +0 -6
  337. package/scripts/smoke-desktop-window-theme.ts +0 -186
  338. package/src/__tests__/git-porcelain.test.ts +0 -47
  339. package/src/__tests__/helpers/set-workspace-mcp.ts +0 -11
  340. package/src/__tests__/message-deleted-hook-dispatch.test.ts +0 -119
  341. package/src/__tests__/resolve-app.test.ts +0 -135
  342. package/src/__tests__/service-account-token.test.ts +0 -247
  343. package/src/__tests__/source-fingerprint.test.ts +0 -184
  344. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +0 -336
  345. package/src/acp/__tests__/helpers/acp-model-option.ts +0 -51
  346. package/src/acp/model-config.test.ts +0 -166
  347. package/src/acp/model-config.ts +0 -138
  348. package/src/api/events/acp-session-model-update.test.ts +0 -88
  349. package/src/api/events/acp-session-model-update.ts +0 -46
  350. package/src/apps/resolve-app.ts +0 -81
  351. package/src/apps/source-fingerprint.ts +0 -266
  352. package/src/calls/__tests__/barge-in-guard.test.ts +0 -81
  353. package/src/calls/__tests__/voice-leg-coordinator.test.ts +0 -229
  354. package/src/calls/__tests__/voice-progress-cadence.test.ts +0 -274
  355. package/src/calls/barge-in-guard.ts +0 -118
  356. package/src/calls/voice-leg-coordinator.ts +0 -189
  357. package/src/calls/voice-progress-cadence.ts +0 -423
  358. package/src/cli/commands/__tests__/apps.test.ts +0 -245
  359. package/src/cli/commands/__tests__/bash.test.ts +0 -53
  360. package/src/cli/commands/__tests__/roadmap.help.test.ts +0 -19
  361. package/src/cli/commands/channels/__tests__/send.test.ts +0 -218
  362. package/src/cli/commands/channels/send.ts +0 -128
  363. package/src/config/__tests__/platform-identity.test.ts +0 -255
  364. package/src/config/acp-agent-write.ts +0 -43
  365. package/src/config/acp-schema.test.ts +0 -81
  366. package/src/config/platform-identity.ts +0 -201
  367. package/src/config/schemas/__tests__/voice.test.ts +0 -148
  368. package/src/config/schemas/voice.ts +0 -150
  369. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +0 -133
  370. package/src/daemon/app-change-notify.ts +0 -45
  371. package/src/desktop/__tests__/fake-desktop.ts +0 -119
  372. package/src/desktop/desktop-chrome-policy.test.ts +0 -71
  373. package/src/desktop/desktop-chrome-policy.ts +0 -42
  374. package/src/desktop/desktop-dependencies.test.ts +0 -102
  375. package/src/desktop/desktop-dependencies.ts +0 -306
  376. package/src/desktop/desktop-feature.test.ts +0 -18
  377. package/src/desktop/desktop-feature.ts +0 -19
  378. package/src/desktop/desktop-panel-config.test.ts +0 -135
  379. package/src/desktop/desktop-panel-config.ts +0 -149
  380. package/src/desktop/desktop-session-manager.test.ts +0 -627
  381. package/src/desktop/desktop-session-manager.ts +0 -814
  382. package/src/desktop/desktop-stream-bridge.test.ts +0 -395
  383. package/src/desktop/desktop-stream-bridge.ts +0 -214
  384. package/src/desktop/desktop-wallpaper.test.ts +0 -191
  385. package/src/desktop/desktop-wallpaper.ts +0 -113
  386. package/src/desktop/desktop-window-theme.ts +0 -170
  387. package/src/i18n/AGENTS.md +0 -22
  388. package/src/i18n/__tests__/i18n.test.ts +0 -108
  389. package/src/i18n/index.ts +0 -16
  390. package/src/i18n/locales.ts +0 -93
  391. package/src/i18n/messages.ts +0 -61
  392. package/src/i18n/title.ts +0 -25
  393. package/src/mcp/__tests__/workspace-mcp-config.test.ts +0 -135
  394. package/src/mcp/spec-schema.ts +0 -111
  395. package/src/mcp/workspace-mcp-config.ts +0 -188
  396. package/src/messaging/providers/__tests__/proactive-address.test.ts +0 -95
  397. package/src/persistence/conversation-title-placeholders.ts +0 -23
  398. package/src/persistence/message-cursor.ts +0 -78
  399. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +0 -149
  400. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +0 -118
  401. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +0 -83
  402. package/src/plugins/defaults/memory/hooks/message-deleted.ts +0 -21
  403. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +0 -126
  404. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +0 -79
  405. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +0 -28
  406. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +0 -9
  407. package/src/providers/gemini-thought-signature.test.ts +0 -63
  408. package/src/providers/gemini-thought-signature.ts +0 -44
  409. package/src/providers/inference/service-account-token.ts +0 -248
  410. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +0 -613
  411. package/src/providers/openai/__tests__/google-thought-signature.test.ts +0 -235
  412. package/src/providers/openai/google-thought-signature.ts +0 -320
  413. package/src/runtime/__tests__/channel-send.test.ts +0 -443
  414. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +0 -89
  415. package/src/runtime/__tests__/live-voice-upgrade.test.ts +0 -58
  416. package/src/runtime/__tests__/runtime-ws-test-utils.ts +0 -72
  417. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +0 -59
  418. package/src/runtime/channel-send.ts +0 -316
  419. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +0 -205
  420. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +0 -127
  421. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
  422. package/src/runtime/routes/channel-send-routes.ts +0 -85
  423. package/src/runtime/routes/desktop-setup-routes.test.ts +0 -74
  424. package/src/runtime/routes/desktop-setup-routes.ts +0 -35
  425. package/src/security/__tests__/ces-http-record-backend.test.ts +0 -137
  426. package/src/security/ces-http-record-backend.ts +0 -120
  427. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +0 -60
  428. package/src/skills/workspace-skill-md-signature.ts +0 -50
  429. package/src/tools/acp/status.test.ts +0 -94
  430. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -57
  431. package/src/tools/terminal/sanitized-bash.ts +0 -105
  432. package/src/util/ca-bundle.ts +0 -13
  433. package/src/workspace/git-porcelain.ts +0 -66
  434. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +0 -258
  435. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +0 -78
  436. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +0 -214
  437. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +0 -102
  438. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +0 -199
  439. /package/src/{calls → live-voice}/progress-phrases.ts +0 -0
@@ -36,18 +36,25 @@ function makeProvider(id: string, displayName: string): MessagingProvider {
36
36
  };
37
37
  }
38
38
 
39
- // "phone" is a provider with a send of its own and no channel transport, so
40
- // its send stays tool-mediated; "outlook" drafts. A channel with a transport
41
- // (slack, telegram) never reaches a provider's send: the tool hands it to
42
- // the transport-backed send, which is what these tests prove.
39
+ // "outlook" is a messaging provider but not a channel (it is not in
40
+ // CHANNEL_IDS): a send through it has no chat conversation to record in.
41
+ // "phone" and "telegram" are channels.
43
42
  const phoneProvider = makeProvider("phone", "Phone");
43
+ const telegramProvider = makeProvider("telegram", "Telegram");
44
44
  const outlookProvider = makeProvider("outlook", "Outlook");
45
- const telegramProvider: MessagingProvider = {
46
- ...makeProvider("telegram", "Telegram"),
47
- sendMessage: undefined,
45
+ // Slack's provider threads exactly where it is told and reports that thread
46
+ // on the result; Telegram's ignores a requested thread and reports none.
47
+ const slackProvider: MessagingProvider = {
48
+ ...makeProvider("slack", "Slack"),
49
+ sendMessage: async (_connection, conversationId, _text, options) => ({
50
+ id: "1700000000.000100",
51
+ timestamp: 123,
52
+ conversationId,
53
+ ...(options?.threadId ? { threadId: options.threadId } : {}),
54
+ }),
48
55
  };
49
-
50
56
  let provider: MessagingProvider = phoneProvider;
57
+
51
58
  let connection: OAuthConnection | undefined = undefined;
52
59
 
53
60
  /** Fires during provider resolution, the first await after the entry guard. */
@@ -67,24 +74,26 @@ mock.module("../config/bundled-skills/messaging/tools/shared.js", () => ({
67
74
  isMailboxAddress: (value: string) => value.includes("@"),
68
75
  }));
69
76
 
70
- // ── The transport-backed send ──
71
-
72
- const ADDRESSABLE = new Set(["slack", "telegram", "discord", "whatsapp"]);
73
- const sendChannelTextMock = mock(async (params: Record<string, unknown>) => ({
74
- channel: params.channel,
75
- chatId: (params.target as { chatId: string }).chatId,
76
- ...((params.target as { threadId?: string }).threadId
77
- ? { threadId: (params.target as { threadId?: string }).threadId }
78
- : {}),
79
- messageIds: ["1700000000.000100"],
80
- lastMessageId: "1700000000.000100",
81
- recordedIn: "home-1",
82
- }));
77
+ // ── Sent-post record dependency mocks ──
83
78
 
84
- mock.module("../runtime/channel-send.js", () => ({
85
- isProactivelyAddressable: (channel: string) => ADDRESSABLE.has(channel),
86
- sendChannelText: sendChannelTextMock,
87
- }));
79
+ const getConversationMock = mock(
80
+ (_id: string) => null as { id: string; createdAt: number } | null,
81
+ );
82
+
83
+ const syncMessageToDiskMock = mock(
84
+ (_conversationId: string, _messageId: string, _createdAtMs: number) => {},
85
+ );
86
+
87
+ const resolveProactiveHomeConversationMock = mock(
88
+ async (_params: Record<string, unknown>) => ({
89
+ conversationId: "home-1",
90
+ createdNewConversation: false,
91
+ }),
92
+ );
93
+
94
+ const recordDeliveredChannelPostMock = mock(
95
+ async (_post: Record<string, unknown>) => ({ messageId: "row-1" }),
96
+ );
88
97
 
89
98
  const mockOutlookCreateDraft = mock(
90
99
  async (_conn: OAuthConnection, _draft: Record<string, unknown>) => ({
@@ -118,155 +127,155 @@ mock.module("../messaging/providers/outlook/client.js", () => ({
118
127
  })),
119
128
  }));
120
129
 
121
- import { run } from "../config/bundled-skills/messaging/tools/messaging-send.js";
130
+ mock.module("../persistence/conversation-crud.js", () => ({
131
+ setConversationProcessingStartedAt: () => {},
132
+ isConversationProcessing: () => false,
133
+ getConversation: getConversationMock,
134
+ reserveMessage: mock(async () => ({ id: "msg-reserve" })),
135
+ }));
122
136
 
123
- const turn = {
124
- workingDir: "/tmp",
125
- conversationId: "conv-A",
126
- assistantId: "ast-1",
127
- trustClass: "guardian" as const,
128
- executionChannel: "slack",
129
- requesterChatId: "D0123456789",
130
- sourceThreadId: "1700000000.000001",
131
- };
137
+ mock.module("../persistence/conversation-disk-view.js", () => ({
138
+ syncMessageToDisk: syncMessageToDiskMock,
139
+ }));
140
+
141
+ mock.module("../notifications/conversation-pairing.js", () => ({
142
+ resolveProactiveHomeConversation: resolveProactiveHomeConversationMock,
143
+ }));
144
+
145
+ mock.module("../persistence/external-conversation-store.js", () => ({
146
+ normalizeExternalThreadId: (value: string | null | undefined) => {
147
+ const trimmed = value?.trim();
148
+ return trimmed ? trimmed : null;
149
+ },
150
+ }));
151
+
152
+ mock.module("../notifications/delivered-post-record.js", () => ({
153
+ recordDeliveredChannelPost: recordDeliveredChannelPostMock,
154
+ }));
155
+
156
+ import { run } from "../config/bundled-skills/messaging/tools/messaging-send.js";
132
157
 
133
158
  describe("messaging-send tool", () => {
134
159
  beforeEach(() => {
135
160
  provider = phoneProvider;
136
161
  connection = undefined;
137
162
  sendMessageMock.mockClear();
138
- sendChannelTextMock.mockClear();
139
163
  mockOutlookCreateDraft.mockClear();
140
164
  mockOutlookCreateReplyDraft.mockClear();
141
165
  onResolveProvider = null;
166
+ getConversationMock.mockClear();
167
+ syncMessageToDiskMock.mockClear();
168
+ resolveProactiveHomeConversationMock.mockClear();
169
+ resolveProactiveHomeConversationMock.mockImplementation(async () => ({
170
+ conversationId: "home-1",
171
+ createdNewConversation: false,
172
+ }));
173
+ recordDeliveredChannelPostMock.mockClear();
174
+ recordDeliveredChannelPostMock.mockImplementation(async () => ({
175
+ messageId: "row-1",
176
+ }));
142
177
  });
143
178
 
144
- describe("a channel with a transport", () => {
145
- test("is sent through the transport-backed send with the turn's snapshot, never a provider", async () => {
146
- const result = await run(
147
- {
148
- platform: "slack",
149
- conversation_id: "C123",
150
- thread_id: "1700000000.000009",
151
- text: "hello",
152
- },
153
- turn,
154
- );
155
-
156
- expect(result.isError).toBe(false);
157
- expect(sendMessageMock).not.toHaveBeenCalled();
158
- expect(sendChannelTextMock).toHaveBeenCalledTimes(1);
159
- expect(sendChannelTextMock.mock.calls[0]![0]).toEqual({
160
- channel: "slack",
161
- target: {
162
- kind: "chat",
163
- chatId: "C123",
164
- threadId: "1700000000.000009",
165
- },
166
- text: "hello",
167
- renderRichly: true,
168
- assistantId: "ast-1",
169
- sender: {
170
- conversationId: "conv-A",
171
- executionChannel: "slack",
172
- requesterChatId: "D0123456789",
173
- sourceThreadId: "1700000000.000001",
174
- },
175
- });
176
- expect(result.content).toBe(
177
- 'Message sent (ID: 1700000000.000100, "thread_id": "1700000000.000009").',
178
- );
179
- });
180
-
181
- test("names the channel outright without resolving a provider, so a channel with no provider is reachable", async () => {
182
- provider = outlookProvider;
183
- const result = await run(
184
- { platform: "discord", conversation_id: "C1", text: "hi" },
185
- turn,
186
- );
187
- expect(result.isError).toBe(false);
188
- expect(sendChannelTextMock.mock.calls[0]![0]).toMatchObject({
189
- channel: "discord",
190
- target: { kind: "chat", chatId: "C1" },
191
- });
192
- });
193
-
194
- test("an auto-detected provider that is a channel goes through the transport too", async () => {
195
- provider = telegramProvider;
196
- const result = await run(
197
- { conversation_id: "123456789", text: "hi" },
198
- turn,
199
- );
200
- expect(result.isError).toBe(false);
201
- expect(sendChannelTextMock.mock.calls[0]![0]).toMatchObject({
202
- channel: "telegram",
203
- target: { kind: "chat", chatId: "123456789" },
204
- });
205
- });
206
-
207
- test("rejects attachments before any send", async () => {
208
- const result = await run(
209
- {
210
- platform: "slack",
211
- conversation_id: "C123",
212
- text: "with a file",
213
- attachment_paths: ["/tmp/does-not-matter.pdf"],
214
- },
215
- turn,
216
- );
217
- expect(result.isError).toBe(true);
218
- expect(result.content).toContain("Gmail and Outlook");
219
- expect(sendChannelTextMock).not.toHaveBeenCalled();
220
- });
179
+ test("passes assistantId from tool context to provider send options", async () => {
180
+ const result = await run(
181
+ {
182
+ platform: "phone",
183
+ conversation_id: "+15550004444",
184
+ text: "test message",
185
+ },
186
+ {
187
+ workingDir: "/tmp",
188
+ conversationId: "conv-1",
189
+ assistantId: "ast-alpha",
190
+ trustClass: "guardian" as const,
191
+ },
192
+ );
193
+
194
+ expect(result.isError).toBe(false);
195
+ expect(sendMessageMock).toHaveBeenCalledWith(
196
+ undefined,
197
+ "+15550004444",
198
+ "test message",
199
+ {
200
+ subject: undefined,
201
+ inReplyTo: undefined,
202
+ threadId: undefined,
203
+ attachments: undefined,
204
+ assistantId: "ast-alpha",
205
+ },
206
+ );
207
+ });
221
208
 
222
- test("refuses an account rather than sending from the channel's default identity", async () => {
223
- const result = await run(
224
- {
225
- platform: "slack",
226
- account: "workspace-b@example.com",
227
- conversation_id: "C123",
228
- text: "hi",
229
- },
230
- turn,
231
- );
232
- expect(result.isError).toBe(true);
233
- expect(result.content).toContain("account does not apply to slack");
234
- expect(sendChannelTextMock).not.toHaveBeenCalled();
235
- });
209
+ test("passes threadId to provider when replying on non-Gmail platform", async () => {
210
+ const result = await run(
211
+ {
212
+ platform: "phone",
213
+ conversation_id: "conv-1",
214
+ text: "reply text",
215
+ thread_id: "thread-abc",
216
+ },
217
+ {
218
+ workingDir: "/tmp",
219
+ conversationId: "conv-1",
220
+ assistantId: "ast-alpha",
221
+ trustClass: "guardian" as const,
222
+ },
223
+ );
224
+
225
+ expect(result.isError).toBe(false);
226
+ expect(sendMessageMock).toHaveBeenCalledWith(
227
+ undefined,
228
+ "conv-1",
229
+ "reply text",
230
+ {
231
+ subject: undefined,
232
+ inReplyTo: undefined,
233
+ threadId: "thread-abc",
234
+ attachments: undefined,
235
+ assistantId: "ast-alpha",
236
+ },
237
+ );
238
+ });
236
239
 
237
- test("a turn cancelled during provider resolution sends nothing", async () => {
238
- provider = telegramProvider;
239
- const controller = new AbortController();
240
- onResolveProvider = () => controller.abort();
241
- await expect(
242
- run(
243
- { conversation_id: "123456789", text: "never sent" },
244
- { ...turn, signal: controller.signal },
245
- ),
246
- ).rejects.toThrow();
247
- expect(sendChannelTextMock).not.toHaveBeenCalled();
248
- });
240
+ test("rejects attachments on platforms that can't carry them", async () => {
241
+ const result = await run(
242
+ {
243
+ platform: "phone",
244
+ conversation_id: "+12025550142",
245
+ text: "with a file",
246
+ attachment_paths: ["/tmp/does-not-matter.pdf"],
247
+ },
248
+ {
249
+ workingDir: "/tmp",
250
+ conversationId: "conv-1",
251
+ assistantId: "ast-alpha",
252
+ trustClass: "guardian" as const,
253
+ },
254
+ );
249
255
 
250
- test("reports the send's refusal as the tool's error", async () => {
251
- sendChannelTextMock.mockImplementationOnce(async () => {
252
- throw new Error('Channel "slack" cannot be addressed by person.');
253
- });
254
- const result = await run(
255
- { platform: "slack", conversation_id: "C123", text: "hi" },
256
- turn,
257
- );
258
- expect(result.isError).toBe(true);
259
- expect(result.content).toContain("cannot be addressed");
260
- });
256
+ expect(result.isError).toBe(true);
257
+ expect(result.content).toContain("Gmail and Outlook");
258
+ expect(sendMessageMock).not.toHaveBeenCalled();
261
259
  });
262
260
 
263
- describe("a provider with a send of its own", () => {
264
- test("passes assistantId from tool context to provider send options", async () => {
261
+ test("reads and forwards attachments onto an Outlook draft", async () => {
262
+ const dir = mkdtempSync(join(tmpdir(), "msg-send-att-"));
263
+ const filePath = join(dir, "report.pdf");
264
+ writeFileSync(filePath, "pdf-bytes");
265
+ provider = outlookProvider;
266
+ connection = {
267
+ id: "outlook-conn-1",
268
+ provider: "outlook",
269
+ } as OAuthConnection;
270
+
271
+ try {
265
272
  const result = await run(
266
273
  {
267
- platform: "phone",
268
- conversation_id: "+12125550100",
269
- text: "test message",
274
+ platform: "outlook",
275
+ conversation_id: "user@example.com",
276
+ text: "see attached",
277
+ subject: "Docs",
278
+ attachment_paths: [filePath],
270
279
  },
271
280
  {
272
281
  workingDir: "/tmp",
@@ -277,227 +286,411 @@ describe("messaging-send tool", () => {
277
286
  );
278
287
 
279
288
  expect(result.isError).toBe(false);
280
- expect(sendChannelTextMock).not.toHaveBeenCalled();
281
- expect(sendMessageMock).toHaveBeenCalledWith(
282
- undefined,
283
- "+12125550100",
284
- "test message",
285
- {
286
- subject: undefined,
287
- inReplyTo: undefined,
288
- threadId: undefined,
289
- attachments: undefined,
290
- assistantId: "ast-alpha",
291
- },
289
+ expect(result.content).toContain("Outlook draft created");
290
+ expect(result.content).toContain("Draft ID: outlook-draft-1");
291
+ expect(sendMessageMock).not.toHaveBeenCalled();
292
+ expect(mockOutlookCreateDraft).toHaveBeenCalledTimes(1);
293
+ expect(mockOutlookCreateDraft).toHaveBeenCalledWith(
294
+ connection,
295
+ expect.objectContaining({
296
+ subject: "Docs",
297
+ body: { contentType: "text", content: "see attached" },
298
+ toRecipients: [{ emailAddress: { address: "user@example.com" } }],
299
+ attachments: [expect.objectContaining({ name: "report.pdf" })],
300
+ }),
292
301
  );
293
- });
302
+ } finally {
303
+ rmSync(dir, { recursive: true, force: true });
304
+ }
305
+ });
294
306
 
295
- test("passes threadId to provider when replying", async () => {
296
- const result = await run(
297
- {
298
- platform: "phone",
299
- conversation_id: "conv-1",
300
- text: "reply text",
301
- thread_id: "thread-abc",
302
- },
303
- {
304
- workingDir: "/tmp",
305
- conversationId: "conv-1",
306
- assistantId: "ast-alpha",
307
- trustClass: "guardian" as const,
308
- },
309
- );
307
+ test("records a threaded channel send in the thread's home, with the delivered thread on the row", async () => {
308
+ provider = slackProvider;
309
+ getConversationMock.mockImplementation(() => ({
310
+ id: "home-topic",
311
+ createdAt: 1700000000000,
312
+ }));
313
+ resolveProactiveHomeConversationMock.mockImplementation(async () => ({
314
+ conversationId: "home-topic",
315
+ createdNewConversation: false,
316
+ }));
317
+
318
+ const result = await run(
319
+ {
320
+ platform: "slack",
321
+ conversation_id: "C123",
322
+ thread_id: "1700000000.000009",
323
+ text: "hello from A",
324
+ },
325
+ {
326
+ workingDir: "/tmp",
327
+ conversationId: "conv-A",
328
+ assistantId: "ast-1",
329
+ trustClass: "guardian" as const,
330
+ },
331
+ );
332
+
333
+ expect(result.isError).toBe(false);
334
+ expect(
335
+ resolveProactiveHomeConversationMock.mock.calls[0]![0],
336
+ ).toMatchObject({
337
+ sourceChannel: "slack",
338
+ externalChatId: "C123",
339
+ threadId: "1700000000.000009",
340
+ });
341
+ expect(recordDeliveredChannelPostMock.mock.calls[0]![0]).toMatchObject({
342
+ conversationId: "home-topic",
343
+ externalChatId: "C123",
344
+ threadId: "1700000000.000009",
345
+ providerMessageId: "1700000000.000100",
346
+ });
347
+ });
310
348
 
311
- expect(result.isError).toBe(false);
312
- expect(sendMessageMock).toHaveBeenCalledWith(
313
- undefined,
314
- "conv-1",
315
- "reply text",
316
- {
317
- subject: undefined,
318
- inReplyTo: undefined,
319
- threadId: "thread-abc",
320
- attachments: undefined,
321
- assistantId: "ast-alpha",
322
- },
323
- );
349
+ test("records a channel send in the chat's home conversation once the provider has it", async () => {
350
+ provider = telegramProvider;
351
+ getConversationMock.mockImplementation(() => ({
352
+ id: "home-1",
353
+ createdAt: 1700000000000,
354
+ }));
355
+
356
+ const result = await run(
357
+ {
358
+ platform: "telegram",
359
+ conversation_id: "123456789",
360
+ text: "hello from A",
361
+ },
362
+ {
363
+ workingDir: "/tmp",
364
+ conversationId: "conv-A",
365
+ assistantId: "ast-1",
366
+ trustClass: "guardian" as const,
367
+ },
368
+ );
369
+
370
+ expect(result.isError).toBe(false);
371
+ expect(resolveProactiveHomeConversationMock).toHaveBeenCalledTimes(1);
372
+ expect(
373
+ resolveProactiveHomeConversationMock.mock.calls[0]![0],
374
+ ).toMatchObject({
375
+ sourceChannel: "telegram",
376
+ externalChatId: "123456789",
377
+ });
378
+ expect(recordDeliveredChannelPostMock).toHaveBeenCalledWith({
379
+ conversationId: "home-1",
380
+ channel: "telegram",
381
+ externalChatId: "123456789",
382
+ text: "hello from A",
383
+ providerMessageId: "msg-1",
384
+ crossPostedFrom: "conv-A",
324
385
  });
386
+ expect(syncMessageToDiskMock).toHaveBeenCalledWith(
387
+ "home-1",
388
+ "row-1",
389
+ 1700000000000,
390
+ );
391
+ });
325
392
 
326
- test("rejects attachments on platforms that can't carry them", async () => {
327
- const result = await run(
328
- {
329
- platform: "phone",
330
- conversation_id: "+12025550142",
331
- text: "with a file",
332
- attachment_paths: ["/tmp/does-not-matter.pdf"],
333
- },
334
- {
335
- workingDir: "/tmp",
336
- conversationId: "conv-1",
337
- assistantId: "ast-alpha",
338
- trustClass: "guardian" as const,
339
- },
340
- );
393
+ test("records nothing when the home conversation is the sender", async () => {
394
+ provider = telegramProvider;
395
+ resolveProactiveHomeConversationMock.mockImplementation(async () => ({
396
+ conversationId: "conv-A",
397
+ createdNewConversation: false,
398
+ }));
399
+
400
+ await run(
401
+ {
402
+ platform: "telegram",
403
+ conversation_id: "123456789",
404
+ text: "hello",
405
+ },
406
+ {
407
+ workingDir: "/tmp",
408
+ conversationId: "conv-A",
409
+ assistantId: "ast-1",
410
+ trustClass: "guardian" as const,
411
+ },
412
+ );
341
413
 
342
- expect(result.isError).toBe(true);
343
- expect(result.content).toContain("Gmail and Outlook");
344
- expect(sendMessageMock).not.toHaveBeenCalled();
345
- });
414
+ expect(recordDeliveredChannelPostMock).not.toHaveBeenCalled();
415
+ });
346
416
 
347
- test("a provider with neither a send nor a transport is refused", async () => {
348
- provider = { ...makeProvider("fax", "Fax"), sendMessage: undefined };
349
- const result = await run(
350
- { platform: "fax", conversation_id: "1", text: "hi" },
351
- turn,
352
- );
353
- expect(result.isError).toBe(true);
354
- expect(result.content).toContain("Fax cannot send from here");
355
- expect(sendChannelTextMock).not.toHaveBeenCalled();
356
- });
417
+ test("records nothing when the post landed in the turn's own thread, even though the home is elsewhere", async () => {
418
+ // A Slack agent-style DM keys one conversation per thread. Its home
419
+ // resolves to the chat's notification conversation, so the home test
420
+ // alone would record this send as a cross-post away from the thread it
421
+ // was made in. The turn's own snapshot (channel, chat, thread) decides
422
+ // instead, against the thread the provider says it delivered into.
423
+ provider = slackProvider;
424
+
425
+ await run(
426
+ {
427
+ platform: "slack",
428
+ conversation_id: "D0123456789",
429
+ thread_id: "1700000000.000001",
430
+ text: "hello",
431
+ },
432
+ {
433
+ workingDir: "/tmp",
434
+ conversationId: "conv-A",
435
+ assistantId: "ast-1",
436
+ trustClass: "guardian" as const,
437
+ executionChannel: "slack",
438
+ requesterChatId: "D0123456789",
439
+ sourceThreadId: "1700000000.000001",
440
+ },
441
+ );
442
+
443
+ expect(resolveProactiveHomeConversationMock).not.toHaveBeenCalled();
444
+ expect(recordDeliveredChannelPostMock).not.toHaveBeenCalled();
357
445
  });
358
446
 
359
- describe("Outlook drafts", () => {
360
- test("reads and forwards attachments onto an Outlook draft", async () => {
361
- const dir = mkdtempSync(join(tmpdir(), "msg-send-att-"));
362
- const filePath = join(dir, "report.pdf");
363
- writeFileSync(filePath, "pdf-bytes");
364
- provider = outlookProvider;
365
- connection = {
366
- id: "outlook-conn-1",
367
- provider: "outlook",
368
- } as OAuthConnection;
447
+ test("records a send into the turn's chat but a different thread as a cross-post", async () => {
448
+ provider = slackProvider;
369
449
 
370
- try {
371
- const result = await run(
372
- {
373
- platform: "outlook",
374
- conversation_id: "user@example.com",
375
- text: "see attached",
376
- subject: "Docs",
377
- attachment_paths: [filePath],
378
- },
379
- {
380
- workingDir: "/tmp",
381
- conversationId: "conv-1",
382
- assistantId: "ast-alpha",
383
- trustClass: "guardian" as const,
384
- },
385
- );
386
-
387
- expect(result.isError).toBe(false);
388
- expect(result.content).toContain("Outlook draft created");
389
- expect(result.content).toContain("Draft ID: outlook-draft-1");
390
- expect(sendMessageMock).not.toHaveBeenCalled();
391
- expect(mockOutlookCreateDraft).toHaveBeenCalledTimes(1);
392
- expect(mockOutlookCreateDraft).toHaveBeenCalledWith(
393
- connection,
394
- expect.objectContaining({
395
- subject: "Docs",
396
- body: { contentType: "text", content: "see attached" },
397
- toRecipients: [{ emailAddress: { address: "user@example.com" } }],
398
- attachments: [expect.objectContaining({ name: "report.pdf" })],
399
- }),
400
- );
401
- } finally {
402
- rmSync(dir, { recursive: true, force: true });
403
- }
404
- });
450
+ await run(
451
+ {
452
+ platform: "slack",
453
+ conversation_id: "D0123456789",
454
+ thread_id: "1700000000.000002",
455
+ text: "hello",
456
+ },
457
+ {
458
+ workingDir: "/tmp",
459
+ conversationId: "conv-A",
460
+ assistantId: "ast-1",
461
+ trustClass: "guardian" as const,
462
+ executionChannel: "slack",
463
+ requesterChatId: "D0123456789",
464
+ sourceThreadId: "1700000000.000001",
465
+ },
466
+ );
467
+
468
+ expect(resolveProactiveHomeConversationMock).toHaveBeenCalledTimes(1);
469
+ expect(recordDeliveredChannelPostMock).toHaveBeenCalledTimes(1);
470
+ });
405
471
 
406
- test("creates an Outlook draft without a To address when conversation_id is not an email", async () => {
407
- provider = outlookProvider;
408
- connection = {
409
- id: "outlook-conn-1",
410
- provider: "outlook",
411
- } as OAuthConnection;
472
+ test("a thread-less delivery never matches a turn that arrived in a thread", async () => {
473
+ provider = slackProvider;
412
474
 
413
- const result = await run(
414
- {
415
- platform: "outlook",
416
- conversation_id: "drafts",
417
- text: "Hey team, here is the update.",
418
- subject: "Team update",
419
- },
420
- {
421
- workingDir: "/tmp",
422
- conversationId: "conv-1",
423
- assistantId: "ast-alpha",
424
- trustClass: "guardian" as const,
425
- },
426
- );
475
+ await run(
476
+ {
477
+ platform: "slack",
478
+ conversation_id: "D0123456789",
479
+ text: "hello",
480
+ },
481
+ {
482
+ workingDir: "/tmp",
483
+ conversationId: "conv-A",
484
+ assistantId: "ast-1",
485
+ trustClass: "guardian" as const,
486
+ executionChannel: "slack",
487
+ requesterChatId: "D0123456789",
488
+ sourceThreadId: "1700000000.000001",
489
+ },
490
+ );
427
491
 
428
- expect(result.isError).toBe(false);
429
- expect(result.content).toContain("Outlook draft created");
430
- expect(result.content).toContain("No recipient set");
431
- expect(result.content).toContain("Open it:");
432
- expect(sendMessageMock).not.toHaveBeenCalled();
433
- expect(mockOutlookCreateDraft).toHaveBeenCalledWith(
434
- connection,
435
- expect.objectContaining({
436
- subject: "Team update",
437
- body: {
438
- contentType: "text",
439
- content: "Hey team, here is the update.",
440
- },
441
- }),
442
- );
443
- const draftArg = mockOutlookCreateDraft.mock.calls[0][1];
444
- expect(draftArg.toRecipients).toBeUndefined();
445
- });
492
+ expect(recordDeliveredChannelPostMock).toHaveBeenCalledTimes(1);
493
+ });
446
494
 
447
- test("creates an Outlook reply draft instead of sending when in_reply_to is set", async () => {
448
- provider = outlookProvider;
449
- connection = {
450
- id: "outlook-conn-1",
451
- provider: "outlook",
452
- } as OAuthConnection;
495
+ test("decides by where the provider delivered, not by the thread requested", async () => {
496
+ // Telegram's provider ignores a requested thread and reports none, so a
497
+ // turn that arrived in a topic and asked for that topic still gets a
498
+ // post in the thread-less chat; that post is recorded, not suppressed.
499
+ provider = telegramProvider;
500
+
501
+ await run(
502
+ {
503
+ platform: "telegram",
504
+ conversation_id: "123456789",
505
+ thread_id: "777",
506
+ text: "hello",
507
+ },
508
+ {
509
+ workingDir: "/tmp",
510
+ conversationId: "conv-A",
511
+ assistantId: "ast-1",
512
+ trustClass: "guardian" as const,
513
+ executionChannel: "telegram",
514
+ requesterChatId: "123456789",
515
+ sourceThreadId: "777",
516
+ },
517
+ );
453
518
 
454
- const result = await run(
455
- {
456
- platform: "outlook",
457
- conversation_id: "user@example.com",
458
- text: "Thanks, that works.",
459
- in_reply_to: "AAMk-original",
460
- },
461
- {
462
- workingDir: "/tmp",
463
- conversationId: "conv-1",
464
- assistantId: "ast-alpha",
465
- trustClass: "guardian" as const,
466
- },
467
- );
519
+ expect(recordDeliveredChannelPostMock).toHaveBeenCalledTimes(1);
520
+ });
468
521
 
469
- expect(result.isError).toBe(false);
470
- expect(mockOutlookCreateReplyDraft).toHaveBeenCalledWith(
471
- connection,
472
- "AAMk-original",
473
- "Thanks, that works.",
474
- );
475
- expect(sendMessageMock).not.toHaveBeenCalled();
476
- expect(result.content).toContain("outlook-reply-draft-1");
477
- });
522
+ test("a turn that arrived through no channel falls back to the home comparison", async () => {
523
+ provider = slackProvider;
524
+ resolveProactiveHomeConversationMock.mockImplementation(async () => ({
525
+ conversationId: "conv-A",
526
+ createdNewConversation: false,
527
+ }));
528
+
529
+ await run(
530
+ {
531
+ platform: "slack",
532
+ conversation_id: "D0123456789",
533
+ text: "hello",
534
+ },
535
+ {
536
+ workingDir: "/tmp",
537
+ conversationId: "conv-A",
538
+ assistantId: "ast-1",
539
+ trustClass: "guardian" as const,
540
+ },
541
+ );
478
542
 
479
- test("errors when Outlook is not connected", async () => {
480
- provider = outlookProvider;
481
- connection = undefined;
543
+ expect(resolveProactiveHomeConversationMock).toHaveBeenCalledTimes(1);
544
+ expect(recordDeliveredChannelPostMock).not.toHaveBeenCalled();
545
+ });
482
546
 
483
- const result = await run(
484
- {
485
- platform: "outlook",
486
- conversation_id: "user@example.com",
487
- text: "hello",
488
- },
489
- {
490
- workingDir: "/tmp",
491
- conversationId: "conv-1",
492
- assistantId: "ast-alpha",
493
- trustClass: "guardian" as const,
547
+ test("records nothing for a provider that is not a channel", async () => {
548
+ provider = outlookProvider;
549
+ connection = {
550
+ id: "outlook-conn-1",
551
+ provider: "outlook",
552
+ } as OAuthConnection;
553
+
554
+ await run(
555
+ {
556
+ platform: "outlook",
557
+ conversation_id: "user@example.com",
558
+ text: "hello",
559
+ },
560
+ {
561
+ workingDir: "/tmp",
562
+ conversationId: "conv-A",
563
+ assistantId: "ast-1",
564
+ trustClass: "guardian" as const,
565
+ },
566
+ );
567
+
568
+ expect(resolveProactiveHomeConversationMock).not.toHaveBeenCalled();
569
+ expect(recordDeliveredChannelPostMock).not.toHaveBeenCalled();
570
+ });
571
+
572
+ test("creates an Outlook draft without a To address when conversation_id is not an email", async () => {
573
+ provider = outlookProvider;
574
+ connection = {
575
+ id: "outlook-conn-1",
576
+ provider: "outlook",
577
+ } as OAuthConnection;
578
+
579
+ const result = await run(
580
+ {
581
+ platform: "outlook",
582
+ conversation_id: "drafts",
583
+ text: "Hey team, here is the update.",
584
+ subject: "Team update",
585
+ },
586
+ {
587
+ workingDir: "/tmp",
588
+ conversationId: "conv-1",
589
+ assistantId: "ast-alpha",
590
+ trustClass: "guardian" as const,
591
+ },
592
+ );
593
+
594
+ expect(result.isError).toBe(false);
595
+ expect(result.content).toContain("Outlook draft created");
596
+ expect(result.content).toContain("No recipient set");
597
+ expect(result.content).toContain("Open it:");
598
+ expect(sendMessageMock).not.toHaveBeenCalled();
599
+ expect(mockOutlookCreateDraft).toHaveBeenCalledWith(
600
+ connection,
601
+ expect.objectContaining({
602
+ subject: "Team update",
603
+ body: {
604
+ contentType: "text",
605
+ content: "Hey team, here is the update.",
494
606
  },
495
- );
607
+ }),
608
+ );
609
+ const draftArg = mockOutlookCreateDraft.mock.calls[0][1];
610
+ expect(draftArg.toRecipients).toBeUndefined();
611
+ });
496
612
 
497
- expect(result.isError).toBe(true);
498
- expect(result.content).toContain("OAuth connection");
499
- expect(mockOutlookCreateDraft).not.toHaveBeenCalled();
613
+ test("creates an Outlook reply draft instead of sending when in_reply_to is set", async () => {
614
+ provider = outlookProvider;
615
+ connection = {
616
+ id: "outlook-conn-1",
617
+ provider: "outlook",
618
+ } as OAuthConnection;
619
+
620
+ const result = await run(
621
+ {
622
+ platform: "outlook",
623
+ conversation_id: "user@example.com",
624
+ text: "Thanks, that works.",
625
+ in_reply_to: "AAMk-original",
626
+ },
627
+ {
628
+ workingDir: "/tmp",
629
+ conversationId: "conv-1",
630
+ assistantId: "ast-alpha",
631
+ trustClass: "guardian" as const,
632
+ },
633
+ );
634
+
635
+ expect(result.isError).toBe(false);
636
+ expect(mockOutlookCreateReplyDraft).toHaveBeenCalledWith(
637
+ connection,
638
+ "AAMk-original",
639
+ "Thanks, that works.",
640
+ );
641
+ expect(sendMessageMock).not.toHaveBeenCalled();
642
+ expect(result.content).toContain("outlook-reply-draft-1");
643
+ });
644
+
645
+ test("errors when Outlook is not connected", async () => {
646
+ provider = outlookProvider;
647
+ connection = undefined;
648
+
649
+ const result = await run(
650
+ {
651
+ platform: "outlook",
652
+ conversation_id: "user@example.com",
653
+ text: "hello",
654
+ },
655
+ {
656
+ workingDir: "/tmp",
657
+ conversationId: "conv-1",
658
+ assistantId: "ast-alpha",
659
+ trustClass: "guardian" as const,
660
+ },
661
+ );
662
+
663
+ expect(result.isError).toBe(true);
664
+ expect(result.content).toContain("OAuth connection");
665
+ expect(mockOutlookCreateDraft).not.toHaveBeenCalled();
666
+ });
667
+
668
+ test("a record failure does not fail the send", async () => {
669
+ provider = telegramProvider;
670
+ getConversationMock.mockImplementation(() => ({
671
+ id: "home-1",
672
+ createdAt: 1700000000000,
673
+ }));
674
+ recordDeliveredChannelPostMock.mockImplementation(async () => {
675
+ throw new Error("DB write failed");
500
676
  });
677
+
678
+ const result = await run(
679
+ {
680
+ platform: "telegram",
681
+ conversation_id: "123456789",
682
+ text: "hello",
683
+ },
684
+ {
685
+ workingDir: "/tmp",
686
+ conversationId: "conv-A",
687
+ assistantId: "ast-1",
688
+ trustClass: "guardian" as const,
689
+ },
690
+ );
691
+
692
+ expect(result.isError).toBe(false);
693
+ expect(result.content).toContain("Message sent");
501
694
  });
502
695
 
503
696
  /**