@vellumai/assistant 0.12.1 → 0.12.2-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 (548) 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/openapi.yaml +327 -52
  6. package/package.json +3 -1
  7. package/scripts/smoke-desktop-window-theme.ts +186 -0
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  9. package/src/__tests__/anthropic-provider.test.ts +0 -63
  10. package/src/__tests__/app-compiler.test.ts +6 -0
  11. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  13. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  14. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  15. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  16. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  17. package/src/__tests__/computer-use-tools.test.ts +19 -7
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +28 -29
  20. package/src/__tests__/config-watcher.test.ts +23 -8
  21. package/src/__tests__/connection-model-compat.test.ts +2 -2
  22. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  23. package/src/__tests__/conversation-pairing.test.ts +26 -0
  24. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  25. package/src/__tests__/conversation-queue.test.ts +48 -207
  26. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  27. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  28. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  29. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  30. package/src/__tests__/conversation-title-service.test.ts +14 -10
  31. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  32. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  33. package/src/__tests__/credential-record-retire.test.ts +171 -1
  34. package/src/__tests__/credential-routes.test.ts +212 -3
  35. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  36. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  37. package/src/__tests__/document-sync-tags.test.ts +25 -0
  38. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  39. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  40. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  41. package/src/__tests__/git-porcelain.test.ts +47 -0
  42. package/src/__tests__/heartbeat-service.test.ts +56 -40
  43. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  44. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  45. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  46. package/src/__tests__/host-cu-proxy.test.ts +539 -0
  47. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  48. package/src/__tests__/host-file-proxy.test.ts +11 -23
  49. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  50. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  51. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  52. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  53. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  54. package/src/__tests__/mcp-health-check.test.ts +8 -11
  55. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  56. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  57. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  58. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  59. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  60. package/src/__tests__/model-intents.test.ts +3 -0
  61. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  62. package/src/__tests__/oauth-commands-routes.test.ts +113 -0
  63. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  64. package/src/__tests__/openai-provider.test.ts +1 -0
  65. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  66. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  67. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  68. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  69. package/src/__tests__/qdrant-manager.test.ts +132 -0
  70. package/src/__tests__/resolve-app.test.ts +135 -0
  71. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  72. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  73. package/src/__tests__/service-account-token.test.ts +247 -0
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  75. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  76. package/src/__tests__/skills.test.ts +9 -4
  77. package/src/__tests__/source-fingerprint.test.ts +184 -0
  78. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  79. package/src/__tests__/subagent-tools.test.ts +3 -2
  80. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  81. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  82. package/src/__tests__/unicode.test.ts +13 -0
  83. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  84. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  85. package/src/__tests__/workspace-git-service.test.ts +26 -0
  86. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  87. package/src/acp/__tests__/agent-process.test.ts +258 -4
  88. package/src/acp/__tests__/auth-required.test.ts +96 -0
  89. package/src/acp/__tests__/client-handler.test.ts +90 -2
  90. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  91. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  92. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  93. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  94. package/src/acp/__tests__/session-manager.test.ts +895 -4
  95. package/src/acp/agent-process.test.ts +4 -1
  96. package/src/acp/agent-process.ts +96 -10
  97. package/src/acp/auth-required.ts +50 -1
  98. package/src/acp/auto-install.test.ts +9 -7
  99. package/src/acp/client-handler.ts +16 -3
  100. package/src/acp/model-config.test.ts +166 -0
  101. package/src/acp/model-config.ts +138 -0
  102. package/src/acp/resolve-agent.test.ts +220 -6
  103. package/src/acp/resolve-agent.ts +88 -23
  104. package/src/acp/session-manager.test.ts +196 -3
  105. package/src/acp/session-manager.ts +441 -18
  106. package/src/acp/types.ts +50 -5
  107. package/src/agent/loop.ts +1 -0
  108. package/src/api/events/acp-session-model-update.test.ts +88 -0
  109. package/src/api/events/acp-session-model-update.ts +46 -0
  110. package/src/api/events/app-files-changed.ts +4 -3
  111. package/src/api/events/notification-intent.ts +2 -0
  112. package/src/api/events/user-message-echo.ts +7 -1
  113. package/src/api/index.ts +6 -0
  114. package/src/api/responses/conversation-message.ts +5 -0
  115. package/src/apps/app-store.ts +1 -1
  116. package/src/apps/resolve-app.ts +81 -0
  117. package/src/apps/source-fingerprint.ts +266 -0
  118. package/src/avatar/identity-avatar.ts +7 -2
  119. package/src/bundler/app-compiler.ts +15 -0
  120. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  121. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  122. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  123. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  124. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  125. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  126. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  127. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  128. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  129. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  130. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  131. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  132. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  133. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  134. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  135. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  136. package/src/calls/barge-in-guard.ts +118 -0
  137. package/src/calls/call-constants.ts +24 -4
  138. package/src/calls/call-controller.ts +499 -199
  139. package/src/calls/call-transport.ts +16 -0
  140. package/src/calls/media-stream-output.ts +32 -0
  141. package/src/calls/media-stream-server.ts +82 -30
  142. package/src/calls/media-stream-stt-session.ts +15 -0
  143. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  144. package/src/calls/resolve-call-tts-provider.ts +33 -2
  145. package/src/calls/tts-call-strategy.ts +9 -5
  146. package/src/calls/types.ts +2 -0
  147. package/src/calls/voice-control-protocol.ts +53 -21
  148. package/src/calls/voice-leg-coordinator.ts +189 -0
  149. package/src/calls/voice-progress-cadence.ts +423 -0
  150. package/src/calls/voice-session-bridge.ts +59 -46
  151. package/src/calls/voice-triage-escalate.ts +175 -56
  152. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  153. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  154. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  155. package/src/cli/commands/apps.help.ts +67 -13
  156. package/src/cli/commands/apps.ts +183 -0
  157. package/src/cli/commands/bash.help.ts +8 -13
  158. package/src/cli/commands/bash.ts +7 -25
  159. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  160. package/src/cli/commands/channels/index.help.ts +70 -2
  161. package/src/cli/commands/channels/index.ts +16 -3
  162. package/src/cli/commands/channels/send.ts +128 -0
  163. package/src/cli/commands/credentials.help.ts +6 -2
  164. package/src/cli/commands/credentials.ts +2 -2
  165. package/src/cli/commands/mcp.help.ts +8 -8
  166. package/src/cli/commands/mcp.ts +1 -1
  167. package/src/cli/commands/roadmap.help.ts +5 -1
  168. package/src/cli/commands/roadmap.ts +1 -1
  169. package/src/cli/lib/bundled-marketplace.json +103 -90
  170. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  171. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  172. package/src/config/__tests__/platform-identity.test.ts +255 -0
  173. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  174. package/src/config/acp-agent-write.ts +43 -0
  175. package/src/config/acp-defaults.test.ts +13 -3
  176. package/src/config/acp-defaults.ts +23 -6
  177. package/src/config/acp-schema.test.ts +81 -0
  178. package/src/config/acp-schema.ts +30 -2
  179. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  180. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  181. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  182. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  183. package/src/config/bundled-skills/computer-use/SKILL.md +20 -2
  184. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  185. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  186. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  187. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  188. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  189. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  190. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  191. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  192. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  193. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  194. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  195. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  196. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  197. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  198. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  199. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  200. package/src/config/default-profile-catalog.ts +1 -1
  201. package/src/config/env.ts +2 -2
  202. package/src/config/feature-flag-registry.json +24 -12
  203. package/src/config/loader.ts +1 -0
  204. package/src/config/memory-v3-gate.test.ts +31 -0
  205. package/src/config/memory-v3-gate.ts +20 -16
  206. package/src/config/platform-identity.ts +201 -0
  207. package/src/config/platform-rehydration.ts +11 -27
  208. package/src/config/schema.ts +2 -2
  209. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  210. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  211. package/src/config/schemas/calls.ts +1 -28
  212. package/src/config/schemas/live-voice.ts +1 -173
  213. package/src/config/schemas/mcp.ts +1 -1
  214. package/src/config/schemas/memory-retrospective.ts +10 -0
  215. package/src/config/schemas/services.ts +8 -0
  216. package/src/config/schemas/skills.ts +0 -14
  217. package/src/config/schemas/voice.ts +150 -0
  218. package/src/credential-execution/process-manager.test.ts +18 -19
  219. package/src/credential-health/credential-health-service.ts +11 -17
  220. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  221. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  222. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  223. package/src/daemon/app-change-notify.ts +45 -0
  224. package/src/daemon/config-watcher.ts +13 -194
  225. package/src/daemon/conversation-error.ts +3 -2
  226. package/src/daemon/conversation-media-retry.ts +2 -1
  227. package/src/daemon/conversation-process.ts +35 -64
  228. package/src/daemon/conversation-runtime-assembly.ts +2 -1
  229. package/src/daemon/conversation-skill-tools.ts +13 -31
  230. package/src/daemon/conversation-store.ts +1 -1
  231. package/src/daemon/conversation-surfaces.ts +15 -9
  232. package/src/daemon/conversation-tool-setup.ts +37 -43
  233. package/src/daemon/conversation.ts +0 -3
  234. package/src/daemon/dictation-profile-store.ts +2 -1
  235. package/src/daemon/handlers/shared.ts +2 -1
  236. package/src/daemon/handlers/skills.ts +6 -3
  237. package/src/daemon/host-bash-proxy.ts +8 -12
  238. package/src/daemon/host-browser-proxy.ts +15 -1
  239. package/src/daemon/host-cu-proxy.ts +186 -19
  240. package/src/daemon/host-file-proxy.ts +8 -10
  241. package/src/daemon/host-proxy-preactivation.ts +2 -1
  242. package/src/daemon/host-transfer-proxy.ts +14 -18
  243. package/src/daemon/lifecycle.ts +0 -5
  244. package/src/daemon/mcp-reload-service.ts +5 -5
  245. package/src/daemon/message-types/sync.ts +1 -0
  246. package/src/daemon/orphan-reaper.ts +1 -1
  247. package/src/daemon/providers-setup.ts +1 -1
  248. package/src/daemon/reaction-history-render.ts +2 -1
  249. package/src/daemon/shutdown-handlers.ts +1 -9
  250. package/src/daemon/skill-memory-refresh.ts +64 -0
  251. package/src/daemon/tool-side-effects.ts +7 -18
  252. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  253. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  254. package/src/desktop/desktop-chrome-policy.ts +47 -0
  255. package/src/desktop/desktop-chrome-session.ts +37 -0
  256. package/src/desktop/desktop-dependencies.test.ts +102 -0
  257. package/src/desktop/desktop-dependencies.ts +308 -0
  258. package/src/desktop/desktop-feature.test.ts +18 -0
  259. package/src/desktop/desktop-feature.ts +19 -0
  260. package/src/desktop/desktop-panel-config.test.ts +135 -0
  261. package/src/desktop/desktop-panel-config.ts +149 -0
  262. package/src/desktop/desktop-session-manager.test.ts +730 -0
  263. package/src/desktop/desktop-session-manager.ts +850 -0
  264. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  265. package/src/desktop/desktop-stream-bridge.ts +214 -0
  266. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  267. package/src/desktop/desktop-wallpaper.ts +113 -0
  268. package/src/desktop/desktop-window-drag.ts +177 -0
  269. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  270. package/src/desktop/desktop-window-manager-config.ts +238 -0
  271. package/src/desktop/desktop-window-theme.ts +170 -0
  272. package/src/heartbeat/heartbeat-service.ts +21 -3
  273. package/src/home/__tests__/feed-writer.test.ts +22 -0
  274. package/src/home/feed-writer.ts +11 -2
  275. package/src/hooks/types.ts +33 -0
  276. package/src/i18n/AGENTS.md +22 -0
  277. package/src/i18n/__tests__/i18n.test.ts +108 -0
  278. package/src/i18n/index.ts +16 -0
  279. package/src/i18n/locales.ts +93 -0
  280. package/src/i18n/messages.ts +61 -0
  281. package/src/i18n/title.ts +25 -0
  282. package/src/inbound/platform-callback-registration.ts +4 -5
  283. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  284. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  285. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  286. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  287. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  288. package/src/live-voice/live-voice-photo.ts +25 -6
  289. package/src/live-voice/live-voice-session.ts +213 -693
  290. package/src/live-voice/protocol.ts +0 -2
  291. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  292. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  293. package/src/mcp/effective-config.ts +3 -3
  294. package/src/mcp/mcp-header-store.ts +29 -45
  295. package/src/mcp/spec-schema.ts +111 -0
  296. package/src/mcp/startup.ts +8 -4
  297. package/src/mcp/workspace-mcp-config.ts +188 -0
  298. package/src/messaging/provider.ts +10 -3
  299. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  300. package/src/messaging/providers/callback-routing.ts +18 -0
  301. package/src/messaging/providers/channel-transport.ts +56 -0
  302. package/src/messaging/providers/discord/transport.ts +17 -0
  303. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  304. package/src/messaging/providers/slack/adapter.ts +7 -44
  305. package/src/messaging/providers/slack/transport.ts +16 -0
  306. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  307. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  308. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  309. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  310. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  311. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  312. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  313. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  314. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  315. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  316. package/src/notifications/adapters/macos.ts +3 -0
  317. package/src/notifications/assistant-reply-producer.ts +8 -3
  318. package/src/notifications/conversation-pairing.ts +36 -16
  319. package/src/notifications/delivered-post-record.ts +11 -1
  320. package/src/notifications/guardian-feed-projection.ts +125 -5
  321. package/src/notifications/home-feed-side-effect.ts +55 -7
  322. package/src/notifications/schedule-result-producer.ts +66 -22
  323. package/src/oauth/AGENTS.md +2 -0
  324. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  325. package/src/oauth/byo-connection.test.ts +105 -0
  326. package/src/oauth/byo-connection.ts +36 -6
  327. package/src/oauth/connection-resolver.test.ts +73 -0
  328. package/src/oauth/connection-resolver.ts +95 -2
  329. package/src/oauth/scope-utils.ts +24 -0
  330. package/src/oauth/seed-providers.ts +204 -25
  331. package/src/outbound-proxy/certs.ts +3 -5
  332. package/src/permissions/checker.test.ts +11 -6
  333. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  334. package/src/persistence/conversation-crud.ts +71 -63
  335. package/src/persistence/conversation-lineage.ts +12 -4
  336. package/src/persistence/conversation-queries.ts +2 -2
  337. package/src/persistence/conversation-title-placeholders.ts +23 -0
  338. package/src/persistence/conversation-title-service.ts +9 -26
  339. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  340. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  341. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  342. package/src/persistence/message-cursor.ts +78 -0
  343. package/src/persistence/message-reads.ts +30 -0
  344. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  345. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  346. package/src/persistence/schema/memory-core.ts +5 -0
  347. package/src/platform/client.test.ts +4 -2
  348. package/src/platform/client.ts +2 -10
  349. package/src/plugin-api/constants.ts +2 -0
  350. package/src/plugin-api/index.ts +1 -0
  351. package/src/plugin-api/types.ts +2 -0
  352. package/src/plugins/defaults/index.ts +2 -0
  353. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  354. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  355. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  356. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  357. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  358. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  359. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  360. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  361. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  362. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  363. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  364. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  365. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  366. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  367. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  368. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  369. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  370. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  371. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  372. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  373. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  374. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  375. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  376. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  377. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  378. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  379. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  380. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  381. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  382. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  383. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  384. package/src/plugins/defaults/memory/startup.ts +28 -2
  385. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  386. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  387. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  388. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  389. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  390. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  391. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  392. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  393. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  394. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  395. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  396. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  397. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  398. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  399. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  400. package/src/plugins/defaults/memory/worker.ts +6 -0
  401. package/src/plugins/mcp-servers.ts +15 -75
  402. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  403. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  404. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  405. package/src/providers/anthropic/client.ts +0 -47
  406. package/src/providers/gemini/client.ts +9 -23
  407. package/src/providers/gemini-thought-signature.test.ts +63 -0
  408. package/src/providers/gemini-thought-signature.ts +44 -0
  409. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  410. package/src/providers/inference/auth.ts +2 -3
  411. package/src/providers/inference/connection-availability.ts +27 -8
  412. package/src/providers/inference/endpoint-probe.ts +10 -1
  413. package/src/providers/inference/resolve-auth.ts +22 -4
  414. package/src/providers/inference/service-account-token.ts +248 -0
  415. package/src/providers/model-catalog.ts +34 -0
  416. package/src/providers/model-intents.ts +10 -5
  417. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  418. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  419. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  420. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  421. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  422. package/src/providers/openai/chat-completions-provider.ts +212 -79
  423. package/src/providers/openai/codex-models.ts +0 -4
  424. package/src/providers/openai/google-thought-signature.ts +320 -0
  425. package/src/providers/opencode/client.test.ts +73 -4
  426. package/src/providers/opencode/client.ts +60 -3
  427. package/src/providers/openrouter/client.ts +20 -0
  428. package/src/providers/outbound-request-sanitize.ts +96 -0
  429. package/src/providers/retry.ts +30 -19
  430. package/src/providers/unparseable-tool-args.ts +5 -1
  431. package/src/runtime/AGENTS.md +3 -1
  432. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  433. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  434. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  435. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  436. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  437. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  438. package/src/runtime/agent-wake.ts +2 -1
  439. package/src/runtime/assistant-event-hub.ts +21 -3
  440. package/src/runtime/auth/same-actor.ts +38 -3
  441. package/src/runtime/channel-send.ts +316 -0
  442. package/src/runtime/host-observe.ts +11 -1
  443. package/src/runtime/http-server.ts +160 -151
  444. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  445. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  446. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  447. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  448. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  449. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  450. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  451. package/src/runtime/routes/acp-routes.test.ts +15 -0
  452. package/src/runtime/routes/acp-routes.ts +72 -9
  453. package/src/runtime/routes/app-management-routes.ts +73 -0
  454. package/src/runtime/routes/app-routes.ts +1 -1
  455. package/src/runtime/routes/channel-send-routes.ts +85 -0
  456. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  457. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  458. package/src/runtime/routes/conversation-routes.ts +16 -3
  459. package/src/runtime/routes/credential-routes.ts +19 -10
  460. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  461. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  462. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  463. package/src/runtime/routes/events-routes.ts +4 -0
  464. package/src/runtime/routes/host-cu-routes.ts +6 -0
  465. package/src/runtime/routes/index.ts +4 -2
  466. package/src/runtime/routes/log-export-routes.ts +2 -0
  467. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  468. package/src/runtime/routes/migration-routes.ts +6 -20
  469. package/src/runtime/routes/oauth-commands-routes.ts +36 -0
  470. package/src/runtime/routes/platform-routes.ts +18 -15
  471. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  472. package/src/runtime/routes/workspace-routes.ts +46 -63
  473. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  474. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  475. package/src/runtime/routes/workspace-utils.ts +17 -0
  476. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  477. package/src/runtime/services/conversation-serializer.ts +16 -3
  478. package/src/runtime/sync/resource-sync-events.ts +2 -1
  479. package/src/schedule/run-script.ts +2 -1
  480. package/src/schedule/schedule-store.ts +9 -2
  481. package/src/schedule/worker.ts +1 -1
  482. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  483. package/src/security/ces-http-record-backend.ts +120 -0
  484. package/src/security/ces-rpc-record-backend.ts +7 -0
  485. package/src/security/untrusted-content.ts +3 -1
  486. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  487. package/src/skills/workspace-skill-md-signature.ts +50 -0
  488. package/src/subagent/consult-context.ts +12 -25
  489. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  490. package/src/tools/acp/list-agents.test.ts +3 -1
  491. package/src/tools/acp/spawn.test.ts +146 -11
  492. package/src/tools/acp/spawn.ts +21 -3
  493. package/src/tools/acp/status.test.ts +94 -0
  494. package/src/tools/acp/status.ts +40 -3
  495. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  496. package/src/tools/browser/browser-execution.ts +1 -1
  497. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  498. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  499. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  500. package/src/tools/browser/network-recorder.ts +2 -1
  501. package/src/tools/capability-offer.test.ts +22 -2
  502. package/src/tools/capability-offer.ts +16 -1
  503. package/src/tools/computer-use/definitions.ts +121 -2
  504. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  505. package/src/tools/credentials/metadata-store.ts +100 -0
  506. package/src/tools/filesystem/search.ts +2 -1
  507. package/src/tools/host-filesystem/edit.test.ts +2 -1
  508. package/src/tools/host-filesystem/edit.ts +2 -2
  509. package/src/tools/host-filesystem/read.test.ts +2 -1
  510. package/src/tools/host-filesystem/read.ts +2 -2
  511. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  512. package/src/tools/host-filesystem/transfer.ts +2 -2
  513. package/src/tools/host-filesystem/write.test.ts +2 -1
  514. package/src/tools/host-filesystem/write.ts +2 -2
  515. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  516. package/src/tools/host-terminal/host-shell.ts +4 -20
  517. package/src/tools/network/web-search.ts +6 -1
  518. package/src/tools/policy-context.ts +6 -7
  519. package/src/tools/skills/scaffold-managed.ts +61 -3
  520. package/src/tools/subagent/spawn.ts +5 -8
  521. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  522. package/src/tools/terminal/sanitized-bash.ts +105 -0
  523. package/src/tools/watch/watch-retro-report.ts +0 -8
  524. package/src/types/host-capabilities.ts +5 -3
  525. package/src/util/__tests__/language-subtag.test.ts +23 -1
  526. package/src/util/ca-bundle.ts +13 -0
  527. package/src/util/language-subtag.ts +16 -0
  528. package/src/util/platform.ts +8 -0
  529. package/src/util/unicode.ts +15 -1
  530. package/src/watch/watch-timeline.ts +2 -2
  531. package/src/workflows/run-manager.ts +2 -1
  532. package/src/workspace/git-porcelain.ts +66 -0
  533. package/src/workspace/git-service.ts +6 -55
  534. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  535. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  536. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  537. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  538. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  539. package/src/workspace/migrations/registry.ts +6 -0
  540. package/src/__tests__/app-source-watcher.test.ts +0 -218
  541. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  542. package/src/__tests__/speaker-identification.test.ts +0 -56
  543. package/src/calls/speaker-identification.ts +0 -233
  544. package/src/daemon/app-source-watcher.ts +0 -218
  545. package/src/messaging/providers/whatsapp/client.ts +0 -29
  546. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  547. package/src/util/fs-watcher-error.ts +0 -36
  548. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -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
