@vellumai/assistant 0.12.1 → 0.12.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -55,6 +55,13 @@ const SCHEDULE_ID = "sched-1";
55
55
  const RUN_ID = "run-1";
56
56
  const RUN_STARTED_AT = 1700000000000;
57
57
 
58
+ /**
59
+ * The synthetic user rows carrying each tool call's result. A delivery counts
60
+ * only when its call reported success, so a case that wants suppression has to
61
+ * supply the matching successful result.
62
+ */
63
+ let toolResultRows: MessageRow[] = [];
64
+
58
65
  const realCrud = await import("../../persistence/conversation-crud.js");
59
66
  mock.module("../../persistence/conversation-crud.js", () => ({
60
67
  ...realCrud,
@@ -66,6 +73,7 @@ mock.module("../../persistence/conversation-crud.js", () => ({
66
73
  messageId === ASSISTANT_MESSAGE_ID
67
74
  ? [...turnRows.map((row) => row.id), ASSISTANT_MESSAGE_ID]
68
75
  : [messageId],
76
+ getMessagesAfter: () => toolResultRows,
69
77
  }));
70
78
 
71
79
  const realAttentionStore =
@@ -114,6 +122,25 @@ function makeToolCallRow(
114
122
  );
115
123
  }
116
124
 
125
+ /** The executor's verdict on the call `makeToolCallRow(name, ...)` made. */
126
+ function makeToolResultRow(name: string, isError = false): MessageRow {
127
+ return makeAssistantRow(
128
+ [
129
+ {
130
+ type: "tool_result",
131
+ tool_use_id: `tu-${name}`,
132
+ content: isError ? "failed" : "ok",
133
+ ...(isError ? { is_error: true } : {}),
134
+ },
135
+ ] as ContentBlock[],
136
+ {
137
+ id: `res-${name}`,
138
+ role: "user",
139
+ createdAt: RUN_STARTED_AT + 150,
140
+ },
141
+ );
142
+ }
143
+
117
144
  function makeAttentionState(
118
145
  overrides: Partial<AttentionState> = {},
119
146
  ): AttentionState {
@@ -189,6 +216,7 @@ beforeEach(() => {
189
216
  notifiedProbeArgs.length = 0;
190
217
  alreadyNotified = false;
191
218
  turnRows = [];
219
+ toolResultRows = [];
192
220
  attentionState = makeAttentionState();
193
221
  assistantRow = makeAssistantRow([
194
222
  { type: "text", text: "**3 new emails** and one calendar change." },
@@ -245,7 +273,8 @@ describe("emitScheduleResultNotification", () => {
245
273
 
246
274
  test("stays silent when the run delivered by email through messaging_send", async () => {
247
275
  // The schedule skill's prescribed route for rich content. It writes no
248
- // notification event, so only the tool call shows the user has the result.
276
+ // notification event, so the successful tool call is what shows the user
277
+ // has the result.
249
278
  turnRows = [
250
279
  makeToolCallRow("messaging_send", {
251
280
  platform: "gmail",
@@ -253,6 +282,7 @@ describe("emitScheduleResultNotification", () => {
253
282
  message: "Inbox digest…",
254
283
  }),
255
284
  ];
285
+ toolResultRows = [makeToolResultRow("messaging_send")];
256
286
  assistantRow = makeAssistantRow([
257
287
  { type: "text", text: "Sent the digest to your inbox." },
258
288
  ] as ContentBlock[]);
@@ -262,20 +292,86 @@ describe("emitScheduleResultNotification", () => {
262
292
  expect(emitCalls).toHaveLength(0);
263
293
  });
264
294
 
265
- test("stays silent when the run posted to Slack through chat.postMessage", async () => {
295
+ test("stays silent when the run posted to a channel through messaging_send", async () => {
296
+ turnRows = [
297
+ makeToolCallRow("messaging_send", {
298
+ platform: "slack",
299
+ conversation_id: "C0123456789",
300
+ text: "the digest",
301
+ }),
302
+ ];
303
+ toolResultRows = [makeToolResultRow("messaging_send")];
304
+ assistantRow = makeAssistantRow([
305
+ { type: "text", text: "Posted the digest to #general." },
306
+ ] as ContentBlock[]);
307
+
308
+ await run();
309
+
310
+ expect(emitCalls).toHaveLength(0);
311
+ });
312
+
313
+ test("notifies when the channel send failed, so the run is not silent", async () => {
314
+ // The failure this guard exists for: the post never landed, the run said
315
+ // so in a conversation nobody has open, and suppressing on the attempt
316
+ // would cost the user the digest and the explanation both.
317
+ turnRows = [
318
+ makeToolCallRow("messaging_send", {
319
+ platform: "slack",
320
+ conversation_id: "C0123456789",
321
+ text: "the digest",
322
+ }),
323
+ ];
324
+ toolResultRows = [makeToolResultRow("messaging_send", true)];
325
+ assistantRow = makeAssistantRow([
326
+ { type: "text", text: "I could not post the digest to #general." },
327
+ ] as ContentBlock[]);
328
+
329
+ await run();
330
+
331
+ expect(emitCalls).toHaveLength(1);
332
+ expect(emitCalls[0].contextPayload.requestedMessage).toBe(
333
+ "I could not post the digest to #general.",
334
+ );
335
+ });
336
+
337
+ test("notifies when a delivery call left no result at all", async () => {
338
+ // An interrupted turn persists the call without its verdict. Unproven is
339
+ // not delivered.
340
+ turnRows = [
341
+ makeToolCallRow("messaging_send", {
342
+ platform: "slack",
343
+ conversation_id: "C0123456789",
344
+ text: "the digest",
345
+ }),
346
+ ];
347
+ toolResultRows = [];
348
+ assistantRow = makeAssistantRow([
349
+ { type: "text", text: "Here is the digest." },
350
+ ] as ContentBlock[]);
351
+
352
+ await run();
353
+
354
+ expect(emitCalls).toHaveLength(1);
355
+ });
356
+
357
+ test("notifies after a Web API post, whose success cannot prove Slack accepted it", async () => {
358
+ // Slack refuses a call with HTTP 200 and `ok: false`, so a successful bash
359
+ // result is no evidence the post landed. This is the best case the route
360
+ // can produce, and it still gets the fallback.
266
361
  turnRows = [
267
362
  makeToolCallRow("bash", {
268
363
  command:
269
364
  'assistant oauth request --provider slack_channel /chat.postMessage --json \'{"channel":"C1","text":"digest"}\'',
270
365
  }),
271
366
  ];
367
+ toolResultRows = [makeToolResultRow("bash")];
272
368
  assistantRow = makeAssistantRow([
273
369
  { type: "text", text: "Posted the digest to #general." },
274
370
  ] as ContentBlock[]);
275
371
 
276
372
  await run();
277
373
 
278
- expect(emitCalls).toHaveLength(0);
374
+ expect(emitCalls).toHaveLength(1);
279
375
  });
280
376
 
281
377
  test("still notifies when the run's tool calls were not deliveries", async () => {
@@ -20,6 +20,7 @@
20
20
 
21
21
  import type { AssistantEvent } from "../../api/index.js";
22
22
  import type { InterfaceId } from "../../channels/types.js";
23
+ import { getAssistantName } from "../../daemon/identity-helpers.js";
23
24
  import { updateMessageContent } from "../../persistence/conversation-crud.js";
24
25
  import { publishConversationMessagesChanged } from "../../runtime/sync/resource-sync-events.js";
25
26
  import { getLogger } from "../../util/logger.js";
@@ -101,12 +102,14 @@ export class VellumAdapter implements ChannelAdapter {
101
102
 
102
103
  const silent =
103
104
  payload.urgency !== "high" && payload.urgency !== "critical";
105
+ const assistantName = getAssistantName()?.trim() || undefined;
104
106
 
105
107
  this.broadcast({
106
108
  type: "notification_intent",
107
109
  deliveryId: payload.deliveryId,
108
110
  correlationId: payload.correlationId,
109
111
  sourceEventName: payload.sourceEventName,
112
+ ...(assistantName ? { assistantName } : {}),
110
113
  title: payload.copy.title,
111
114
  body: payload.copy.body,
112
115
  deepLinkMetadata: payload.deepLinkTarget,
@@ -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();
@@ -31,6 +31,10 @@ import {
31
31
  patchFeedItemContent,
32
32
  readHomeFeed,
33
33
  } from "../home/feed-writer.js";
34
+ import {
35
+ getMemoryCheckpoint,
36
+ setMemoryCheckpoint,
37
+ } from "../persistence/checkpoints.js";
34
38
  import { getLogger } from "../util/logger.js";
35
39
  import {
36
40
  buildToolApprovalSourceView,
@@ -166,11 +170,17 @@ export interface GuardianFeedReceiptParams {
166
170
  *
167
171
  * The item keeps its title and summary; `guardianRequest` flips to the
168
172
  * terminal status (which is what removes the client's action buttons),
169
- * and urgency drops to `medium` so the item leaves the protected
170
- * "Needs attention" treatment and becomes an ordinary clearable
171
- * notification. Returns false only when the write itself failed, so the
172
- * withdrawal fan-out can hold its per-request receipt back and retry
173
- * (same contract as the other surfaces it settles). A request with no
173
+ * urgency drops to `medium` so the item leaves the protected "Needs
174
+ * attention" treatment and becomes an ordinary clearable notification,
175
+ * and an item still marked `new` is marked `seen`: a request settled on
176
+ * any surface is no longer something the user has to review, and leaving
177
+ * it unread is a false attention signal. That clear happens only on the
178
+ * edge into terminal, so a status the user set later (including marking
179
+ * the receipt unread again) survives a caller that receipts twice.
180
+ *
181
+ * Returns false only when the write itself failed, so the withdrawal
182
+ * fan-out can hold its per-request receipt back and retry (same
183
+ * contract as the other surfaces it settles). A request with no
174
184
  * projection item resolves true: the pending writer converges the
175
185
  * write-vs-resolve race by re-checking canonical status after its
176
186
  * append, so there is nothing here for a retry to fix.
@@ -197,6 +207,15 @@ export async function writeGuardianFeedReceipt(
197
207
  }
198
208
  const updated = await patchFeedItemContent(itemId, {
199
209
  urgency: "medium",
210
+ // Only on the pending-to-terminal edge. A later receipt for the
211
+ // same request (the fan-out retries per surface, reconciliation
212
+ // heals drift) must not undo a user who marked the receipt unread
213
+ // again; from `new` specifically, because a pending item the user
214
+ // dismissed outright stays dismissed.
215
+ status: (existing) =>
216
+ existing.status === "new" && isPendingGuardianFeedItem(existing)
217
+ ? "seen"
218
+ : existing.status,
200
219
  guardianRequest: (existing) => ({
201
220
  ...existing,
202
221
  status: params.status,
@@ -268,6 +287,103 @@ export async function receiptGuardianFeedItemIfRequestTerminal(
268
287
  */
269
288
  const RECONCILE_BACKFILL_LIMIT = 50;
270
289
 
290
+ /** Ceiling on rows healed per round, mirroring the backfill bound. */
291
+ const LEGACY_UNREAD_HEAL_LIMIT = 50;
292
+
293
+ /**
294
+ * Completion sentinel for the one-time heal below, in the daemon's
295
+ * checkpoint ledger (the same shape as the lexical backfill's
296
+ * `lexical:messages:backfill_complete`). It is the only thing that
297
+ * separates the two kinds of terminal receipt stored at `status: "new"`:
298
+ * one that never passed the pending-to-terminal edge, and one the user
299
+ * marked unread after it did. The rows are byte-identical (terminal
300
+ * projection, urgency `medium`), so the boundary is recorded rather
301
+ * than inferred.
302
+ */
303
+ const LEGACY_UNREAD_HEAL_COMPLETE_KEY =
304
+ "guardian_feed:receipt_unread_heal_complete";
305
+
306
+ /**
307
+ * Clear unread on terminal guardian receipts that never passed the
308
+ * pending-to-terminal edge in `writeGuardianFeedReceipt`.
309
+ *
310
+ * Nothing else reaches those rows: the edge runs only while a
311
+ * projection still reads pending, reconciliation skips items whose
312
+ * projection is already terminal, and guardian items carry no
313
+ * `expiresAt`. Without this pass they read unread indefinitely.
314
+ *
315
+ * Runs once per assistant, not once per boot. A recurring pass would
316
+ * take back a deliberate "mark unread" every minute, and a per-boot pass
317
+ * would take it back on the next restart; the sentinel is what makes a
318
+ * later `new` unambiguously the user's, so it is never touched.
319
+ *
320
+ * Same user-choice semantics as the edge, enforced the same way: the
321
+ * transition is re-evaluated inside the writer's coalescing queue, moves
322
+ * only `new`, and leaves `seen`, `acted_on` and `dismissed` alone.
323
+ * Bounded per round, and the sentinel is written only once a round drains
324
+ * with nothing left and nothing failed, so an interrupted pass resumes.
325
+ */
326
+ export async function healLegacyGuardianReceiptUnread(): Promise<void> {
327
+ try {
328
+ if (getMemoryCheckpoint(LEGACY_UNREAD_HEAL_COMPLETE_KEY) === "1") {
329
+ return;
330
+ }
331
+ } catch (err) {
332
+ // A DB that cannot answer skips the round rather than risk re-healing
333
+ // a row the user has since marked unread.
334
+ log.warn({ err }, "Guardian receipt unread heal: checkpoint unreadable");
335
+ return;
336
+ }
337
+
338
+ const stale = readHomeFeed().items.filter(
339
+ (item) =>
340
+ item.guardianRequest !== undefined &&
341
+ !isPendingGuardianFeedItem(item) &&
342
+ item.status === "new",
343
+ );
344
+
345
+ const batch = stale.slice(0, LEGACY_UNREAD_HEAL_LIMIT);
346
+ let failed = 0;
347
+ for (const item of batch) {
348
+ // Re-tested at write time rather than trusted from the read above,
349
+ // so a row the edge transition or the user changed in between keeps
350
+ // whatever it was given.
351
+ const updated = await patchFeedItemContent(item.id, {
352
+ status: (existing) =>
353
+ existing.status === "new" && !isPendingGuardianFeedItem(existing)
354
+ ? "seen"
355
+ : existing.status,
356
+ });
357
+ if (!updated) {
358
+ failed++;
359
+ }
360
+ }
361
+
362
+ if (batch.length > 0) {
363
+ log.info(
364
+ {
365
+ healed: batch.length - failed,
366
+ failed,
367
+ remaining: stale.length - batch.length,
368
+ },
369
+ "Cleared unread on terminal guardian receipts that never passed the edge",
370
+ );
371
+ }
372
+
373
+ if (failed > 0 || stale.length > batch.length) {
374
+ // Not finished: leave the sentinel unset so the next round resumes.
375
+ return;
376
+ }
377
+
378
+ try {
379
+ setMemoryCheckpoint(LEGACY_UNREAD_HEAL_COMPLETE_KEY, "1");
380
+ } catch (err) {
381
+ // The heal itself landed; only the sentinel is missing, so the next
382
+ // round repeats a pass that now has nothing to do.
383
+ log.warn({ err }, "Guardian receipt unread heal: checkpoint not persisted");
384
+ }
385
+ }
386
+
271
387
  /**
272
388
  * Converge the feed against canonical guardian state. Run from the
273
389
  * guardian expiry sweep, so drift heals on the same cadence expiry does:
@@ -285,6 +401,10 @@ const RECONCILE_BACKFILL_LIMIT = 50;
285
401
  * not be mistaken for "everything resolved".
286
402
  */
287
403
  export async function reconcileGuardianFeedProjections(): Promise<void> {
404
+ // Ahead of the gateway read: the heal heads off stored rows alone and
405
+ // must not be held up by an unreachable gateway.
406
+ await healLegacyGuardianReceiptUnread();
407
+
288
408
  let pending: GuardianRequestWire[];
289
409
  try {
290
410
  pending = await listGuardianRequests({ status: "pending" });
@@ -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