@vellumai/assistant 0.12.0 → 0.12.1-staging.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 (406) hide show
  1. package/AGENTS.md +5 -1
  2. package/ARCHITECTURE.md +11 -12
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/openapi.yaml +292 -2
  5. package/package.json +1 -1
  6. package/scripts/smoke-desktop-window-theme.ts +186 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  8. package/src/__tests__/app-compiler.test.ts +6 -0
  9. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  10. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  11. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  12. package/src/__tests__/computer-use-tools.test.ts +17 -5
  13. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  14. package/src/__tests__/config-schema.test.ts +28 -29
  15. package/src/__tests__/config-watcher.test.ts +23 -8
  16. package/src/__tests__/connection-model-compat.test.ts +2 -2
  17. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  18. package/src/__tests__/conversation-pairing.test.ts +26 -0
  19. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  20. package/src/__tests__/conversation-queue.test.ts +48 -207
  21. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  22. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  23. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  24. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  25. package/src/__tests__/conversation-title-service.test.ts +14 -10
  26. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  27. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  28. package/src/__tests__/credential-record-retire.test.ts +171 -1
  29. package/src/__tests__/credential-routes.test.ts +212 -3
  30. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  31. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  32. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  33. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  34. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  35. package/src/__tests__/git-porcelain.test.ts +47 -0
  36. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  37. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  38. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  39. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  40. package/src/__tests__/host-file-proxy.test.ts +11 -23
  41. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  42. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  43. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  44. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  45. package/src/__tests__/mcp-health-check.test.ts +8 -11
  46. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  47. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  48. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  49. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  50. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  51. package/src/__tests__/model-intents.test.ts +3 -0
  52. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  53. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +13 -0
  55. package/src/__tests__/resolve-app.test.ts +135 -0
  56. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  57. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  58. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  59. package/src/__tests__/source-fingerprint.test.ts +184 -0
  60. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  61. package/src/__tests__/subagent-tools.test.ts +3 -2
  62. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  63. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  64. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  65. package/src/__tests__/workspace-git-service.test.ts +26 -0
  66. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  67. package/src/acp/__tests__/agent-process.test.ts +258 -4
  68. package/src/acp/__tests__/auth-required.test.ts +96 -0
  69. package/src/acp/__tests__/client-handler.test.ts +90 -2
  70. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  71. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  72. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  73. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  74. package/src/acp/__tests__/session-manager.test.ts +895 -4
  75. package/src/acp/agent-process.test.ts +4 -1
  76. package/src/acp/agent-process.ts +94 -9
  77. package/src/acp/auth-required.ts +50 -1
  78. package/src/acp/auto-install.test.ts +9 -7
  79. package/src/acp/client-handler.ts +16 -3
  80. package/src/acp/model-config.test.ts +166 -0
  81. package/src/acp/model-config.ts +138 -0
  82. package/src/acp/resolve-agent.test.ts +220 -6
  83. package/src/acp/resolve-agent.ts +88 -23
  84. package/src/acp/session-manager.test.ts +196 -3
  85. package/src/acp/session-manager.ts +441 -18
  86. package/src/acp/types.ts +50 -5
  87. package/src/api/events/acp-session-model-update.test.ts +88 -0
  88. package/src/api/events/acp-session-model-update.ts +46 -0
  89. package/src/api/events/app-files-changed.ts +4 -3
  90. package/src/api/index.ts +6 -0
  91. package/src/apps/app-store.ts +1 -1
  92. package/src/apps/resolve-app.ts +81 -0
  93. package/src/apps/source-fingerprint.ts +266 -0
  94. package/src/bundler/app-compiler.ts +15 -0
  95. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  96. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  97. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  98. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  99. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  100. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  101. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  102. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  103. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  104. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  105. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  106. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  107. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  108. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  109. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  110. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  111. package/src/calls/barge-in-guard.ts +118 -0
  112. package/src/calls/call-constants.ts +24 -4
  113. package/src/calls/call-controller.ts +499 -199
  114. package/src/calls/call-transport.ts +16 -0
  115. package/src/calls/media-stream-output.ts +32 -0
  116. package/src/calls/media-stream-server.ts +82 -30
  117. package/src/calls/media-stream-stt-session.ts +15 -0
  118. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  119. package/src/calls/resolve-call-tts-provider.ts +33 -2
  120. package/src/calls/tts-call-strategy.ts +9 -5
  121. package/src/calls/types.ts +2 -0
  122. package/src/calls/voice-control-protocol.ts +53 -21
  123. package/src/calls/voice-leg-coordinator.ts +189 -0
  124. package/src/calls/voice-progress-cadence.ts +423 -0
  125. package/src/calls/voice-session-bridge.ts +59 -46
  126. package/src/calls/voice-triage-escalate.ts +175 -56
  127. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  128. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  129. package/src/cli/commands/apps.help.ts +67 -13
  130. package/src/cli/commands/apps.ts +183 -0
  131. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  132. package/src/cli/commands/channels/index.help.ts +70 -2
  133. package/src/cli/commands/channels/index.ts +16 -3
  134. package/src/cli/commands/channels/send.ts +128 -0
  135. package/src/cli/commands/credentials.help.ts +6 -2
  136. package/src/cli/commands/credentials.ts +2 -2
  137. package/src/cli/commands/mcp.help.ts +8 -8
  138. package/src/cli/commands/mcp.ts +1 -1
  139. package/src/cli/commands/roadmap.help.ts +5 -1
  140. package/src/cli/commands/roadmap.ts +1 -1
  141. package/src/cli/lib/bundled-marketplace.json +103 -90
  142. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  143. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  144. package/src/config/__tests__/platform-identity.test.ts +117 -0
  145. package/src/config/__tests__/platform-rehydration.test.ts +93 -0
  146. package/src/config/acp-agent-write.ts +43 -0
  147. package/src/config/acp-defaults.test.ts +13 -3
  148. package/src/config/acp-defaults.ts +23 -6
  149. package/src/config/acp-schema.test.ts +81 -0
  150. package/src/config/acp-schema.ts +30 -2
  151. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  152. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  153. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  154. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  155. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  156. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  157. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  158. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  159. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  160. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  161. package/src/config/default-profile-catalog.ts +1 -1
  162. package/src/config/env.ts +2 -2
  163. package/src/config/feature-flag-registry.json +8 -12
  164. package/src/config/memory-v3-gate.test.ts +31 -0
  165. package/src/config/memory-v3-gate.ts +20 -16
  166. package/src/config/platform-identity.ts +144 -0
  167. package/src/config/platform-rehydration.ts +75 -21
  168. package/src/config/schema.ts +2 -2
  169. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  170. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  171. package/src/config/schemas/calls.ts +1 -28
  172. package/src/config/schemas/live-voice.ts +1 -173
  173. package/src/config/schemas/mcp.ts +1 -1
  174. package/src/config/schemas/memory-retrospective.ts +10 -0
  175. package/src/config/schemas/skills.ts +0 -14
  176. package/src/config/schemas/voice.ts +150 -0
  177. package/src/credential-execution/process-manager.test.ts +18 -19
  178. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  179. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  180. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  181. package/src/daemon/app-change-notify.ts +45 -0
  182. package/src/daemon/config-watcher.ts +13 -194
  183. package/src/daemon/conversation-process.ts +35 -64
  184. package/src/daemon/conversation-skill-tools.ts +13 -31
  185. package/src/daemon/conversation-store.ts +1 -1
  186. package/src/daemon/conversation-surfaces.ts +10 -8
  187. package/src/daemon/conversation-tool-setup.ts +37 -43
  188. package/src/daemon/conversation.ts +0 -3
  189. package/src/daemon/host-bash-proxy.ts +8 -12
  190. package/src/daemon/host-browser-proxy.ts +15 -1
  191. package/src/daemon/host-file-proxy.ts +8 -10
  192. package/src/daemon/host-transfer-proxy.ts +14 -18
  193. package/src/daemon/lifecycle.ts +0 -5
  194. package/src/daemon/mcp-reload-service.ts +5 -5
  195. package/src/daemon/message-types/sync.ts +1 -0
  196. package/src/daemon/providers-setup.ts +1 -1
  197. package/src/daemon/shutdown-handlers.ts +1 -9
  198. package/src/daemon/skill-memory-refresh.ts +64 -0
  199. package/src/daemon/tool-side-effects.ts +7 -18
  200. package/src/desktop/__tests__/fake-desktop.ts +119 -0
  201. package/src/desktop/desktop-chrome-policy.test.ts +71 -0
  202. package/src/desktop/desktop-chrome-policy.ts +42 -0
  203. package/src/desktop/desktop-dependencies.test.ts +102 -0
  204. package/src/desktop/desktop-dependencies.ts +306 -0
  205. package/src/desktop/desktop-feature.test.ts +18 -0
  206. package/src/desktop/desktop-feature.ts +19 -0
  207. package/src/desktop/desktop-panel-config.test.ts +135 -0
  208. package/src/desktop/desktop-panel-config.ts +149 -0
  209. package/src/desktop/desktop-session-manager.test.ts +627 -0
  210. package/src/desktop/desktop-session-manager.ts +814 -0
  211. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  212. package/src/desktop/desktop-stream-bridge.ts +214 -0
  213. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  214. package/src/desktop/desktop-wallpaper.ts +113 -0
  215. package/src/desktop/desktop-window-theme.ts +170 -0
  216. package/src/hooks/types.ts +33 -0
  217. package/src/i18n/AGENTS.md +22 -0
  218. package/src/i18n/__tests__/i18n.test.ts +108 -0
  219. package/src/i18n/index.ts +16 -0
  220. package/src/i18n/locales.ts +93 -0
  221. package/src/i18n/messages.ts +61 -0
  222. package/src/i18n/title.ts +25 -0
  223. package/src/inbound/platform-callback-registration.ts +4 -5
  224. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  225. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  226. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  227. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  228. package/src/live-voice/live-voice-session.ts +213 -693
  229. package/src/live-voice/protocol.ts +0 -2
  230. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  231. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  232. package/src/mcp/effective-config.ts +3 -3
  233. package/src/mcp/mcp-header-store.ts +29 -45
  234. package/src/mcp/spec-schema.ts +111 -0
  235. package/src/mcp/startup.ts +8 -4
  236. package/src/mcp/workspace-mcp-config.ts +188 -0
  237. package/src/messaging/provider.ts +10 -3
  238. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  239. package/src/messaging/providers/callback-routing.ts +18 -0
  240. package/src/messaging/providers/channel-transport.ts +56 -0
  241. package/src/messaging/providers/discord/transport.ts +17 -0
  242. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  243. package/src/messaging/providers/slack/adapter.ts +7 -44
  244. package/src/messaging/providers/slack/transport.ts +16 -0
  245. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  246. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  247. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  248. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  249. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  250. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  251. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  252. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  253. package/src/notifications/assistant-reply-producer.ts +8 -3
  254. package/src/notifications/conversation-pairing.ts +36 -16
  255. package/src/notifications/delivered-post-record.ts +11 -1
  256. package/src/notifications/home-feed-side-effect.ts +55 -7
  257. package/src/outbound-proxy/certs.ts +3 -5
  258. package/src/permissions/checker.test.ts +11 -6
  259. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  260. package/src/persistence/conversation-crud.ts +71 -63
  261. package/src/persistence/conversation-lineage.ts +12 -4
  262. package/src/persistence/conversation-title-placeholders.ts +23 -0
  263. package/src/persistence/conversation-title-service.ts +9 -26
  264. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  265. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  266. package/src/persistence/message-cursor.ts +78 -0
  267. package/src/persistence/message-reads.ts +30 -0
  268. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  269. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  270. package/src/persistence/schema/memory-core.ts +5 -0
  271. package/src/platform/client.test.ts +2 -0
  272. package/src/platform/client.ts +2 -10
  273. package/src/plugin-api/constants.ts +2 -0
  274. package/src/plugin-api/index.ts +1 -0
  275. package/src/plugin-api/types.ts +2 -0
  276. package/src/plugins/defaults/index.ts +2 -0
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  280. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  281. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  282. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  283. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  284. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  285. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  286. package/src/plugins/defaults/memory/graph/capability-seed.ts +4 -6
  287. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  288. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  289. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  290. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  291. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  292. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  293. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  294. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  295. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  296. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  297. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  298. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  299. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  300. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  301. package/src/plugins/defaults/memory/startup.ts +6 -1
  302. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  303. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  304. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
  305. package/src/plugins/defaults/memory/worker.ts +6 -0
  306. package/src/plugins/mcp-servers.ts +15 -75
  307. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  308. package/src/providers/gemini/client.ts +9 -23
  309. package/src/providers/gemini-thought-signature.test.ts +63 -0
  310. package/src/providers/gemini-thought-signature.ts +44 -0
  311. package/src/providers/model-catalog.ts +34 -0
  312. package/src/providers/model-intents.ts +10 -5
  313. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  314. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  315. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  316. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  317. package/src/providers/openai/chat-completions-provider.ts +192 -78
  318. package/src/providers/openai/codex-models.ts +0 -4
  319. package/src/providers/openai/google-thought-signature.ts +320 -0
  320. package/src/runtime/AGENTS.md +2 -0
  321. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  322. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  323. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  324. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  325. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  326. package/src/runtime/auth/same-actor.ts +38 -3
  327. package/src/runtime/channel-send.ts +316 -0
  328. package/src/runtime/http-server.ts +160 -151
  329. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  330. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  331. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  332. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  333. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  334. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  335. package/src/runtime/routes/acp-routes.test.ts +15 -0
  336. package/src/runtime/routes/acp-routes.ts +72 -9
  337. package/src/runtime/routes/app-management-routes.ts +73 -0
  338. package/src/runtime/routes/app-routes.ts +1 -1
  339. package/src/runtime/routes/channel-send-routes.ts +85 -0
  340. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  341. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  342. package/src/runtime/routes/credential-routes.ts +19 -10
  343. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  344. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  345. package/src/runtime/routes/diagnostics-routes.ts +55 -3
  346. package/src/runtime/routes/index.ts +4 -0
  347. package/src/runtime/routes/log-export-routes.ts +2 -0
  348. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  349. package/src/runtime/routes/migration-routes.ts +8 -21
  350. package/src/runtime/routes/platform-routes.ts +18 -15
  351. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  352. package/src/runtime/services/conversation-serializer.ts +16 -3
  353. package/src/runtime/sync/resource-sync-events.ts +2 -1
  354. package/src/schedule/worker.ts +1 -1
  355. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  356. package/src/security/ces-http-record-backend.ts +120 -0
  357. package/src/security/ces-rpc-record-backend.ts +7 -0
  358. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  359. package/src/skills/workspace-skill-md-signature.ts +50 -0
  360. package/src/subagent/consult-context.ts +12 -25
  361. package/src/tools/acp/list-agents.test.ts +3 -1
  362. package/src/tools/acp/spawn.test.ts +146 -11
  363. package/src/tools/acp/spawn.ts +21 -3
  364. package/src/tools/acp/status.test.ts +94 -0
  365. package/src/tools/acp/status.ts +40 -3
  366. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  367. package/src/tools/capability-offer.test.ts +22 -2
  368. package/src/tools/capability-offer.ts +16 -1
  369. package/src/tools/computer-use/definitions.ts +4 -2
  370. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  371. package/src/tools/credentials/metadata-store.ts +100 -0
  372. package/src/tools/host-filesystem/edit.test.ts +2 -1
  373. package/src/tools/host-filesystem/edit.ts +2 -2
  374. package/src/tools/host-filesystem/read.test.ts +2 -1
  375. package/src/tools/host-filesystem/read.ts +2 -2
  376. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  377. package/src/tools/host-filesystem/transfer.ts +2 -2
  378. package/src/tools/host-filesystem/write.test.ts +2 -1
  379. package/src/tools/host-filesystem/write.ts +2 -2
  380. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  381. package/src/tools/host-terminal/host-shell.ts +4 -20
  382. package/src/tools/policy-context.ts +6 -7
  383. package/src/tools/subagent/spawn.ts +5 -8
  384. package/src/tools/terminal/__tests__/safe-env.test.ts +6 -6
  385. package/src/tools/terminal/safe-env.ts +23 -3
  386. package/src/tools/watch/watch-retro-report.ts +0 -8
  387. package/src/util/__tests__/language-subtag.test.ts +23 -1
  388. package/src/util/ca-bundle.ts +13 -0
  389. package/src/util/language-subtag.ts +16 -0
  390. package/src/util/platform.ts +8 -0
  391. package/src/workspace/git-porcelain.ts +66 -0
  392. package/src/workspace/git-service.ts +5 -54
  393. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  394. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  395. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  396. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  397. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  398. package/src/workspace/migrations/registry.ts +6 -0
  399. package/src/__tests__/app-source-watcher.test.ts +0 -218
  400. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  401. package/src/__tests__/speaker-identification.test.ts +0 -56
  402. package/src/calls/speaker-identification.ts +0 -233
  403. package/src/daemon/app-source-watcher.ts +0 -218
  404. package/src/messaging/providers/whatsapp/client.ts +0 -29
  405. package/src/util/fs-watcher-error.ts +0 -36
  406. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -5,6 +5,7 @@
