@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,967 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { UISurfaceCompleteEvent } from "../api/events/ui-surface-complete.js";
4
+ import type { AssistantEvent } from "../api/index.js";
5
+ import type { TrustContext } from "../daemon/trust-context-types.js";
6
+ import {
7
+ createMockLoggerModule,
8
+ makeMockLogger,
9
+ } from "./helpers/mock-logger.js";
10
+
11
+ /** Interleaved record of persist writes and broadcasts, for ordering asserts. */
12
+ let callOrder: string[] = [];
13
+
14
+ let broadcastedMessages: AssistantEvent[] = [];
15
+ const realEventHub = await import("../runtime/assistant-event-hub.js");
16
+ mock.module("../runtime/assistant-event-hub.js", () => ({
17
+ ...realEventHub,
18
+ broadcastMessage: (msg: AssistantEvent) => {
19
+ callOrder.push(`broadcast:${msg.type}`);
20
+ broadcastedMessages.push(msg);
21
+ },
22
+ }));
23
+
24
+ let loggedWarnings: Array<{ fields: Record<string, unknown>; msg: string }> =
25
+ [];
26
+ mock.module("../util/logger.js", () =>
27
+ createMockLoggerModule({
28
+ getLogger: () => ({
29
+ debug: () => {},
30
+ info: () => {},
31
+ warn: (fields: Record<string, unknown>, msg: string) =>
32
+ loggedWarnings.push({ fields, msg }),
33
+ error: () => {},
34
+ fatal: () => {},
35
+ trace: () => {},
36
+ child: makeMockLogger,
37
+ }),
38
+ }),
39
+ );
40
+
41
+ // Stand-in for the persisted message store: `getMessages` reads it and
42
+ // `updateMessageContent` writes back through JSON, so a read after a write
43
+ // sees exactly what a client's turn-end history reseed would fetch.
44
+ type StoredRow = {
45
+ id: string;
46
+ conversationId: string;
47
+ role: string;
48
+ content: Array<Record<string, unknown>>;
49
+ createdAt: number;
50
+ metadata: string | null;
51
+ };
52
+ let storedRows: StoredRow[] = [];
53
+ let contentWrites: Array<{
54
+ id: string;
55
+ blocks: Array<Record<string, unknown>>;
56
+ }> = [];
57
+ /**
58
+ * Full-history scans this action cost. `getMessages` is the dominant per-turn
59
+ * main-thread cost on large conversations, so the counts are pinned.
60
+ */
61
+ let getMessagesCalls = 0;
62
+ /** When set, every persisted write throws it, standing in for a SQLite lock. */
63
+ let persistError: Error | null = null;
64
+
65
+ const realCrud = await import("../persistence/conversation-crud.js");
66
+ mock.module("../persistence/conversation-crud.js", () => ({
67
+ ...realCrud,
68
+ getMessages: (conversationId: string) => {
69
+ getMessagesCalls++;
70
+ // Fresh copies per call, like the real row mapper: a hit held across a
71
+ // write must not alias the store.
72
+ return storedRows
73
+ .filter((r) => r.conversationId === conversationId)
74
+ .map((r) => ({ ...r, content: structuredClone(r.content) }));
75
+ },
76
+ updateMessageContent: (id: string, content: string) => {
77
+ if (persistError) {
78
+ throw persistError;
79
+ }
80
+ const blocks = JSON.parse(content) as Array<Record<string, unknown>>;
81
+ callOrder.push("persist");
82
+ contentWrites.push({ id, blocks });
83
+ const row = storedRows.find((r) => r.id === id);
84
+ if (row) {
85
+ row.content = blocks;
86
+ }
87
+ },
88
+ }));
89
+
90
+ function resetSurfaceState(): void {
91
+ broadcastedMessages = [];
92
+ contentWrites = [];
93
+ storedRows = [];
94
+ callOrder = [];
95
+ loggedWarnings = [];
96
+ getMessagesCalls = 0;
97
+ persistError = null;
98
+ }
99
+
100
+ // Import must come AFTER mock.module so the surface module picks up the
101
+ // mocked event hub and persistence functions.
102
+ const {
103
+ completeSurfaceAndNotify,
104
+ createSurfaceMutex,
105
+ handleSurfaceAction,
106
+ markSurfaceCompleted,
107
+ surfaceProxyResolver,
108
+ } = await import("../daemon/conversation-surfaces.js");
109
+
110
+ import type { Conversation } from "../daemon/conversation.js";
111
+ import type { SurfaceStateEntry } from "../daemon/conversation-surface-state.js";
112
+ import type { SurfaceType } from "../daemon/message-protocol.js";
113
+ import { asConversation } from "./helpers/mock-conversation.js";
114
+
115
+ const CONVERSATION_ID = "conv-history-restored-1";
116
+
117
+ /** The trust the surface-action route stamps on for a local guardian caller. */
118
+ const GUARDIAN_TRUST: TrustContext = {
119
+ trustClass: "guardian",
120
+ sourceChannel: "vellum",
121
+ };
122
+
123
+ type EnqueueResult = { queued: boolean; requestId: string; rejected?: boolean };
124
+
125
+ function makeContext(
126
+ enqueueResult: EnqueueResult = { queued: false, requestId: "req-1" },
127
+ overrides: { trustContext?: TrustContext; queueDepth?: number } = {},
128
+ ): Conversation & { enqueuedContents: string[] } {
129
+ const enqueuedContents: string[] = [];
130
+ return asConversation({
131
+ conversationId: CONVERSATION_ID,
132
+ trustContext: overrides.trustContext ?? GUARDIAN_TRUST,
133
+ sendToClient: () => {},
134
+ pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
135
+ lastSurfaceAction: new Map<
136
+ string,
137
+ { actionId: string; data?: Record<string, unknown> }
138
+ >(),
139
+ surfaceState: new Map(),
140
+ surfaceUndoStacks: new Map<string, string[]>(),
141
+ accumulatedSurfaceState: new Map<string, Record<string, unknown>>(),
142
+ surfaceActionRequestIds: new Set<string>(),
143
+ currentTurnSurfaces: [],
144
+ pendingStandaloneSurfaces: new Map(),
145
+ recentlyCompletedStandaloneSurfaces: new Map(),
146
+ isProcessing: () => false,
147
+ enqueueMessage: (options) => {
148
+ enqueuedContents.push(options.content);
149
+ return enqueueResult;
150
+ },
151
+ getQueueDepth: () => overrides.queueDepth ?? 0,
152
+ processMessage: async () => "msg-1",
153
+ withSurface: createSurfaceMutex(),
154
+ enqueuedContents,
155
+ });
156
+ }
157
+
158
+ function seedSurfaceRow(
159
+ surfaceId: string,
160
+ surfaceType: string,
161
+ metadata: string | null = null,
162
+ ): void {
163
+ storedRows = [
164
+ {
165
+ id: "msg-with-surface",
166
+ conversationId: CONVERSATION_ID,
167
+ role: "assistant",
168
+ content: [
169
+ { type: "text", text: "Which one?" },
170
+ {
171
+ type: "ui_surface",
172
+ surfaceId,
173
+ surfaceType,
174
+ data: {
175
+ options: [
176
+ { id: "inbox", title: "Clean up my inbox" },
177
+ { id: "calendar", title: "Plan my week" },
178
+ ],
179
+ },
180
+ },
181
+ ],
182
+ createdAt: 0,
183
+ metadata,
184
+ },
185
+ ];
186
+ }
187
+
188
+ function seedConfirmationRow(
189
+ surfaceId: string,
190
+ data: Record<string, unknown>,
191
+ ): void {
192
+ storedRows = [
193
+ {
194
+ id: "msg-with-confirmation",
195
+ conversationId: CONVERSATION_ID,
196
+ role: "assistant",
197
+ content: [
198
+ { type: "text", text: "Are you sure?" },
199
+ { type: "ui_surface", surfaceId, surfaceType: "confirmation", data },
200
+ ],
201
+ createdAt: 0,
202
+ metadata: null,
203
+ },
204
+ ];
205
+ }
206
+
207
+ /**
208
+ * Re-read the persisted block the way a history reseed would. Newest row
209
+ * first, matching the production scan.
210
+ */
211
+ function readPersistedSurface(
212
+ surfaceId: string,
213
+ ): Record<string, unknown> | undefined {
214
+ for (let i = storedRows.length - 1; i >= 0; i--) {
215
+ const block = storedRows[i].content.find(
216
+ (b) => b.type === "ui_surface" && b.surfaceId === surfaceId,
217
+ );
218
+ if (block) {
219
+ return block;
220
+ }
221
+ }
222
+ return undefined;
223
+ }
224
+
225
+ function completionBroadcasts(surfaceId: string): UISurfaceCompleteEvent[] {
226
+ return broadcastedMessages.filter(
227
+ (m): m is UISurfaceCompleteEvent =>
228
+ m.type === "ui_surface_complete" && m.surfaceId === surfaceId,
229
+ );
230
+ }
231
+
232
+ // The payload the web client posts when a user picks an option.
233
+ const CHOICE_PAYLOAD = {
234
+ choiceId: "inbox",
235
+ choiceTitle: "Clean up my inbox",
236
+ selectedIds: ["inbox"],
237
+ selectedTitles: ["Clean up my inbox"],
238
+ };
239
+
240
+ describe("history-restored surface completion", () => {
241
+ beforeEach(resetSurfaceState);
242
+
243
+ test("one-shot choice surface with no in-memory state is completed and persisted", async () => {
244
+ const surfaceId = "surface-choice-history-1";
245
+ seedSurfaceRow(surfaceId, "choice");
246
+ // Neither live map holds anything, so the type can only come from history.
247
+ const ctx = makeContext();
248
+
249
+ // The history-restored branch signals acceptance by returning nothing; the
250
+ // HTTP route maps that to `{ ok: true }`.
251
+ const result = await handleSurfaceAction(
252
+ ctx,
253
+ surfaceId,
254
+ "inbox",
255
+ CHOICE_PAYLOAD,
256
+ );
257
+ expect(result).toBeUndefined();
258
+
259
+ // The turn is still enqueued.
260
+ expect(ctx.enqueuedContents).toHaveLength(1);
261
+
262
+ // A re-read of persisted history reports the surface as answered, which is
263
+ // exactly what the client's turn-end reseed fetches.
264
+ const persisted = readPersistedSurface(surfaceId);
265
+ expect(persisted?.completed).toBe(true);
266
+ expect(persisted?.completionSummary).toBe(
267
+ 'User chose: "Clean up my inbox"',
268
+ );
269
+
270
+ const completions = completionBroadcasts(surfaceId);
271
+ expect(completions).toHaveLength(1);
272
+ expect(completions[0].summary).toBe('User chose: "Clean up my inbox"');
273
+ });
274
+
275
+ test("a history-restored confirmation quotes its persisted confirmLabel", async () => {
276
+ const surfaceId = "surface-confirm-history-1";
277
+ seedConfirmationRow(surfaceId, {
278
+ message: "Delete this project?",
279
+ confirmLabel: "Delete forever",
280
+ cancelLabel: "Keep it",
281
+ });
282
+ // Neither live map holds anything, so the labels come from history.
283
+ const ctx = makeContext();
284
+
285
+ await handleSurfaceAction(ctx, surfaceId, "confirm", {});
286
+
287
+ expect(readPersistedSurface(surfaceId)?.completionSummary).toBe(
288
+ 'User chose: "Delete forever"',
289
+ );
290
+ const completions = completionBroadcasts(surfaceId);
291
+ expect(completions).toHaveLength(1);
292
+ expect(completions[0].summary).toBe('User chose: "Delete forever"');
293
+ });
294
+
295
+ test("a history-restored confirmation quotes its persisted cancelLabel", async () => {
296
+ const surfaceId = "surface-confirm-history-2";
297
+ seedConfirmationRow(surfaceId, {
298
+ message: "Delete this project?",
299
+ confirmLabel: "Delete forever",
300
+ cancelLabel: "Keep it",
301
+ });
302
+ const ctx = makeContext();
303
+
304
+ await handleSurfaceAction(ctx, surfaceId, "cancel", {});
305
+
306
+ expect(readPersistedSurface(surfaceId)?.completionSummary).toBe(
307
+ 'User chose: "Keep it"',
308
+ );
309
+ });
310
+
311
+ test("in-memory surface state supplies the type when it is still present", async () => {
312
+ const surfaceId = "surface-choice-history-2";
313
+ // Nothing persisted: the type can only come from the live entry.
314
+ storedRows = [];
315
+ const ctx = makeContext();
316
+ ctx.surfaceState.set(surfaceId, {
317
+ surfaceType: "choice",
318
+ data: {
319
+ options: [{ id: "inbox", title: "Clean up my inbox" }],
320
+ selectionMode: "single",
321
+ },
322
+ });
323
+
324
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
325
+
326
+ const completions = completionBroadcasts(surfaceId);
327
+ expect(completions).toHaveLength(1);
328
+ expect(completions[0].summary).toBe('User chose: "Clean up my inbox"');
329
+ });
330
+
331
+ test("a non-one-shot persisted surface type is not auto-completed", async () => {
332
+ const surfaceId = "surface-page-history-1";
333
+ seedSurfaceRow(surfaceId, "dynamic_page");
334
+ const ctx = makeContext();
335
+
336
+ await handleSurfaceAction(ctx, surfaceId, "answer_selected", {
337
+ choiceId: "inbox",
338
+ });
339
+
340
+ expect(ctx.enqueuedContents).toHaveLength(1);
341
+ expect(readPersistedSurface(surfaceId)?.completed).toBeUndefined();
342
+ expect(contentWrites).toHaveLength(0);
343
+ expect(completionBroadcasts(surfaceId)).toHaveLength(0);
344
+ });
345
+
346
+ test("a rejected enqueue leaves the surface answerable", async () => {
347
+ const surfaceId = "surface-choice-history-3";
348
+ seedSurfaceRow(surfaceId, "choice");
349
+ const ctx = makeContext({
350
+ queued: false,
351
+ requestId: "req-rejected",
352
+ rejected: true,
353
+ });
354
+
355
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
356
+
357
+ expect(readPersistedSurface(surfaceId)?.completed).toBeUndefined();
358
+ expect(contentWrites).toHaveLength(0);
359
+ expect(completionBroadcasts(surfaceId)).toHaveLength(0);
360
+ expect(ctx.surfaceActionRequestIds.size).toBe(0);
361
+ });
362
+
363
+ test("an explicit _completeSurface request completes a non-one-shot surface", async () => {
364
+ const surfaceId = "surface-page-history-2";
365
+ seedSurfaceRow(surfaceId, "dynamic_page");
366
+ const ctx = makeContext();
367
+
368
+ await handleSurfaceAction(ctx, surfaceId, "answer_selected", {
369
+ _completeSurface: true,
370
+ _completionSummary: "Answered",
371
+ });
372
+
373
+ expect(readPersistedSurface(surfaceId)?.completed).toBe(true);
374
+ expect(readPersistedSurface(surfaceId)?.completionSummary).toBe("Answered");
375
+ const completions = completionBroadcasts(surfaceId);
376
+ expect(completions).toHaveLength(1);
377
+ expect(completions[0].summary).toBe("Answered");
378
+ });
379
+
380
+ test("the pending branch completes a one-shot surface too", async () => {
381
+ const surfaceId = "surface-choice-pending-1";
382
+ seedSurfaceRow(surfaceId, "choice");
383
+ const ctx = makeContext();
384
+ ctx.pendingSurfaceActions.set(surfaceId, { surfaceType: "choice" });
385
+ ctx.surfaceState.set(surfaceId, {
386
+ surfaceType: "choice",
387
+ data: {
388
+ options: [
389
+ { id: "inbox", title: "Clean up my inbox" },
390
+ { id: "calendar", title: "Plan my week" },
391
+ ],
392
+ selectionMode: "single",
393
+ },
394
+ });
395
+
396
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
397
+
398
+ expect(ctx.enqueuedContents).toHaveLength(1);
399
+ expect(ctx.pendingSurfaceActions.has(surfaceId)).toBe(false);
400
+ expect(readPersistedSurface(surfaceId)?.completed).toBe(true);
401
+
402
+ const completions = completionBroadcasts(surfaceId);
403
+ expect(completions).toHaveLength(1);
404
+ const completion = completions[0];
405
+ expect(completion.summary).toBe('User chose: "Clean up my inbox"');
406
+ // The pending branch carries the submitted payload on the broadcast.
407
+ expect(completion.submittedData).toEqual(CHOICE_PAYLOAD);
408
+ });
409
+
410
+ test("the history-restored branch carries the submitted payload too", async () => {
411
+ const surfaceId = "surface-choice-history-4";
412
+ seedSurfaceRow(surfaceId, "choice");
413
+ const ctx = makeContext();
414
+
415
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
416
+
417
+ expect(completionBroadcasts(surfaceId)[0].submittedData).toEqual(
418
+ CHOICE_PAYLOAD,
419
+ );
420
+ });
421
+
422
+ test("the completion omits submittedData entirely when there is none", async () => {
423
+ const surfaceId = "surface-confirm-history-3";
424
+ seedConfirmationRow(surfaceId, { message: "Delete?" });
425
+ const ctx = makeContext();
426
+
427
+ await handleSurfaceAction(ctx, surfaceId, "confirm");
428
+
429
+ expect(completionBroadcasts(surfaceId)[0]).not.toHaveProperty(
430
+ "submittedData",
431
+ );
432
+ });
433
+
434
+ test("the completion is persisted before it is broadcast", async () => {
435
+ const surfaceId = "surface-choice-history-5";
436
+ seedSurfaceRow(surfaceId, "choice");
437
+ const ctx = makeContext();
438
+
439
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
440
+
441
+ // A broadcast that outran a failed persist would leave the client showing
442
+ // a completed card the next reseed reverts.
443
+ expect(callOrder).toEqual(["persist", "broadcast:ui_surface_complete"]);
444
+ });
445
+ });
446
+
447
+ describe("history-restored surface completion: requester provenance", () => {
448
+ beforeEach(resetSurfaceState);
449
+
450
+ test("the type-driven rule does not read a guardian-provenance surface for an untrusted requester", async () => {
451
+ const surfaceId = "surface-choice-guardian-1";
452
+ seedSurfaceRow(
453
+ surfaceId,
454
+ "choice",
455
+ JSON.stringify({ provenanceTrustClass: "guardian" }),
456
+ );
457
+ const ctx = makeContext(undefined, {
458
+ trustContext: { trustClass: "unknown", sourceChannel: "vellum" },
459
+ });
460
+
461
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
462
+
463
+ // The live path hides this row from this actor, so the persisted fallback
464
+ // must not hand back its type or its labels. The filter covers that read
465
+ // only: an explicit `_completeSurface` request skips it, and the write
466
+ // scan is unfiltered.
467
+ expect(readPersistedSurface(surfaceId)?.completed).toBeUndefined();
468
+ expect(contentWrites).toHaveLength(0);
469
+ expect(completionBroadcasts(surfaceId)).toHaveLength(0);
470
+ });
471
+
472
+ test("an untrusted requester completes a contact-provenance surface", async () => {
473
+ const surfaceId = "surface-choice-contact-1";
474
+ seedSurfaceRow(
475
+ surfaceId,
476
+ "choice",
477
+ JSON.stringify({ provenanceTrustClass: "trusted_contact" }),
478
+ );
479
+ const ctx = makeContext(undefined, {
480
+ trustContext: { trustClass: "unknown", sourceChannel: "vellum" },
481
+ });
482
+
483
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
484
+
485
+ expect(readPersistedSurface(surfaceId)?.completed).toBe(true);
486
+ expect(completionBroadcasts(surfaceId)).toHaveLength(1);
487
+ });
488
+ });
489
+
490
+ describe("history-restored surface completion: scan cost", () => {
491
+ beforeEach(resetSurfaceState);
492
+
493
+ test("a one-shot action with no live state costs two full scans", async () => {
494
+ const surfaceId = "surface-scan-1";
495
+ seedSurfaceRow(surfaceId, "choice");
496
+ const ctx = makeContext();
497
+
498
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
499
+
500
+ // One read to resolve the type and the labels, then the write's own scan.
501
+ // The write scans unfiltered while the read is provenance-filtered, so
502
+ // each resolves its own row rather than sharing one.
503
+ expect(getMessagesCalls).toBe(2);
504
+ expect(readPersistedSurface(surfaceId)?.completed).toBe(true);
505
+ });
506
+
507
+ test("an explicit completion request with no live state costs one full scan", async () => {
508
+ const surfaceId = "surface-scan-2";
509
+ seedSurfaceRow(surfaceId, "dynamic_page");
510
+ const ctx = makeContext();
511
+
512
+ await handleSurfaceAction(ctx, surfaceId, "answer_selected", {
513
+ _completeSurface: true,
514
+ _completionSummary: "Answered",
515
+ });
516
+
517
+ // The request carries its own summary, so only the write scans.
518
+ expect(getMessagesCalls).toBe(1);
519
+ expect(readPersistedSurface(surfaceId)?.completionSummary).toBe("Answered");
520
+ });
521
+
522
+ test("a non-one-shot action with live state costs no scan at all", async () => {
523
+ const surfaceId = "surface-scan-3";
524
+ seedSurfaceRow(surfaceId, "dynamic_page");
525
+ const ctx = makeContext();
526
+ ctx.surfaceState.set(surfaceId, {
527
+ surfaceType: "dynamic_page",
528
+ data: { html: "<p>page</p>" },
529
+ });
530
+
531
+ await handleSurfaceAction(ctx, surfaceId, "answer_selected", {
532
+ choiceId: "inbox",
533
+ });
534
+
535
+ expect(getMessagesCalls).toBe(0);
536
+ expect(completionBroadcasts(surfaceId)).toHaveLength(0);
537
+ });
538
+
539
+ test("a non-one-shot action with no live state costs one decisive scan", async () => {
540
+ const surfaceId = "surface-scan-4";
541
+ seedSurfaceRow(surfaceId, "dynamic_page");
542
+ const ctx = makeContext();
543
+
544
+ await handleSurfaceAction(ctx, surfaceId, "answer_selected", {
545
+ choiceId: "inbox",
546
+ });
547
+
548
+ // The persisted type is the only thing that can rule out completion here,
549
+ // so this read is the decision input, not a discarded one.
550
+ expect(getMessagesCalls).toBe(1);
551
+ expect(completionBroadcasts(surfaceId)).toHaveLength(0);
552
+ expect(contentWrites).toHaveLength(0);
553
+ });
554
+
555
+ test("a rejected action scans nothing", async () => {
556
+ const surfaceId = "surface-scan-5";
557
+ seedSurfaceRow(surfaceId, "choice");
558
+ const ctx = makeContext({
559
+ queued: false,
560
+ requestId: "req-rejected",
561
+ rejected: true,
562
+ });
563
+
564
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
565
+
566
+ expect(getMessagesCalls).toBe(0);
567
+ });
568
+
569
+ test("a pending one-shot action costs one full scan", async () => {
570
+ const surfaceId = "surface-scan-6";
571
+ seedSurfaceRow(surfaceId, "choice");
572
+ const ctx = makeContext();
573
+ ctx.pendingSurfaceActions.set(surfaceId, { surfaceType: "choice" });
574
+ ctx.surfaceState.set(surfaceId, {
575
+ surfaceType: "choice",
576
+ data: {
577
+ options: [{ id: "inbox", title: "Clean up my inbox" }],
578
+ selectionMode: "single",
579
+ },
580
+ });
581
+
582
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
583
+
584
+ // The pending entry supplies the type, so only the write scans.
585
+ expect(getMessagesCalls).toBe(1);
586
+ expect(readPersistedSurface(surfaceId)?.completed).toBe(true);
587
+ });
588
+ });
589
+
590
+ describe("surface action queue rejection", () => {
591
+ beforeEach(resetSurfaceState);
592
+
593
+ test("a rejected history-restored action is logged with queue depth", async () => {
594
+ const surfaceId = "surface-rejected-1";
595
+ seedSurfaceRow(surfaceId, "choice");
596
+ const ctx = makeContext(
597
+ { queued: false, requestId: "req-rejected", rejected: true },
598
+ { queueDepth: 7 },
599
+ );
600
+
601
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
602
+
603
+ const rejection = loggedWarnings.find((w) =>
604
+ w.msg.startsWith("Surface action rejected by the message queue"),
605
+ );
606
+ expect(rejection).toBeDefined();
607
+ expect(rejection?.fields).toMatchObject({
608
+ conversationId: CONVERSATION_ID,
609
+ surfaceId,
610
+ actionId: "inbox",
611
+ queueDepth: 7,
612
+ });
613
+ });
614
+
615
+ test("a rejected pending action is logged with queue depth", async () => {
616
+ const surfaceId = "surface-rejected-2";
617
+ seedSurfaceRow(surfaceId, "choice");
618
+ const ctx = makeContext(
619
+ { queued: false, requestId: "req-rejected", rejected: true },
620
+ { queueDepth: 3 },
621
+ );
622
+ ctx.pendingSurfaceActions.set(surfaceId, { surfaceType: "choice" });
623
+
624
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
625
+
626
+ const rejection = loggedWarnings.find((w) =>
627
+ w.msg.startsWith("Surface action rejected by the message queue"),
628
+ );
629
+ expect(rejection?.fields).toMatchObject({
630
+ surfaceId,
631
+ actionId: "inbox",
632
+ surfaceType: "choice",
633
+ queueDepth: 3,
634
+ });
635
+ expect(completionBroadcasts(surfaceId)).toHaveLength(0);
636
+ });
637
+ });
638
+
639
+ describe("surfaces completed without ever awaiting an action", () => {
640
+ beforeEach(resetSurfaceState);
641
+
642
+ test("a one-shot surface shown with await_action false completes on its first action", async () => {
643
+ const ctx = makeContext();
644
+
645
+ const shown = await surfaceProxyResolver(ctx, "ui_show", {
646
+ surface_type: "choice",
647
+ await_action: false,
648
+ title: "Pick one",
649
+ data: {
650
+ options: [
651
+ { id: "inbox", title: "Clean up my inbox" },
652
+ { id: "calendar", title: "Plan my week" },
653
+ ],
654
+ },
655
+ });
656
+ expect(shown.isError).toBe(false);
657
+ const { surfaceId } = JSON.parse(shown.content) as { surfaceId: string };
658
+
659
+ // `ui_show` registers no pending entry when it is not awaiting an action,
660
+ // so the action lands on the no-pending branch.
661
+ expect(ctx.pendingSurfaceActions.has(surfaceId)).toBe(false);
662
+ expect(ctx.surfaceState.has(surfaceId)).toBe(true);
663
+
664
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
665
+
666
+ const completions = completionBroadcasts(surfaceId);
667
+ expect(completions).toHaveLength(1);
668
+ expect(completions[0].summary).toBe('User chose: "Clean up my inbox"');
669
+ });
670
+
671
+ test("a retried action after a pending completion completes the surface again", async () => {
672
+ const surfaceId = "surface-retry-1";
673
+ seedSurfaceRow(surfaceId, "choice");
674
+ const ctx = makeContext();
675
+ ctx.pendingSurfaceActions.set(surfaceId, { surfaceType: "choice" });
676
+ ctx.surfaceState.set(surfaceId, {
677
+ surfaceType: "choice",
678
+ data: {
679
+ options: [{ id: "inbox", title: "Clean up my inbox" }],
680
+ selectionMode: "single",
681
+ },
682
+ });
683
+
684
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
685
+ // The pending entry is consumed, but `surfaceState` survives, so a
686
+ // duplicate POST re-runs the completion instead of being dropped.
687
+ expect(ctx.pendingSurfaceActions.has(surfaceId)).toBe(false);
688
+
689
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
690
+
691
+ expect(completionBroadcasts(surfaceId)).toHaveLength(2);
692
+ expect(contentWrites).toHaveLength(2);
693
+ expect(readPersistedSurface(surfaceId)?.completed).toBe(true);
694
+ });
695
+ });
696
+
697
+ describe("markSurfaceCompleted in-memory patching", () => {
698
+ beforeEach(resetSurfaceState);
699
+
700
+ test("marks only the newest in-memory copy of a duplicated surfaceId", () => {
701
+ const surfaceId = "surface-duplicated-1";
702
+ const older = { type: "ui_surface", surfaceId } as Record<string, unknown>;
703
+ const newer = { type: "ui_surface", surfaceId } as Record<string, unknown>;
704
+ const messages = [{ content: [older] }, { content: [newer] }];
705
+
706
+ markSurfaceCompleted(
707
+ { conversationId: CONVERSATION_ID, messages },
708
+ surfaceId,
709
+ "Done",
710
+ );
711
+
712
+ // The DB half patches the newest row only; marking every in-memory copy
713
+ // would make a reseed revert the extras.
714
+ expect(newer.completed).toBe(true);
715
+ expect(newer.completionSummary).toBe("Done");
716
+ expect(older.completed).toBeUndefined();
717
+ });
718
+
719
+ test("leaves in-memory messages pending when the persisted write throws", () => {
720
+ const surfaceId = "surface-rollback-1";
721
+ seedSurfaceRow(surfaceId, "choice");
722
+ const block = { type: "ui_surface", surfaceId } as Record<string, unknown>;
723
+ persistError = new Error("database is locked");
724
+
725
+ expect(
726
+ markSurfaceCompleted(
727
+ { conversationId: CONVERSATION_ID, messages: [{ content: [block] }] },
728
+ surfaceId,
729
+ "Done",
730
+ ),
731
+ ).toBe(false);
732
+
733
+ // A patched in-memory block over a pending DB row is the same divergence
734
+ // the completion write exists to close.
735
+ expect(block.completed).toBeUndefined();
736
+ expect(block.completionSummary).toBeUndefined();
737
+ expect(readPersistedSurface(surfaceId)?.completed).toBeUndefined();
738
+ });
739
+
740
+ test("patches in-memory messages when there is no persisted block to write", () => {
741
+ const surfaceId = "surface-memory-only-1";
742
+ const block = { type: "ui_surface", surfaceId } as Record<string, unknown>;
743
+
744
+ expect(
745
+ markSurfaceCompleted(
746
+ { conversationId: CONVERSATION_ID, messages: [{ content: [block] }] },
747
+ surfaceId,
748
+ "Done",
749
+ ),
750
+ ).toBe(true);
751
+
752
+ expect(block.completed).toBe(true);
753
+ expect(block.completionSummary).toBe("Done");
754
+ });
755
+ });
756
+
757
+ describe("completion summary sourcing", () => {
758
+ beforeEach(resetSurfaceState);
759
+
760
+ test("keeps live surface data when the persisted block carries none", async () => {
761
+ const surfaceId = "surface-live-data-1";
762
+ storedRows = [
763
+ {
764
+ id: "msg-typed-dataless",
765
+ conversationId: CONVERSATION_ID,
766
+ role: "assistant",
767
+ content: [
768
+ { type: "ui_surface", surfaceId, surfaceType: "confirmation" },
769
+ ],
770
+ createdAt: 0,
771
+ metadata: null,
772
+ },
773
+ ];
774
+ const ctx = makeContext();
775
+ // `liveSurfaceType` and `liveSurfaceData` are independent inputs, so live
776
+ // labels must survive a persisted read that only resolves the type.
777
+ ctx.surfaceState.set(surfaceId, {
778
+ data: { message: "Delete it?", confirmLabel: "Delete forever" },
779
+ } as unknown as SurfaceStateEntry);
780
+
781
+ await handleSurfaceAction(ctx, surfaceId, "confirm", {});
782
+
783
+ expect(completionBroadcasts(surfaceId)[0].summary).toBe(
784
+ 'User chose: "Delete forever"',
785
+ );
786
+ });
787
+ });
788
+
789
+ /** The live entries a `ui_dismiss` with a recorded action tears down. */
790
+ function seedAnsweredSurface(ctx: Conversation, surfaceId: string): void {
791
+ ctx.pendingSurfaceActions.set(surfaceId, { surfaceType: "choice" });
792
+ ctx.lastSurfaceAction.set(surfaceId, {
793
+ actionId: "inbox",
794
+ data: CHOICE_PAYLOAD,
795
+ });
796
+ ctx.surfaceState.set(surfaceId, {
797
+ surfaceType: "choice",
798
+ data: {
799
+ options: [{ id: "inbox", title: "Clean up my inbox" }],
800
+ selectionMode: "single",
801
+ },
802
+ });
803
+ ctx.surfaceUndoStacks.set(surfaceId, ["{}"]);
804
+ ctx.accumulatedSurfaceState.set(surfaceId, { selectedIds: ["inbox"] });
805
+ }
806
+
807
+ function liveSurfaceKeysRemaining(
808
+ ctx: Conversation,
809
+ surfaceId: string,
810
+ ): string[] {
811
+ return (
812
+ [
813
+ ["pendingSurfaceActions", ctx.pendingSurfaceActions],
814
+ ["lastSurfaceAction", ctx.lastSurfaceAction],
815
+ ["surfaceState", ctx.surfaceState],
816
+ ["surfaceUndoStacks", ctx.surfaceUndoStacks],
817
+ ["accumulatedSurfaceState", ctx.accumulatedSurfaceState],
818
+ ] as const
819
+ )
820
+ .filter(([, map]) => map.has(surfaceId))
821
+ .map(([name]) => name);
822
+ }
823
+
824
+ describe("surface completion when the persisted write does not land", () => {
825
+ beforeEach(resetSurfaceState);
826
+
827
+ test("a completion whose write throws is not broadcast", async () => {
828
+ const surfaceId = "surface-persist-throws-1";
829
+ seedSurfaceRow(surfaceId, "choice");
830
+ const ctx = makeContext();
831
+ persistError = new Error("database is locked");
832
+
833
+ const result = await handleSurfaceAction(
834
+ ctx,
835
+ surfaceId,
836
+ "inbox",
837
+ CHOICE_PAYLOAD,
838
+ );
839
+
840
+ // A persistence hiccup must not fail the action or drop the user's turn.
841
+ expect(result).toBeUndefined();
842
+ expect(ctx.enqueuedContents).toHaveLength(1);
843
+
844
+ // The persisted block is still pending, so a client told the card was
845
+ // completed would watch the next reseed revert it.
846
+ expect(readPersistedSurface(surfaceId)?.completed).toBeUndefined();
847
+ expect(completionBroadcasts(surfaceId)).toHaveLength(0);
848
+ });
849
+
850
+ test("a completion with no persisted block is still broadcast", async () => {
851
+ const surfaceId = "surface-no-persisted-block-1";
852
+ // Nothing in history: the write is a no-op because there is nothing to
853
+ // write, and nothing can revert the client's completed card.
854
+ const ctx = makeContext();
855
+ ctx.surfaceState.set(surfaceId, {
856
+ surfaceType: "choice",
857
+ data: {
858
+ options: [{ id: "inbox", title: "Clean up my inbox" }],
859
+ selectionMode: "single",
860
+ },
861
+ });
862
+
863
+ await handleSurfaceAction(ctx, surfaceId, "inbox", CHOICE_PAYLOAD);
864
+
865
+ expect(readPersistedSurface(surfaceId)).toBeUndefined();
866
+ expect(contentWrites).toHaveLength(0);
867
+ expect(completionBroadcasts(surfaceId)).toHaveLength(1);
868
+ });
869
+
870
+ test("a guardian card withdrawal is announced even when its write throws", () => {
871
+ const surfaceId = "surface-withdrawn-1";
872
+ seedSurfaceRow(surfaceId, "confirmation");
873
+ persistError = new Error("database is locked");
874
+
875
+ completeSurfaceAndNotify(CONVERSATION_ID, surfaceId, "Approved");
876
+
877
+ // The request is already resolved, so withholding the announcement would
878
+ // strand a live, clickable approval card for a decided request.
879
+ expect(readPersistedSurface(surfaceId)?.completed).toBeUndefined();
880
+ const completions = completionBroadcasts(surfaceId);
881
+ expect(completions).toHaveLength(1);
882
+ expect(completions[0].summary).toBe("Approved");
883
+ });
884
+
885
+ test("a ui_dismiss completion whose write throws stays retryable", async () => {
886
+ const surfaceId = "surface-dismiss-persist-throws-1";
887
+ seedSurfaceRow(surfaceId, "choice");
888
+ const ctx = makeContext();
889
+ const sentToClient: AssistantEvent[] = [];
890
+ ctx.sendToClient = (msg) => {
891
+ sentToClient.push(msg);
892
+ };
893
+ seedAnsweredSurface(ctx, surfaceId);
894
+ persistError = new Error("database is locked");
895
+
896
+ const result = await surfaceProxyResolver(ctx, "ui_dismiss", {
897
+ surface_id: surfaceId,
898
+ });
899
+
900
+ // Claiming success would have the model treat a card the next reseed
901
+ // restores as gone, inviting a duplicate action.
902
+ expect(result.isError).toBe(true);
903
+ expect(result.content).not.toContain("completed");
904
+ expect(
905
+ sentToClient.filter((m) => m.type === "ui_surface_complete"),
906
+ ).toHaveLength(0);
907
+ expect(readPersistedSurface(surfaceId)?.completed).toBeUndefined();
908
+
909
+ // Everything a retry needs survives.
910
+ expect(liveSurfaceKeysRemaining(ctx, surfaceId)).toEqual([
911
+ "pendingSurfaceActions",
912
+ "lastSurfaceAction",
913
+ "surfaceState",
914
+ "surfaceUndoStacks",
915
+ "accumulatedSurfaceState",
916
+ ]);
917
+ });
918
+
919
+ test("a ui_dismiss completion that lands clears the live state", async () => {
920
+ const surfaceId = "surface-dismiss-persist-lands-1";
921
+ seedSurfaceRow(surfaceId, "choice");
922
+ const ctx = makeContext();
923
+ const sentToClient: AssistantEvent[] = [];
924
+ ctx.sendToClient = (msg) => {
925
+ sentToClient.push(msg);
926
+ };
927
+ seedAnsweredSurface(ctx, surfaceId);
928
+
929
+ const result = await surfaceProxyResolver(ctx, "ui_dismiss", {
930
+ surface_id: surfaceId,
931
+ });
932
+
933
+ expect(result.isError).toBe(false);
934
+ expect(result.content).toBe("Surface completed");
935
+ expect(
936
+ sentToClient.filter((m) => m.type === "ui_surface_complete"),
937
+ ).toHaveLength(1);
938
+ expect(readPersistedSurface(surfaceId)?.completed).toBe(true);
939
+ expect(liveSurfaceKeysRemaining(ctx, surfaceId)).toEqual([]);
940
+ });
941
+
942
+ test("a ui_dismiss with no recorded action clears the live state even when the write throws", async () => {
943
+ const surfaceId = "surface-dismiss-no-action-1";
944
+ seedSurfaceRow(surfaceId, "card");
945
+ const ctx = makeContext();
946
+ const sentToClient: AssistantEvent[] = [];
947
+ ctx.sendToClient = (msg) => {
948
+ sentToClient.push(msg);
949
+ };
950
+ ctx.pendingSurfaceActions.set(surfaceId, { surfaceType: "card" });
951
+ ctx.surfaceState.set(surfaceId, { surfaceType: "card", data: {} });
952
+ persistError = new Error("database is locked");
953
+
954
+ const result = await surfaceProxyResolver(ctx, "ui_dismiss", {
955
+ surface_id: surfaceId,
956
+ });
957
+
958
+ // The passive-dismiss branch owns its own persistence and drops the card
959
+ // outright, so the completion gate must not hold its cleanup back.
960
+ expect(result.isError).toBe(false);
961
+ expect(result.content).toBe("Surface dismissed");
962
+ expect(
963
+ sentToClient.filter((m) => m.type === "ui_surface_dismiss"),
964
+ ).toHaveLength(1);
965
+ expect(liveSurfaceKeysRemaining(ctx, surfaceId)).toEqual([]);
966
+ });
967
+ });