@vellumai/assistant 0.11.2 → 0.11.3-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 (720) hide show
  1. package/ARCHITECTURE.md +58 -3
  2. package/docs/trusted-contact-access.md +9 -5
  3. package/docs/vellum-doctor.md +195 -0
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  7. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +19 -2
  9. package/node_modules/@vellumai/environments/package.json +2 -1
  10. package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
  11. package/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
  12. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
  13. package/node_modules/@vellumai/environments/src/index.ts +17 -6
  14. package/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
  15. package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
  16. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  19. package/node_modules/@vellumai/gateway-client/src/index.ts +4 -0
  20. package/node_modules/@vellumai/gateway-client/src/verification-session-contract.ts +91 -6
  21. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
  22. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
  23. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +2 -0
  24. package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
  25. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +15 -1
  26. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +17 -2
  27. package/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  28. package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  29. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  30. package/node_modules/@vellumai/slack-text/src/index.ts +16 -2
  31. package/openapi.yaml +589 -30
  32. package/package.json +1 -1
  33. package/src/__tests__/agent-image-optimize.test.ts +4 -4
  34. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +113 -66
  35. package/src/__tests__/agent-loop-override-profile.test.ts +38 -48
  36. package/src/__tests__/agent-loop-subagent-billing-headers.test.ts +166 -0
  37. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
  38. package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
  39. package/src/__tests__/answered-question-persistence.test.ts +240 -0
  40. package/src/__tests__/anthropic-provider.test.ts +165 -30
  41. package/src/__tests__/app-control-flow.test.ts +9 -8
  42. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  43. package/src/__tests__/attachments-store-heic-normalize.test.ts +16 -12
  44. package/src/__tests__/attachments-store.test.ts +130 -68
  45. package/src/__tests__/attachments.test.ts +36 -36
  46. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/call-start-guardian-guard.test.ts +18 -4
  48. package/src/__tests__/canned-reply-release.test.ts +86 -0
  49. package/src/__tests__/channel-guardian.test.ts +59 -29
  50. package/src/__tests__/channel-readiness-telegram-remote.test.ts +154 -0
  51. package/src/__tests__/channel-setup-panel-ack.test.ts +6 -5
  52. package/src/__tests__/client-os-metadata-persistence.test.ts +103 -4
  53. package/src/__tests__/compactor-image-manifest-trust.test.ts +1 -1
  54. package/src/__tests__/consult-deadline.test.ts +65 -0
  55. package/src/__tests__/conversation-agent-loop.test.ts +438 -2
  56. package/src/__tests__/conversation-disk-view-integration.test.ts +5 -1
  57. package/src/__tests__/conversation-disk-view.test.ts +4 -4
  58. package/src/__tests__/conversation-error.test.ts +22 -3
  59. package/src/__tests__/conversation-fork-crud.test.ts +5 -1
  60. package/src/__tests__/conversation-fork-referential.test.ts +352 -0
  61. package/src/__tests__/conversation-fork-retrospective.test.ts +5 -1
  62. package/src/__tests__/conversation-group-tools.test.ts +32 -2
  63. package/src/__tests__/conversation-inference-profile-route.test.ts +27 -1
  64. package/src/__tests__/conversation-initial-prompt.test.ts +51 -22
  65. package/src/__tests__/conversation-key-store-bootstrap-cleanup.test.ts +119 -0
  66. package/src/__tests__/conversation-key-store-origin.test.ts +105 -0
  67. package/src/__tests__/conversation-list-recency-ordering.test.ts +104 -0
  68. package/src/__tests__/conversation-origin-at-creation.test.ts +153 -0
  69. package/src/__tests__/conversation-origin-channel-filter.test.ts +95 -0
  70. package/src/__tests__/conversation-placement-promotion.test.ts +172 -0
  71. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +123 -0
  72. package/src/__tests__/conversation-queue.test.ts +433 -1
  73. package/src/__tests__/conversation-retry-route.test.ts +3 -1
  74. package/src/__tests__/conversation-routes-hidden-queue.test.ts +64 -1
  75. package/src/__tests__/conversation-store-ephemeral.test.ts +4 -0
  76. package/src/__tests__/conversation-store.test.ts +26 -10
  77. package/src/__tests__/conversation-summarize-route.test.ts +1 -1
  78. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +15 -6
  79. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -5
  80. package/src/__tests__/conversation-surfaces-app-control.test.ts +12 -11
  81. package/src/__tests__/conversation-surfaces-app-open.test.ts +11 -5
  82. package/src/__tests__/conversation-surfaces-data-persist.test.ts +5 -4
  83. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +967 -0
  84. package/src/__tests__/conversation-surfaces-persisted-info.test.ts +312 -0
  85. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +6 -5
  86. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -8
  87. package/src/__tests__/conversation-surfaces-standalone.test.ts +21 -11
  88. package/src/__tests__/conversation-surfaces-state-update.test.ts +7 -11
  89. package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -6
  90. package/src/__tests__/conversation-surfaces-task-progress.test.ts +11 -6
  91. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +9 -6
  92. package/src/__tests__/conversation-tool-setup-attribution.test.ts +70 -7
  93. package/src/__tests__/conversation-wait-for-idle.test.ts +6 -11
  94. package/src/__tests__/credential-prompt-route.test.ts +57 -7
  95. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  96. package/src/__tests__/cu-unified-flow.test.ts +89 -28
  97. package/src/__tests__/custom-group-listing-visibility.test.ts +1 -1
  98. package/src/__tests__/db-schedule-syntax-migration.test.ts +4 -1
  99. package/src/__tests__/discord-access-request-privacy.test.ts +366 -0
  100. package/src/__tests__/discord-requester-notice-privacy.test.ts +87 -0
  101. package/src/__tests__/disk-pressure-guard.test.ts +55 -55
  102. package/src/__tests__/disk-pressure-routes.test.ts +10 -10
  103. package/src/__tests__/disk-pressure-tools.test.ts +2 -2
  104. package/src/__tests__/disk-usage.test.ts +35 -20
  105. package/src/__tests__/document-sync-tags.test.ts +379 -0
  106. package/src/__tests__/document-update-default-surface.test.ts +28 -0
  107. package/src/__tests__/dynamic-skill-background-guard.test.ts +2 -2
  108. package/src/__tests__/emit-signal-routing-intent.test.ts +3 -4
  109. package/src/__tests__/ensure-conversation-exists.test.ts +8 -5
  110. package/src/__tests__/events-dev-bypass-actor.test.ts +183 -1
  111. package/src/__tests__/evict-conversations-for-reload.test.ts +90 -0
  112. package/src/__tests__/file-list-tool.test.ts +12 -10
  113. package/src/__tests__/file-ops-service.test.ts +151 -44
  114. package/src/__tests__/file-read-container-boundary.test.ts +137 -0
  115. package/src/__tests__/file-read-tool.test.ts +21 -9
  116. package/src/__tests__/file-url-path-guard.test.ts +83 -0
  117. package/src/__tests__/filesystem-tools.test.ts +82 -66
  118. package/src/__tests__/guardian-outbound-http.test.ts +8 -8
  119. package/src/__tests__/helpers/gateway-verification-sessions-stub.ts +14 -1
  120. package/src/__tests__/helpers/hub-clients.ts +42 -0
  121. package/src/__tests__/helpers/mock-conversation.ts +46 -0
  122. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +61 -57
  123. package/src/__tests__/history-repair.test.ts +112 -12
  124. package/src/__tests__/host-bash-proxy.test.ts +10 -5
  125. package/src/__tests__/host-bash-routes.test.ts +67 -0
  126. package/src/__tests__/host-browser-proxy.test.ts +2 -2
  127. package/src/__tests__/host-cu-proxy.test.ts +3 -7
  128. package/src/__tests__/host-file-proxy.test.ts +3 -7
  129. package/src/__tests__/host-file-read-tool.test.ts +5 -1
  130. package/src/__tests__/host-transfer-proxy-targeted.test.ts +6 -4
  131. package/src/__tests__/http-conversation-lineage.test.ts +2 -5
  132. package/src/__tests__/http-user-message-parity.test.ts +44 -0
  133. package/src/__tests__/identity-routes.test.ts +13 -13
  134. package/src/__tests__/image-conversion.test.ts +29 -26
  135. package/src/__tests__/image-source-path-reinject.test.ts +2 -2
  136. package/src/__tests__/inference-profile-session-handler.test.ts +95 -0
  137. package/src/__tests__/inference-profile-session-ipc.test.ts +25 -0
  138. package/src/__tests__/introduction-card-resolver.test.ts +6 -1
  139. package/src/__tests__/list-messages-attachments.test.ts +56 -32
  140. package/src/__tests__/list-messages-background-tool-completion.test.ts +4 -4
  141. package/src/__tests__/list-messages-client-message-id.test.ts +2 -2
  142. package/src/__tests__/list-messages-hidden-metadata.test.ts +16 -16
  143. package/src/__tests__/list-messages-page-latest.test.ts +61 -61
  144. package/src/__tests__/list-messages-provider-error.test.ts +6 -6
  145. package/src/__tests__/list-messages-queued.test.ts +49 -6
  146. package/src/__tests__/list-messages-system-card.test.ts +4 -4
  147. package/src/__tests__/list-messages-tool-merge.test.ts +9 -9
  148. package/src/__tests__/llm-resolver-override-or-default.test.ts +141 -0
  149. package/src/__tests__/llm-usage-store.test.ts +55 -0
  150. package/src/__tests__/manual-token-reconciliation.test.ts +45 -0
  151. package/src/__tests__/mcp-tool-annotations-risk.test.ts +133 -0
  152. package/src/__tests__/media-stream-server-integration.test.ts +36 -0
  153. package/src/__tests__/migration-export-http.test.ts +1 -1
  154. package/src/__tests__/migration-import-commit-http.test.ts +1 -1
  155. package/src/__tests__/migration-import-preflight-http.test.ts +1 -1
  156. package/src/__tests__/migration-validate-http.test.ts +1 -1
  157. package/src/__tests__/mtime-cache.test.ts +65 -3
  158. package/src/__tests__/notification-platform-adapter.test.ts +104 -0
  159. package/src/__tests__/notification-schedule-notify-dedup.test.ts +1 -1
  160. package/src/__tests__/oauth-provider-profiles.test.ts +4 -2
  161. package/src/__tests__/oauth-provider-seed-logos.test.ts +65 -0
  162. package/src/__tests__/oauth-provider-serializer.test.ts +17 -0
  163. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  164. package/src/__tests__/openai-responses-prompt-cache.test.ts +5 -2
  165. package/src/__tests__/path-policy.test.ts +164 -1
  166. package/src/__tests__/persist-media-references.test.ts +2 -2
  167. package/src/__tests__/persist-unsendable-image-downscale.test.ts +5 -5
  168. package/src/__tests__/persist-unsendable-image.test.ts +23 -23
  169. package/src/__tests__/persona-resolver.test.ts +154 -0
  170. package/src/__tests__/plugin-api-webhook-url.test.ts +170 -0
  171. package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -0
  172. package/src/__tests__/processing-flag-persist-failure.test.ts +15 -10
  173. package/src/__tests__/profile-availability-incomplete.test.ts +113 -0
  174. package/src/__tests__/profiler-routes.test.ts +13 -13
  175. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +593 -0
  176. package/src/__tests__/proxy-approval-callback.test.ts +3 -3
  177. package/src/__tests__/queued-message-cancel-and-steer.test.ts +189 -0
  178. package/src/__tests__/queued-message-steer-actor-scoping.test.ts +164 -0
  179. package/src/__tests__/resolve-trust-class.test.ts +20 -0
  180. package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -1
  181. package/src/__tests__/run-due-schedules.test.ts +174 -0
  182. package/src/__tests__/runtime-attachment-metadata.test.ts +15 -7
  183. package/src/__tests__/scaffold-managed-skill-tool.test.ts +166 -0
  184. package/src/__tests__/schedule-routes.test.ts +730 -8
  185. package/src/__tests__/schedule-store.test.ts +540 -0
  186. package/src/__tests__/schedule-tools.test.ts +103 -0
  187. package/src/__tests__/scheduler-wake.test.ts +5 -0
  188. package/src/__tests__/secret-routes-platform-proxy.test.ts +29 -1
  189. package/src/__tests__/secure-keys.test.ts +91 -0
  190. package/src/__tests__/server-history-render.test.ts +97 -7
  191. package/src/__tests__/size-guard.test.ts +8 -8
  192. package/src/__tests__/skill-docs-sync-guard.test.ts +3 -2
  193. package/src/__tests__/skill-tool-factory.test.ts +247 -0
  194. package/src/__tests__/skills-install-staging.test.ts +15 -6
  195. package/src/__tests__/slack-edit-ordering-characterization.test.ts +103 -0
  196. package/src/__tests__/slack-inbound-verification.test.ts +4 -1
  197. package/src/__tests__/slack-mention-binding-invariance.test.ts +103 -0
  198. package/src/__tests__/slack-mention-provider-leak-audit.test.ts +135 -0
  199. package/src/__tests__/starter-task-flow.test.ts +5 -4
  200. package/src/__tests__/steer-on-enqueue-question.test.ts +3 -2
  201. package/src/__tests__/stt-language-catalog-parity.test.ts +38 -0
  202. package/src/__tests__/subagent-allowlist-validation.test.ts +26 -4
  203. package/src/__tests__/subagent-cron-run-attribution.test.ts +301 -0
  204. package/src/__tests__/subagent-disposal.test.ts +10 -0
  205. package/src/__tests__/subagent-fork-notifications.test.ts +10 -0
  206. package/src/__tests__/subagent-fork-prompt-role.test.ts +85 -11
  207. package/src/__tests__/subagent-fork-spawn.test.ts +15 -7
  208. package/src/__tests__/subagent-manager-notify.test.ts +148 -0
  209. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  210. package/src/__tests__/subagent-persistence.test.ts +141 -0
  211. package/src/__tests__/subagent-role-registry.test.ts +172 -63
  212. package/src/__tests__/subagent-role-resolution.test.ts +88 -0
  213. package/src/__tests__/subagent-spawn-and-await.test.ts +48 -0
  214. package/src/__tests__/subagent-spawn-tool-fork.test.ts +111 -10
  215. package/src/__tests__/subagent-terminal-message.test.ts +88 -1
  216. package/src/__tests__/subagent-tool-gate-mode.test.ts +225 -10
  217. package/src/__tests__/subagent-tools.test.ts +1901 -108
  218. package/src/__tests__/surface-completion-compaction-boundary.test.ts +124 -0
  219. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +298 -0
  220. package/src/__tests__/system-prompt.test.ts +82 -7
  221. package/src/__tests__/tool-grant-request-escalation.test.ts +40 -1
  222. package/src/__tests__/tool-result-spool.test.ts +74 -18
  223. package/src/__tests__/tool-side-effects-documents.test.ts +174 -0
  224. package/src/__tests__/tool-side-effects-hook-failures.test.ts +129 -0
  225. package/src/__tests__/tools-audio-read.test.ts +10 -10
  226. package/src/__tests__/tools-get-route.test.ts +85 -12
  227. package/src/__tests__/tools-list-cli.test.ts +171 -0
  228. package/src/__tests__/top-level-renderer.test.ts +54 -1
  229. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +68 -1
  230. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -1
  231. package/src/__tests__/turn-tail-chain.test.ts +135 -0
  232. package/src/__tests__/ui-choice-copy-surfaces.test.ts +6 -4
  233. package/src/__tests__/ui-visual-surface.test.ts +5 -4
  234. package/src/__tests__/ui-work-result-surface.test.ts +5 -4
  235. package/src/__tests__/unified-turn-context-visible-app.test.ts +7 -8
  236. package/src/__tests__/usage-routes.test.ts +62 -0
  237. package/src/__tests__/verification-outbound-delivery.test.ts +176 -0
  238. package/src/__tests__/visible-app-context.test.ts +20 -4
  239. package/src/__tests__/web-search-history.test.ts +96 -0
  240. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +6 -6
  241. package/src/__tests__/workspace-migration-140-repair-seed-pinned-memory-v3-live.test.ts +330 -0
  242. package/src/acp/failure-error.ts +3 -5
  243. package/src/agent/attachments.ts +26 -24
  244. package/src/agent/history-repair/history-repair.ts +27 -26
  245. package/src/agent/image-optimize.ts +6 -6
  246. package/src/agent/loop.ts +40 -22
  247. package/src/agent/message-types.ts +3 -3
  248. package/src/api/constants/document-tools.ts +55 -0
  249. package/src/api/events/question-answered.ts +53 -0
  250. package/src/api/events/tool-result.ts +7 -0
  251. package/src/api/index.ts +13 -0
  252. package/src/api/responses/conversation-message.ts +11 -0
  253. package/src/api/surfaces-oauth-connect.test.ts +96 -0
  254. package/src/api/surfaces.ts +36 -8
  255. package/src/approvals/guardian-channel-delivery.ts +83 -0
  256. package/src/approvals/guardian-expiry-notifier.ts +13 -11
  257. package/src/approvals/guardian-request-resolvers.ts +222 -135
  258. package/src/calls/__tests__/voice-session-bridge.test.ts +50 -0
  259. package/src/calls/media-stream-server.ts +9 -1
  260. package/src/calls/voice-session-bridge.ts +38 -0
  261. package/src/channels/__tests__/gateway-verification-sessions.test.ts +3 -3
  262. package/src/channels/config.ts +7 -2
  263. package/src/channels/gateway-verification-sessions.ts +7 -2
  264. package/src/channels/types.ts +7 -1
  265. package/src/cli/commands/__tests__/cache.test.ts +7 -4
  266. package/src/cli/commands/__tests__/cli-test-harness.ts +82 -0
  267. package/src/cli/commands/__tests__/conversations-wake.test.ts +51 -6
  268. package/src/cli/commands/__tests__/inference-models.test.ts +14 -0
  269. package/src/cli/commands/__tests__/inference-profiles.test.ts +44 -1
  270. package/src/cli/commands/__tests__/inference-providers.test.ts +7 -0
  271. package/src/cli/commands/__tests__/plugins.test.ts +692 -0
  272. package/src/cli/commands/__tests__/schedules.test.ts +136 -37
  273. package/src/cli/commands/conversations.ts +7 -3
  274. package/src/cli/commands/credentials.help.ts +7 -0
  275. package/src/cli/commands/inference-profiles.ts +9 -3
  276. package/src/cli/commands/inference.help.ts +16 -2
  277. package/src/cli/commands/mcp.help.ts +4 -2
  278. package/src/cli/commands/memory/memory-retrospective.ts +18 -0
  279. package/src/cli/commands/notifications.ts +3 -6
  280. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +16 -0
  281. package/src/cli/commands/oauth/connect-surface-guidance.ts +25 -7
  282. package/src/cli/commands/oauth/connect.test.ts +26 -1
  283. package/src/cli/commands/oauth/connect.ts +4 -1
  284. package/src/cli/commands/oauth/index.help.ts +3 -1
  285. package/src/cli/commands/oauth/providers.ts +3 -0
  286. package/src/cli/commands/plugins.help.ts +25 -2
  287. package/src/cli/commands/plugins.ts +289 -10
  288. package/src/cli/commands/schedules.help.ts +14 -12
  289. package/src/cli/commands/schedules.ts +19 -5
  290. package/src/cli/commands/tools.help.ts +4 -2
  291. package/src/cli/commands/tools.ts +38 -0
  292. package/src/cli/lib/__tests__/inspect-plugin.test.ts +1 -0
  293. package/src/cli/lib/__tests__/install-from-github.test.ts +79 -0
  294. package/src/cli/lib/__tests__/install-from-platform.test.ts +47 -1
  295. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +86 -7
  296. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +122 -0
  297. package/src/cli/lib/install-from-github.ts +61 -0
  298. package/src/cli/lib/install-from-platform.ts +13 -2
  299. package/src/cli/lib/plugin-surfaces.ts +124 -4
  300. package/src/cli/lib/upgrade-plugin.ts +18 -0
  301. package/src/cli.ts +1 -1
  302. package/src/config/__tests__/deployment-context-defaults.test.ts +73 -0
  303. package/src/config/__tests__/memory-retrospective-schema.test.ts +40 -0
  304. package/src/config/__tests__/plugin-updates-schema.test.ts +43 -0
  305. package/src/config/__tests__/profile-tool-support.test.ts +131 -0
  306. package/src/config/bundled-skills/AGENTS.md +1 -1
  307. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  308. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +3 -3
  309. package/src/config/bundled-skills/schedule/SKILL.md +5 -1
  310. package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
  311. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  312. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +1 -0
  313. package/src/config/bundled-skills/subagent/SKILL.md +53 -22
  314. package/src/config/bundled-skills/subagent/TOOLS.json +14 -13
  315. package/src/config/bundled-skills/visualize/SKILL.md +1 -1
  316. package/src/config/default-profile-catalog.ts +6 -2
  317. package/src/config/feature-flag-registry.json +25 -17
  318. package/src/config/llm-resolver.ts +45 -15
  319. package/src/config/loader.ts +70 -1
  320. package/src/config/profile-tool-support.ts +110 -0
  321. package/src/config/sanitize-for-transfer.ts +11 -0
  322. package/src/config/schema.ts +4 -0
  323. package/src/config/schemas/__tests__/stt.test.ts +20 -5
  324. package/src/config/schemas/channels.ts +13 -0
  325. package/src/config/schemas/memory-retrospective.ts +25 -0
  326. package/src/config/schemas/plugin-updates.ts +55 -0
  327. package/src/config/schemas/services.ts +8 -4
  328. package/src/config/schemas/stt.ts +17 -5
  329. package/src/config/schemas/timeouts.ts +1 -1
  330. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +16 -0
  331. package/src/contacts/guardian-delivery-reader.ts +29 -2
  332. package/src/context/compactor.ts +4 -4
  333. package/src/context/post-turn-tool-result-truncation.ts +32 -16
  334. package/src/context/strip-injections.ts +42 -0
  335. package/src/context/tool-result-spool.ts +32 -18
  336. package/src/credential-execution/ces-runtime.ts +3 -0
  337. package/src/credential-execution/client.ts +22 -1
  338. package/src/credential-execution/process-manager.test.ts +99 -0
  339. package/src/credential-execution/process-manager.ts +62 -16
  340. package/src/daemon/__tests__/abort-null-controller.test.ts +141 -5
  341. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +8 -12
  342. package/src/daemon/__tests__/install-assistant-command.test.ts +234 -0
  343. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +2 -4
  344. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +20 -29
  345. package/src/daemon/channel-ui-capability.ts +4 -4
  346. package/src/daemon/conversation-agent-loop-handlers.ts +117 -61
  347. package/src/daemon/conversation-agent-loop.ts +318 -148
  348. package/src/daemon/conversation-attachments.ts +1 -1
  349. package/src/daemon/conversation-error.ts +8 -1
  350. package/src/daemon/conversation-initial-prompt.ts +16 -38
  351. package/src/daemon/conversation-lifecycle.ts +154 -32
  352. package/src/daemon/conversation-messaging.ts +106 -43
  353. package/src/daemon/conversation-process.ts +131 -33
  354. package/src/daemon/conversation-queue-manager.ts +10 -0
  355. package/src/daemon/conversation-runtime-assembly.ts +9 -4
  356. package/src/daemon/conversation-store.ts +52 -3
  357. package/src/daemon/conversation-surface-state.ts +9 -0
  358. package/src/daemon/conversation-surfaces.ts +403 -210
  359. package/src/daemon/conversation-tool-setup.ts +56 -7
  360. package/src/daemon/conversation-turn-finalize.ts +134 -89
  361. package/src/daemon/conversation.ts +111 -27
  362. package/src/daemon/disk-pressure-guard-lifecycle.ts +2 -2
  363. package/src/daemon/disk-pressure-guard.ts +3 -3
  364. package/src/daemon/doordash-steps.ts +5 -5
  365. package/src/daemon/external-plugins-bootstrap.ts +7 -1
  366. package/src/daemon/handlers/config-channels.ts +48 -5
  367. package/src/daemon/handlers/conversation-history.ts +1 -1
  368. package/src/daemon/handlers/conversations.ts +62 -17
  369. package/src/daemon/handlers/shared.ts +12 -0
  370. package/src/daemon/handlers/skills.ts +11 -1
  371. package/src/daemon/identity-helpers.ts +38 -10
  372. package/src/daemon/install-assistant-command.ts +322 -0
  373. package/src/daemon/lifecycle.ts +14 -3
  374. package/src/daemon/message-types/conversations.ts +6 -7
  375. package/src/daemon/message-types/sync.ts +1 -0
  376. package/src/daemon/persist-media-references.ts +23 -14
  377. package/src/daemon/process-message.ts +6 -6
  378. package/src/daemon/tool-setup-types.ts +19 -122
  379. package/src/daemon/tool-side-effects.ts +17 -94
  380. package/src/daemon/trust-context-types.ts +39 -0
  381. package/src/daemon/trust-context.ts +14 -6
  382. package/src/daemon/turn-tail-chain.ts +73 -0
  383. package/src/daemon/web-search-history.ts +47 -33
  384. package/src/heartbeat/heartbeat-service.ts +6 -6
  385. package/src/ipc/__tests__/socket-path.test.ts +21 -0
  386. package/src/ipc/assistant-server.ts +17 -10
  387. package/src/ipc/cli-client.ts +10 -1
  388. package/src/ipc/routes/__tests__/documents-sync-ipc-routes.test.ts +56 -0
  389. package/src/ipc/routes/documents-sync-ipc-routes.ts +42 -0
  390. package/src/ipc/socket-cleanup.ts +9 -7
  391. package/src/ipc/socket-path.ts +4 -96
  392. package/src/live-voice/__tests__/live-voice-archive.test.ts +16 -16
  393. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +209 -0
  394. package/src/live-voice/__tests__/live-voice-connection.test.ts +20 -0
  395. package/src/live-voice/__tests__/live-voice-integration.test.ts +7 -5
  396. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +265 -0
  397. package/src/live-voice/__tests__/live-voice-stt.test.ts +6 -1
  398. package/src/live-voice/__tests__/live-voice-vad.test.ts +35 -15
  399. package/src/live-voice/__tests__/protocol.test.ts +52 -0
  400. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +3 -0
  401. package/src/live-voice/live-voice-archive.ts +13 -13
  402. package/src/live-voice/live-voice-connection.ts +7 -1
  403. package/src/live-voice/live-voice-photo.ts +169 -0
  404. package/src/live-voice/live-voice-session.ts +108 -4
  405. package/src/live-voice/protocol.ts +89 -1
  406. package/src/mcp/client.ts +10 -0
  407. package/src/messaging/providers/__tests__/callback-routing.test.ts +5 -1
  408. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +66 -4
  409. package/src/messaging/providers/callback-routing.ts +1 -0
  410. package/src/messaging/providers/discord/api.ts +312 -0
  411. package/src/messaging/providers/discord/dm-delivery.test.ts +204 -0
  412. package/src/messaging/providers/discord/render.test.ts +204 -0
  413. package/src/messaging/providers/discord/render.ts +195 -0
  414. package/src/messaging/providers/discord/send.test.ts +179 -0
  415. package/src/messaging/providers/discord/send.ts +212 -0
  416. package/src/messaging/providers/discord/transport.ts +81 -0
  417. package/src/messaging/providers/index.ts +3 -1
  418. package/src/messaging/providers/retry-policy.test.ts +247 -0
  419. package/src/messaging/providers/retry-policy.ts +206 -0
  420. package/src/messaging/providers/slack/mention-source.test.ts +285 -0
  421. package/src/messaging/providers/slack/mention-source.ts +287 -0
  422. package/src/messaging/providers/telegram-bot/api.ts +47 -130
  423. package/src/messaging/providers/whatsapp/api.ts +47 -136
  424. package/src/monitoring/__tests__/db-integrity-sample.test.ts +4 -1
  425. package/src/monitoring/__tests__/plugin-auto-update.test.ts +332 -0
  426. package/src/monitoring/db-integrity-sample.ts +6 -1
  427. package/src/monitoring/plugin-auto-update.ts +359 -0
  428. package/src/monitoring/resource-sampler.ts +21 -5
  429. package/src/monitoring/worker.ts +13 -0
  430. package/src/notifications/__tests__/assistant-reply-producer.test.ts +375 -2
  431. package/src/notifications/__tests__/broadcaster.test.ts +37 -1
  432. package/src/notifications/__tests__/copy-composer.test.ts +165 -0
  433. package/src/notifications/__tests__/emit-signal-pipeline-failure.test.ts +205 -0
  434. package/src/notifications/__tests__/notification-utils.test.ts +249 -0
  435. package/src/notifications/adapters/platform.ts +31 -2
  436. package/src/notifications/assistant-reply-producer.ts +79 -32
  437. package/src/notifications/broadcaster.ts +30 -10
  438. package/src/notifications/copy-composer.ts +69 -0
  439. package/src/notifications/emit-signal.ts +82 -15
  440. package/src/notifications/events-store.ts +10 -2
  441. package/src/notifications/home-feed-side-effect.ts +6 -3
  442. package/src/notifications/notification-utils.ts +173 -5
  443. package/src/notifications/signal.ts +12 -0
  444. package/src/oauth/AGENTS.md +4 -2
  445. package/src/oauth/byo-connection.ts +6 -1
  446. package/src/oauth/credential-token-resolver.ts +3 -10
  447. package/src/oauth/manual-token-connection.ts +29 -64
  448. package/src/oauth/manual-token-providers.ts +62 -0
  449. package/src/oauth/provider-serializer.ts +13 -0
  450. package/src/oauth/seed-providers.ts +62 -11
  451. package/src/onboarding/onboarding-events-store.ts +91 -1
  452. package/src/permissions/question-prompter.test.ts +4 -0
  453. package/src/permissions/question-prompter.ts +21 -4
  454. package/src/persistence/__tests__/conversation-lineage.test.ts +248 -0
  455. package/src/persistence/attachments-store.ts +15 -15
  456. package/src/persistence/conversation-attention-store.ts +30 -10
  457. package/src/persistence/conversation-bootstrap.ts +17 -0
  458. package/src/persistence/conversation-crud.ts +363 -97
  459. package/src/persistence/conversation-group-migration.ts +1 -1
  460. package/src/persistence/conversation-key-store.ts +38 -10
  461. package/src/persistence/conversation-lineage.ts +226 -0
  462. package/src/persistence/conversation-plugin-facade.ts +28 -3
  463. package/src/persistence/conversation-queries.ts +207 -33
  464. package/src/persistence/conversation-types.ts +107 -1
  465. package/src/persistence/jobs-store.ts +32 -8
  466. package/src/persistence/llm-usage-store.ts +22 -0
  467. package/src/persistence/migrations/361-normalize-managed-connection-rows.test.ts +168 -0
  468. package/src/persistence/migrations/361-normalize-managed-connection-rows.ts +76 -0
  469. package/src/persistence/migrations/362-add-conversation-subagent-kind.test.ts +187 -0
  470. package/src/persistence/migrations/362-add-conversation-subagent-kind.ts +93 -0
  471. package/src/persistence/migrations/363-backfill-schedule-inference-profile.test.ts +229 -0
  472. package/src/persistence/migrations/363-backfill-schedule-inference-profile.ts +127 -0
  473. package/src/persistence/migrations/364-add-schedule-source-key.test.ts +120 -0
  474. package/src/persistence/migrations/364-add-schedule-source-key.ts +43 -0
  475. package/src/persistence/migrations/365-add-conversation-fork-strategy.ts +27 -0
  476. package/src/persistence/schema/conversations.ts +32 -0
  477. package/src/persistence/schema/infrastructure.ts +3 -0
  478. package/src/persistence/steps.ts +30 -0
  479. package/src/persistence/subagent-store.ts +148 -6
  480. package/src/platform/managed-speech.test.ts +20 -0
  481. package/src/platform/managed-speech.ts +9 -0
  482. package/src/plugin-api/index.ts +8 -0
  483. package/src/plugin-api/webhook-url.ts +151 -0
  484. package/src/plugins/__tests__/installed-plugin-dirs.test.ts +82 -0
  485. package/src/plugins/__tests__/source-fingerprint.test.ts +5 -1
  486. package/src/plugins/collect-source-versions.ts +4 -27
  487. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +5 -5
  488. package/src/plugins/defaults/exploration-drift/package.json +1 -1
  489. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +4 -2
  490. package/src/plugins/defaults/image-recovery/recover.ts +67 -57
  491. package/src/plugins/defaults/index.ts +1 -1
  492. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +64 -18
  493. package/src/plugins/defaults/memory/__tests__/jobs-worker-outcome-contract.test.ts +242 -0
  494. package/src/plugins/defaults/memory/__tests__/jobs-worker-retrospective-sweep.test.ts +24 -1
  495. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +48 -3
  496. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +511 -19
  497. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +39 -3
  498. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +493 -0
  499. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +5 -4
  500. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -1
  501. package/src/plugins/defaults/memory/job-handlers.ts +87 -5
  502. package/src/plugins/defaults/memory/jobs-worker.ts +60 -6
  503. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +11 -0
  504. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +29 -0
  505. package/src/plugins/defaults/memory/memory-retrospective-job.ts +342 -60
  506. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +23 -3
  507. package/src/plugins/defaults/memory/substrate/consolidation-lock.ts +9 -4
  508. package/src/plugins/defaults/memory/substrate/sweep-job.ts +7 -0
  509. package/src/plugins/defaults/memory/v1/graph/injection.ts +1 -1
  510. package/src/plugins/defaults/memory/v2/router.ts +8 -0
  511. package/src/plugins/defaults/turn-context/unified-turn-context.ts +1 -2
  512. package/src/plugins/external-plugin-loader.ts +17 -8
  513. package/src/plugins/installed-plugin-dirs.ts +101 -0
  514. package/src/plugins/mtime-cache.ts +34 -22
  515. package/src/plugins/types.ts +69 -8
  516. package/src/prompts/persona-resolver.ts +6 -2
  517. package/src/prompts/system-prompt.ts +72 -17
  518. package/src/providers/__tests__/inference.test.ts +40 -0
  519. package/src/providers/__tests__/preflight-resolved-config.test.ts +58 -1
  520. package/src/providers/__tests__/retry-callsite.test.ts +67 -0
  521. package/src/providers/__tests__/vellum-connection-routing.test.ts +42 -11
  522. package/src/providers/anthropic/client.ts +112 -95
  523. package/src/providers/connection-resolution.ts +35 -1
  524. package/src/providers/gemini/client.ts +5 -5
  525. package/src/providers/inference/adapter-factory.ts +16 -10
  526. package/src/providers/inference/auth.ts +16 -0
  527. package/src/providers/inference/connection-availability.ts +164 -12
  528. package/src/providers/inference/connections.ts +25 -1
  529. package/src/providers/media-resolve.ts +21 -11
  530. package/src/providers/model-catalog.ts +15 -0
  531. package/src/providers/openai/chat-completions-provider.ts +4 -4
  532. package/src/providers/openai/responses-provider.ts +12 -25
  533. package/src/providers/registry.ts +9 -4
  534. package/src/providers/retry.ts +25 -0
  535. package/src/providers/server-tool-pairing.ts +134 -0
  536. package/src/providers/speech-to-text/__tests__/resolve.test.ts +187 -6
  537. package/src/providers/speech-to-text/deepgram.ts +1 -0
  538. package/src/providers/speech-to-text/resolve.ts +80 -10
  539. package/src/providers/speech-to-text/vellum-managed.ts +7 -1
  540. package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +3 -1
  541. package/src/providers/types.ts +26 -6
  542. package/src/providers/vellum-model-routing.ts +6 -7
  543. package/src/runtime/AGENTS.md +2 -0
  544. package/src/runtime/__tests__/agent-wake.test.ts +16 -16
  545. package/src/runtime/__tests__/background-job-runner.test.ts +0 -144
  546. package/src/runtime/__tests__/desktop-presence.test.ts +162 -0
  547. package/src/runtime/agent-wake.ts +63 -24
  548. package/src/runtime/assistant-event-hub.ts +87 -12
  549. package/src/runtime/auth/__tests__/guard-tests.test.ts +2 -0
  550. package/src/runtime/auth/__tests__/scopes.test.ts +20 -0
  551. package/src/runtime/auth/same-actor.ts +56 -8
  552. package/src/runtime/auth/scopes.ts +12 -0
  553. package/src/runtime/auth/types.ts +2 -0
  554. package/src/runtime/background-job-runner.ts +5 -27
  555. package/src/runtime/channel-readiness-service.ts +59 -3
  556. package/src/runtime/channel-readiness-types.ts +18 -0
  557. package/src/runtime/desktop-presence.ts +69 -0
  558. package/src/runtime/http-server.ts +12 -0
  559. package/src/runtime/local-actor-identity.ts +11 -9
  560. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  561. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  562. package/src/runtime/routes/__tests__/client-routes.test.ts +186 -14
  563. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +547 -1
  564. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +162 -7
  565. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +14 -10
  566. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +340 -3
  567. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +28 -1
  568. package/src/runtime/routes/__tests__/plugins-routes.test.ts +173 -1
  569. package/src/runtime/routes/__tests__/question-routes.test.ts +64 -0
  570. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +20 -0
  571. package/src/runtime/routes/__tests__/skill-history-route.test.ts +116 -0
  572. package/src/runtime/routes/__tests__/wake-conversation-routes.test.ts +60 -2
  573. package/src/runtime/routes/archive-utils.ts +16 -12
  574. package/src/runtime/routes/attachment-routes.ts +13 -6
  575. package/src/runtime/routes/canned-reply-release.ts +54 -0
  576. package/src/runtime/routes/channel-verification-routes.ts +2 -27
  577. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +5 -0
  578. package/src/runtime/routes/client-routes.ts +114 -2
  579. package/src/runtime/routes/conversation-cli-routes.ts +10 -12
  580. package/src/runtime/routes/conversation-list-routes.ts +59 -10
  581. package/src/runtime/routes/conversation-management-routes.ts +47 -18
  582. package/src/runtime/routes/conversation-query-routes.ts +32 -53
  583. package/src/runtime/routes/conversation-routes.ts +360 -293
  584. package/src/runtime/routes/credential-prompt-routes.ts +23 -2
  585. package/src/runtime/routes/debug-routes.ts +4 -1
  586. package/src/runtime/routes/default-provider-routes.ts +7 -11
  587. package/src/runtime/routes/documents-routes.ts +44 -1
  588. package/src/runtime/routes/events-routes.ts +28 -1
  589. package/src/runtime/routes/host-app-control-routes.ts +2 -0
  590. package/src/runtime/routes/host-bash-routes.ts +2 -0
  591. package/src/runtime/routes/host-browser-routes.ts +2 -0
  592. package/src/runtime/routes/host-cu-routes.ts +2 -0
  593. package/src/runtime/routes/host-file-routes.ts +2 -0
  594. package/src/runtime/routes/host-transfer-routes.ts +4 -0
  595. package/src/runtime/routes/identity-routes.ts +6 -6
  596. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  597. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -3
  598. package/src/runtime/routes/inbound-stages/background-dispatch.ts +14 -0
  599. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -2
  600. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +14 -11
  601. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -17
  602. package/src/runtime/routes/inference-profile-availability-guard.ts +171 -0
  603. package/src/runtime/routes/inference-profile-session-handler.ts +26 -2
  604. package/src/runtime/routes/inference-profile-session-routes.ts +2 -1
  605. package/src/runtime/routes/inference-profiles-routes.ts +152 -73
  606. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -16
  607. package/src/runtime/routes/llm-call-sites-routes.ts +13 -0
  608. package/src/runtime/routes/log-export-routes.ts +1 -1
  609. package/src/runtime/routes/notification-routes.ts +0 -14
  610. package/src/runtime/routes/oauth-providers.ts +1 -0
  611. package/src/runtime/routes/plugins-routes.ts +106 -1
  612. package/src/runtime/routes/profiler-routes.ts +4 -2
  613. package/src/runtime/routes/question-routes.ts +28 -10
  614. package/src/runtime/routes/retrospective-routes.ts +9 -3
  615. package/src/runtime/routes/schedule-routes.ts +210 -12
  616. package/src/runtime/routes/secret-routes.ts +40 -23
  617. package/src/runtime/routes/settings-routes.ts +41 -13
  618. package/src/runtime/routes/skills-routes.ts +79 -0
  619. package/src/runtime/routes/ui-snapshot-routes.ts +1 -0
  620. package/src/runtime/routes/wake-conversation-routes.ts +23 -0
  621. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  622. package/src/runtime/services/conversation-serializer.ts +24 -9
  623. package/src/runtime/sync/documents-sidecar-publish.test.ts +101 -0
  624. package/src/runtime/sync/resource-sync-events.ts +79 -1
  625. package/src/runtime/sync/worker-daemon-notify.test.ts +34 -0
  626. package/src/runtime/sync/worker-daemon-notify.ts +44 -6
  627. package/src/runtime/verification-outbound-actions.ts +250 -222
  628. package/src/runtime/verification-templates.ts +41 -30
  629. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +614 -0
  630. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1066 -0
  631. package/src/schedule/inference-profile.ts +64 -3
  632. package/src/schedule/plugin-schedule-declarations.ts +518 -0
  633. package/src/schedule/plugin-schedule-reconciler.ts +605 -0
  634. package/src/schedule/plugin-schedules-gate.ts +23 -0
  635. package/src/schedule/schedule-store.ts +520 -26
  636. package/src/schedule/scheduler.ts +38 -0
  637. package/src/security/credential-backend.ts +51 -4
  638. package/src/security/encrypted-store.ts +64 -42
  639. package/src/security/secure-keys.ts +55 -7
  640. package/src/skills/catalog-install.ts +20 -7
  641. package/src/skills/inline-command-runner.ts +2 -7
  642. package/src/skills/skill-history.test.ts +269 -0
  643. package/src/skills/skill-history.ts +250 -0
  644. package/src/skills/skillssh-registry.ts +1 -1
  645. package/src/stt/daemon-batch-transcriber.ts +8 -6
  646. package/src/subagent/__tests__/consult-prompt.test.ts +25 -0
  647. package/src/subagent/consult-prompt.ts +3 -1
  648. package/src/subagent/consult-transcript.ts +1 -1
  649. package/src/subagent/manager.ts +304 -44
  650. package/src/subagent/progress-events.ts +35 -0
  651. package/src/subagent/role-resolution.ts +88 -0
  652. package/src/subagent/types.ts +302 -67
  653. package/src/subagent/validate-allowlists.ts +3 -4
  654. package/src/telegram/__tests__/webhook-health.test.ts +73 -1
  655. package/src/telegram/webhook-health.ts +101 -3
  656. package/src/telemetry/__tests__/telemetry-event-fixtures.ts +2 -0
  657. package/src/telemetry/activation-funnel.ts +6 -1
  658. package/src/telemetry/live-voice-funnel.ts +75 -0
  659. package/src/telemetry/telemetry-event-sources.ts +6 -0
  660. package/src/telemetry/telemetry-wire-source.json +1 -1
  661. package/src/telemetry/telemetry-wire.generated.ts +2 -0
  662. package/src/telemetry/types.ts +23 -4
  663. package/src/telemetry/usage-telemetry-reporter.test.ts +4 -0
  664. package/src/tools/__tests__/subagent-workflow-tool-input-schemas.test.ts +1 -1
  665. package/src/tools/ask-question/ask-question-tool.test.ts +118 -3
  666. package/src/tools/ask-question/ask-question-tool.ts +56 -2
  667. package/src/tools/browser/browser-execution.ts +1 -1
  668. package/src/tools/calls/call-start.ts +8 -5
  669. package/src/tools/conversation-groups/move_to_group.ts +10 -12
  670. package/src/tools/document/document-tool.ts +11 -4
  671. package/src/tools/execution-timeout.ts +9 -2
  672. package/src/tools/executor.ts +7 -6
  673. package/src/tools/filesystem/edit.ts +1 -1
  674. package/src/tools/filesystem/list.ts +1 -1
  675. package/src/tools/filesystem/read.ts +7 -13
  676. package/src/tools/filesystem/search.ts +23 -21
  677. package/src/tools/filesystem/write.ts +4 -1
  678. package/src/tools/host-filesystem/edit.ts +7 -2
  679. package/src/tools/host-filesystem/read.ts +21 -8
  680. package/src/tools/host-filesystem/write.ts +10 -2
  681. package/src/tools/host-terminal/host-shell.ts +2 -2
  682. package/src/tools/mcp/mcp-tool-factory.ts +25 -1
  683. package/src/tools/permission-checker.ts +8 -3
  684. package/src/tools/schedule/delete.ts +13 -1
  685. package/src/tools/schedule/list.ts +14 -3
  686. package/src/tools/schedule/update.ts +56 -33
  687. package/src/tools/shared/filesystem/audio-read.ts +6 -4
  688. package/src/tools/shared/filesystem/file-ops-service.ts +176 -116
  689. package/src/tools/shared/filesystem/image-read.ts +12 -12
  690. package/src/tools/shared/filesystem/path-policy.ts +70 -5
  691. package/src/tools/shared/filesystem/size-guard.ts +6 -6
  692. package/src/tools/shared/input-misuse.ts +72 -0
  693. package/src/tools/skills/scaffold-managed.ts +82 -0
  694. package/src/tools/skills/skill-tool-factory.ts +14 -1
  695. package/src/tools/subagent/consult-deadline.ts +12 -7
  696. package/src/tools/subagent/message.ts +3 -1
  697. package/src/tools/subagent/read.ts +66 -4
  698. package/src/tools/subagent/spawn.ts +500 -78
  699. package/src/tools/tool-approval-handler.ts +45 -4
  700. package/src/tools/types.ts +22 -3
  701. package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
  702. package/src/tools/ui-surface/visual-validation.ts +2 -0
  703. package/src/usage/__tests__/subagent-attribution.test.ts +109 -0
  704. package/src/usage/subagent-attribution.ts +108 -0
  705. package/src/util/__tests__/sqlite-retry.test.ts +112 -0
  706. package/src/util/abort-reasons.ts +16 -0
  707. package/src/util/ansi.ts +44 -0
  708. package/src/util/disk-usage.ts +17 -13
  709. package/src/util/image-conversion.ts +20 -13
  710. package/src/util/schedule-source-key.ts +23 -0
  711. package/src/util/worker-process.ts +7 -1
  712. package/src/workspace/git-service.ts +33 -9
  713. package/src/workspace/migrations/140-repair-seed-pinned-memory-v3-live.ts +260 -0
  714. package/src/workspace/migrations/141-stt-english-default-to-multilingual.ts +111 -0
  715. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +126 -0
  716. package/src/workspace/migrations/registry.ts +4 -0
  717. package/src/workspace/top-level-renderer.ts +15 -2
  718. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -357
  719. package/src/daemon/install-symlink.ts +0 -207
  720. package/src/notifications/deferred-emit.ts +0 -147
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Regression tests for what a failed emit leaves behind in the events store.
3
+ *
4
+ * `createEvent` claims the producer's dedupe key as its first pipeline step.
5
+ * A failure after that point releases the claim, so the retry is not
6
+ * short-circuited as "deduplicated at event store level" by an emit that
7
+ * never reached a verdict. A completed emit keeps its claim so real
8
+ * duplicates still collapse.
9
+ *
10
+ * A failure that already reached a channel keeps its claim too: the retry
11
+ * would broadcast under a fresh decision id, and delivery dedupe is keyed to
12
+ * the decision, so every channel that already went out would go out twice.
13
+ *
14
+ * The events store, its DB, and the deterministic checks are the real ones;
15
+ * only the decision engine, the dispatch step, and the adapter graph around
16
+ * them are stubbed.
17
+ */
18
+
19
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
20
+
21
+ const dispatchDecisionMock = mock();
22
+
23
+ mock.module("../../channels/config.js", () => ({
24
+ getDeliverableChannels: () => ["vellum"],
25
+ }));
26
+
27
+ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
28
+ getGuardianDelivery: async () => [],
29
+ guardianForChannel: () => undefined,
30
+ }));
31
+
32
+ mock.module("../../platform/client.js", () => ({
33
+ VellumPlatformClient: class {
34
+ static async create(): Promise<null> {
35
+ return null;
36
+ }
37
+ },
38
+ isPlatformClientConfigured: async () => false,
39
+ }));
40
+
41
+ mock.module("../broadcaster.js", () => ({
42
+ NotificationBroadcaster: class {
43
+ constructor(_adapters: unknown[]) {}
44
+ setOnConversationCreated(_fn: unknown) {}
45
+ },
46
+ }));
47
+
48
+ const DEDUPE_KEY = "schedule-definition-error:plugin:news/digest:2026-01-01";
49
+
50
+ mock.module("../decision-engine.js", () => ({
51
+ evaluateSignal: async () => ({
52
+ shouldNotify: true,
53
+ selectedChannels: ["vellum"],
54
+ reasoningSummary: "deliver",
55
+ renderedCopy: {
56
+ vellum: { title: "Plugin schedule error", body: "digest failed to load" },
57
+ },
58
+ dedupeKey: DEDUPE_KEY,
59
+ confidence: 0.9,
60
+ fallbackUsed: false,
61
+ }),
62
+ enforceRoutingIntent: (decision: unknown) => decision,
63
+ }));
64
+
65
+ mock.module("../decisions-store.js", () => ({
66
+ updateDecision: () => {},
67
+ }));
68
+
69
+ mock.module("../home-feed-side-effect.js", () => ({
70
+ writeHomeFeedItemForSignal: async () => {},
71
+ }));
72
+
73
+ mock.module("../runtime-dispatch.js", () => ({
74
+ dispatchDecision: (...args: unknown[]) => dispatchDecisionMock(...args),
75
+ }));
76
+
77
+ import { getDb } from "../../persistence/db-connection.js";
78
+ import { initializeDb } from "../../persistence/db-init.js";
79
+ import { notificationEvents } from "../../persistence/schema/index.js";
80
+ import type { EmitSignalParams } from "../emit-signal.js";
81
+ import type { NotificationDeliveryResult } from "../types.js";
82
+
83
+ await initializeDb();
84
+
85
+ const { emitNotificationSignal } = await import("../emit-signal.js");
86
+
87
+ function emit() {
88
+ const params: EmitSignalParams<string> = {
89
+ sourceEventName: "schedule.definition_error",
90
+ sourceChannel: "scheduler",
91
+ sourceContextId: "plugin:news/digest",
92
+ dedupeKey: DEDUPE_KEY,
93
+ contextPayload: { pluginName: "news", scheduleName: "digest" },
94
+ attentionHints: {
95
+ requiresAction: false,
96
+ urgency: "medium",
97
+ isAsyncBackground: true,
98
+ visibleInSourceNow: false,
99
+ },
100
+ };
101
+ return emitNotificationSignal(params);
102
+ }
103
+
104
+ /**
105
+ * Stub one dispatch as a broadcast that settles the given channels and then
106
+ * throws on a later channel's prep. What already happened leaves the
107
+ * broadcast through the results sink; the return value dies with the throw.
108
+ */
109
+ function throwAfterChannels(...settled: NotificationDeliveryResult[]): void {
110
+ dispatchDecisionMock.mockImplementationOnce(
111
+ (
112
+ _signal: unknown,
113
+ _decision: unknown,
114
+ _broadcaster: unknown,
115
+ options: { resultsSink?: NotificationDeliveryResult[] },
116
+ ) => {
117
+ options.resultsSink?.push(...settled);
118
+ throw new Error("conversation pairing failed for a later channel");
119
+ },
120
+ );
121
+ }
122
+
123
+ beforeEach(() => {
124
+ getDb().delete(notificationEvents).run();
125
+ dispatchDecisionMock.mockReset();
126
+ dispatchDecisionMock.mockResolvedValue({
127
+ dispatched: true,
128
+ reason: "Dispatched to 1/1 channels",
129
+ deliveryResults: [],
130
+ });
131
+ });
132
+
133
+ describe("emitNotificationSignal dedupe claim on failure", () => {
134
+ test("a retry after a mid-pipeline failure dispatches", async () => {
135
+ dispatchDecisionMock.mockRejectedValueOnce(new Error("transient outage"));
136
+
137
+ const failed = await emit();
138
+ expect(failed.pipelineFailed).toBe(true);
139
+ expect(failed.dispatched).toBe(false);
140
+
141
+ const retried = await emit();
142
+ expect(retried.pipelineFailed).toBe(false);
143
+ expect(retried.deduplicated).toBe(false);
144
+ expect(retried.dispatched).toBe(true);
145
+
146
+ // The failed attempt keeps its row for the audit trail; only its claim on
147
+ // the dedupe key is released.
148
+ const rows = getDb().select().from(notificationEvents).all();
149
+ expect(rows).toHaveLength(2);
150
+ expect(rows.find((r) => r.id === failed.signalId)?.dedupeKey).toBeNull();
151
+ expect(rows.find((r) => r.id === retried.signalId)?.dedupeKey).toBe(
152
+ DEDUPE_KEY,
153
+ );
154
+ });
155
+
156
+ // "sent" is a completed channel delivery; "pending" is the platform push
157
+ // that passed its outcome deadline and is still in flight. Both are
158
+ // side effects a retry would repeat.
159
+ for (const status of ["sent", "pending"] as const) {
160
+ test(`a failure after a ${status} channel keeps its dedupe key`, async () => {
161
+ throwAfterChannels({
162
+ channel: "vellum",
163
+ destination: "vellum",
164
+ status,
165
+ });
166
+
167
+ const partial = await emit();
168
+ expect(partial.pipelineFailed).toBe(true);
169
+
170
+ const retried = await emit();
171
+ expect(retried.deduplicated).toBe(true);
172
+ expect(retried.dispatched).toBe(false);
173
+ expect(dispatchDecisionMock).toHaveBeenCalledTimes(1);
174
+
175
+ const rows = getDb().select().from(notificationEvents).all();
176
+ expect(rows).toHaveLength(1);
177
+ expect(rows[0]!.dedupeKey).toBe(DEDUPE_KEY);
178
+ });
179
+ }
180
+
181
+ test("a failure after only skipped and failed channels releases its key", async () => {
182
+ throwAfterChannels(
183
+ { channel: "slack", destination: "slack", status: "skipped" },
184
+ { channel: "telegram", destination: "telegram", status: "failed" },
185
+ );
186
+
187
+ const failed = await emit();
188
+ expect(failed.pipelineFailed).toBe(true);
189
+
190
+ const retried = await emit();
191
+ expect(retried.deduplicated).toBe(false);
192
+ expect(retried.dispatched).toBe(true);
193
+ });
194
+
195
+ test("a completed emit keeps its dedupe key, so a repeat is deduplicated", async () => {
196
+ const first = await emit();
197
+ expect(first.dispatched).toBe(true);
198
+
199
+ const second = await emit();
200
+ expect(second.deduplicated).toBe(true);
201
+ expect(second.dispatched).toBe(false);
202
+ expect(second.pipelineFailed).toBe(false);
203
+ expect(dispatchDecisionMock).toHaveBeenCalledTimes(1);
204
+ });
205
+ });
@@ -0,0 +1,249 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ describeMedia,
5
+ mediaEmbeds,
6
+ stripMarkdownForPreview,
7
+ } from "../notification-utils.js";
8
+
9
+ /**
10
+ * The producer collapses whitespace after flattening, so most assertions read
11
+ * the flattened text the way a lock screen would.
12
+ */
13
+ function preview(value: string): string {
14
+ return stripMarkdownForPreview(value).replace(/\s+/g, " ").trim();
15
+ }
16
+
17
+ describe("stripMarkdownForPreview", () => {
18
+ describe("media embeds", () => {
19
+ test("drops an embed whole, alt text included", () => {
20
+ expect(
21
+ preview("![vellum scene](vellum://workspace/clients/vellum-cut.mp4)"),
22
+ ).toBe("");
23
+ });
24
+
25
+ test("reduces an embed-only reply to empty so the fallback is reachable", () => {
26
+ const reply = [
27
+ "![vellum scene](vellum://workspace/clients/web/public/cut.mp4)",
28
+ "![hero animation](vellum://workspace/repos/hero.mp4)",
29
+ ].join(" ");
30
+ expect(preview(reply)).toBe("");
31
+ });
32
+
33
+ test("keeps the surrounding prose", () => {
34
+ expect(
35
+ preview("Here is the scene: ![vellum scene](vellum://workspace/a.mp4)"),
36
+ ).toBe("Here is the scene:");
37
+ });
38
+
39
+ test("leaves no stray bang behind", () => {
40
+ expect(preview("![alt](vellum://workspace/a.png)")).not.toContain("!");
41
+ });
42
+
43
+ // An embed needs no surrounding whitespace, so dropping it outright would
44
+ // fuse the prose on either side into one word.
45
+ test("separates the prose an inline embed sat between", () => {
46
+ expect(preview("before![chart](https://example.com/a.png)after")).toBe(
47
+ "before after",
48
+ );
49
+ });
50
+
51
+ test("unwraps a plain link to its text", () => {
52
+ expect(preview("See [report.pdf](vellum://workspace/report.pdf)")).toBe(
53
+ "See report.pdf",
54
+ );
55
+ });
56
+ });
57
+
58
+ // Fenced content is code, not prose. Flattening it would rewrite the very
59
+ // thing being previewed.
60
+ describe("fenced code", () => {
61
+ test("drops fence lines and keeps the fenced code", () => {
62
+ expect(preview("Fixed it:\n```ts\nconst a = 1;\n```")).toBe(
63
+ "Fixed it: const a = 1;",
64
+ );
65
+ });
66
+
67
+ test("leaves a comment inside a fence with its hash", () => {
68
+ expect(preview("Run:\n```sh\n# build first\nmake\n```")).toBe(
69
+ "Run: # build first make",
70
+ );
71
+ });
72
+
73
+ test("leaves a rule and a piped expression inside a fence alone", () => {
74
+ expect(preview("```sh\n---\n| a | b |\n```")).toBe("--- | a | b |");
75
+ });
76
+
77
+ test("still flattens prose after the closing fence", () => {
78
+ expect(preview("```\ncode\n```\n## After")).toBe("code After");
79
+ });
80
+ });
81
+
82
+ describe("block markers", () => {
83
+ test("drops heading markers", () => {
84
+ expect(preview("## Summary\nAll green.")).toBe("Summary All green.");
85
+ });
86
+
87
+ test("drops blockquote markers", () => {
88
+ expect(preview("> quoted line")).toBe("quoted line");
89
+ });
90
+
91
+ test("drops unordered and ordered list markers", () => {
92
+ expect(preview("- one\n- two")).toBe("one two");
93
+ expect(preview("1. first\n2) second")).toBe("first second");
94
+ });
95
+
96
+ test("drops horizontal rules", () => {
97
+ expect(preview("before\n---\nafter")).toBe("before after");
98
+ });
99
+ });
100
+
101
+ describe("tables", () => {
102
+ test("unwraps cells and drops the delimiter row", () => {
103
+ const table = "| Env | Key |\n|---|---|\n| dev | 4Y4L |";
104
+ expect(preview(table)).toBe("Env Key dev 4Y4L");
105
+ });
106
+
107
+ test("leaves a pipe in prose alone", () => {
108
+ expect(preview("run ls | grep foo")).toBe("run ls | grep foo");
109
+ });
110
+ });
111
+
112
+ // Cases a hand-rolled marker scanner gets wrong and the parser gets right.
113
+ describe("syntax that needs a real parser", () => {
114
+ test("reads a destination whose URL has balanced parentheses", () => {
115
+ expect(preview("![chart](https://cdn.example.com/a_(1).png)")).toBe("");
116
+ });
117
+
118
+ test("closes a fence only on its own delimiter", () => {
119
+ expect(preview("```js\n~~~\nconst a = 1;\n~~~\n```")).toBe(
120
+ "~~~ const a = 1; ~~~",
121
+ );
122
+ });
123
+
124
+ test("flattens a GFM table that omits its outer pipes", () => {
125
+ expect(preview("Env | Key\n--- | ---\ndev | 4Y4L")).toBe(
126
+ "Env Key dev 4Y4L",
127
+ );
128
+ });
129
+
130
+ test("keeps embed syntax quoted inside a code span", () => {
131
+ expect(preview("Use `![alt](https://example.com/a.png)` here")).toBe(
132
+ "Use ![alt](https://example.com/a.png) here",
133
+ );
134
+ });
135
+
136
+ test("keeps escaped embed syntax", () => {
137
+ expect(preview("\\![alt](https://example.com/a.png)")).toContain("alt");
138
+ });
139
+ });
140
+
141
+ // The in-app renderer registers no `rehype-raw`, so raw HTML displays as
142
+ // nothing there. The preview matches rather than showing markup the reader
143
+ // never saw.
144
+ describe("raw HTML", () => {
145
+ test("keeps the text between a tag pair and drops the tags", () => {
146
+ expect(preview("<strong>done</strong>")).toBe("done");
147
+ });
148
+
149
+ test("drops an HTML comment rather than leaking its contents", () => {
150
+ expect(preview("<!-- internal note -->")).toBe("");
151
+ });
152
+
153
+ test("keeps the prose around a dropped tag", () => {
154
+ expect(preview("All <em>green</em> now")).toBe("All green now");
155
+ });
156
+ });
157
+
158
+ describe("what it must not touch", () => {
159
+ test("passes plain prose through unchanged", () => {
160
+ const prose = "Done. The deploy finished and the tests are green.";
161
+ expect(stripMarkdownForPreview(prose)).toBe(prose);
162
+ });
163
+
164
+ test("preserves line structure for multi-line callers", () => {
165
+ expect(stripMarkdownForPreview("first\n\nsecond")).toContain("\n");
166
+ });
167
+
168
+ test("returns empty for empty input", () => {
169
+ expect(stripMarkdownForPreview("")).toBe("");
170
+ });
171
+ });
172
+ });
173
+
174
+ describe("mediaEmbeds", () => {
175
+ const alts = (value: string): string[] =>
176
+ mediaEmbeds(value).map((embed) => embed.alt);
177
+
178
+ test("recovers the alts a flattened reply dropped", () => {
179
+ expect(
180
+ alts(
181
+ "![the Q3 chart](https://cdn.example.com/q3.png) ![scene](vellum://workspace/a.mp4)",
182
+ ),
183
+ ).toEqual(["the Q3 chart", "scene"]);
184
+ });
185
+
186
+ test("keeps an empty alt as a placeholder rather than dropping it", () => {
187
+ expect(alts("![](https://cdn.example.com/a.png)")).toEqual([""]);
188
+ });
189
+
190
+ test("ignores plain links", () => {
191
+ expect(alts("[report.pdf](vellum://workspace/r.pdf)")).toEqual([]);
192
+ });
193
+
194
+ test("returns nothing for prose", () => {
195
+ expect(alts("Done, all green.")).toEqual([]);
196
+ });
197
+
198
+ // The parser resolves an image description to plain text, so a formatted alt
199
+ // cannot reintroduce the markdown this module exists to remove.
200
+ test("returns a formatted alt already flattened", () => {
201
+ expect(alts("![**chart**](https://example.com/a.png)")).toEqual(["chart"]);
202
+ });
203
+
204
+ test("ignores embed syntax quoted inside a code span", () => {
205
+ expect(alts("Use `![alt](https://example.com/a.png)` here")).toEqual([]);
206
+ });
207
+
208
+ // Only untracked embeds need naming from their alt: a caller holding
209
+ // attachment metadata has already counted the tracked ones.
210
+ describe("tracking", () => {
211
+ test("marks workspace and host destinations tracked", () => {
212
+ expect(
213
+ mediaEmbeds(
214
+ "![a](vellum://workspace/a.mp4) ![b](vellum://host/tmp/b.png)",
215
+ ).map((embed) => embed.tracked),
216
+ ).toEqual([true, true]);
217
+ });
218
+
219
+ test("marks a remote destination untracked", () => {
220
+ expect(
221
+ mediaEmbeds("![a](https://cdn.example.com/a.png)")[0]?.tracked,
222
+ ).toBe(false);
223
+ });
224
+
225
+ test("marks a reference embed untracked", () => {
226
+ expect(
227
+ mediaEmbeds("![a][ref]\n\n[ref]: vellum://workspace/a.png")[0]?.tracked,
228
+ ).toBe(false);
229
+ });
230
+ });
231
+ });
232
+
233
+ describe("describeMedia", () => {
234
+ test("names a single label", () => {
235
+ expect(describeMedia(["cut.mp4"])).toBe("Sent cut.mp4");
236
+ });
237
+
238
+ test("counts several rather than listing them", () => {
239
+ expect(describeMedia(["a.png", "b.png"])).toBe("Sent 2 attachments");
240
+ });
241
+
242
+ test("falls back to generic copy for a label that sanitizes away", () => {
243
+ expect(describeMedia(["\0\x07"])).toBe("Sent an attachment");
244
+ });
245
+
246
+ test("returns empty for no labels, leaving the fallback to the caller", () => {
247
+ expect(describeMedia([])).toBe("");
248
+ });
249
+ });
@@ -18,6 +18,11 @@ import {
18
18
  isRetryableStatus,
19
19
  sleep,
20
20
  } from "../../util/retry.js";