5
5
  import { beforeEach, describe, expect, mock, test } from "bun:test";
6
6
 
7
7
  import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
8
+ import { MESSAGE_KEYS } from "../../i18n/index.js";
8
9
  import type { AttentionState } from "../../persistence/conversation-attention-store.js";
9
10
  import type {
10
11
  ConversationRow,
@@ -589,6 +590,60 @@ describe("emitAssistantReplyNotification", () => {
589
590
  });
590
591
  });
591
592
 
593
+ // A conversation whose title is still being written stores the message key
594
+ // itself, which is a non-empty string the sanitizer has no reason to reject.
595
+ // Sending it would put a raw key on the lock screen, so it counts as absent.
596
+ test("omits requestedTitle while the title is still generating", async () => {
597
+ conversationRow = makeConversation({
598
+ title: MESSAGE_KEYS.CONVERSATION_TITLE_GENERATING,
599
+ });
600
+
601
+ await run();
602
+
603
+ expect(emitCalls).toHaveLength(1);
604
+ expect(emitCalls[0].contextPayload).toEqual({
605
+ requestedMessage: "Sure, here is the plan.",
606
+ });
607
+ });
608
+
609
+ test("omits requestedTitle for the legacy generating placeholder", async () => {
610
+ conversationRow = makeConversation({ title: "Generating title..." });
611
+
612
+ await run();
613
+
614
+ expect(emitCalls).toHaveLength(1);
615
+ expect(emitCalls[0].contextPayload).toEqual({
616
+ requestedMessage: "Sure, here is the plan.",
617
+ });
618
+ });
619
+
620
+ test("omits requestedTitle for the untitled placeholder key", async () => {
621
+ conversationRow = makeConversation({
622
+ title: MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED,
623
+ });
624
+
625
+ await run();
626
+
627
+ expect(emitCalls).toHaveLength(1);
628
+ expect(emitCalls[0].contextPayload).toEqual({
629
+ requestedMessage: "Sure, here is the plan.",
630
+ });
631
+ });
632
+
633
+ // Only an exact key is a system constant. A title the user or the model
634
+ // wrote that happens to contain one is their copy and travels as written.
635
+ test("keeps a title that merely contains a message key", async () => {
636
+ conversationRow = makeConversation({
637
+ title: `notes on ${MESSAGE_KEYS.CONVERSATION_TITLE_GENERATING}`,
638
+ });
639
+
640
+ await run();
641
+
642
+ expect(emitCalls[0].contextPayload.requestedTitle).toBe(
643
+ "notes on conversation.title.generating",
644
+ );
645
+ });
646
+
592
647
  test("caps the preview at 200 chars", async () => {
593
648
  assistantRow = makeAssistantRow([
594
649
  { type: "text", text: "a".repeat(300) },
@@ -26,6 +26,15 @@ const messageRewrites: Array<{ messageId: string; content: string }> = [];
26
26
  /** messageId -> the conversation it belongs to, for the scoped lookup. */
27
27
  const messageOwners = new Map<string, string>();
28
28
  let conversationRow: { conversationType: string } | null = null;
29
+ /**
30
+ * Rows for specific ids, consulted before the catch-all `conversationRow`,
31
+ * so a test can hand the signal's source and the delivery's paired
32
+ * conversation different shapes.
33
+ */
34
+ const conversationRowsById = new Map<
35
+ string,
36
+ { conversationType: string; source?: string }
37
+ >();
29
38
  let conversationLookupShouldThrow = false;
30
39
  let messageAppendShouldThrow = false;
31
40
  let messageRewriteShouldThrow = false;
@@ -71,7 +80,7 @@ mock.module("../../persistence/conversation-crud.js", () => ({
71
80
  if (conversationLookupShouldThrow) {
72
81
  throw new Error("simulated conversation lookup failure");
73
82
  }
74
- return conversationRow;
83
+ return conversationRowsById.get(id) ?? conversationRow;
75
84
  },
76
85
  addMessage: async (
77
86
  conversationId: string,
@@ -164,6 +173,7 @@ beforeEach(() => {
164
173
  messageRewrites.length = 0;
165
174
  messageOwners.clear();
166
175
  conversationRow = null;
176
+ conversationRowsById.clear();
167
177
  conversationLookupShouldThrow = false;
168
178
  messageAppendShouldThrow = false;
169
179
  messageRewriteShouldThrow = false;
@@ -258,7 +268,11 @@ describe("writeHomeFeedItemForSignal", () => {
258
268
  },
259
269
  });
260
270
 
261
- const item = await writeHomeFeedItemForSignal(signal, decision);
271
+ const item = await writeHomeFeedItemForSignal(
272
+ signal,
273
+ decision,
274
+ makeVellumDelivery(),
275
+ );
262
276
 
263
277
  expect(item).not.toBeNull();
264
278
  expect(appendCalls).toHaveLength(1);
@@ -267,7 +281,10 @@ describe("writeHomeFeedItemForSignal", () => {
267
281
  "The deployment completed successfully.",
268
282
  );
269
283
  expect(appendCalls[0]!.conversationId).toBe("conv-source-1");
270
- expect(conversationLookups).toEqual(["conv-source-1"]);
284
+ expect(appendCalls[0]!.metadata?.notificationConversationMessageId).toBeUndefined();
285
+ expect(messageAppends).toEqual([]);
286
+ expect(messagesInvalidated).toEqual([]);
287
+ expect(conversationLookups).toEqual(["conv-source-1", "conv-source-1"]);
271
288
  });
272
289
 
273
290
  test("isAsyncBackground hint writes even when sourceContextId does not resolve", async () => {
@@ -402,6 +419,139 @@ describe("writeHomeFeedItemForSignal", () => {
402
419
  expect(appendCalls[0]!.conversationId).toBe("paired-delivery-conv-id");
403
420
  });
404
421
 
422
+ describe("assistant-initiated thread deliveries", () => {
423
+ // Under the assistant-initiated-threads flag, conversation pairing
424
+ // promotes a background assistant.share into a standard conversation
425
+ // stamped `assistant_initiated`, which the sidebar's assistant section
426
+ // renders. That thread is the share's surface; a feed item for the
427
+ // same signal put it in the bell a second time.
428
+ const assistantThreadDelivery = () =>
429
+ makeVellumDelivery({ conversationId: "assistant-thread-1" });
430
+
431
+ function stampAssistantThread(): void {
432
+ conversationRowsById.set("assistant-thread-1", {
433
+ conversationType: "standard",
434
+ source: "assistant_initiated",
435
+ });
436
+ }
437
+
438
+ function shareFromHeartbeat(): NotificationSignal {
439
+ return makeSignal({
440
+ sourceChannel: "assistant_tool",
441
+ sourceEventName: "assistant.share",
442
+ sourceContextId: "conv-source-1",
443
+ contextPayload: { title: "Doctor checklist built" },
444
+ });
445
+ }
446
+
447
+ const shareDecision = () =>
448
+ makeDecision({
449
+ selectedChannels: ["vellum"],
450
+ renderedCopy: {
451
+ vellum: {
452
+ title: "Doctor checklist built",
453
+ body: "Your doctor-visit one-pager is ready.",
454
+ },
455
+ },
456
+ });
457
+
458
+ test("a background share promoted into an assistant thread does not mirror", async () => {
459
+ conversationRow = { conversationType: "background" };
460
+ stampAssistantThread();
461
+
462
+ const item = await writeHomeFeedItemForSignal(
463
+ shareFromHeartbeat(),
464
+ shareDecision(),
465
+ assistantThreadDelivery(),
466
+ );
467
+
468
+ expect(item).toBeNull();
469
+ expect(appendCalls).toHaveLength(0);
470
+ expect(messageAppends).toHaveLength(0);
471
+ });
472
+
473
+ test("the async-background hint does not override the assistant-thread suppression", async () => {
474
+ conversationRow = null;
475
+ stampAssistantThread();
476
+ const signal = makeSignal({
477
+ sourceChannel: "assistant_tool",
478
+ sourceEventName: "assistant.share",
479
+ sourceContextId: "heartbeat",
480
+ contextPayload: { title: "Bilbao solo trip is planned" },
481
+ attentionHints: {
482
+ requiresAction: false,
483
+ urgency: "medium",
484
+ isAsyncBackground: true,
485
+ visibleInSourceNow: false,
486
+ },
487
+ });
488
+
489
+ const item = await writeHomeFeedItemForSignal(
490
+ signal,
491
+ shareDecision(),
492
+ assistantThreadDelivery(),
493
+ );
494
+
495
+ expect(item).toBeNull();
496
+ expect(appendCalls).toHaveLength(0);
497
+ });
498
+
499
+ test("a share paired with an ordinary notification thread still mirrors", async () => {
500
+ // Flag off, or any producer that did not promote: pairing writes a
501
+ // `notification`-sourced row, which stays in Chats and the bell.
502
+ conversationRow = { conversationType: "background" };
503
+ conversationRowsById.set("assistant-thread-1", {
504
+ conversationType: "standard",
505
+ source: "notification",
506
+ });
507
+
508
+ const item = await writeHomeFeedItemForSignal(
509
+ shareFromHeartbeat(),
510
+ shareDecision(),
511
+ assistantThreadDelivery(),
512
+ );
513
+
514
+ expect(item).not.toBeNull();
515
+ expect(appendCalls).toHaveLength(1);
516
+ });
517
+
518
+ test("a guardian request keeps its bell item even when paired with an assistant thread", async () => {
519
+ // The bell is the request's canonical home: nothing else renders the
520
+ // pending card, so the assistant-thread suppression must not reach it.
521
+ conversationRow = { conversationType: "standard" };
522
+ stampAssistantThread();
523
+ const signal = makeSignal({
524
+ sourceChannel: "assistant_tool",
525
+ sourceEventName: "guardian.question",
526
+ contextPayload: {
527
+ title: "Approve this?",
528
+ questionText: "May I send the email?",
529
+ requestId: "req-1",
530
+ },
531
+ attentionHints: {
532
+ requiresAction: true,
533
+ urgency: "medium",
534
+ isAsyncBackground: false,
535
+ visibleInSourceNow: false,
536
+ },
537
+ });
538
+
539
+ const item = await writeHomeFeedItemForSignal(
540
+ signal,
541
+ makeDecision({
542
+ selectedChannels: ["vellum"],
543
+ renderedCopy: {
544
+ vellum: { title: "Approve this?", body: "May I send the email?" },
545
+ },
546
+ }),
547
+ assistantThreadDelivery(),
548
+ );
549
+
550
+ expect(item).not.toBeNull();
551
+ expect(appendCalls).toHaveLength(1);
552
+ });
553
+ });
554
+
405
555
  test("source conversation id wins over the paired delivery fallback when both are available", async () => {
406
556
  // When the producer's `sourceContextId` already points at a real
407
557
  // conversation (the canonical "where the work happened"), prefer it
@@ -22,6 +22,7 @@ import {
22
22
  getMessageById,
23
23
  parseMessageMetadata,
24
24
  } from "../persistence/conversation-crud.js";
25
+ import { isReplaceableTitle } from "../persistence/conversation-title-placeholders.js";
25
26
  import {
26
27
  isDesktopOriginatedUserMessage,
27
28
  isReplyPushIneligibleUserMessage,
@@ -247,9 +248,13 @@ export async function emitAssistantReplyNotification(params: {
247
248
  // lock screen. Absent `requestedTitle` lets the decision branch derive a
248
249
  // title from the body, which reads better than an empty or placeholder
249
250
  // conversation title.
250
- const requestedTitle = sanitizeNotificationTitle(
251
- flattenTitleWhitespace(conversation.title ?? ""),
252
- );
251
+ //
252
+ // Placeholder titles are plausible non-empty strings and survive
253
+ // sanitizing. They count as absent so the body supplies the title instead.
254
+ const storedTitle = conversation.title?.trim() ?? "";
255
+ const requestedTitle = isReplaceableTitle(storedTitle)
256
+ ? ""
257
+ : sanitizeNotificationTitle(flattenTitleWhitespace(storedTitle));
253
258
 
254
259
  // Read as close to the emit as possible: nothing short-circuits on it.
255
260
  // Presence only speaks for a turn the desktop itself opened, on that row's
@@ -130,6 +130,7 @@ export interface PairingOptions {
130
130
  * transactional request or a system alert.
131
131
  */
132
132
  const ASSISTANT_SHARE_EVENT = "assistant.share";
133
+ const ASSISTANT_REPLY_EVENT = "chat.assistant_reply";
133
134
 
134
135
  /**
135
136
  * Promote a background share into an assistant-initiated thread, under the
@@ -263,7 +264,25 @@ export async function pairDeliveryWithConversation(
263
264
  // notification can appear. The home feed aims its "Go to Conversation"
264
265
  // button at the same row whenever it mirrors the signal.
265
266
  //
266
- // So this is deliberately not gated on home-feed eligibility: a signal the
267
+ // `chat.assistant_reply` already has its complete reply in that transcript.
268
+ // Its notification body is a lock-screen preview, so appending it would
269
+ // create a second, truncated assistant row. Keep the conversation target
270
+ // for deep links without writing the preview into the transcript.
271
+ if (
272
+ strategy === "start_new_conversation" &&
273
+ !signal.requiresConversation &&
274
+ signal.sourceEventName === ASSISTANT_REPLY_EVENT
275
+ ) {
276
+ return {
277
+ conversationId: resolveSourceConversationId(signal),
278
+ messageId: null,
279
+ strategy,
280
+ createdNewConversation: false,
281
+ conversationFallbackUsed: false,
282
+ };
283
+ }
284
+
285
+ // This is deliberately not gated on home-feed eligibility: a signal the
267
286
  // feed declines to mirror still reaches the user through the banner, and
268
287
  // this row is what makes that landing honest.
269
288
  if (strategy === "start_new_conversation" && !signal.requiresConversation) {
@@ -748,44 +767,45 @@ async function resolveChannelDeliveryHome(params: {
748
767
  * Indexing is skipped for parity with the other notification write paths:
749
768
  * notification copy is delivery audit, not conversational memory.
750
769
  */
751
- async function appendBodyToSourceConversation(
752
- signal: NotificationSignal,
753
- channel: NotificationChannel,
754
- messageContent: string,
755
- ): Promise<{ conversationId: string; messageId: string } | null> {
756
- const sourceContextId = signal.sourceContextId;
757
- if (!sourceContextId) {
770
+ function resolveSourceConversationId(signal: NotificationSignal): string | null {
771
+ if (!signal.sourceContextId) {
758
772
  return null;
759
773
  }
760
-
761
- let existing: ReturnType<typeof getConversation>;
762
774
  try {
763
- existing = getConversation(sourceContextId);
775
+ return getConversation(signal.sourceContextId)?.id ?? null;
764
776
  } catch {
765
777
  return null;
766
778
  }
767
- if (!existing) {
779
+ }
780
+
781
+ async function appendBodyToSourceConversation(
782
+ signal: NotificationSignal,
783
+ channel: NotificationChannel,
784
+ messageContent: string,
785
+ ): Promise<{ conversationId: string; messageId: string } | null> {
786
+ const conversationId = resolveSourceConversationId(signal);
787
+ if (!conversationId) {
768
788
  return null;
769
789
  }
770
790
 
771
- const message = await addMessage(existing.id, "assistant", messageContent, {
791
+ const message = await addMessage(conversationId, "assistant", messageContent, {
772
792
  skipIndexing: true,
773
793
  });
774
794
  // `addMessage` projects attention metadata alone, so a client with this
775
795
  // conversation open needs the messages tag to refetch the transcript. A
776
796
  // notification the user taps through to has every chance of landing on an
777
797
  // already-open conversation.
778
- publishConversationMessagesChanged(existing.id);
798
+ publishConversationMessagesChanged(conversationId);
779
799
 
780
800
  log.info(
781
801
  {
782
802
  signalId: signal.signalId,
783
803
  channel,
784
- conversationId: existing.id,
804
+ conversationId,
785
805
  messageId: message.id,
786
806
  },
787
807
  "Appended notification body to producing conversation",
788
808
  );
789
809
 
790
- return { conversationId: existing.id, messageId: message.id };
810
+ return { conversationId, messageId: message.id };
791
811
  }
@@ -39,6 +39,12 @@ export interface DeliveredChannelPost {
39
39
  text: string;
40
40
  /** The message id the channel assigned when it acknowledged the post. */
41
41
  providerMessageId: string;
42
+ /**
43
+ * The ids of the further posts the same text became when the channel
44
+ * split it, in send order. Each is reconciled onto the row like the first,
45
+ * so a reaction, edit, or delete naming any chunk resolves to this row.
46
+ */
47
+ additionalProviderMessageIds?: readonly string[];
42
48
  /**
43
49
  * The conversation whose turn made the post, when a different one from
44
50
  * the home it is recorded in (the messaging tool sending from a scheduled
@@ -75,7 +81,11 @@ export async function recordDeliveredChannelPost(
75
81
  : {}),
76
82
  },
77
83
  });
78
- await makeSentMessageIdReconciler(row.id)(post.providerMessageId);
84
+ const reconcile = makeSentMessageIdReconciler(row.id);
85
+ await reconcile(post.providerMessageId);
86
+ for (const id of post.additionalProviderMessageIds ?? []) {
87
+ await reconcile(id);
88
+ }
79
89
  // A resident conversation reloads its history on the next turn so the
80
90
  // post is in context; the client refetches the transcript.
81
91
  findConversation(post.conversationId)?.markHistoryStale();
@@ -23,7 +23,10 @@ import {
23
23
  getMessageById,
24
24
  updateMessageContent,
25
25
  } from "../persistence/conversation-crud.js";
26
- import { isBackgroundConversationType } from "../persistence/conversation-types.js";
26
+ import {
27
+ ASSISTANT_INITIATED_SOURCE,
28
+ isBackgroundConversationType,
29
+ } from "../persistence/conversation-types.js";
27
30
  import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
28
31
  import { getLogger } from "../util/logger.js";
29
32
  import { normalizeTitle, stripMarkdown } from "../util/short-title.js";
@@ -272,6 +275,11 @@ async function resolveOwnedConversationMessageId(
272
275
  sourceConversationId: string | undefined,
273
276
  summary: string,
274
277
  ): Promise<string | undefined> {
278
+ // The completed reply is already the source conversation's canonical row.
279
+ // This signal's body is a compact push preview, not conversation content.
280
+ if (signal.sourceEventName === "chat.assistant_reply") {
281
+ return undefined;
282
+ }
275
283
  if (vellumDelivery?.conversationId) {
276
284
  return vellumDelivery.conversationId === sourceConversationId
277
285
  ? vellumDelivery.messageId
@@ -495,6 +503,18 @@ function deriveDetailPanelKind(
495
503
  * `isAsyncBackground` hint. `chat.assistant_reply` also mirrors: it is the
496
504
  * durable in-app record for the push sent after a user leaves a chat, while
497
505
  * retaining the normal interactive conversation as its navigation target.
506
+ *
507
+ * A delivery that materialized an assistant-initiated thread never mirrors.
508
+ * Under the `assistant-initiated-threads` flag, `conversation-pairing.ts`
509
+ * promotes a background `assistant.share` into a standard conversation
510
+ * stamped {@link ASSISTANT_INITIATED_SOURCE}, and that thread's row in the
511
+ * sidebar's assistant section is the share's one surface. Mirroring it here
512
+ * as well put the same share in the bell a second time, labeled by its
513
+ * source channel ("Heartbeat"). The check reads the paired conversation's
514
+ * `source` rather than re-deriving the promotion rule, so it holds however
515
+ * that rule narrows or widens. Guardian requests are checked first on
516
+ * purpose: the bell is their canonical home whatever conversation they pair
517
+ * with.
498
518
  */
499
519
  function resolveHomeFeedMirror(
500
520
  signal: NotificationSignal,
@@ -525,6 +545,15 @@ function resolveHomeFeedMirror(
525
545
  : fallbackConversationId;
526
546
  const sourceScheduleJobId = sourceRow?.scheduleJobId ?? undefined;
527
547
 
548
+ // Guardian requests always project into the feed: the "Needs
549
+ // attention" item is the request's canonical home, and the request
550
+ // blocks on the guardian whatever kind of conversation raised it.
551
+ if (isGuardianRequestSignalEvent(signal.sourceEventName)) {
552
+ return { mirror: true, sourceConversationId, sourceScheduleJobId };
553
+ }
554
+ if (isAssistantInitiatedThreadDelivery(fallbackConversationId)) {
555
+ return { mirror: false };
556
+ }
528
557
  if (
529
558
  signal.sourceChannel === "assistant_tool" ||
530
559
  signal.sourceEventName === "chat.assistant_reply"
@@ -534,18 +563,37 @@ function resolveHomeFeedMirror(
534
563
  if (signal.attentionHints.isAsyncBackground) {
535
564
  return { mirror: true, sourceConversationId, sourceScheduleJobId };
536
565
  }
537
- // Guardian requests always project into the feed: the "Needs
538
- // attention" item is the request's canonical home, and the request
539
- // blocks on the guardian whatever kind of conversation raised it.
540
- if (isGuardianRequestSignalEvent(signal.sourceEventName)) {
541
- return { mirror: true, sourceConversationId, sourceScheduleJobId };
542
- }
543
566
  if (isBackgroundConversationType(sourceRow?.conversationType)) {
544
567
  return { mirror: true, sourceConversationId, sourceScheduleJobId };
545
568
  }
546
569
  return { mirror: false };
547
570
  }
548
571
 
572
+ /**
573
+ * Whether the vellum delivery this signal produced landed in a thread the
574
+ * sidebar's assistant section already shows.
575
+ *
576
+ * Best-effort like the source lookup above: a missing id, a missing row, or
577
+ * a lookup failure all read as "not an assistant thread", so a storage
578
+ * hiccup degrades to the pre-existing behavior (a bell row) rather than
579
+ * dropping the notification everywhere.
580
+ */
581
+ function isAssistantInitiatedThreadDelivery(
582
+ deliveryConversationId: string | undefined,
583
+ ): boolean {
584
+ if (!deliveryConversationId) {
585
+ return false;
586
+ }
587
+ try {
588
+ return (
589
+ getConversation(deliveryConversationId)?.source ===
590
+ ASSISTANT_INITIATED_SOURCE
591
+ );
592
+ } catch {
593
+ return false;
594
+ }
595
+ }
596
+
549
597
  /**
550
598
  * Routing-intent enforcement can prune `selectedChannels` without also
551
599
  * pruning `renderedCopy`, so iterating `renderedCopy` directly risks
@@ -2,6 +2,8 @@ import { X509Certificate } from "node:crypto";
2
2
  import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
 
5
+ import { writeCombinedCABundle } from "../util/ca-bundle.js";
6
+
5
7
  const CA_CERT_FILENAME = "ca.pem";
6
8
  const CA_KEY_FILENAME = "ca-key.pem";
7
9
  const COMBINED_CA_FILENAME = "combined-ca-bundle.pem";
@@ -263,11 +265,7 @@ export async function ensureCombinedCABundle(
263
265
  }
264
266
 
265
267
  try {
266
- const [systemCAs, proxyCACert] = await Promise.all([
267
- readFile(systemBundlePath, "utf-8"),
268
- readFile(caCertPath, "utf-8"),
269
- ]);
270
- await writeFile(combinedPath, systemCAs + "\n" + proxyCACert);
268
+ await writeCombinedCABundle(systemBundlePath, caCertPath, combinedPath);
271
269
  return combinedPath;
272
270
  } catch {
273
271
  return null;
@@ -15,13 +15,16 @@ mock.module("../config/assistant-feature-flags.js", () => ({
15
15
  }));
16
16
 
17
17
  // `buildPolicyContext` (used by the integration tests below) precomputes the
18
- // proc-to-skills gate via `isV3TierActive`. Drive it through this slot so a
19
- // test can put the production threading path in the active / inactive state.
18
+ // retrospective skill-improvement gate. Keep the v3 tier active while the
19
+ // dedicated gate changes so the production threading test covers the new
20
+ // opt-out rather than the old v3 condition.
20
21
  let mockV3TierActive = true;
22
+ let mockSkillImprovementActive = true;
21
23
  mock.module("../config/memory-v3-gate.js", () => ({
22
24
  isMemoryEnabled: (config?: { memory?: { enabled?: boolean } }) =>
23
25
  config?.memory?.enabled !== false,
24
26
  isV3TierActive: () => mockV3TierActive,
27
+ isSkillImprovementActive: () => mockSkillImprovementActive,
25
28
  isMemoryV3Live: () => mockV3TierActive,
26
29
  usesConceptPageMemory: (memory?: {
27
30
  enabled?: boolean;
@@ -219,6 +222,7 @@ describe("Permission Checker (gateway IPC)", () => {
219
222
  mockCachedThreshold = "low";
220
223
  mockRefreshedThreshold = null;
221
224
  mockV3TierActive = true;
225
+ mockSkillImprovementActive = true;
222
226
  thresholdCallLog.length = 0;
223
227
  mockSkillCatalog = [];
224
228
  mockResolvedSkill = null;
@@ -1173,10 +1177,11 @@ describe("Permission Checker (gateway IPC)", () => {
1173
1177
  });
1174
1178
 
1175
1179
  test("grant does NOT fire when proc-to-skills is inactive, even for the retrospective turn", async () => {
1176
- // Same retrospective ToolContext, but the feature is inactive (flag off
1177
- // or v3 not live). buildPolicyContext stamps `procToSkillsActive: false`,
1178
- // so the grant is dead and the high-risk scaffold prompts.
1179
- mockV3TierActive = false;
1180
+ // The retrospective keeps its live v3 tier, but this independent flag is
1181
+ // off. buildPolicyContext stamps `procToSkillsActive: false`, so the
1182
+ // grant is dead and the high-risk scaffold prompts.
1183
+ mockV3TierActive = true;
1184
+ mockSkillImprovementActive = false;
1180
1185
  mockIpcClassifyRiskResult = {
1181
1186
  risk: "high",
1182
1187
  reason: "Skill scaffold",
@@ -26,6 +26,7 @@
26
26
  */
27
27
  import type { AgentEvent } from "../agent/loop.js";
28
28
  import { getConfig } from "../config/loader.js";
29
+ import { resolvePlatformAssistantIdOrNull } from "../config/platform-identity.js";
29
30
  import type { CompactionLogsClickHouseConfig } from "../config/schemas/compaction-logs.js";
30
31
  import { credentialKey } from "../security/credential-key.js";
31
32
  import { getSecureKeyAsync } from "../security/secure-keys.js";
@@ -136,7 +137,7 @@ export interface ClickHouseCompactionLogStoreDeps {
136
137
  resolveUrl?: () => Promise<string | null>;
137
138
  /** Override the credential read for `clickhouse:password`. */
138
139
  resolvePassword?: () => Promise<string | null>;
139
- /** Override the credential read for `vellum:platform_assistant_id`. */
140
+ /** Override the platform assistant id (in-memory identity, then vault). */
140
141
  resolveAssistantId?: () => Promise<string | null>;
141
142
  /** Override fetch for testing. */
142
143
  fetchImpl?: CompactionLogFetch;
@@ -163,8 +164,7 @@ export class ClickHouseCompactionLogStore {
163
164
  deps.resolvePassword ??
164
165
  (() => readCredentialOrNull("clickhouse", "password"));
165
166
  this.resolveAssistantId =
166
- deps.resolveAssistantId ??
167
- (() => readCredentialOrNull("vellum", "platform_assistant_id"));
167
+ deps.resolveAssistantId ?? resolvePlatformAssistantIdOrNull;
168
168
  this.fetchImpl = deps.fetchImpl ?? globalThis.fetch.bind(globalThis);
169
169
  }
170
170