@vellumai/assistant 0.12.1-staging.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) 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/openapi.yaml +1 -291
  5. package/package.json +1 -1
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +10 -18
  7. package/src/__tests__/app-compiler.test.ts +0 -6
  8. package/src/__tests__/app-control-tool-schemas.test.ts +2 -5
  9. package/src/__tests__/app-dir-path-guard.test.ts +3 -1
  10. package/src/__tests__/app-source-watcher.test.ts +218 -0
  11. package/src/{calls/__tests__ → __tests__}/call-controller.test.ts +82 -503
  12. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +2 -5
  13. package/src/__tests__/computer-use-tools.test.ts +5 -17
  14. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  15. package/src/__tests__/config-schema.test.ts +29 -28
  16. package/src/__tests__/config-watcher-skill-reseed.test.ts +359 -0
  17. package/src/__tests__/config-watcher.test.ts +8 -23
  18. package/src/__tests__/connection-model-compat.test.ts +2 -2
  19. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  20. package/src/__tests__/conversation-pairing.test.ts +0 -26
  21. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -4
  22. package/src/__tests__/conversation-queue.test.ts +207 -48
  23. package/src/__tests__/conversation-retry-discard.test.ts +1 -54
  24. package/src/__tests__/conversation-skill-tools.test.ts +0 -27
  25. package/src/__tests__/conversation-surfaces-app-control.test.ts +3 -8
  26. package/src/__tests__/conversation-surfaces-task-progress.test.ts +0 -67
  27. package/src/__tests__/conversation-title-service.test.ts +10 -14
  28. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +5 -3
  29. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -51
  30. package/src/__tests__/credential-record-retire.test.ts +1 -171
  31. package/src/__tests__/credential-routes.test.ts +3 -212
  32. package/src/__tests__/credential-security-invariants.test.ts +1 -2
  33. package/src/__tests__/cu-unified-flow.test.ts +2 -8
  34. package/src/__tests__/drain-kick-guard.test.ts +2 -0
  35. package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
  36. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  37. package/src/__tests__/host-bash-proxy.test.ts +45 -26
  38. package/src/__tests__/host-browser-proxy.test.ts +17 -6
  39. package/src/__tests__/host-file-proxy-targeted.test.ts +0 -23
  40. package/src/__tests__/host-file-proxy.test.ts +23 -11
  41. package/src/__tests__/host-transfer-proxy-targeted.test.ts +9 -30
  42. package/src/__tests__/llm-catalog-parity.test.ts +0 -25
  43. package/src/__tests__/mcp-auth-routes.test.ts +9 -6
  44. package/src/__tests__/mcp-config-secret-boundary.test.ts +136 -78
  45. package/src/__tests__/mcp-health-check.test.ts +11 -8
  46. package/src/__tests__/mcp-list-plugin-servers.test.ts +10 -8
  47. package/src/{calls/__tests__ → __tests__}/media-stream-audio-transcode.test.ts +1 -1
  48. package/src/{calls/__tests__ → __tests__}/media-stream-output.test.ts +8 -37
  49. package/src/{calls/__tests__ → __tests__}/media-stream-parser.test.ts +1 -1
  50. package/src/{calls/__tests__ → __tests__}/media-stream-server-integration.test.ts +57 -150
  51. package/src/{calls/__tests__ → __tests__}/media-stream-stt-session.test.ts +5 -5
  52. package/src/{calls/__tests__ → __tests__}/media-turn-detector.test.ts +1 -1
  53. package/src/__tests__/messages-after-tiebreaker.test.ts +0 -91
  54. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  55. package/src/__tests__/messaging-send-tool.test.ts +548 -355
  56. package/src/__tests__/model-intents.test.ts +0 -3
  57. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  58. package/src/__tests__/platform-client-verify-credential.test.ts +0 -2
  59. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -13
  60. package/src/__tests__/script-proxy-certs.test.ts +0 -35
  61. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  62. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  63. package/src/__tests__/speaker-identification.test.ts +56 -0
  64. package/src/__tests__/subagent-tool-gate-mode.test.ts +3 -19
  65. package/src/__tests__/subagent-tools.test.ts +2 -3
  66. package/src/__tests__/telephony-tts-guard.test.ts +3 -38
  67. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +0 -4
  68. package/src/{calls/__tests__/voice-session-bridge-turn-flow.test.ts → __tests__/voice-session-bridge.test.ts} +12 -20
  69. package/src/__tests__/watch-retro-tool-availability.test.ts +17 -8
  70. package/src/__tests__/workspace-git-service.test.ts +0 -26
  71. package/src/acp/__tests__/agent-process.test.ts +4 -258
  72. package/src/acp/__tests__/auth-required.test.ts +0 -96
  73. package/src/acp/__tests__/client-handler.test.ts +2 -90
  74. package/src/acp/__tests__/session-manager-persistence.test.ts +1 -5
  75. package/src/acp/__tests__/session-manager-resume.test.ts +6 -270
  76. package/src/acp/__tests__/session-manager-usage.test.ts +2 -4
  77. package/src/acp/__tests__/session-manager.test.ts +4 -895
  78. package/src/acp/agent-process.test.ts +1 -4
  79. package/src/acp/agent-process.ts +9 -94
  80. package/src/acp/auth-required.ts +1 -50
  81. package/src/acp/auto-install.test.ts +7 -9
  82. package/src/acp/client-handler.ts +3 -16
  83. package/src/acp/resolve-agent.test.ts +6 -220
  84. package/src/acp/resolve-agent.ts +23 -88
  85. package/src/acp/session-manager.test.ts +3 -196
  86. package/src/acp/session-manager.ts +18 -441
  87. package/src/acp/types.ts +5 -50
  88. package/src/api/events/app-files-changed.ts +3 -4
  89. package/src/api/index.ts +0 -6
  90. package/src/apps/app-store.ts +1 -1
  91. package/src/bundler/app-compiler.ts +0 -15
  92. package/src/calls/__tests__/voice-control-protocol.test.ts +25 -58
  93. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -37
  94. package/src/calls/__tests__/voice-triage-escalate.test.ts +0 -105
  95. package/src/calls/call-constants.ts +4 -24
  96. package/src/calls/call-controller.ts +199 -499
  97. package/src/calls/call-transport.ts +0 -16
  98. package/src/calls/media-stream-output.ts +0 -32
  99. package/src/calls/media-stream-server.ts +30 -82
  100. package/src/calls/media-stream-stt-session.ts +0 -15
  101. package/src/calls/resolve-call-tts-provider.ts +2 -33
  102. package/src/calls/speaker-identification.ts +233 -0
  103. package/src/calls/tts-call-strategy.ts +5 -9
  104. package/src/calls/types.ts +0 -2
  105. package/src/calls/voice-control-protocol.ts +21 -53
  106. package/src/calls/voice-session-bridge.ts +46 -59
  107. package/src/calls/voice-triage-escalate.ts +56 -175
  108. package/src/cli/commands/apps.help.ts +13 -67
  109. package/src/cli/commands/apps.ts +0 -183
  110. package/src/cli/commands/channels/index.help.ts +2 -70
  111. package/src/cli/commands/channels/index.ts +3 -16
  112. package/src/cli/commands/credentials.help.ts +2 -6
  113. package/src/cli/commands/credentials.ts +2 -2
  114. package/src/cli/commands/mcp.help.ts +8 -8
  115. package/src/cli/commands/mcp.ts +1 -1
  116. package/src/cli/commands/roadmap.help.ts +1 -5
  117. package/src/cli/commands/roadmap.ts +1 -1
  118. package/src/cli/lib/bundled-marketplace.json +90 -103
  119. package/src/config/__tests__/default-profile-catalog.test.ts +0 -22
  120. package/src/config/__tests__/memory-retrospective-schema.test.ts +1 -15
  121. package/src/config/__tests__/platform-rehydration.test.ts +0 -93
  122. package/src/config/acp-defaults.test.ts +3 -13
  123. package/src/config/acp-defaults.ts +6 -23
  124. package/src/config/acp-schema.ts +2 -30
  125. package/src/config/bundled-skills/acp/SKILL.md +14 -22
  126. package/src/config/bundled-skills/acp/TOOLS.json +1 -5
  127. package/src/config/bundled-skills/app-builder/SKILL.md +9 -12
  128. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  129. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  130. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +139 -67
  131. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  132. package/src/config/bundled-skills/screen-annotation/SKILL.md +1 -8
  133. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +1 -4
  134. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  135. package/src/config/default-profile-catalog.ts +1 -1
  136. package/src/config/env.ts +2 -2
  137. package/src/config/feature-flag-registry.json +12 -8
  138. package/src/config/memory-v3-gate.test.ts +0 -31
  139. package/src/config/memory-v3-gate.ts +16 -20
  140. package/src/config/platform-rehydration.ts +21 -75
  141. package/src/config/schema.ts +2 -2
  142. package/src/config/schemas/__tests__/live-voice.test.ts +158 -8
  143. package/src/config/schemas/calls.ts +28 -1
  144. package/src/config/schemas/live-voice.ts +173 -1
  145. package/src/config/schemas/mcp.ts +1 -1
  146. package/src/config/schemas/memory-retrospective.ts +0 -10
  147. package/src/config/schemas/skills.ts +14 -0
  148. package/src/credential-execution/process-manager.test.ts +19 -18
  149. package/src/daemon/__tests__/conversation-tool-setup.test.ts +121 -66
  150. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -4
  151. package/src/daemon/app-source-watcher.ts +218 -0
  152. package/src/daemon/config-watcher.ts +194 -13
  153. package/src/daemon/conversation-process.ts +64 -35
  154. package/src/daemon/conversation-skill-tools.ts +31 -13
  155. package/src/daemon/conversation-store.ts +1 -1
  156. package/src/daemon/conversation-surfaces.ts +8 -10
  157. package/src/daemon/conversation-tool-setup.ts +43 -37
  158. package/src/daemon/conversation.ts +3 -0
  159. package/src/daemon/host-bash-proxy.ts +12 -8
  160. package/src/daemon/host-browser-proxy.ts +1 -15
  161. package/src/daemon/host-file-proxy.ts +10 -8
  162. package/src/daemon/host-transfer-proxy.ts +18 -14
  163. package/src/daemon/lifecycle.ts +5 -0
  164. package/src/daemon/mcp-reload-service.ts +5 -5
  165. package/src/daemon/message-types/sync.ts +0 -1
  166. package/src/daemon/providers-setup.ts +1 -1
  167. package/src/daemon/shutdown-handlers.ts +9 -1
  168. package/src/daemon/skill-memory-refresh.ts +0 -64
  169. package/src/daemon/tool-side-effects.ts +18 -7
  170. package/src/hooks/types.ts +0 -33
  171. package/src/inbound/platform-callback-registration.ts +5 -4
  172. package/src/live-voice/__tests__/live-voice-metrics.test.ts +3 -0
  173. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  174. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  175. package/src/{calls → live-voice}/__tests__/progress-narration.test.ts +4 -4
  176. package/src/{calls → live-voice}/__tests__/progress-phrases.test.ts +1 -1
  177. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +69 -13
  178. package/src/live-voice/live-voice-session.ts +693 -213
  179. package/src/{calls → live-voice}/progress-narration.ts +2 -2
  180. package/src/live-voice/protocol.ts +2 -0
  181. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  182. package/src/mcp/effective-config.ts +3 -3
  183. package/src/mcp/mcp-header-store.ts +45 -29
  184. package/src/mcp/startup.ts +4 -8
  185. package/src/messaging/provider.ts +3 -10
  186. package/src/messaging/providers/callback-routing.ts +0 -18
  187. package/src/messaging/providers/channel-transport.ts +0 -56
  188. package/src/messaging/providers/discord/transport.ts +0 -17
  189. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +17 -6
  190. package/src/messaging/providers/slack/adapter.ts +44 -7
  191. package/src/messaging/providers/slack/transport.ts +0 -16
  192. package/src/messaging/providers/telegram-bot/adapter.ts +48 -1
  193. package/src/messaging/providers/telegram-bot/client.ts +26 -0
  194. package/src/messaging/providers/telegram-bot/transport.ts +0 -20
  195. package/src/messaging/providers/whatsapp/adapter.ts +48 -1
  196. package/src/messaging/providers/whatsapp/client.ts +29 -0
  197. package/src/messaging/providers/whatsapp/transport.ts +0 -18
  198. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +0 -8
  199. package/src/notifications/__tests__/assistant-reply-producer.test.ts +0 -55
  200. package/src/notifications/__tests__/home-feed-side-effect.test.ts +3 -153
  201. package/src/notifications/assistant-reply-producer.ts +3 -8
  202. package/src/notifications/conversation-pairing.ts +16 -36
  203. package/src/notifications/delivered-post-record.ts +1 -11
  204. package/src/notifications/home-feed-side-effect.ts +7 -55
  205. package/src/outbound-proxy/certs.ts +5 -3
  206. package/src/permissions/checker.test.ts +6 -11
  207. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  208. package/src/persistence/conversation-crud.ts +63 -71
  209. package/src/persistence/conversation-lineage.ts +4 -12
  210. package/src/persistence/conversation-title-service.ts +26 -9
  211. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  212. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  213. package/src/persistence/message-reads.ts +0 -30
  214. package/src/persistence/migrations/157-invite-contact-id.ts +6 -18
  215. package/src/persistence/schema/memory-core.ts +0 -5
  216. package/src/platform/client.test.ts +0 -2
  217. package/src/platform/client.ts +10 -2
  218. package/src/plugin-api/constants.ts +0 -2
  219. package/src/plugin-api/index.ts +0 -1
  220. package/src/plugin-api/types.ts +0 -2
  221. package/src/plugins/defaults/index.ts +0 -2
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +0 -46
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +5 -9
  224. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +10 -20
  225. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +0 -219
  226. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +0 -31
  227. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +0 -63
  228. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +0 -4
  229. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -4
  230. package/src/plugins/defaults/memory/hooks/init.ts +0 -10
  231. package/src/plugins/defaults/memory/hooks/shutdown.ts +0 -2
  232. package/src/plugins/defaults/memory/jobs-worker.ts +0 -13
  233. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +60 -34
  234. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +5 -3
  235. package/src/plugins/defaults/memory/memory-retrospective-job.ts +3 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-state.ts +71 -312
  237. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +1 -2
  238. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +3 -5
  239. package/src/plugins/defaults/memory/startup.ts +1 -6
  240. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +9 -7
  241. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  242. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +0 -89
  243. package/src/plugins/defaults/memory/worker.ts +0 -6
  244. package/src/plugins/mcp-servers.ts +75 -15
  245. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  246. package/src/providers/gemini/client.ts +23 -9
  247. package/src/providers/model-catalog.ts +0 -34
  248. package/src/providers/model-intents.ts +5 -10
  249. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +18 -146
  250. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +1 -83
  251. package/src/providers/openai/chat-completions-provider.ts +78 -192
  252. package/src/providers/openai/codex-models.ts +4 -0
  253. package/src/runtime/AGENTS.md +0 -2
  254. package/src/runtime/auth/same-actor.ts +3 -38
  255. package/src/runtime/http-server.ts +151 -160
  256. package/src/runtime/routes/__tests__/acp-routes.test.ts +4 -159
  257. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +1 -108
  258. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +12 -16
  259. package/src/runtime/routes/acp-routes.test.ts +0 -15
  260. package/src/runtime/routes/acp-routes.ts +9 -72
  261. package/src/runtime/routes/app-management-routes.ts +0 -73
  262. package/src/runtime/routes/app-routes.ts +1 -1
  263. package/src/runtime/routes/conversation-list-routes.ts +3 -14
  264. package/src/runtime/routes/conversation-query-routes.ts +0 -40
  265. package/src/runtime/routes/credential-routes.ts +10 -19
  266. package/src/runtime/routes/diagnostics-routes.ts +3 -55
  267. package/src/runtime/routes/index.ts +0 -4
  268. package/src/runtime/routes/log-export-routes.ts +0 -2
  269. package/src/runtime/routes/mcp-auth-routes.ts +47 -39
  270. package/src/runtime/routes/migration-routes.ts +21 -8
  271. package/src/runtime/routes/platform-routes.ts +15 -18
  272. package/src/runtime/services/__tests__/conversation-serializer.test.ts +0 -28
  273. package/src/runtime/services/conversation-serializer.ts +3 -16
  274. package/src/runtime/sync/resource-sync-events.ts +1 -2
  275. package/src/schedule/worker.ts +1 -1
  276. package/src/security/ces-rpc-record-backend.ts +0 -7
  277. package/src/subagent/consult-context.ts +25 -12
  278. package/src/tools/acp/list-agents.test.ts +1 -3
  279. package/src/tools/acp/spawn.test.ts +11 -146
  280. package/src/tools/acp/spawn.ts +3 -21
  281. package/src/tools/acp/status.ts +3 -40
  282. package/src/tools/app-control/skill-proxy-bridge.ts +1 -2
  283. package/src/tools/capability-offer.test.ts +2 -22
  284. package/src/tools/capability-offer.ts +1 -16
  285. package/src/tools/computer-use/definitions.ts +2 -4
  286. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -2
  287. package/src/tools/credentials/metadata-store.ts +0 -100
  288. package/src/tools/host-filesystem/edit.test.ts +1 -2
  289. package/src/tools/host-filesystem/edit.ts +2 -2
  290. package/src/tools/host-filesystem/read.test.ts +1 -2
  291. package/src/tools/host-filesystem/read.ts +2 -2
  292. package/src/tools/host-filesystem/transfer.test.ts +1 -2
  293. package/src/tools/host-filesystem/transfer.ts +2 -2
  294. package/src/tools/host-filesystem/write.test.ts +1 -2
  295. package/src/tools/host-filesystem/write.ts +2 -2
  296. package/src/tools/host-terminal/host-shell.test.ts +0 -16
  297. package/src/tools/host-terminal/host-shell.ts +20 -4
  298. package/src/tools/policy-context.ts +7 -6
  299. package/src/tools/subagent/spawn.ts +8 -5
  300. package/src/tools/watch/watch-retro-report.ts +8 -0
  301. package/src/util/__tests__/language-subtag.test.ts +1 -23
  302. package/src/util/fs-watcher-error.ts +36 -0
  303. package/src/util/language-subtag.ts +0 -16
  304. package/src/util/platform.ts +0 -8
  305. package/src/workspace/git-service.ts +54 -5
  306. package/src/workspace/migrations/registry.ts +0 -6
  307. package/scripts/smoke-desktop-window-theme.ts +0 -186
  308. package/src/__tests__/git-porcelain.test.ts +0 -47
  309. package/src/__tests__/helpers/set-workspace-mcp.ts +0 -11
  310. package/src/__tests__/message-deleted-hook-dispatch.test.ts +0 -119
  311. package/src/__tests__/resolve-app.test.ts +0 -135
  312. package/src/__tests__/source-fingerprint.test.ts +0 -184
  313. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +0 -336
  314. package/src/acp/__tests__/helpers/acp-model-option.ts +0 -51
  315. package/src/acp/model-config.test.ts +0 -166
  316. package/src/acp/model-config.ts +0 -138
  317. package/src/api/events/acp-session-model-update.test.ts +0 -88
  318. package/src/api/events/acp-session-model-update.ts +0 -46
  319. package/src/apps/resolve-app.ts +0 -81
  320. package/src/apps/source-fingerprint.ts +0 -266
  321. package/src/calls/__tests__/barge-in-guard.test.ts +0 -81
  322. package/src/calls/__tests__/voice-leg-coordinator.test.ts +0 -229
  323. package/src/calls/__tests__/voice-progress-cadence.test.ts +0 -274
  324. package/src/calls/barge-in-guard.ts +0 -118
  325. package/src/calls/voice-leg-coordinator.ts +0 -189
  326. package/src/calls/voice-progress-cadence.ts +0 -423
  327. package/src/cli/commands/__tests__/apps.test.ts +0 -245
  328. package/src/cli/commands/__tests__/roadmap.help.test.ts +0 -19
  329. package/src/cli/commands/channels/__tests__/send.test.ts +0 -218
  330. package/src/cli/commands/channels/send.ts +0 -128
  331. package/src/config/__tests__/platform-identity.test.ts +0 -117
  332. package/src/config/acp-agent-write.ts +0 -43
  333. package/src/config/acp-schema.test.ts +0 -81
  334. package/src/config/platform-identity.ts +0 -144
  335. package/src/config/schemas/__tests__/voice.test.ts +0 -148
  336. package/src/config/schemas/voice.ts +0 -150
  337. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +0 -133
  338. package/src/daemon/app-change-notify.ts +0 -45
  339. package/src/desktop/__tests__/fake-desktop.ts +0 -119
  340. package/src/desktop/desktop-chrome-policy.test.ts +0 -71
  341. package/src/desktop/desktop-chrome-policy.ts +0 -42
  342. package/src/desktop/desktop-dependencies.test.ts +0 -102
  343. package/src/desktop/desktop-dependencies.ts +0 -306
  344. package/src/desktop/desktop-feature.test.ts +0 -18
  345. package/src/desktop/desktop-feature.ts +0 -19
  346. package/src/desktop/desktop-panel-config.test.ts +0 -135
  347. package/src/desktop/desktop-panel-config.ts +0 -149
  348. package/src/desktop/desktop-session-manager.test.ts +0 -627
  349. package/src/desktop/desktop-session-manager.ts +0 -814
  350. package/src/desktop/desktop-stream-bridge.test.ts +0 -395
  351. package/src/desktop/desktop-stream-bridge.ts +0 -214
  352. package/src/desktop/desktop-wallpaper.test.ts +0 -191
  353. package/src/desktop/desktop-wallpaper.ts +0 -113
  354. package/src/desktop/desktop-window-theme.ts +0 -170
  355. package/src/i18n/AGENTS.md +0 -22
  356. package/src/i18n/__tests__/i18n.test.ts +0 -108
  357. package/src/i18n/index.ts +0 -16
  358. package/src/i18n/locales.ts +0 -93
  359. package/src/i18n/messages.ts +0 -61
  360. package/src/i18n/title.ts +0 -25
  361. package/src/mcp/__tests__/workspace-mcp-config.test.ts +0 -135
  362. package/src/mcp/spec-schema.ts +0 -111
  363. package/src/mcp/workspace-mcp-config.ts +0 -188
  364. package/src/messaging/providers/__tests__/proactive-address.test.ts +0 -95
  365. package/src/persistence/conversation-title-placeholders.ts +0 -23
  366. package/src/persistence/message-cursor.ts +0 -78
  367. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +0 -149
  368. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +0 -118
  369. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +0 -83
  370. package/src/plugins/defaults/memory/hooks/message-deleted.ts +0 -21
  371. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +0 -126
  372. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +0 -79
  373. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +0 -28
  374. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +0 -9
  375. package/src/providers/gemini-thought-signature.test.ts +0 -63
  376. package/src/providers/gemini-thought-signature.ts +0 -44
  377. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +0 -613
  378. package/src/providers/openai/__tests__/google-thought-signature.test.ts +0 -235
  379. package/src/providers/openai/google-thought-signature.ts +0 -320
  380. package/src/runtime/__tests__/channel-send.test.ts +0 -443
  381. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +0 -89
  382. package/src/runtime/__tests__/live-voice-upgrade.test.ts +0 -58
  383. package/src/runtime/__tests__/runtime-ws-test-utils.ts +0 -72
  384. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +0 -59
  385. package/src/runtime/channel-send.ts +0 -316
  386. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +0 -205
  387. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +0 -127
  388. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
  389. package/src/runtime/routes/channel-send-routes.ts +0 -85
  390. package/src/runtime/routes/desktop-setup-routes.test.ts +0 -74
  391. package/src/runtime/routes/desktop-setup-routes.ts +0 -35
  392. package/src/security/__tests__/ces-http-record-backend.test.ts +0 -137
  393. package/src/security/ces-http-record-backend.ts +0 -120
  394. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +0 -60
  395. package/src/skills/workspace-skill-md-signature.ts +0 -50
  396. package/src/tools/acp/status.test.ts +0 -94
  397. package/src/util/ca-bundle.ts +0 -13
  398. package/src/workspace/git-porcelain.ts +0 -66
  399. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +0 -258
  400. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +0 -78
  401. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +0 -214
  402. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +0 -102
  403. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +0 -199
  404. /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
  /**