21
+ import {
22
+ describeMedia,
23
+ mediaEmbeds,
24
+ stripMarkdownForPreview,
25
+ } from "../notification-utils.js";
21
26
  import type {
22
27
  ChannelAdapter,
23
28
  ChannelDeliveryObserver,
@@ -38,6 +43,30 @@ const RETRY_DELAYS_MS = [250, 1_000, 4_000] as const;
38
43
  // dispatch's outcome -- a hung platform must fail the attempt, not stall it.
39
44
  const ATTEMPT_TIMEOUT_MS = 5_000;
40
45
 
46
+ /**
47
+ * Flatten one alert field for a plain-text surface, recovering copy when
48
+ * flattening leaves nothing behind.
49
+ *
50
+ * An APNs/FCM alert renders no markdown, so markers arrive as literal
51
+ * punctuation. Flattening here covers every event routed to this channel
52
+ * without altering what other channels receive, some of which render markdown
53
+ * deliberately. Newlines survive: iOS renders them, and guardian copy carries a
54
+ * deliberate paragraph break.
55
+ *
56
+ * The recovery is load-bearing rather than cosmetic. Copy composed entirely of
57
+ * media embeds, which the pass-through path copies into both the title and the
58
+ * body, flattens to nothing, and the pipeline's empty-copy guards all run
59
+ * upstream of this adapter. The platform's serializer rejects a blank title,
60
+ * so an unrecovered field costs the whole notification, not just its wording.
61
+ */
62
+ function flattenAlertField(value: string): string {
63
+ const flattened = stripMarkdownForPreview(value);
64
+ if (flattened.trim().length > 0) {
65
+ return flattened;
66
+ }
67
+ return describeMedia(mediaEmbeds(value).map((embed) => embed.alt)) || value;
68
+ }
69
+
41
70
  /** Whether a fetch error is the per-attempt abort timeout firing. */
42
71
  function isAttemptTimeout(err: unknown): boolean {
43
72
  const name = (err as { name?: unknown } | null)?.name;
@@ -108,8 +137,8 @@ export class PlatformPushAdapter implements ChannelAdapter {
108
137
  const body: DispatchBody = {
109
138
  delivery_id: payload.correlationId ?? payload.deliveryId,
110
139
  source_event_name: payload.sourceEventName,
111
- title: payload.copy.title,
112
- body: payload.copy.body,
140
+ title: flattenAlertField(payload.copy.title),
141
+ body: flattenAlertField(payload.copy.body),
113
142
  deep_link_metadata: payload.deepLinkTarget,
114
143
  context_payload: payload.contextPayload,
115
144
  target_guardian_principal_id: targetGuardianPrincipalId,
@@ -23,20 +23,28 @@ import {
23
23
  parseMessageMetadata,
24
24
  } from "../persistence/conversation-crud.js";
25
25
  import {
26
+ isMacOriginatedUserMessage,
26
27
  isReplyPushIneligibleUserMessage,
27
28
  resolveConversationKind,
28
29
  } from "../persistence/conversation-types.js";
29
30
  import { stringifyMessageContent } from "../persistence/message-content.js";
31
+ import { isDesktopAttended } from "../runtime/desktop-presence.js";
30
32
  import { safeParseRecord } from "../util/json.js";
31
33
  import { emitNotificationSignal } from "./emit-signal.js";
32
34
  import {
35
+ describeMedia,
36
+ mediaEmbeds,
33
37
  sanitizeMessagePreview,
34
38
  sanitizeNotificationTitle,
39
+ stripMarkdownForPreview,
35
40
  } from "./notification-utils.js";
36
41
 
37
42
  /** Kill switch for this producer, on by default. */
38
43
  const ASSISTANT_REPLY_PUSH_FLAG = "assistant-reply-push" as const;
39
44
 
45
+ /** Gates the desktop-attended suppression below, on by default. */
46
+ const DESKTOP_PRESENCE_FLAG = "desktop-presence-suppression" as const;
47
+
40
48
  /**
41
49
  * Collapse whitespace runs ahead of the sanitizers' truncation: blank lines and
42
50
  * list indentation would otherwise eat into the copy's length budget.
@@ -46,31 +54,42 @@ function collapseWhitespace(value: string): string {
46
54
  }
47
55
 
48
56
  /**
49
- * Body for a reply whose only user-visible output is attachments. Generated
50
- * files and images are linked to the assistant row by
51
- * `resolveAssistantAttachments` before the turn's terminal SSE, and are
52
- * exposed separately from the row's content blocks, so a file-generation reply
53
- * reaches this producer with no text to preview. Reads attachment metadata
54
- * only (no base64), and only once the text preview has already come up empty.
57
+ * Body naming every piece of media a reply produced, for a reply whose text
58
+ * flattened away.
59
+ *
60
+ * Two sources, counted together so a reply mixing them is not undercounted.
61
+ * Attachment rows cover generated files, `<vellum-attachment />` directives,
62
+ * and `vellum://` embeds alike; they are linked to the assistant row by
63
+ * `resolveAssistantAttachments` before the turn's terminal SSE and exposed
64
+ * separately from its content blocks. Remote embeds leave no row, so their alt
65
+ * text is the only label they have.
55
66
  *
56
- * Returns an empty string when the row has no attachments either, which is the
57
- * genuinely-empty reply the caller suppresses.
67
+ * Tracked embeds are dropped from the second source rather than added twice:
68
+ * a `vellum://` embed is already one of the attachment rows.
69
+ *
70
+ * Reads attachment metadata only (no base64), and the caller invokes this only
71
+ * once the text preview has come up empty.
58
72
  */
59
- function describeAttachmentOnlyReply(assistantMessageId: string): string {
60
- const attachments = getAttachmentMetadataForMessage(assistantMessageId);
61
- if (attachments.length === 0) {
62
- return "";
63
- }
64
- if (attachments.length > 1) {
65
- return `Sent ${attachments.length} attachments`;
66
- }
67
- // Filenames are model- and tool-authored, so one is sanitized before it can
68
- // reach the lock screen. A filename that sanitizes away leaves generic copy
69
- // rather than a dangling "Sent ".
70
- const filename = sanitizeMessagePreview(
71
- collapseWhitespace(attachments[0].originalFilename),
73
+ function describeReplyMedia(text: string, assistantMessageId: string): string {
74
+ const filenames = getAttachmentMetadataForMessage(assistantMessageId).map(
75
+ (attachment) => attachment.originalFilename,
72
76
  );
73
- return filename ? `Sent ${filename}` : "Sent an attachment";
77
+ const embeds = mediaEmbeds(text);
78
+ const tracked = embeds.filter((embed) => embed.tracked);
79
+
80
+ // Resolution is allowed to fail: `resolveAssistantAttachments` skips a file
81
+ // that is missing, oversized, unreadable, or denied at the host-read
82
+ // approval, leaving a tracked embed with no row to stand for it. Rows at
83
+ // least matching the tracked embeds means each one resolved (any surplus
84
+ // being generated files); a shortfall is that many embeds falling back to
85
+ // their own alt rather than vanishing from the count.
86
+ const unresolved = Math.max(0, tracked.length - filenames.length);
87
+
88
+ return describeMedia([
89
+ ...filenames,
90
+ ...tracked.slice(tracked.length - unresolved).map((embed) => embed.alt),
91
+ ...embeds.filter((embed) => !embed.tracked).map((embed) => embed.alt),
92
+ ]);
74
93
  }
75
94
 
76
95
  /**
@@ -93,6 +112,23 @@ function readSuppressionMarkers(
93
112
  return metadataJson ? safeParseRecord(metadataJson) : undefined;
94
113
  }
95
114
 
115
+ /**
116
+ * Desktop attendance, kept fail-open here: a presence read that fails has to
117
+ * send the push, not reach the producer's catch and silence it.
118
+ *
119
+ * No `actorPrincipalId`: the platform delivers this push to the assistant
120
+ * owner's device tokens, and a pod has exactly one owner, so any attended
121
+ * macOS client is that owner's.
122
+ */
123
+ function readDesktopAttended(rlog: pino.Logger): boolean {
124
+ try {
125
+ return isDesktopAttended();
126
+ } catch (err) {
127
+ rlog.warn({ err }, "Desktop presence read failed; treating as unattended");
128
+ return false;
129
+ }
130
+ }
131
+
96
132
  export async function emitAssistantReplyNotification(params: {
97
133
  conversationId: string;
98
134
  assistantMessageId: string;
@@ -161,14 +197,16 @@ export async function emitAssistantReplyNotification(params: {
161
197
  return;
162
198
  }
163
199
 
164
- // A reply whose output is entirely attachments has no text to preview, so
165
- // fall back to naming them rather than suppressing a real reply. A reply
166
- // with neither text nor attachments stays silent.
200
+ // A reply whose output is entirely media has no text to preview, so name
201
+ // the media rather than suppressing a real reply. Markdown is flattened
202
+ // first: a lock screen renders none of it, and an embed-only reply has to
203
+ // reduce to empty for the fallback to be reachable. A reply with neither
204
+ // text nor media stays silent.
205
+ const text = stringifyMessageContent(assistantRow.content);
167
206
  const preview =
168
207
  sanitizeMessagePreview(
169
- collapseWhitespace(stringifyMessageContent(assistantRow.content)),
170
- ) ||
171
- sanitizeMessagePreview(describeAttachmentOnlyReply(assistantMessageId));
208
+ collapseWhitespace(stripMarkdownForPreview(text)),
209
+ ) || sanitizeMessagePreview(describeReplyMedia(text, assistantMessageId));
172
210
  if (!preview) {
173
211
  return;
174
212
  }
@@ -182,6 +220,15 @@ export async function emitAssistantReplyNotification(params: {
182
220
  collapseWhitespace(conversation.title ?? ""),
183
221
  );
184
222
 
223
+ // Read as close to the emit as possible: nothing short-circuits on it.
224
+ // Presence only speaks for a turn the Mac itself opened, on that row's own
225
+ // OS evidence. A turn sent from the phone still needs its push while the
226
+ // Mac sits idle within the desktop client's attendance window.
227
+ const desktopAttended =
228
+ isAssistantFeatureFlagEnabled(DESKTOP_PRESENCE_FLAG) &&
229
+ isMacOriginatedUserMessage(initiatingMetadata) &&
230
+ readDesktopAttended(rlog);
231
+
185
232
  await emitNotificationSignal({
186
233
  sourceEventName: "chat.assistant_reply",
187
234
  sourceChannel: "vellum",
@@ -196,10 +243,10 @@ export async function emitAssistantReplyNotification(params: {
196
243
  // opting into v2.
197
244
  urgency: "medium",
198
245
  isAsyncBackground: false,
199
- // The daemon cannot tell "still viewing" from "just left" at turn end.
200
- // The viewing-on-iPhone case is covered by iOS suppressing remote
201
- // pushes while the app is foregrounded.
202
- visibleInSourceNow: false,
246
+ // Read weakly, as "at the machine this landed on": the attended Mac
247
+ // that opened the turn renders the reply in-app, and its Dock unread
248
+ // badge carries it while the window is hidden.
249
+ visibleInSourceNow: desktopAttended,
203
250
  },
204
251
  contextPayload: {
205
252
  ...(requestedTitle ? { requestedTitle } : {}),