@vellumai/assistant 0.11.2 → 0.11.3-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 (709) 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-queue.test.ts +265 -1
  72. package/src/__tests__/conversation-retry-route.test.ts +3 -1
  73. package/src/__tests__/conversation-store-ephemeral.test.ts +4 -0
  74. package/src/__tests__/conversation-store.test.ts +26 -10
  75. package/src/__tests__/conversation-summarize-route.test.ts +1 -1
  76. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +15 -6
  77. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -5
  78. package/src/__tests__/conversation-surfaces-app-control.test.ts +12 -11
  79. package/src/__tests__/conversation-surfaces-app-open.test.ts +11 -5
  80. package/src/__tests__/conversation-surfaces-data-persist.test.ts +5 -4
  81. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +967 -0
  82. package/src/__tests__/conversation-surfaces-persisted-info.test.ts +312 -0
  83. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +6 -5
  84. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -8
  85. package/src/__tests__/conversation-surfaces-standalone.test.ts +21 -11
  86. package/src/__tests__/conversation-surfaces-state-update.test.ts +7 -11
  87. package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -6
  88. package/src/__tests__/conversation-surfaces-task-progress.test.ts +11 -6
  89. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +9 -6
  90. package/src/__tests__/conversation-tool-setup-attribution.test.ts +70 -7
  91. package/src/__tests__/conversation-wait-for-idle.test.ts +6 -11
  92. package/src/__tests__/credential-prompt-route.test.ts +57 -7
  93. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  94. package/src/__tests__/cu-unified-flow.test.ts +89 -28
  95. package/src/__tests__/custom-group-listing-visibility.test.ts +1 -1
  96. package/src/__tests__/db-schedule-syntax-migration.test.ts +4 -1
  97. package/src/__tests__/discord-access-request-privacy.test.ts +366 -0
  98. package/src/__tests__/discord-requester-notice-privacy.test.ts +87 -0
  99. package/src/__tests__/disk-pressure-guard.test.ts +55 -55
  100. package/src/__tests__/disk-pressure-routes.test.ts +10 -10
  101. package/src/__tests__/disk-pressure-tools.test.ts +2 -2
  102. package/src/__tests__/disk-usage.test.ts +35 -20
  103. package/src/__tests__/document-sync-tags.test.ts +379 -0
  104. package/src/__tests__/document-update-default-surface.test.ts +28 -0
  105. package/src/__tests__/dynamic-skill-background-guard.test.ts +2 -2
  106. package/src/__tests__/emit-signal-routing-intent.test.ts +3 -4
  107. package/src/__tests__/ensure-conversation-exists.test.ts +8 -5
  108. package/src/__tests__/events-dev-bypass-actor.test.ts +183 -1
  109. package/src/__tests__/evict-conversations-for-reload.test.ts +90 -0
  110. package/src/__tests__/file-list-tool.test.ts +12 -10
  111. package/src/__tests__/file-ops-service.test.ts +151 -44
  112. package/src/__tests__/file-read-container-boundary.test.ts +137 -0
  113. package/src/__tests__/file-read-tool.test.ts +21 -9
  114. package/src/__tests__/file-url-path-guard.test.ts +83 -0
  115. package/src/__tests__/filesystem-tools.test.ts +82 -66
  116. package/src/__tests__/guardian-outbound-http.test.ts +8 -8
  117. package/src/__tests__/helpers/gateway-verification-sessions-stub.ts +14 -1
  118. package/src/__tests__/helpers/hub-clients.ts +42 -0
  119. package/src/__tests__/helpers/mock-conversation.ts +46 -0
  120. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +61 -57
  121. package/src/__tests__/history-repair.test.ts +112 -12
  122. package/src/__tests__/host-bash-proxy.test.ts +10 -5
  123. package/src/__tests__/host-bash-routes.test.ts +67 -0
  124. package/src/__tests__/host-browser-proxy.test.ts +2 -2
  125. package/src/__tests__/host-cu-proxy.test.ts +3 -7
  126. package/src/__tests__/host-file-proxy.test.ts +3 -7
  127. package/src/__tests__/host-file-read-tool.test.ts +5 -1
  128. package/src/__tests__/host-transfer-proxy-targeted.test.ts +6 -4
  129. package/src/__tests__/http-conversation-lineage.test.ts +2 -5
  130. package/src/__tests__/http-user-message-parity.test.ts +44 -0
  131. package/src/__tests__/identity-routes.test.ts +13 -13
  132. package/src/__tests__/image-conversion.test.ts +29 -26
  133. package/src/__tests__/image-source-path-reinject.test.ts +2 -2
  134. package/src/__tests__/inference-profile-session-handler.test.ts +95 -0
  135. package/src/__tests__/inference-profile-session-ipc.test.ts +25 -0
  136. package/src/__tests__/introduction-card-resolver.test.ts +6 -1
  137. package/src/__tests__/list-messages-attachments.test.ts +56 -32
  138. package/src/__tests__/list-messages-background-tool-completion.test.ts +4 -4
  139. package/src/__tests__/list-messages-client-message-id.test.ts +2 -2
  140. package/src/__tests__/list-messages-hidden-metadata.test.ts +16 -16
  141. package/src/__tests__/list-messages-page-latest.test.ts +61 -61
  142. package/src/__tests__/list-messages-provider-error.test.ts +6 -6
  143. package/src/__tests__/list-messages-queued.test.ts +49 -6
  144. package/src/__tests__/list-messages-system-card.test.ts +4 -4
  145. package/src/__tests__/list-messages-tool-merge.test.ts +9 -9
  146. package/src/__tests__/llm-resolver-override-or-default.test.ts +141 -0
  147. package/src/__tests__/llm-usage-store.test.ts +55 -0
  148. package/src/__tests__/manual-token-reconciliation.test.ts +45 -0
  149. package/src/__tests__/mcp-tool-annotations-risk.test.ts +133 -0
  150. package/src/__tests__/media-stream-server-integration.test.ts +36 -0
  151. package/src/__tests__/migration-export-http.test.ts +1 -1
  152. package/src/__tests__/migration-import-commit-http.test.ts +1 -1
  153. package/src/__tests__/migration-import-preflight-http.test.ts +1 -1
  154. package/src/__tests__/migration-validate-http.test.ts +1 -1
  155. package/src/__tests__/mtime-cache.test.ts +65 -3
  156. package/src/__tests__/notification-platform-adapter.test.ts +104 -0
  157. package/src/__tests__/notification-schedule-notify-dedup.test.ts +1 -1
  158. package/src/__tests__/oauth-provider-profiles.test.ts +4 -2
  159. package/src/__tests__/oauth-provider-serializer.test.ts +17 -0
  160. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  161. package/src/__tests__/openai-responses-prompt-cache.test.ts +5 -2
  162. package/src/__tests__/path-policy.test.ts +164 -1
  163. package/src/__tests__/persist-media-references.test.ts +2 -2
  164. package/src/__tests__/persist-unsendable-image-downscale.test.ts +5 -5
  165. package/src/__tests__/persist-unsendable-image.test.ts +23 -23
  166. package/src/__tests__/persona-resolver.test.ts +154 -0
  167. package/src/__tests__/plugin-api-webhook-url.test.ts +170 -0
  168. package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -0
  169. package/src/__tests__/processing-flag-persist-failure.test.ts +15 -10
  170. package/src/__tests__/profile-availability-incomplete.test.ts +113 -0
  171. package/src/__tests__/profiler-routes.test.ts +13 -13
  172. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +593 -0
  173. package/src/__tests__/proxy-approval-callback.test.ts +3 -3
  174. package/src/__tests__/queued-message-cancel-and-steer.test.ts +189 -0
  175. package/src/__tests__/queued-message-steer-actor-scoping.test.ts +164 -0
  176. package/src/__tests__/resolve-trust-class.test.ts +20 -0
  177. package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -1
  178. package/src/__tests__/run-due-schedules.test.ts +174 -0
  179. package/src/__tests__/runtime-attachment-metadata.test.ts +15 -7
  180. package/src/__tests__/scaffold-managed-skill-tool.test.ts +166 -0
  181. package/src/__tests__/schedule-routes.test.ts +730 -8
  182. package/src/__tests__/schedule-store.test.ts +540 -0
  183. package/src/__tests__/schedule-tools.test.ts +103 -0
  184. package/src/__tests__/scheduler-wake.test.ts +5 -0
  185. package/src/__tests__/secret-routes-platform-proxy.test.ts +29 -1
  186. package/src/__tests__/secure-keys.test.ts +91 -0
  187. package/src/__tests__/server-history-render.test.ts +97 -7
  188. package/src/__tests__/size-guard.test.ts +8 -8
  189. package/src/__tests__/skill-docs-sync-guard.test.ts +3 -2
  190. package/src/__tests__/skill-tool-factory.test.ts +247 -0
  191. package/src/__tests__/skills-install-staging.test.ts +15 -6
  192. package/src/__tests__/slack-edit-ordering-characterization.test.ts +103 -0
  193. package/src/__tests__/slack-inbound-verification.test.ts +4 -1
  194. package/src/__tests__/slack-mention-binding-invariance.test.ts +103 -0
  195. package/src/__tests__/slack-mention-provider-leak-audit.test.ts +135 -0
  196. package/src/__tests__/starter-task-flow.test.ts +5 -4
  197. package/src/__tests__/steer-on-enqueue-question.test.ts +3 -2
  198. package/src/__tests__/stt-language-catalog-parity.test.ts +38 -0
  199. package/src/__tests__/subagent-allowlist-validation.test.ts +26 -4
  200. package/src/__tests__/subagent-cron-run-attribution.test.ts +301 -0
  201. package/src/__tests__/subagent-disposal.test.ts +10 -0
  202. package/src/__tests__/subagent-fork-notifications.test.ts +10 -0
  203. package/src/__tests__/subagent-fork-prompt-role.test.ts +85 -11
  204. package/src/__tests__/subagent-fork-spawn.test.ts +15 -7
  205. package/src/__tests__/subagent-manager-notify.test.ts +148 -0
  206. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  207. package/src/__tests__/subagent-persistence.test.ts +141 -0
  208. package/src/__tests__/subagent-role-registry.test.ts +172 -63
  209. package/src/__tests__/subagent-role-resolution.test.ts +88 -0
  210. package/src/__tests__/subagent-spawn-and-await.test.ts +48 -0
  211. package/src/__tests__/subagent-spawn-tool-fork.test.ts +111 -10
  212. package/src/__tests__/subagent-terminal-message.test.ts +88 -1
  213. package/src/__tests__/subagent-tool-gate-mode.test.ts +225 -10
  214. package/src/__tests__/subagent-tools.test.ts +1901 -108
  215. package/src/__tests__/surface-completion-compaction-boundary.test.ts +124 -0
  216. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +298 -0
  217. package/src/__tests__/system-prompt.test.ts +82 -7
  218. package/src/__tests__/tool-result-spool.test.ts +74 -18
  219. package/src/__tests__/tool-side-effects-documents.test.ts +174 -0
  220. package/src/__tests__/tool-side-effects-hook-failures.test.ts +129 -0
  221. package/src/__tests__/tools-audio-read.test.ts +10 -10
  222. package/src/__tests__/tools-get-route.test.ts +85 -12
  223. package/src/__tests__/tools-list-cli.test.ts +171 -0
  224. package/src/__tests__/top-level-renderer.test.ts +54 -1
  225. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -1
  226. package/src/__tests__/turn-tail-chain.test.ts +135 -0
  227. package/src/__tests__/ui-choice-copy-surfaces.test.ts +6 -4
  228. package/src/__tests__/ui-visual-surface.test.ts +5 -4
  229. package/src/__tests__/ui-work-result-surface.test.ts +5 -4
  230. package/src/__tests__/unified-turn-context-visible-app.test.ts +7 -8
  231. package/src/__tests__/usage-routes.test.ts +62 -0
  232. package/src/__tests__/verification-outbound-delivery.test.ts +176 -0
  233. package/src/__tests__/visible-app-context.test.ts +20 -4
  234. package/src/__tests__/web-search-history.test.ts +96 -0
  235. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +6 -6
  236. package/src/__tests__/workspace-migration-140-repair-seed-pinned-memory-v3-live.test.ts +330 -0
  237. package/src/acp/failure-error.ts +3 -5
  238. package/src/agent/attachments.ts +26 -24
  239. package/src/agent/history-repair/history-repair.ts +27 -26
  240. package/src/agent/image-optimize.ts +6 -6
  241. package/src/agent/loop.ts +40 -22
  242. package/src/agent/message-types.ts +3 -3
  243. package/src/api/constants/document-tools.ts +55 -0
  244. package/src/api/events/question-answered.ts +53 -0
  245. package/src/api/events/tool-result.ts +7 -0
  246. package/src/api/index.ts +13 -0
  247. package/src/api/responses/conversation-message.ts +11 -0
  248. package/src/api/surfaces-oauth-connect.test.ts +96 -0
  249. package/src/api/surfaces.ts +36 -8
  250. package/src/approvals/guardian-channel-delivery.ts +83 -0
  251. package/src/approvals/guardian-expiry-notifier.ts +13 -11
  252. package/src/approvals/guardian-request-resolvers.ts +216 -132
  253. package/src/calls/__tests__/voice-session-bridge.test.ts +50 -0
  254. package/src/calls/media-stream-server.ts +9 -1
  255. package/src/calls/voice-session-bridge.ts +38 -0
  256. package/src/channels/__tests__/gateway-verification-sessions.test.ts +3 -3
  257. package/src/channels/config.ts +7 -2
  258. package/src/channels/gateway-verification-sessions.ts +7 -2
  259. package/src/channels/types.ts +7 -1
  260. package/src/cli/commands/__tests__/cache.test.ts +7 -4
  261. package/src/cli/commands/__tests__/cli-test-harness.ts +82 -0
  262. package/src/cli/commands/__tests__/conversations-wake.test.ts +51 -6
  263. package/src/cli/commands/__tests__/inference-models.test.ts +14 -0
  264. package/src/cli/commands/__tests__/inference-profiles.test.ts +44 -1
  265. package/src/cli/commands/__tests__/inference-providers.test.ts +7 -0
  266. package/src/cli/commands/__tests__/plugins.test.ts +692 -0
  267. package/src/cli/commands/__tests__/schedules.test.ts +136 -37
  268. package/src/cli/commands/conversations.ts +7 -3
  269. package/src/cli/commands/credentials.help.ts +7 -0
  270. package/src/cli/commands/inference-profiles.ts +9 -3
  271. package/src/cli/commands/inference.help.ts +16 -2
  272. package/src/cli/commands/mcp.help.ts +4 -2
  273. package/src/cli/commands/memory/memory-retrospective.ts +18 -0
  274. package/src/cli/commands/notifications.ts +3 -6
  275. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +16 -0
  276. package/src/cli/commands/oauth/connect-surface-guidance.ts +25 -7
  277. package/src/cli/commands/oauth/connect.test.ts +26 -1
  278. package/src/cli/commands/oauth/connect.ts +4 -1
  279. package/src/cli/commands/oauth/index.help.ts +3 -1
  280. package/src/cli/commands/oauth/providers.ts +3 -0
  281. package/src/cli/commands/plugins.help.ts +25 -2
  282. package/src/cli/commands/plugins.ts +289 -10
  283. package/src/cli/commands/schedules.help.ts +14 -12
  284. package/src/cli/commands/schedules.ts +19 -5
  285. package/src/cli/commands/tools.help.ts +4 -2
  286. package/src/cli/commands/tools.ts +38 -0
  287. package/src/cli/lib/__tests__/inspect-plugin.test.ts +1 -0
  288. package/src/cli/lib/__tests__/install-from-github.test.ts +79 -0
  289. package/src/cli/lib/__tests__/install-from-platform.test.ts +47 -1
  290. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +86 -7
  291. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +122 -0
  292. package/src/cli/lib/install-from-github.ts +61 -0
  293. package/src/cli/lib/install-from-platform.ts +13 -2
  294. package/src/cli/lib/plugin-surfaces.ts +124 -4
  295. package/src/cli/lib/upgrade-plugin.ts +18 -0
  296. package/src/cli.ts +1 -1
  297. package/src/config/__tests__/deployment-context-defaults.test.ts +73 -0
  298. package/src/config/__tests__/memory-retrospective-schema.test.ts +40 -0
  299. package/src/config/__tests__/plugin-updates-schema.test.ts +43 -0
  300. package/src/config/__tests__/profile-tool-support.test.ts +131 -0
  301. package/src/config/bundled-skills/AGENTS.md +1 -1
  302. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  303. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +3 -3
  304. package/src/config/bundled-skills/schedule/SKILL.md +5 -1
  305. package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
  306. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  307. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +1 -0
  308. package/src/config/bundled-skills/subagent/SKILL.md +53 -22
  309. package/src/config/bundled-skills/subagent/TOOLS.json +14 -13
  310. package/src/config/bundled-skills/visualize/SKILL.md +1 -1
  311. package/src/config/default-profile-catalog.ts +6 -2
  312. package/src/config/feature-flag-registry.json +25 -17
  313. package/src/config/llm-resolver.ts +45 -15
  314. package/src/config/loader.ts +70 -1
  315. package/src/config/profile-tool-support.ts +110 -0
  316. package/src/config/sanitize-for-transfer.ts +11 -0
  317. package/src/config/schema.ts +4 -0
  318. package/src/config/schemas/__tests__/stt.test.ts +20 -5
  319. package/src/config/schemas/channels.ts +13 -0
  320. package/src/config/schemas/memory-retrospective.ts +25 -0
  321. package/src/config/schemas/plugin-updates.ts +55 -0
  322. package/src/config/schemas/services.ts +8 -4
  323. package/src/config/schemas/stt.ts +17 -5
  324. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +16 -0
  325. package/src/contacts/guardian-delivery-reader.ts +29 -2
  326. package/src/context/compactor.ts +4 -4
  327. package/src/context/post-turn-tool-result-truncation.ts +32 -16
  328. package/src/context/strip-injections.ts +42 -0
  329. package/src/context/tool-result-spool.ts +32 -18
  330. package/src/credential-execution/ces-runtime.ts +3 -0
  331. package/src/credential-execution/client.ts +22 -1
  332. package/src/credential-execution/process-manager.test.ts +99 -0
  333. package/src/credential-execution/process-manager.ts +62 -16
  334. package/src/daemon/__tests__/abort-null-controller.test.ts +141 -5
  335. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +8 -12
  336. package/src/daemon/__tests__/install-assistant-command.test.ts +234 -0
  337. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +2 -4
  338. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +20 -29
  339. package/src/daemon/channel-ui-capability.ts +4 -4
  340. package/src/daemon/conversation-agent-loop-handlers.ts +117 -61
  341. package/src/daemon/conversation-agent-loop.ts +296 -145
  342. package/src/daemon/conversation-attachments.ts +1 -1
  343. package/src/daemon/conversation-error.ts +8 -1
  344. package/src/daemon/conversation-initial-prompt.ts +16 -38
  345. package/src/daemon/conversation-lifecycle.ts +154 -32
  346. package/src/daemon/conversation-messaging.ts +85 -42
  347. package/src/daemon/conversation-process.ts +76 -28
  348. package/src/daemon/conversation-runtime-assembly.ts +9 -4
  349. package/src/daemon/conversation-store.ts +52 -3
  350. package/src/daemon/conversation-surface-state.ts +9 -0
  351. package/src/daemon/conversation-surfaces.ts +403 -210
  352. package/src/daemon/conversation-tool-setup.ts +56 -7
  353. package/src/daemon/conversation-turn-finalize.ts +134 -89
  354. package/src/daemon/conversation.ts +105 -27
  355. package/src/daemon/disk-pressure-guard-lifecycle.ts +2 -2
  356. package/src/daemon/disk-pressure-guard.ts +3 -3
  357. package/src/daemon/doordash-steps.ts +5 -5
  358. package/src/daemon/external-plugins-bootstrap.ts +7 -1
  359. package/src/daemon/handlers/config-channels.ts +48 -5
  360. package/src/daemon/handlers/conversation-history.ts +1 -1
  361. package/src/daemon/handlers/conversations.ts +62 -17
  362. package/src/daemon/handlers/shared.ts +12 -0
  363. package/src/daemon/handlers/skills.ts +11 -1
  364. package/src/daemon/identity-helpers.ts +38 -10
  365. package/src/daemon/install-assistant-command.ts +322 -0
  366. package/src/daemon/lifecycle.ts +14 -3
  367. package/src/daemon/message-types/conversations.ts +6 -7
  368. package/src/daemon/message-types/sync.ts +1 -0
  369. package/src/daemon/persist-media-references.ts +23 -14
  370. package/src/daemon/process-message.ts +6 -6
  371. package/src/daemon/tool-setup-types.ts +19 -122
  372. package/src/daemon/tool-side-effects.ts +17 -94
  373. package/src/daemon/trust-context.ts +14 -6
  374. package/src/daemon/turn-tail-chain.ts +73 -0
  375. package/src/daemon/web-search-history.ts +47 -33
  376. package/src/heartbeat/heartbeat-service.ts +6 -6
  377. package/src/ipc/__tests__/socket-path.test.ts +21 -0
  378. package/src/ipc/assistant-server.ts +17 -10
  379. package/src/ipc/cli-client.ts +10 -1
  380. package/src/ipc/routes/__tests__/documents-sync-ipc-routes.test.ts +56 -0
  381. package/src/ipc/routes/documents-sync-ipc-routes.ts +42 -0
  382. package/src/ipc/socket-cleanup.ts +9 -7
  383. package/src/ipc/socket-path.ts +4 -96
  384. package/src/live-voice/__tests__/live-voice-archive.test.ts +16 -16
  385. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +209 -0
  386. package/src/live-voice/__tests__/live-voice-connection.test.ts +20 -0
  387. package/src/live-voice/__tests__/live-voice-integration.test.ts +7 -5
  388. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +265 -0
  389. package/src/live-voice/__tests__/live-voice-stt.test.ts +6 -1
  390. package/src/live-voice/__tests__/live-voice-vad.test.ts +35 -15
  391. package/src/live-voice/__tests__/protocol.test.ts +52 -0
  392. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +3 -0
  393. package/src/live-voice/live-voice-archive.ts +13 -13
  394. package/src/live-voice/live-voice-connection.ts +7 -1
  395. package/src/live-voice/live-voice-photo.ts +169 -0
  396. package/src/live-voice/live-voice-session.ts +108 -4
  397. package/src/live-voice/protocol.ts +89 -1
  398. package/src/mcp/client.ts +10 -0
  399. package/src/messaging/providers/__tests__/callback-routing.test.ts +5 -1
  400. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +66 -4
  401. package/src/messaging/providers/callback-routing.ts +1 -0
  402. package/src/messaging/providers/discord/api.ts +312 -0
  403. package/src/messaging/providers/discord/dm-delivery.test.ts +204 -0
  404. package/src/messaging/providers/discord/render.test.ts +204 -0
  405. package/src/messaging/providers/discord/render.ts +195 -0
  406. package/src/messaging/providers/discord/send.test.ts +179 -0
  407. package/src/messaging/providers/discord/send.ts +212 -0
  408. package/src/messaging/providers/discord/transport.ts +81 -0
  409. package/src/messaging/providers/index.ts +3 -1
  410. package/src/messaging/providers/retry-policy.test.ts +247 -0
  411. package/src/messaging/providers/retry-policy.ts +206 -0
  412. package/src/messaging/providers/slack/mention-source.test.ts +285 -0
  413. package/src/messaging/providers/slack/mention-source.ts +287 -0
  414. package/src/messaging/providers/telegram-bot/api.ts +47 -130
  415. package/src/messaging/providers/whatsapp/api.ts +47 -136
  416. package/src/monitoring/__tests__/db-integrity-sample.test.ts +4 -1
  417. package/src/monitoring/__tests__/plugin-auto-update.test.ts +332 -0
  418. package/src/monitoring/db-integrity-sample.ts +6 -1
  419. package/src/monitoring/plugin-auto-update.ts +359 -0
  420. package/src/monitoring/resource-sampler.ts +21 -5
  421. package/src/monitoring/worker.ts +13 -0
  422. package/src/notifications/__tests__/assistant-reply-producer.test.ts +375 -2
  423. package/src/notifications/__tests__/broadcaster.test.ts +37 -1
  424. package/src/notifications/__tests__/copy-composer.test.ts +165 -0
  425. package/src/notifications/__tests__/emit-signal-pipeline-failure.test.ts +205 -0
  426. package/src/notifications/__tests__/notification-utils.test.ts +249 -0
  427. package/src/notifications/adapters/platform.ts +31 -2
  428. package/src/notifications/assistant-reply-producer.ts +79 -32
  429. package/src/notifications/broadcaster.ts +30 -10
  430. package/src/notifications/copy-composer.ts +69 -0
  431. package/src/notifications/emit-signal.ts +82 -15
  432. package/src/notifications/events-store.ts +10 -2
  433. package/src/notifications/home-feed-side-effect.ts +6 -3
  434. package/src/notifications/notification-utils.ts +173 -5
  435. package/src/notifications/signal.ts +12 -0
  436. package/src/oauth/byo-connection.ts +6 -1
  437. package/src/oauth/credential-token-resolver.ts +3 -10
  438. package/src/oauth/manual-token-connection.ts +29 -64
  439. package/src/oauth/manual-token-providers.ts +62 -0
  440. package/src/oauth/provider-serializer.ts +13 -0
  441. package/src/oauth/seed-providers.ts +56 -8
  442. package/src/onboarding/onboarding-events-store.ts +91 -1
  443. package/src/permissions/question-prompter.test.ts +4 -0
  444. package/src/permissions/question-prompter.ts +21 -4
  445. package/src/persistence/__tests__/conversation-lineage.test.ts +248 -0
  446. package/src/persistence/attachments-store.ts +15 -15
  447. package/src/persistence/conversation-attention-store.ts +30 -10
  448. package/src/persistence/conversation-bootstrap.ts +17 -0
  449. package/src/persistence/conversation-crud.ts +363 -97
  450. package/src/persistence/conversation-group-migration.ts +1 -1
  451. package/src/persistence/conversation-key-store.ts +38 -10
  452. package/src/persistence/conversation-lineage.ts +226 -0
  453. package/src/persistence/conversation-plugin-facade.ts +28 -3
  454. package/src/persistence/conversation-queries.ts +207 -33
  455. package/src/persistence/conversation-types.ts +107 -1
  456. package/src/persistence/jobs-store.ts +32 -8
  457. package/src/persistence/llm-usage-store.ts +22 -0
  458. package/src/persistence/migrations/361-normalize-managed-connection-rows.test.ts +168 -0
  459. package/src/persistence/migrations/361-normalize-managed-connection-rows.ts +76 -0
  460. package/src/persistence/migrations/362-add-conversation-subagent-kind.test.ts +187 -0
  461. package/src/persistence/migrations/362-add-conversation-subagent-kind.ts +93 -0
  462. package/src/persistence/migrations/363-backfill-schedule-inference-profile.test.ts +229 -0
  463. package/src/persistence/migrations/363-backfill-schedule-inference-profile.ts +127 -0
  464. package/src/persistence/migrations/364-add-schedule-source-key.test.ts +120 -0
  465. package/src/persistence/migrations/364-add-schedule-source-key.ts +43 -0
  466. package/src/persistence/migrations/365-add-conversation-fork-strategy.ts +27 -0
  467. package/src/persistence/schema/conversations.ts +32 -0
  468. package/src/persistence/schema/infrastructure.ts +3 -0
  469. package/src/persistence/steps.ts +30 -0
  470. package/src/persistence/subagent-store.ts +148 -6
  471. package/src/platform/managed-speech.test.ts +20 -0
  472. package/src/platform/managed-speech.ts +9 -0
  473. package/src/plugin-api/index.ts +8 -0
  474. package/src/plugin-api/webhook-url.ts +151 -0
  475. package/src/plugins/__tests__/installed-plugin-dirs.test.ts +82 -0
  476. package/src/plugins/__tests__/source-fingerprint.test.ts +5 -1
  477. package/src/plugins/collect-source-versions.ts +4 -27
  478. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +5 -5
  479. package/src/plugins/defaults/exploration-drift/package.json +1 -1
  480. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +4 -2
  481. package/src/plugins/defaults/image-recovery/recover.ts +67 -57
  482. package/src/plugins/defaults/index.ts +1 -1
  483. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +64 -18
  484. package/src/plugins/defaults/memory/__tests__/jobs-worker-outcome-contract.test.ts +242 -0
  485. package/src/plugins/defaults/memory/__tests__/jobs-worker-retrospective-sweep.test.ts +24 -1
  486. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +48 -3
  487. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +511 -19
  488. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +39 -3
  489. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +493 -0
  490. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +5 -4
  491. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -1
  492. package/src/plugins/defaults/memory/job-handlers.ts +87 -5
  493. package/src/plugins/defaults/memory/jobs-worker.ts +60 -6
  494. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +11 -0
  495. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +29 -0
  496. package/src/plugins/defaults/memory/memory-retrospective-job.ts +342 -60
  497. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +23 -3
  498. package/src/plugins/defaults/memory/substrate/consolidation-lock.ts +9 -4
  499. package/src/plugins/defaults/memory/substrate/sweep-job.ts +7 -0
  500. package/src/plugins/defaults/memory/v1/graph/injection.ts +1 -1
  501. package/src/plugins/defaults/memory/v2/router.ts +8 -0
  502. package/src/plugins/defaults/turn-context/unified-turn-context.ts +1 -2
  503. package/src/plugins/external-plugin-loader.ts +17 -8
  504. package/src/plugins/installed-plugin-dirs.ts +101 -0
  505. package/src/plugins/mtime-cache.ts +34 -22
  506. package/src/plugins/types.ts +69 -8
  507. package/src/prompts/persona-resolver.ts +6 -2
  508. package/src/prompts/system-prompt.ts +72 -17
  509. package/src/providers/__tests__/inference.test.ts +40 -0
  510. package/src/providers/__tests__/preflight-resolved-config.test.ts +58 -1
  511. package/src/providers/__tests__/retry-callsite.test.ts +67 -0
  512. package/src/providers/__tests__/vellum-connection-routing.test.ts +42 -11
  513. package/src/providers/anthropic/client.ts +112 -95
  514. package/src/providers/connection-resolution.ts +35 -1
  515. package/src/providers/gemini/client.ts +5 -5
  516. package/src/providers/inference/adapter-factory.ts +16 -10
  517. package/src/providers/inference/auth.ts +16 -0
  518. package/src/providers/inference/connection-availability.ts +164 -12
  519. package/src/providers/inference/connections.ts +25 -1
  520. package/src/providers/media-resolve.ts +21 -11
  521. package/src/providers/model-catalog.ts +15 -0
  522. package/src/providers/openai/chat-completions-provider.ts +4 -4
  523. package/src/providers/openai/responses-provider.ts +12 -25
  524. package/src/providers/registry.ts +9 -4
  525. package/src/providers/retry.ts +25 -0
  526. package/src/providers/server-tool-pairing.ts +134 -0
  527. package/src/providers/speech-to-text/__tests__/resolve.test.ts +187 -6
  528. package/src/providers/speech-to-text/deepgram.ts +1 -0
  529. package/src/providers/speech-to-text/resolve.ts +80 -10
  530. package/src/providers/speech-to-text/vellum-managed.ts +7 -1
  531. package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +3 -1
  532. package/src/providers/types.ts +26 -6
  533. package/src/providers/vellum-model-routing.ts +6 -7
  534. package/src/runtime/AGENTS.md +2 -0
  535. package/src/runtime/__tests__/agent-wake.test.ts +16 -16
  536. package/src/runtime/__tests__/background-job-runner.test.ts +0 -144
  537. package/src/runtime/__tests__/desktop-presence.test.ts +162 -0
  538. package/src/runtime/agent-wake.ts +63 -24
  539. package/src/runtime/assistant-event-hub.ts +87 -12
  540. package/src/runtime/auth/__tests__/guard-tests.test.ts +2 -0
  541. package/src/runtime/auth/__tests__/scopes.test.ts +20 -0
  542. package/src/runtime/auth/same-actor.ts +56 -8
  543. package/src/runtime/auth/scopes.ts +12 -0
  544. package/src/runtime/auth/types.ts +2 -0
  545. package/src/runtime/background-job-runner.ts +5 -27
  546. package/src/runtime/channel-readiness-service.ts +59 -3
  547. package/src/runtime/channel-readiness-types.ts +18 -0
  548. package/src/runtime/desktop-presence.ts +69 -0
  549. package/src/runtime/http-server.ts +12 -0
  550. package/src/runtime/local-actor-identity.ts +11 -9
  551. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  552. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  553. package/src/runtime/routes/__tests__/client-routes.test.ts +186 -14
  554. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +547 -1
  555. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +162 -7
  556. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +14 -10
  557. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +340 -3
  558. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +28 -1
  559. package/src/runtime/routes/__tests__/plugins-routes.test.ts +173 -1
  560. package/src/runtime/routes/__tests__/question-routes.test.ts +64 -0
  561. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +20 -0
  562. package/src/runtime/routes/__tests__/skill-history-route.test.ts +116 -0
  563. package/src/runtime/routes/__tests__/wake-conversation-routes.test.ts +60 -2
  564. package/src/runtime/routes/archive-utils.ts +16 -12
  565. package/src/runtime/routes/attachment-routes.ts +13 -6
  566. package/src/runtime/routes/canned-reply-release.ts +54 -0
  567. package/src/runtime/routes/channel-verification-routes.ts +2 -27
  568. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +5 -0
  569. package/src/runtime/routes/client-routes.ts +114 -2
  570. package/src/runtime/routes/conversation-cli-routes.ts +10 -12
  571. package/src/runtime/routes/conversation-list-routes.ts +59 -10
  572. package/src/runtime/routes/conversation-management-routes.ts +47 -18
  573. package/src/runtime/routes/conversation-query-routes.ts +32 -53
  574. package/src/runtime/routes/conversation-routes.ts +350 -293
  575. package/src/runtime/routes/credential-prompt-routes.ts +23 -2
  576. package/src/runtime/routes/debug-routes.ts +4 -1
  577. package/src/runtime/routes/default-provider-routes.ts +7 -11
  578. package/src/runtime/routes/documents-routes.ts +44 -1
  579. package/src/runtime/routes/events-routes.ts +28 -1
  580. package/src/runtime/routes/host-app-control-routes.ts +2 -0
  581. package/src/runtime/routes/host-bash-routes.ts +2 -0
  582. package/src/runtime/routes/host-browser-routes.ts +2 -0
  583. package/src/runtime/routes/host-cu-routes.ts +2 -0
  584. package/src/runtime/routes/host-file-routes.ts +2 -0
  585. package/src/runtime/routes/host-transfer-routes.ts +4 -0
  586. package/src/runtime/routes/identity-routes.ts +6 -6
  587. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  588. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -3
  589. package/src/runtime/routes/inbound-stages/background-dispatch.ts +14 -0
  590. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -2
  591. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +14 -11
  592. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -17
  593. package/src/runtime/routes/inference-profile-availability-guard.ts +171 -0
  594. package/src/runtime/routes/inference-profile-session-handler.ts +26 -2
  595. package/src/runtime/routes/inference-profile-session-routes.ts +2 -1
  596. package/src/runtime/routes/inference-profiles-routes.ts +152 -73
  597. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -16
  598. package/src/runtime/routes/llm-call-sites-routes.ts +13 -0
  599. package/src/runtime/routes/log-export-routes.ts +1 -1
  600. package/src/runtime/routes/notification-routes.ts +0 -14
  601. package/src/runtime/routes/oauth-providers.ts +1 -0
  602. package/src/runtime/routes/plugins-routes.ts +106 -1
  603. package/src/runtime/routes/profiler-routes.ts +4 -2
  604. package/src/runtime/routes/question-routes.ts +28 -10
  605. package/src/runtime/routes/retrospective-routes.ts +9 -3
  606. package/src/runtime/routes/schedule-routes.ts +210 -12
  607. package/src/runtime/routes/secret-routes.ts +40 -23
  608. package/src/runtime/routes/settings-routes.ts +41 -13
  609. package/src/runtime/routes/skills-routes.ts +79 -0
  610. package/src/runtime/routes/ui-snapshot-routes.ts +1 -0
  611. package/src/runtime/routes/wake-conversation-routes.ts +23 -0
  612. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  613. package/src/runtime/services/conversation-serializer.ts +24 -9
  614. package/src/runtime/sync/documents-sidecar-publish.test.ts +101 -0
  615. package/src/runtime/sync/resource-sync-events.ts +79 -1
  616. package/src/runtime/sync/worker-daemon-notify.test.ts +34 -0
  617. package/src/runtime/sync/worker-daemon-notify.ts +44 -6
  618. package/src/runtime/verification-outbound-actions.ts +250 -222
  619. package/src/runtime/verification-templates.ts +41 -30
  620. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +614 -0
  621. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1066 -0
  622. package/src/schedule/inference-profile.ts +64 -3
  623. package/src/schedule/plugin-schedule-declarations.ts +518 -0
  624. package/src/schedule/plugin-schedule-reconciler.ts +605 -0
  625. package/src/schedule/plugin-schedules-gate.ts +23 -0
  626. package/src/schedule/schedule-store.ts +520 -26
  627. package/src/schedule/scheduler.ts +38 -0
  628. package/src/security/credential-backend.ts +51 -4
  629. package/src/security/encrypted-store.ts +64 -42
  630. package/src/security/secure-keys.ts +55 -7
  631. package/src/skills/catalog-install.ts +20 -7
  632. package/src/skills/inline-command-runner.ts +2 -7
  633. package/src/skills/skill-history.test.ts +269 -0
  634. package/src/skills/skill-history.ts +250 -0
  635. package/src/skills/skillssh-registry.ts +1 -1
  636. package/src/stt/daemon-batch-transcriber.ts +8 -6
  637. package/src/subagent/__tests__/consult-prompt.test.ts +25 -0
  638. package/src/subagent/consult-prompt.ts +3 -1
  639. package/src/subagent/consult-transcript.ts +1 -1
  640. package/src/subagent/manager.ts +295 -42
  641. package/src/subagent/progress-events.ts +35 -0
  642. package/src/subagent/role-resolution.ts +88 -0
  643. package/src/subagent/types.ts +302 -67
  644. package/src/subagent/validate-allowlists.ts +3 -4
  645. package/src/telegram/__tests__/webhook-health.test.ts +73 -1
  646. package/src/telegram/webhook-health.ts +101 -3
  647. package/src/telemetry/__tests__/telemetry-event-fixtures.ts +2 -0
  648. package/src/telemetry/activation-funnel.ts +6 -1
  649. package/src/telemetry/live-voice-funnel.ts +75 -0
  650. package/src/telemetry/telemetry-event-sources.ts +6 -0
  651. package/src/telemetry/telemetry-wire-source.json +1 -1
  652. package/src/telemetry/telemetry-wire.generated.ts +2 -0
  653. package/src/telemetry/types.ts +23 -4
  654. package/src/telemetry/usage-telemetry-reporter.test.ts +4 -0
  655. package/src/tools/__tests__/subagent-workflow-tool-input-schemas.test.ts +1 -1
  656. package/src/tools/ask-question/ask-question-tool.test.ts +118 -3
  657. package/src/tools/ask-question/ask-question-tool.ts +56 -2
  658. package/src/tools/browser/browser-execution.ts +1 -1
  659. package/src/tools/calls/call-start.ts +8 -5
  660. package/src/tools/conversation-groups/move_to_group.ts +10 -12
  661. package/src/tools/document/document-tool.ts +11 -4
  662. package/src/tools/executor.ts +7 -6
  663. package/src/tools/filesystem/edit.ts +1 -1
  664. package/src/tools/filesystem/list.ts +1 -1
  665. package/src/tools/filesystem/read.ts +7 -13
  666. package/src/tools/filesystem/search.ts +23 -21
  667. package/src/tools/filesystem/write.ts +4 -1
  668. package/src/tools/host-filesystem/edit.ts +7 -2
  669. package/src/tools/host-filesystem/read.ts +21 -8
  670. package/src/tools/host-filesystem/write.ts +10 -2
  671. package/src/tools/host-terminal/host-shell.ts +2 -2
  672. package/src/tools/mcp/mcp-tool-factory.ts +25 -1
  673. package/src/tools/permission-checker.ts +8 -3
  674. package/src/tools/schedule/delete.ts +13 -1
  675. package/src/tools/schedule/list.ts +14 -3
  676. package/src/tools/schedule/update.ts +56 -33
  677. package/src/tools/shared/filesystem/audio-read.ts +6 -4
  678. package/src/tools/shared/filesystem/file-ops-service.ts +176 -116
  679. package/src/tools/shared/filesystem/image-read.ts +12 -12
  680. package/src/tools/shared/filesystem/path-policy.ts +70 -5
  681. package/src/tools/shared/filesystem/size-guard.ts +6 -6
  682. package/src/tools/shared/input-misuse.ts +72 -0
  683. package/src/tools/skills/scaffold-managed.ts +82 -0
  684. package/src/tools/skills/skill-tool-factory.ts +14 -1
  685. package/src/tools/subagent/consult-deadline.ts +12 -7
  686. package/src/tools/subagent/message.ts +3 -1
  687. package/src/tools/subagent/read.ts +66 -4
  688. package/src/tools/subagent/spawn.ts +500 -78
  689. package/src/tools/types.ts +22 -3
  690. package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
  691. package/src/tools/ui-surface/visual-validation.ts +2 -0
  692. package/src/usage/__tests__/subagent-attribution.test.ts +109 -0
  693. package/src/usage/subagent-attribution.ts +108 -0
  694. package/src/util/__tests__/sqlite-retry.test.ts +112 -0
  695. package/src/util/abort-reasons.ts +16 -0
  696. package/src/util/ansi.ts +44 -0
  697. package/src/util/disk-usage.ts +17 -13
  698. package/src/util/image-conversion.ts +20 -13
  699. package/src/util/schedule-source-key.ts +23 -0
  700. package/src/util/worker-process.ts +7 -1
  701. package/src/workspace/git-service.ts +33 -9
  702. package/src/workspace/migrations/140-repair-seed-pinned-memory-v3-live.ts +260 -0
  703. package/src/workspace/migrations/141-stt-english-default-to-multilingual.ts +111 -0
  704. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +126 -0
  705. package/src/workspace/migrations/registry.ts +4 -0
  706. package/src/workspace/top-level-renderer.ts +15 -2
  707. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -357
  708. package/src/daemon/install-symlink.ts +0 -207
  709. package/src/notifications/deferred-emit.ts +0 -147