@@ -32,6 +32,7 @@ import { getAttentionStateByConversationIds } from "../persistence/conversation-
32
32
  import {
33
33
  getAssistantMessageIdsInTurn,
34
34
  getMessageById,
35
+ getMessagesAfter,
35
36
  type MessageRow,
36
37
  } from "../persistence/conversation-crud.js";
37
38
  import { stringifyMessageContent } from "../persistence/message-content.js";
@@ -39,7 +40,6 @@ import {
39
40
  isPrivateAssistantText,
40
41
  projectPersistedAssistantContent,
41
42
  } from "../persistence/user-facing-content.js";
42
- import type { ContentBlock } from "../providers/types.js";
43
43
  import { emitNotificationSignal } from "./emit-signal.js";
44
44
  import { hasNotifiedSourceContextSince } from "./events-store.js";
45
45
  import {
@@ -79,28 +79,72 @@ export interface ScheduleResultNotificationParams {
79
79
  }
80
80
 
81
81
  /**
82
- * Whether a tool call in the run's turn delivered the result somewhere the
83
- * user will see it, outside the notification pipeline.
82
+ * Whether the run delivered its result through the messaging tool, and the
83
+ * call succeeded.
84
84
  *
85
- * The schedule skill prescribes two such routes for rich content the
86
- * messaging tool for email, and the Slack Web API's `chat.postMessage` through
87
- * bash and neither writes a `notification_events` row, so the pipeline probe
88
- * cannot see them. Without this check a well-authored Slack digest would post
85
+ * The messaging tool is the route the schedule skill prescribes for rich
86
+ * content, and it writes no `notification_events` row, so the pipeline probe
87
+ * cannot see it. Without this check a well-authored Slack digest would post
89
88
  * its summary and then get a second notification whose body is "Posted the
90
- * digest to #general." This is a recognized-routes list, not a general "did
91
- * the run do anything?" heuristic: a route that is not here gets the fallback,
92
- * which is the safe failure.
89
+ * digest to #general."
90
+ *
91
+ * Both conditions are load-bearing. The route has to be recognized, because
92
+ * this is a known-routes list rather than a general "did the run do anything?"
93
+ * heuristic. And the call has to have succeeded: a `tool_use` block records
94
+ * only that the model called the tool, so a send the channel refused leaves
95
+ * the same block as one that landed, and suppressing on it costs the user both
96
+ * the digest and the run's explanation of the failure. The executor's verdict
97
+ * is on the `tool_result` the call produced, so success is read from there,
98
+ * and a call with no result counts as undelivered. Either condition unmet gets
99
+ * the fallback, which is the safe failure.
100
+ *
101
+ * The result read starts at the run's first row, so it covers the results
102
+ * interleaved through the turn. Rows from a later turn in a reused
103
+ * conversation can come back too and are harmless, because only this run's
104
+ * own call ids are matched.
105
+ *
106
+ * A Slack Web API post through bash is deliberately not a route, because its
107
+ * success proves nothing about the post. Slack refuses a call with HTTP 200
108
+ * and `ok: false` in the body, and the authenticated-request command fails
109
+ * only on a non-2xx status, so a post Slack refused still leaves a successful
110
+ * tool result. A run that posts that way gets the fallback too: at worst a
111
+ * duplicate, never a silence.
93
112
  */
94
- function isDirectDelivery(block: ContentBlock): boolean {
95
- if (block.type !== "tool_use") {
96
- return false;
113
+ function deliveredThroughMessagingTool(
114
+ conversationId: string,
115
+ runRows: readonly MessageRow[],
116
+ ): boolean {
117
+ const callIds = new Set<string>();
118
+ for (const row of runRows) {
119
+ for (const block of row.content) {
120
+ if (block.type === "tool_use" && block.name === "messaging_send") {
121
+ callIds.add(block.id);
122
+ }
123
+ }
97
124
  }
98
- if (block.name === "messaging_send") {
99
- return true;
125
+ if (callIds.size === 0) {
126
+ return false;
100
127
  }
101
- if (block.name === "bash") {
102
- const command = (block.input as { command?: unknown } | undefined)?.command;
103
- return typeof command === "string" && command.includes("chat.postMessage");
128
+ const [firstRunRow] = runRows;
129
+ for (const row of getMessagesAfter(conversationId, {
130
+ id: firstRunRow.id,
131
+ createdAt: firstRunRow.createdAt,
132
+ })) {
133
+ if (row.role !== "user") {
134
+ continue;
135
+ }
136
+ for (const block of row.content) {
137
+ // guard:allow-tool-result-only: the local executor's verdict on a
138
+ // delivery it ran. A server-side `web_search_tool_result` carries no
139
+ // `is_error` and never delivers anything.
140
+ if (
141
+ block.type === "tool_result" &&
142
+ block.is_error !== true &&
143
+ callIds.has(block.tool_use_id)
144
+ ) {
145
+ return true;
146
+ }
147
+ }
104
148
  }
105
149
  return false;
106
150
  }
@@ -237,11 +281,11 @@ export async function emitScheduleResultNotification(
237
281
  return;
238
282
  }
239
283
 
240
- // The run delivered around the pipeline — an email through the messaging
241
- // tool, a Slack post through the Web API. The user has the result; a
242
- // notification reading "posted it" on top would be the duplicate.
284
+ // The run delivered around the pipeline through the messaging tool, and
285
+ // the call succeeded. The user has the result; a notification reading
286
+ // "posted it" on top would be the duplicate.
243
287
  const runRows = collectRunRows(latestRow, conversationId, runStartedAt);
244
- if (runRows.some((row) => row.content.some(isDirectDelivery))) {
288
+ if (deliveredThroughMessagingTool(conversationId, runRows)) {
245
289
  return;
246
290
  }
247
291
 
@@ -8,6 +8,8 @@ When introducing a new built-in OAuth integration (one that appears in `seed-pro
8
8
 
9
9
  Add an entry to `PROVIDER_SEED_DATA`. Required fields: `provider`, `authorizeUrl`, `tokenExchangeUrl`, `defaultScopes`, `displayLabel`, `description`, `dashboardUrl`, `clientIdPlaceholder`, `logoUrl`, and `injectionTemplates`. Optional: `availableScopes` — either a structured array of `{scope, description?}` objects or a URL string pointing to the provider's scope documentation. See existing entries for the full shape. The `provider` key must be snake_case and is used as the canonical identifier everywhere else.
10
10
 
11
+ `injectionTemplates` is also a security boundary, not only an injection rule. `assistant oauth request` and `assistant channels request` accept an absolute URL only when its host matches one of the entry's `hostPattern` values (`assertOAuthRequestUrlAllowed` in `../runtime/routes/oauth-commands-routes.ts`), so that list is the full set of hosts the credential may be sent to. A pattern matches exactly unless it starts with `*.`; reserve the wildcard for per-tenant hosts that cannot be enumerated (`*.salesforce.com`), and name a fixed host the provider documents (`files.slack.com`) by name. The guard sees only the URL the caller passes: the connection follows a redirect itself, and a cross-origin hop carries no `Authorization`, so a redirect target never receives the credential and never needs listing.
12
+
11
13
  If the provider will support managed mode, set `managedServiceConfigKey` to a slug matching the key you will add to `ServicesSchema` (e.g. `"acme-oauth"`).
12
14
 
13
15
  ### 2. _(managed only)_ Add a service schema — `../config/schemas/services.ts`
@@ -127,6 +127,60 @@ describe("PROVIDER_SEED_DATA managed mode wiring", () => {
127
127
  expect(figma.featureFlag).toBe("figma-oauth");
128
128
  });
129
129
 
130
+ test("shopify keeps its per-tenant URL templates", () => {
131
+ // Shopify has no global OAuth host: every endpoint lives on the
132
+ // merchant's own domain. Hardcoding any host here would send every
133
+ // merchant's authorization to the wrong store, so the {tenant_host}
134
+ // placeholder the platform substitutes must survive.
135
+ const shopify = PROVIDER_SEED_DATA.shopify;
136
+ expect(shopify).toBeDefined();
137
+ for (const url of [
138
+ shopify.authorizeUrl,
139
+ shopify.tokenExchangeUrl,
140
+ shopify.refreshUrl,
141
+ shopify.baseUrl,
142
+ shopify.identityUrl,
143
+ ]) {
144
+ expect(url).toContain("{tenant_host}");
145
+ }
146
+ });
147
+
148
+ test("shopify requests comma-separated scopes", () => {
149
+ // Shopify parses `scope` as a comma-separated list; the OAuth2 default
150
+ // space separator is read as one malformed scope and the whole
151
+ // authorization request is rejected.
152
+ expect(PROVIDER_SEED_DATA.shopify.scopeSeparator).toBe(",");
153
+ });
154
+
155
+ test("shopify does not request approval-gated scopes", () => {
156
+ // Shopify rejects the entire authorization request when the app cannot
157
+ // grant a requested scope -- the same failure that broke monday and
158
+ // Figma. These scopes all require Shopify's prior approval.
159
+ const gated = [
160
+ "read_all_orders",
161
+ "read_customer_payment_methods",
162
+ "read_own_subscription_contracts",
163
+ "write_own_subscription_contracts",
164
+ "read_shopify_payments_dispute_evidences",
165
+ "write_shopify_payments_dispute_evidences",
166
+ ];
167
+ for (const scope of gated) {
168
+ expect(PROVIDER_SEED_DATA.shopify.defaultScopes).not.toContain(scope);
169
+ }
170
+ });
171
+
172
+ test("shopify injects the Admin API's own auth header", () => {
173
+ // The Admin API reads X-Shopify-Access-Token and ignores an
174
+ // Authorization: Bearer header entirely, so a Bearer template here would
175
+ // make every proxied request unauthenticated.
176
+ const templates = PROVIDER_SEED_DATA.shopify.injectionTemplates;
177
+ expect(templates).toBeDefined();
178
+ const [template] = templates ?? [];
179
+ expect(template?.headerName).toBe("X-Shopify-Access-Token");
180
+ expect(template?.valuePrefix).toBe("");
181
+ expect(template?.hostPattern).toBe("*.myshopify.com");
182
+ });
183
+
130
184
  test("every managedServiceConfigKey resolves to a ServicesSchema key", () => {
131
185
  // Cross-repo invariant: a provider with managedServiceConfigKey but no
132
186
  // matching ServicesSchema entry silently falls back to BYO mode in