@@ -1,43 +1,16 @@
1
1
  /**
2
2
  * Construction-time system-prompt resolution for a new conversation.
3
3
  *
4
- * The dependency seams (`fetchDelivery`, `deps.warm`, `deps.build`) exist so the
5
- * warm-then-build sequencing can be unit-tested without mocking the
6
- * widely-imported guardian-delivery / system-prompt modules — global module
7
- * mocks of those leak across test files in the shared-process runner.
4
+ * The dependency seams (`deps.warm`, `deps.build`) exist so the warm-then-build
5
+ * sequencing can be unit-tested without mocking the widely-imported
6
+ * guardian-delivery / system-prompt modules — global module mocks of those leak
7
+ * across test files in the shared-process runner.
8
8
  */
9
- import { getGuardianDeliveryFresh } from "../contacts/guardian-delivery-reader.js";
9
+ import { warmGuardianBindings } from "../contacts/guardian-delivery-reader.js";
10
10
  import { buildSystemPrompt } from "../prompts/system-prompt.js";
11
11
  import type { ConversationCreateOptions } from "./handlers/shared.js";
12
12
  import type { TrustContext } from "./trust-context-types.js";
13
13
 
14
- type GuardianDeliveryFetch = (input?: {
15
- channelTypes?: string[];
16
- }) => Promise<unknown>;
17
-
18
- /**
19
- * Warm both guardian-binding cache keys the desktop/native persona resolver
20
- * reads: the `"vellum"`-channel key for `peekGuardianForChannel("vellum")` and
21
- * the unfiltered key for its `peekAnyGuardian()` fallback. Warming only one
22
- * would still freeze `users/default.md` when the guardian lives on a non-vellum
23
- * channel (phone / Telegram).
24
- *
25
- * Uses the FRESH reader so warmup bypasses a stale/empty cached entry: a
26
- * gateway-side binding write (onboarding, rebind) does not invalidate the daemon
27
- * cache, so a non-fresh read could return an empty binding cached before the
28
- * guardian existed and freeze `users/default.md` until the TTL expires. The
29
- * fresh read repopulates the cache the sync `peek*` resolvers then read.
30
- * Best-effort — the reader swallows failures.
31
- */
32
- export async function warmGuardianBindings(
33
- fetchDelivery: GuardianDeliveryFetch = getGuardianDeliveryFresh,
34
- ): Promise<void> {
35
- await Promise.all([
36
- fetchDelivery({ channelTypes: ["vellum"] }),
37
- fetchDelivery(),
38
- ]);
39
- }
40
-
41
14
  /**
42
15
  * Resolve the system prompt to freeze onto a newly constructed conversation.
43
16
  *
@@ -47,11 +20,16 @@ export async function warmGuardianBindings(
47
20
  *
48
21
  * - An explicit `systemPromptOverride` (including an empty string) is used
49
22
  * verbatim.
50
- * - A channel-routed conversation that already carries the requester's
51
- * `trustContext` (e.g. Slack / Telegram inbound) builds with it, so the
52
- * persona resolves the *requester's* `users/<slug>.md` (a DB contact lookup)
53
- * rather than the guardian/default profile.
54
- * - Otherwise (no construction-time identity — the local vellum app sets trust
23
+ * - A channel-routed conversation carrying a non-guardian requester
24
+ * `trustContext` (e.g. Slack / Telegram inbound) builds with it directly:
25
+ * the requester's persona (`users/<slug>.md`) resolves via a DB contact
26
+ * lookup that needs no guardian binding.
27
+ * - A guardian-class `trustContext` (background and scheduled turns, memory
28
+ * consolidation, managed desktop) warms the guardian binding first: its
29
+ * persona resolution reads the sync guardian-delivery cache, which is cold
30
+ * in worker processes (schedule worker, memory worker) and after TTL expiry
31
+ * in the daemon.
32
+ * - Otherwise (no construction-time identity: the local vellum app sets trust
55
33
  * after creation) the guardian binding is warmed first so the persona slot
56
34
  * resolves the guardian's `users/<slug>.md` instead of `users/default.md` on
57
35
  * a cold cache.
@@ -67,7 +45,7 @@ export async function resolveInitialSystemPrompt(
67
45
  return storedOptions.systemPromptOverride;
68
46
  }
69
47
  const trustContext = storedOptions?.trustContext;
70
- if (trustContext === undefined) {
48
+ if (trustContext === undefined || trustContext.trustClass === "guardian") {
71
49
  await (deps.warm ?? warmGuardianBindings)();
72
50
  }
73
51
  const build =
@@ -12,6 +12,7 @@ import { getConfig } from "../config/loader.js";
12
12
  import { usesConceptPageMemory } from "../config/memory-v3-gate.js";
13
13
  import type { PermissionPrompter } from "../permissions/prompter.js";
14
14
  import type { SecretPrompter } from "../permissions/secret-prompter.js";
15
+ import { isSuppressedQueuedMessage } from "../persistence/conversation-types.js";
15
16
  import {
16
17
  enqueueMemoryJob,
17
18
  isMemoryEnabled,
@@ -23,10 +24,17 @@ import { resolveCapabilities } from "../runtime/capabilities.js";
23
24
  import { isAutoAnalysisConversation } from "../runtime/services/auto-analysis-guard.js";
24
25
  import { unregisterConversationSender } from "../tools/browser/browser-screencast.js";
25
26
  import { disposeToolProfiler } from "../tools/tool-profiler.js";
26
- import { type AbortReason, createAbortReason } from "../util/abort-reasons.js";
27
+ import {
28
+ type AbortReason,
29
+ createAbortReason,
30
+ isUserInterruptAbort,
31
+ } from "../util/abort-reasons.js";
27
32
  import { getLogger } from "../util/logger.js";
28
33
  import { unregisterCallNotifiers } from "./conversation-notifiers.js";
29
- import type { MessageQueue } from "./conversation-queue-manager.js";
34
+ import type {
35
+ MessageQueue,
36
+ QueueDrainReason,
37
+ } from "./conversation-queue-manager.js";
30
38
  import { resetSkillToolProjection } from "./conversation-skill-tools.js";
31
39
  import type { SurfaceData, SurfaceType } from "./message-protocol.js";
32
40
 
@@ -112,6 +120,12 @@ export interface AbortContext {
112
120
  >;
113
121
  accumulatedSurfaceState: Map<string, Record<string, unknown>>;
114
122
  readonly queue: MessageQueue;
123
+ /** Armed when an interrupt leaves queued messages behind, so the drain
124
+ * repairs tool_use blocks the killed turn abandoned. */
125
+ pendingInterruptRepair: boolean;
126
+ /** Never rejects. Called only when an interrupt preserves the queue but no
127
+ * live turn is left to reach its `finally` and drain it. */
128
+ kickDrainQueue(reason?: QueueDrainReason, origin?: string): Promise<void>;
115
129
  }
116
130
 
117
131
  export interface DisposeContext extends AbortContext {
@@ -129,18 +143,133 @@ export interface DisposeContext extends AbortContext {
129
143
 
130
144
  // ── abort ─────────────────────────────────────────────────────────────
131
145
 
146
+ /** The conversation surface {@link forceClearStaleProcessing} needs. */
147
+ export interface StaleProcessingContext {
148
+ readonly conversationId: string;
149
+ setProcessing(value: boolean): void;
150
+ }
151
+
152
+ /**
153
+ * Release a processing flag that no live turn owns.
154
+ *
155
+ * Callers reach this when `isProcessing()` is true but `abortController` is
156
+ * null: the turn that owned the flag already tore its controller down (the
157
+ * agent loop nulls `abortController` before clearing the flag) or died without
158
+ * ever installing one. Either way no agent-loop release is going to run to
159
+ * clear it, so signalling the controller would be a silent no-op (`?.abort()`
160
+ * on null does nothing) and the conversation would stay wedged: every later
161
+ * submit is rejected with "already processing" and Stop appears dead.
162
+ * `setProcessing(false)` also nulls the persisted column and emits the metadata
163
+ * invalidation that drives clients to idle.
164
+ *
165
+ * `origin` names the calling site for log correlation.
166
+ */
167
+ export function forceClearStaleProcessing(
168
+ ctx: StaleProcessingContext,
169
+ origin: string,
170
+ ): void {
171
+ log.warn(
172
+ { conversationId: ctx.conversationId, origin },
173
+ "Processing latched with no live abort controller; force-clearing the stale processing flag",
174
+ );
175
+ ctx.setProcessing(false);
176
+ }
177
+
178
+ /**
179
+ * Keep the queue intact across a user interrupt so the messages queued behind
180
+ * the stopped turn still run.
181
+ *
182
+ * A user interrupt ends the turn in flight and nothing else: the aborted turn
183
+ * unwinds to the agent loop's `finally`, which calls `kickDrainQueue`, so the
184
+ * queued messages become the next turn. This is the same drain-after-abort
185
+ * sequence `steerToMessage` relies on.
186
+ *
187
+ * An interrupt can land mid-tool, leaving `tool_use` blocks with no results, so
188
+ * arm the same repair a steer arms: the drained turn then opens with synthetic
189
+ * results instead of a dangling call.
190
+ *
191
+ * `hasLiveTurn` is false when the processing flag was force-cleared because no
192
+ * controller was left to signal. No agent-loop `finally` runs in that case, so
193
+ * the drain is kicked here instead; otherwise the preserved messages wait for
194
+ * an unrelated later turn to finish.
195
+ */
196
+ function preserveQueueAcrossInterrupt(
197
+ ctx: AbortContext,
198
+ { hasLiveTurn }: { hasLiveTurn: boolean },
199
+ ): void {
200
+ if (ctx.queue.isEmpty) {
201
+ return;
202
+ }
203
+ ctx.pendingInterruptRepair = true;
204
+ log.info(
205
+ {
206
+ conversationId: ctx.conversationId,
207
+ queueDepth: ctx.queue.length,
208
+ hasLiveTurn,
209
+ },
210
+ "User interrupt: preserving queued messages for the interrupted turn's drain",
211
+ );
212
+ if (!hasLiveTurn) {
213
+ void ctx.kickDrainQueue("loop_complete", "abortConversation:no_live_turn");
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Discard the queue when the conversation itself is going away (dispose,
219
+ * eviction, voice supersession, subagent teardown): there is no turn left for
220
+ * a queued message to run on.
221
+ *
222
+ * This runs whether or not the in-memory flag reads processing. A turn that
223
+ * cleared the flag can still be unwinding through its awaited turn-boundary
224
+ * commit with a `kickDrainQueue` ahead of it, and a queue left populated there
225
+ * drains onto a conversation whose state (and possibly whose durable row) is
226
+ * already gone.
227
+ *
228
+ * Each sender gets two events, both load-bearing. `generation_cancelled`
229
+ * closes out the turn the message was waiting on; `message_queued_deleted` is
230
+ * the terminal event for the queued row itself, the same one a user-issued
231
+ * DELETE emits (`deleteQueuedMessage`). Without the second, a client holds the
232
+ * pending indicator forever, since no `message_dequeued` is coming. Rows with
233
+ * no client-visible queued counterpart ({@link isSuppressedQueuedMessage} —
234
+ * hidden sends and daemon-injected subagent/ACP/wake notifications) are
235
+ * suppressed for the same reason they get no queued ack: they have no client
236
+ * row to close.
237
+ */
238
+ function discardQueueOnAbort(ctx: AbortContext): void {
239
+ for (const queued of ctx.queue) {
240
+ queued.onEvent({
241
+ type: "generation_cancelled",
242
+ conversationId: ctx.conversationId,
243
+ });
244
+ if (isSuppressedQueuedMessage(queued.metadata)) {
245
+ continue;
246
+ }
247
+ queued.onEvent({
248
+ type: "message_queued_deleted",
249
+ conversationId: ctx.conversationId,
250
+ requestId: queued.requestId,
251
+ ...(queued.clientMessageId
252
+ ? { clientMessageId: queued.clientMessageId }
253
+ : {}),
254
+ });
255
+ }
256
+ ctx.queue.clear();
257
+ }
258
+
132
259
  export function abortConversation(
133
260
  ctx: AbortContext,
134
261
  reason?: AbortReason,
135
262
  ): void {
136
- if (ctx.isProcessing()) {
137
- const effectiveReason =
138
- reason ??
139
- createAbortReason(
140
- "preempted_by_new_message",
141
- "abortConversation:default",
142
- ctx.conversationId,
143
- );
263
+ const effectiveReason =
264
+ reason ??
265
+ createAbortReason(
266
+ "preempted_by_new_message",
267
+ "abortConversation:default",
268
+ ctx.conversationId,
269
+ );
270
+ const hasLiveTurn = ctx.abortController !== null;
271
+ const wasProcessing = ctx.isProcessing();
272
+ if (wasProcessing) {
144
273
  log.info(
145
274
  { conversationId: ctx.conversationId, abortReason: effectiveReason },
146
275
  "Aborting in-flight processing",
@@ -153,21 +282,7 @@ export function abortConversation(
153
282
  // clear it here and risk clobbering a client's optimistic state.
154
283
  ctx.abortController.abort(effectiveReason);
155
284
  } else {
156
- // The flag is set but there is no live controller to signal: the turn
157
- // that owned it already tore its controller down (the agent-loop
158
- // `finally` nulls `abortController` before clearing the flag) or died
159
- // without ever installing one. Either way no agent-loop `finally` is
160
- // going to run to clear the flag. Without this branch the abort is a
161
- // silent no-op — `?.abort()` does nothing — and the conversation stays
162
- // wedged: every later submit is rejected with "already processing" and
163
- // Stop appears dead. Force-clear the flag directly so the conversation
164
- // frees up. `setProcessing(false)` also nulls the persisted column and
165
- // emits the metadata invalidation that drives clients to idle.
166
- log.warn(
167
- { conversationId: ctx.conversationId },
168
- "Abort requested while processing but no live abort controller — force-clearing stale processing flag",
169
- );
170
- ctx.setProcessing(false);
285
+ forceClearStaleProcessing(ctx, "abortConversation");
171
286
  }
172
287
  ctx.prompter.dispose();
173
288
  ctx.secretPrompter.dispose();
@@ -175,13 +290,6 @@ export function abortConversation(
175
290
  ctx.surfaceActionRequestIds.clear();
176
291
  ctx.surfaceState.clear();
177
292
  ctx.accumulatedSurfaceState.clear();
178
- for (const queued of ctx.queue) {
179
- queued.onEvent({
180
- type: "generation_cancelled",
181
- conversationId: ctx.conversationId,
182
- });
183
- }
184
- ctx.queue.clear();
185
293
  } else {
186
294
  // The in-memory flag reads idle, but a cancel must still clear a persisted
187
295
  // `processing_started_at` that outlived the turn that set it. This is the
@@ -201,6 +309,20 @@ export function abortConversation(
201
309
  // reports idle in-memory to clients, so no refetch needs to be pushed.
202
310
  ctx.setProcessing(false);
203
311
  }
312
+
313
+ // The queue decision is independent of the processing flag: an interrupt
314
+ // hands its queue to the drain, and every other abort kind is a teardown
315
+ // that must take the queue with it even when the flag already reads idle.
316
+ if (isUserInterruptAbort(effectiveReason)) {
317
+ // An idle conversation counts as having a live turn for drain purposes:
318
+ // either a turn is unwinding toward its own `kickDrainQueue`, or the queue
319
+ // is empty. Kicking a second drain here would race the first.
320
+ preserveQueueAcrossInterrupt(ctx, {
321
+ hasLiveTurn: hasLiveTurn || !wasProcessing,
322
+ });
323
+ } else {
324
+ discardQueueOnAbort(ctx);
325
+ }
204
326
  }
205
327
 
206
328
  // ── dispose ──────────────────────────────────────────────────────────
@@ -48,7 +48,7 @@ import {
48
48
  extractAttachmentStoredPaths,
49
49
  extractImageSourcePaths,
50
50
  getConversation,
51
- isHiddenMessageMetadata,
51
+ isSuppressedQueuedMessage,
52
52
  provenanceFromTrustContext,
53
53
  setConversationOriginChannelIfUnset,
54
54
  setConversationOriginInterfaceIfUnset,
@@ -217,11 +217,11 @@ export interface MessagingConversationContext {
217
217
  currentTurnAuthContext?: AuthContext;
218
218
  currentTurnSourceActorPrincipalId?: string;
219
219
  /**
220
- * OS surface reported by the connected client ("web" | "ios" | "macos" |
221
- * "android"), re-applied from transport metadata on every inbound message.
220
+ * OS surface reported by the connected client, re-applied from transport
221
+ * metadata on every inbound message.
222
222
  * Persisted under `metadata.client.os` so turn telemetry can attribute the
223
- * real platform — the transport `interfaceId` is "web" for the web, iOS,
224
- * and macOS apps alike (they share the web renderer).
223
+ * real platform. The transport `interfaceId` is "web" for browser, mobile,
224
+ * and desktop apps because they share the web renderer.
225
225
  */
226
226
  clientOs?: string;
227
227
  getTurnChannelContext(): TurnChannelContext | null;
@@ -254,15 +254,15 @@ function serializeUserContentBlocks(
254
254
  * by the attachments as inline base64 blocks. The regular upload path persists
255
255
  * `workspace_ref` blocks instead (see `persistQueuedMessageBody`).
256
256
  */
257
- export function serializePersistedUserMessageContent(
257
+ export async function serializePersistedUserMessageContent(
258
258
  content: string,
259
259
  displayContent: string | undefined,
260
260
  attachments: MessageAttachmentInput[],
261
- ): string {
261
+ ): Promise<string> {
262
262
  return serializeUserContentBlocks(
263
263
  content,
264
264
  displayContent,
265
- attachmentsToContentBlocks(attachments),
265
+ await attachmentsToContentBlocks(attachments),
266
266
  );
267
267
  }
268
268
 
@@ -273,15 +273,15 @@ export function serializePersistedUserMessageContent(
273
273
  * the stored original — keeping the per-turn token estimate accurate without a
274
274
  * disk read on the hot path.
275
275
  */
276
- function computeReferenceImageDimensions(
276
+ async function computeReferenceImageDimensions(
277
277
  attachmentId: string,
278
278
  mediaType: string,
279
- ): { width: number; height: number } | null {
279
+ ): Promise<{ width: number; height: number } | null> {
280
280
  const bytes = getAttachmentContent(attachmentId);
281
281
  if (!bytes) {
282
282
  return null;
283
283
  }
284
- const optimized = optimizeImageForTransport(
284
+ const optimized = await optimizeImageForTransport(
285
285
  bytes.toString("base64"),
286
286
  mediaType,
287
287
  );
@@ -321,11 +321,11 @@ type MaterializeOutcome =
321
321
  * endpoint's own rejection); only a recoverable store-write failure is
322
322
  * `transient` (inline fallback).
323
323
  */
324
- function materializeUserAttachment(
324
+ async function materializeUserAttachment(
325
325
  conversationId: string,
326
326
  conversationCreatedAt: number,
327
327
  a: MessageAttachmentInput,
328
- ): MaterializeOutcome {
328
+ ): Promise<MaterializeOutcome> {
329
329
  try {
330
330
  if (a.id && attachmentExists(a.id)) {
331
331
  const stored = scopeAttachmentToMessageConversation(
@@ -348,7 +348,7 @@ function materializeUserAttachment(
348
348
  }
349
349
  return {
350
350
  kind: "stored",
351
- stored: createInlineAttachment(
351
+ stored: await createInlineAttachment(
352
352
  conversationId,
353
353
  conversationCreatedAt,
354
354
  a.filename,
@@ -376,10 +376,10 @@ function materializeUserAttachment(
376
376
  }
377
377
 
378
378
  /** Build the `workspace_ref` content block for a materialized attachment. */
379
- function referenceBlockForAttachment(
379
+ async function referenceBlockForAttachment(
380
380
  a: MessageAttachmentInput,
381
381
  stored: { id: string; mimeType: string; sizeBytes: number },
382
- ): ContentBlock {
382
+ ): Promise<ContentBlock> {
383
383
  const ref: AttachmentReferenceInput = {
384
384
  attachmentId: stored.id,
385
385
  filename: a.filename,
@@ -388,7 +388,10 @@ function referenceBlockForAttachment(
388
388
  extractedText: a.extractedText,
389
389
  };
390
390
  if (stored.mimeType.toLowerCase().startsWith("image/")) {
391
- const dims = computeReferenceImageDimensions(stored.id, stored.mimeType);
391
+ const dims = await computeReferenceImageDimensions(
392
+ stored.id,
393
+ stored.mimeType,
394
+ );
392
395
  if (dims) {
393
396
  ref.width = dims.width;
394
397
  ref.height = dims.height;
@@ -399,13 +402,13 @@ function referenceBlockForAttachment(
399
402
 
400
403
  /** Inline base64 fallback block for an attachment that could not be stored as
401
404
  * a reference, so the upload survives a reload. Null when there are no bytes. */
402
- function inlineBlockForAttachment(
405
+ async function inlineBlockForAttachment(
403
406
  a: MessageAttachmentInput,
404
- ): ContentBlock | null {
407
+ ): Promise<ContentBlock | null> {
405
408
  if (!a.data) {
406
409
  return null;
407
410
  }
408
- return attachmentsToContentBlocks([a])[0] ?? null;
411
+ return (await attachmentsToContentBlocks([a]))[0] ?? null;
409
412
  }
410
413
 
411
414
  /**
@@ -419,15 +422,15 @@ function inlineBlockForAttachment(
419
422
  * survives; a validation/upload rejection is dropped (never reaches content or
420
423
  * the model).
421
424
  */
422
- function prepareUserAttachmentReferences(
425
+ async function prepareUserAttachmentReferences(
423
426
  conversationId: string,
424
427
  conversationCreatedAt: number,
425
428
  attachments: MessageAttachmentInput[],
426
- ): PreparedUserAttachment[] {
429
+ ): Promise<PreparedUserAttachment[]> {
427
430
  const prepared: PreparedUserAttachment[] = [];
428
431
  for (let i = 0; i < attachments.length; i++) {
429
432
  const a = attachments[i];
430
- const outcome = materializeUserAttachment(
433
+ const outcome = await materializeUserAttachment(
431
434
  conversationId,
432
435
  conversationCreatedAt,
433
436
  a,
@@ -435,7 +438,7 @@ function prepareUserAttachmentReferences(
435
438
  if (outcome.kind === "stored") {
436
439
  prepared.push({
437
440
  position: i,
438
- block: referenceBlockForAttachment(a, outcome.stored),
441
+ block: await referenceBlockForAttachment(a, outcome.stored),
439
442
  link: { attachmentId: outcome.stored.id },
440
443
  });
441
444
  continue;
@@ -445,7 +448,7 @@ function prepareUserAttachmentReferences(
445
448
  }
446
449
  // transient: keep the upload by inlining its bytes (dropped only when the
447
450
  // recoverable failure left us with no bytes to inline).
448
- const inline = inlineBlockForAttachment(a);
451
+ const inline = await inlineBlockForAttachment(a);
449
452
  if (inline) {
450
453
  prepared.push({ position: i, block: inline });
451
454
  } else {
@@ -633,16 +636,17 @@ export function enqueueMessage(
633
636
  }
634
637
  // Ack the accepted enqueue on the sender's event sink. Emitting here,
635
638
  // rather than at each ingress call site, is what guarantees every path
636
- // that queues (HTTP send, surface actions, agent wake, subagent
637
- // notifications) surfaces the queued row live. Hidden sends are
638
- // suppressed from the transcript at every stage, including this ack,
639
- // and `position` counts visible items only: both mirror the
640
- // list-messages queued-snapshot filter so a live ack and a cold reload
641
- // render the same row at the same position.
642
- if (!isHiddenMessageMetadata(metadata)) {
639
+ // that queues a person's prompt (HTTP send, surface actions, CLI signal)
640
+ // surfaces the queued row live. Rows with no client-visible counterpart —
641
+ // hidden sends and daemon-injected notifications (subagent/ACP/wake) — are
642
+ // suppressed from the transcript at every stage, including this ack, and
643
+ // `position` counts visible items only: both mirror the list-messages
644
+ // queued-snapshot filter so a live ack and a cold reload render the same
645
+ // row at the same position.
646
+ if (!isSuppressedQueuedMessage(metadata)) {
643
647
  const position = ctx.queue
644
648
  .snapshot()
645
- .filter((item) => !isHiddenMessageMetadata(item.metadata)).length;
649
+ .filter((item) => !isSuppressedQueuedMessage(item.metadata)).length;
646
650
  onEvent?.({
647
651
  type: "message_queued",
648
652
  conversationId: ctx.conversationId,
@@ -699,6 +703,19 @@ export interface PersistMessageOptions {
699
703
  * thread it: the queue round-trips `metadata`, not these options.
700
704
  */
701
705
  scripted?: boolean;
706
+ /**
707
+ * OS surface this row's own request or transport reported, threaded by the
708
+ * ingress that built it (the send route's request body, a queued message's
709
+ * `transport`). Stamps `metadata.clientOsFromRequest` when it matches the
710
+ * `client.os` this row persists.
711
+ *
712
+ * `ctx.clientOs` alone is not that evidence: it is a live conversation
713
+ * field only a transport-carrying message refreshes, so a transport-less
714
+ * turn (surface action, signal ingress) inherits whatever an earlier send
715
+ * left there. Omitting this option therefore reads as "inherited", which is
716
+ * what a consumer that must not misattribute a turn to a surface needs.
717
+ */
718
+ requestClientOs?: string;
702
719
  }
703
720
 
704
721
  // ── persistUserMessage ───────────────────────────────────────────────
@@ -767,9 +784,9 @@ export async function persistUserMessage(
767
784
  return result;
768
785
  } catch (err) {
769
786
  // Clear the flag, but never let a clear failure mask the original error
770
- // or skip the bookkeeping reset. `setProcessing` reverts its own
771
- // in-memory flag when its persist throws, so the conversation is left
772
- // consistent either way.
787
+ // or skip the bookkeeping reset. `setProcessing(false)` releases in memory
788
+ // regardless of its advisory mirror write, so the guard here is purely
789
+ // defensive against future changes.
773
790
  try {
774
791
  ctx.setProcessing(false);
775
792
  } catch (clearErr) {
@@ -807,6 +824,7 @@ export async function persistQueuedMessageBody(
807
824
  displayContent,
808
825
  clientMessageId,
809
826
  skipIndexing,
827
+ requestClientOs,
810
828
  } = options;
811
829
  const attachmentInputs: MessageAttachmentInput[] = attachments.map(
812
830
  (attachment) => ({
@@ -818,7 +836,7 @@ export async function persistQueuedMessageBody(
818
836
  filePath: attachment.filePath,
819
837
  }),
820
838
  );
821
- const cleanMessage = createUserMessage(content, attachmentInputs);
839
+ const cleanMessage = await createUserMessage(content, attachmentInputs);
822
840
  let pushedToHistory = false;
823
841
 
824
842
  try {
@@ -839,13 +857,19 @@ export async function persistQueuedMessageBody(
839
857
  // non-boolean through would be worse than dropping it: sqlite stores it
840
858
  // verbatim, and `turn-events-store` narrows anything that isn't 1 to
841
859
  // `false`, turning a junk string into a confident "the user typed this".
860
+ // `clientOsFromRequest` comes out for the same reason and a sharper one:
861
+ // it is derived below from what this persist can actually see, and a bag
862
+ // value surviving the spread would let a caller assert an origin the row
863
+ // never reported.
842
864
  const {
843
865
  slackInbound: rawSlackInbound,
844
866
  scripted: rawScriptedFromMetadata,
867
+ clientOsFromRequest: _rawClientOsFromRequest,
845
868
  ...metadataWithoutSlackInbound
846
869
  } = (metadata ?? {}) as Record<string, unknown> & {
847
870
  slackInbound?: SlackInboundMessageMetadata;
848
871
  scripted?: unknown;
872
+ clientOsFromRequest?: unknown;
849
873
  };
850
874
  const slackMeta = buildSlackMetaForPersistence({
851
875
  slackInbound: rawSlackInbound,
@@ -895,6 +919,22 @@ export async function persistQueuedMessageBody(
895
919
  const clientBag =
896
920
  Object.keys(clientEntries).length > 0 ? { client: clientEntries } : {};
897
921
 
922
+ // Per-row evidence for the `client.os` stamped just above, kept as a
923
+ // sibling of the bag so `TurnTelemetryEvent.client` stays exactly the
924
+ // forwarded `$.client`. Set only when this row itself reported the OS:
925
+ // through the caller's own client bag (the request's client-metadata
926
+ // headers, round-tripped through the queue) or through this row's
927
+ // transport, which `requestClientOs` carries. An inherited `ctx.clientOs`
928
+ // names the surface of an EARLIER turn, so it leaves the marker off and a
929
+ // consumer reading origin (the reply-push presence gate) treats the turn
930
+ // as coming from somewhere unknown.
931
+ const callerOs = callerClient?.os;
932
+ const resolvedRequestClientOs = parseClientOs(requestClientOs);
933
+ const clientOsFromRequest =
934
+ (typeof callerOs === "string" && callerOs.length > 0) ||
935
+ (resolvedRequestClientOs !== null &&
936
+ resolvedRequestClientOs === clientOs);
937
+
898
938
  const mergedMetadata = {
899
939
  ...metadataWithoutSlackInbound,
900
940
  ...provenance,
@@ -911,6 +951,7 @@ export async function persistQueuedMessageBody(
911
951
  }
912
952
  : {}),
913
953
  ...clientBag,
954
+ ...(clientOsFromRequest ? { clientOsFromRequest: true } : {}),
914
955
  ...(imageSourcePaths ? { imageSourcePaths } : {}),
915
956
  ...(slackMeta ? { slackMeta } : {}),
916
957
  // Scripted-turn marker, forwarded by `turn-events-store` onto
@@ -944,7 +985,7 @@ export async function persistQueuedMessageBody(
944
985
  // once the message id exists.
945
986
  const conversationCreatedAt =
946
987
  getConversation(ctx.conversationId)?.createdAt ?? Date.now();
947
- const preparedAttachments = prepareUserAttachmentReferences(
988
+ const preparedAttachments = await prepareUserAttachmentReferences(
948
989
  ctx.conversationId,
949
990
  conversationCreatedAt,
950
991
  attachmentInputs,
@@ -1011,9 +1052,9 @@ export async function persistQueuedMessageBody(
1011
1052
  // by rewriting that block to inline base64 so the upload survives even
1012
1053
  // though the store anchor was lost, then persist the corrected content.
1013
1054
  let repairedBlocks: ContentBlock[] | null = null;
1014
- preparedAttachments.forEach((p, idx) => {
1055
+ for (const [idx, p] of preparedAttachments.entries()) {
1015
1056
  if (!p.link) {
1016
- return;
1057
+ continue;
1017
1058
  }
1018
1059
  try {
1019
1060
  const scopedAttachmentId = linkAttachmentToMessage(
@@ -1024,7 +1065,9 @@ export async function persistQueuedMessageBody(
1024
1065
  attachmentInputs[p.position].storedPath =
1025
1066
  getFilePathForAttachment(scopedAttachmentId) ?? undefined;
1026
1067
  } catch (err) {
1027
- const inline = inlineBlockForAttachment(attachmentInputs[p.position]);
1068
+ const inline = await inlineBlockForAttachment(
1069
+ attachmentInputs[p.position],
1070
+ );
1028
1071
  log.error(
1029
1072
  { attachmentId: p.link.attachmentId, err, repaired: inline != null },
1030
1073
  "Failed to link user attachment; repairing persisted content to inline",
@@ -1034,7 +1077,7 @@ export async function persistQueuedMessageBody(
1034
1077
  repairedBlocks[idx] = inline;
1035
1078
  }
1036
1079
  }
1037
- });
1080
+ }
1038
1081
  if (repairedBlocks) {
1039
1082
  updateMessageContent(
1040
1083
  persistedUserMessage.id,