@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
@@ -2,13 +2,16 @@ import { z } from "zod";
2
2
 
3
3
  import type { AssistantEvent } from "../../api/index.js";
4
4
  import { validateInferenceProfileKey } from "../../config/inference-profile-validation.js";
5
- import { resolveDefaultProfileKey } from "../../config/llm-resolver.js";
6
5
  import { getConfig } from "../../config/loader.js";
6
+ import { profileSupportsTools } from "../../config/profile-tool-support.js";
7
7
  import { findConversation } from "../../daemon/conversation-registry.js";
8
+ import { getMessages } from "../../persistence/conversation-crud.js";
8
9
  import {
9
- getConversationOverrideProfile,
10
- getMessages,
11
- } from "../../persistence/conversation-crud.js";
10
+ countRecentSimilarSpawns,
11
+ normalizeSpawnObjective,
12
+ type RecentSimilarSpawns,
13
+ type SimilarSpawnTally,
14
+ } from "../../persistence/subagent-store.js";
12
15
  import type { ContentBlock, Message } from "../../providers/types.js";
13
16
  import { buildAdvisorContext } from "../../subagent/consult-context.js";
14
17
  import {
@@ -20,7 +23,15 @@ import {
20
23
  getSubagentManager,
21
24
  SubagentAbortedError,
22
25
  } from "../../subagent/index.js";
23
- import type { SubagentRole } from "../../subagent/types.js";
26
+ import {
27
+ type ResolvedSubagentRole,
28
+ resolveSubagentRole,
29
+ } from "../../subagent/role-resolution.js";
30
+ import {
31
+ SUBAGENT_OUTPUT_CONTRACTS,
32
+ type SubagentOutputContract,
33
+ type SubagentRole,
34
+ } from "../../subagent/types.js";
24
35
  import { getLogger } from "../../util/logger.js";
25
36
  import {
26
37
  invalidToolInputResult,
@@ -33,11 +44,12 @@ const log = getLogger("subagent-spawn");
33
44
 
34
45
  /**
35
46
  * Idle ceiling on a single advisor consult: abort only after this much time
36
- * passes with NO streamed token (thinking or text). A reasoning advisor profile
37
- * streams its reasoning while it works, so a fixed wall-clock ceiling would kill
38
- * it mid-thought; an idle window instead fires only when the consult is
39
- * genuinely stalled (or never starts). Generous enough to also span
40
- * time-to-first-token over a large inherited transcript.
47
+ * passes with NO sign of forward progress, meaning neither a streamed token
48
+ * (thinking or text) nor tool activity. A reasoning advisor profile streams its
49
+ * reasoning while it works, so a fixed wall-clock ceiling would kill it
50
+ * mid-thought; an idle window instead fires only when the consult is genuinely
51
+ * stalled (or never starts). Generous enough to also span time-to-first-token
52
+ * over a large inherited transcript.
41
53
  */
42
54
  const ADVISOR_IDLE_TIMEOUT_MS = 60_000;
43
55
 
@@ -49,6 +61,62 @@ const ADVISOR_IDLE_TIMEOUT_MS = 60_000;
49
61
  */
50
62
  const ADVISOR_MAX_TIMEOUT_MS = 300_000;
51
63
 
64
+ /**
65
+ * Tool calls a single advisor consult may make before it is stopped and asked
66
+ * to answer with what it has.
67
+ *
68
+ * The consult BLOCKS the user-facing turn, and every tool call it makes
69
+ * re-arms the idle window, so a reading advisor is otherwise bounded only by
70
+ * the absolute backstop: five minutes of silence in the chat. A consult is
71
+ * meant to check a decisive fact or two, not to survey a codebase, so a handful
72
+ * of reads is the whole budget; past it the guidance written so far is worth
73
+ * more to the caller than the reads it was still queuing.
74
+ */
75
+ const ADVISOR_MAX_TOOL_CALLS = 8;
76
+
77
+ /** How far back the repeat-spawn guard looks for near-identical runs. */
78
+ const LOOP_GUARD_WINDOW_MS = 24 * 60 * 60 * 1000;
79
+
80
+ /**
81
+ * Near-identical runs one conversation may complete inside the window before
82
+ * the next spawn is held for confirmation. A couple of repeats is normal work;
83
+ * a fourth completed run of one objective in a single conversation is a loop.
84
+ * Runs that failed, were aborted, or were interrupted never count, so a retry
85
+ * after a bad run is not what trips this.
86
+ */
87
+ const LOOP_GUARD_CONVERSATION_LIMIT = 3;
88
+
89
+ /**
90
+ * Near-identical runs the whole assistant may complete inside the window,
91
+ * counted the same way. Higher than the per-conversation limit so the same
92
+ * audit asked for in a few separate chats still goes through, while a standing
93
+ * re-run of one objective does not.
94
+ */
95
+ const LOOP_GUARD_ASSISTANT_LIMIT = 10;
96
+
97
+ /**
98
+ * Near-identical runs one conversation may have IN FLIGHT before the next
99
+ * spawn is held. Lower than the completed limits because nothing has come back
100
+ * yet: a burst of copies fired before any of them finishes is the runaway shape
101
+ * the guard exists for, and the completed counts are blind to it for as long as
102
+ * the burst lasts.
103
+ */
104
+ const LOOP_GUARD_IN_FLIGHT_CONVERSATION_LIMIT = 2;
105
+
106
+ /** The same ceiling assistant-wide, scaled like the completed limits. */
107
+ const LOOP_GUARD_IN_FLIGHT_ASSISTANT_LIMIT = 4;
108
+
109
+ /**
110
+ * The tier a `verdict` spawn runs on when it names no `inference_profile`.
111
+ *
112
+ * Checking a criterion against evidence that already exists is mechanical:
113
+ * find the file, read the value, say PASS or FAIL. A premium model buys
114
+ * nothing there, and completion checks are frequent enough that paying
115
+ * investigation rates for them dominates delegated spend, so the contract that
116
+ * makes a spawn a check also picks the tier the check is worth.
117
+ */
118
+ const VERDICT_PROFILE_KEY = "cost-optimized";
119
+
52
120
  /**
53
121
  * Model-input schema, `safeParse`d at the top of {@link executeSubagentSpawn}.
54
122
  * Same in-tool pattern and TOOLS.json drift guard as the other bundled-skill
@@ -56,15 +124,16 @@ const ADVISOR_MAX_TIMEOUT_MS = 300_000;
56
124
  * framework.
57
125
  *
58
126
  * `fork` / `send_result_to_user` (deliberate `=== true` / `!== false`
59
- * coercions) and `role` (SubagentManager.spawn's "Invalid subagent role"
60
- * error, asserted by tests, owns non-enum values) are deliberately
61
- * UNDECLARED — loose passthrough.
127
+ * coercions) and `role` (any string resolves, see `resolveSubagentRole`) are
128
+ * deliberately UNDECLARED: loose passthrough.
62
129
  */
63
130
  export const subagentSpawnInputSchema = z.looseObject({
64
131
  label: nullAsOmitted(z.string()),
65
132
  objective: nullAsOmitted(z.string()),
66
133
  context: nullAsOmitted(z.string()),
67
134
  inference_profile: z.string().optional(),
135
+ confirm_repeat: nullAsOmitted(z.boolean()),
136
+ output_contract: nullAsOmitted(z.enum(SUBAGENT_OUTPUT_CONTRACTS)),
68
137
  });
69
138
 
70
139
  export async function executeSubagentSpawn(
@@ -81,8 +150,13 @@ export async function executeSubagentSpawn(
81
150
  const objective = parsed.objective;
82
151
  const extraContext = parsed.context;
83
152
  const fork = input.fork === true;
84
- const role = (input.role as string | undefined) ?? undefined;
153
+ const requestedRole =
154
+ typeof input.role === "string" && input.role.trim().length > 0
155
+ ? input.role
156
+ : undefined;
157
+ const resolvedRole = resolveSubagentRole(requestedRole);
85
158
  const inferenceProfile = parsed.inference_profile;
159
+ const outputContract = parsed.output_contract;
86
160
 
87
161
  // For fork mode, sendResultToUser defaults to false unless explicitly set to true.
88
162
  // For regular mode, sendResultToUser defaults to true (existing behavior).
@@ -97,6 +171,11 @@ export async function executeSubagentSpawn(
97
171
  };
98
172
  }
99
173
 
174
+ const contractError = outputContractError(outputContract, resolvedRole.role);
175
+ if (contractError) {
176
+ return { content: contractError, isError: true };
177
+ }
178
+
100
179
  let requestedOverrideProfile: string | undefined;
101
180
  let forceOverrideProfile = false;
102
181
  if (inferenceProfile !== undefined) {
@@ -122,10 +201,10 @@ export async function executeSubagentSpawn(
122
201
  };
123
202
  }
124
203
 
125
- // ── Advisor role: synchronous, tool-less, stronger-model consult ──
204
+ // ── Advisor role: synchronous, read-only, stronger-model consult ──
126
205
  // Branch before the fire-and-forget path: the advisor blocks on the run and
127
206
  // returns its guidance as the tool result in the same turn.
128
- if (role === "advisor") {
207
+ if (resolvedRole.role === "advisor") {
129
208
  return runAdvisorConsult({
130
209
  context,
131
210
  label,
@@ -135,6 +214,21 @@ export async function executeSubagentSpawn(
135
214
  });
136
215
  }
137
216
 
217
+ // ── Repeat-spawn guard ───────────────────────────────────────────
218
+ // Re-running an objective that already completed several times in the last
219
+ // day buys the same answer twice, so the guard hands the repetition back to
220
+ // the caller with what it has already cost. It never blocks: `confirm_repeat`
221
+ // always spawns, and the advisor consult returned above is never guarded.
222
+ if (parsed.confirm_repeat !== true) {
223
+ const guardResult = repeatSpawnGuardResult(
224
+ context.conversationId,
225
+ objective,
226
+ );
227
+ if (guardResult) {
228
+ return guardResult;
229
+ }
230
+ }
231
+
138
232
  // ── Fork mode: resolve parent context ────────────────────────────
139
233
  let forkFields:
140
234
  | {
@@ -165,43 +259,52 @@ export async function executeSubagentSpawn(
165
259
  };
166
260
  }
167
261
 
168
- // The subagent runs as its own background conversation, so the agent
169
- // loop's background-skip rule would zero out any inherited profile.
170
- // Forward the invoker's profile explicitly via `SubagentConfig` so
171
- // `SubagentManager.spawn` passes it into the subagent's `runAgentLoop` as
172
- // `options.overrideProfile`.
262
+ // A subagent resolves the `subagentSpawn` call site's own default profile.
263
+ // A profile pinned on a conversation is a choice about that conversation,
264
+ // not about the work it delegates, and carrying it into a child would apply
265
+ // the pinned model's price and its tool-calling reliability to every spawn
266
+ // the turn makes.
173
267
  //
174
- // Resolution order: an explicit spawn-time profile, then the per-turn
175
- // `context.overrideProfile` (populated by `runAgentLoopImpl` from its
176
- // resolved `turnOverrideProfile`, covering per-conversation overrides and
177
- // tool-routed switches), then a row read, and finally the resolved DEFAULT
178
- // profile of the call site that invoked us. That last fallback is what makes
179
- // a subagent match its invoker when the invoking turn ran purely on its
180
- // call-site default the workspace `activeProfile` for `mainAgent`, or the
181
- // call site's own winning default (a `llm.callSites.<callSite>` pin or the
182
- // catalog intent, e.g. `cost-optimized`) for a heartbeat/background
183
- // invoker (`resolveDefaultProfileKey` runs the same winner selection as
184
- // dispatch, minus a per-turn override).
268
+ // Landing on the call site's own profile means passing NO override: the
269
+ // child already runs its loop under `callSite: "subagentSpawn"`, so the
270
+ // resolver picks that profile on its own. Passing it explicitly would select
271
+ // the same model but register as an override, and `resolveUsageAttribution`
272
+ // classifies an override winner as `conversation`, which would file every
273
+ // subagent's spend under a pin the caller never set and break the call-site
274
+ // breakdown this telemetry exists for.
185
275
  //
186
- // An explicit `llm.callSites.subagentSpawn` profile must still win over
187
- // the invoker-default tier: that tier is a matching heuristic, not a user
188
- // choice, and any override outranks the call-site pin under single-winner
189
- // resolution so the heuristic is only forwarded when no explicit pin
190
- // exists. An explicit `inference_profile` argument keeps
191
- // `forceOverrideProfile` and wins outright; the row read short-circuits
192
- // to `undefined` for the background subagent conversation and for tool
193
- // calls outside an agent-loop turn.
276
+ // An explicit `inference_profile` argument keeps `forceOverrideProfile` and
277
+ // wins outright, unless the catalog states it cannot call tools, in which
278
+ // case it falls back to the same default with a note on the result.
279
+ const config = getConfig();
280
+ const llm = config.llm;
281
+
282
+ let profileNote: string | undefined;
194
283
  let inheritedOverrideProfile = requestedOverrideProfile;
195
284
  if (inheritedOverrideProfile === undefined) {
196
- const llm = getConfig().llm;
197
- inheritedOverrideProfile =
198
- context.overrideProfile ??
199
- getConversationOverrideProfile(context.conversationId) ??
200
- (llm.callSites?.subagentSpawn?.profile == null
201
- ? resolveDefaultProfileKey(context.invokingCallSite ?? "mainAgent", llm)
202
- : undefined);
285
+ if (outputContract === "verdict") {
286
+ // A verdict is a check rather than an investigation, so it takes the
287
+ // cheap tier. The tier is this tool's preset, not a caller's choice, and
288
+ // an override outranks a call site's own profile under single-winner
289
+ // resolution, so the preset is only applied when the user pinned
290
+ // nothing: with an explicit `llm.callSites.subagentSpawn` profile the
291
+ // field is left unset and that pin wins. An `inference_profile` argument
292
+ // beats both and never enters this branch at all.
293
+ inheritedOverrideProfile =
294
+ llm.callSites?.subagentSpawn?.profile == null
295
+ ? VERDICT_PROFILE_KEY
296
+ : undefined;
297
+ }
298
+ // Every other spawn leaves this unset, which is what lands it on the
299
+ // subagentSpawn default rather than anything the parent turn was pinned to.
300
+ } else if (profileSupportsTools(inheritedOverrideProfile, config) === false) {
301
+ profileNote = `requested profile "${inheritedOverrideProfile}" is not verified for tool calling; ran on the default profile instead.`;
302
+ inheritedOverrideProfile = undefined;
303
+ forceOverrideProfile = false;
203
304
  }
204
305
 
306
+ const roleNote = resolvedRoleNote(resolvedRole);
307
+
205
308
  try {
206
309
  const subagentId = await manager.spawn(
207
310
  {
@@ -210,13 +313,28 @@ export async function executeSubagentSpawn(
210
313
  objective,
211
314
  context: extraContext,
212
315
  sendResultToUser,
213
- // Regular forks omit the role so they default to general; the advisor
214
- // role is special-cased earlier via runAdvisorConsult, not here.
215
- ...(!fork && role ? { role: role as SubagentRole } : {}),
316
+ // The resolved type is passed for every spawn, fork included: a fork
317
+ // that named no role resolves to `builder`, which imposes no
318
+ // allowlist, so it keeps the parent's tool surface, which is what the
319
+ // system prompt it inherits describes. The advisor is special-cased
320
+ // earlier via runAdvisorConsult, not here.
321
+ role: resolvedRole.role,
322
+ ...(resolvedRole.personaText
323
+ ? { persona: resolvedRole.personaText }
324
+ : {}),
325
+ ...(outputContract ? { outputContract } : {}),
326
+ // Declare the spawn mode so delegated LLM spend is separable in
327
+ // telemetry: every variety shares `llm_call_site = "subagentSpawn"`,
328
+ // and a fork's inherited transcript costs very differently from a
329
+ // fresh objective-only spawn.
330
+ spawnMode: fork ? "fork" : "regular",
216
331
  ...(inheritedOverrideProfile
217
332
  ? { overrideProfile: inheritedOverrideProfile }
218
333
  : {}),
219
334
  ...(forceOverrideProfile ? { forceOverrideProfile: true } : {}),
335
+ // Delegated work belongs to the firing that triggered the invoking
336
+ // turn, so the child's usage rows carry the parent's stamp.
337
+ ...(context.cronRunId ? { cronRunId: context.cronRunId } : {}),
220
338
  ...(context.toolUseId ? { parentToolUseId: context.toolUseId } : {}),
221
339
  ...forkFields,
222
340
  },
@@ -228,7 +346,10 @@ export async function executeSubagentSpawn(
228
346
  subagentId,
229
347
  label,
230
348
  status: "pending",
349
+ role: resolvedRole.role,
231
350
  ...(fork ? { isFork: true } : {}),
351
+ ...(profileNote ? { note: profileNote } : {}),
352
+ ...(roleNote ? { roleNote } : {}),
232
353
  message: fork
233
354
  ? `Forked subagent "${label}" spawned with full parent context. You will be notified automatically when it completes or fails - do NOT poll subagent_status. Continue the conversation normally.`
234
355
  : `Subagent "${label}" spawned. You will be notified automatically when it completes or fails - do NOT poll subagent_status. Continue the conversation normally.`,
@@ -241,6 +362,181 @@ export async function executeSubagentSpawn(
241
362
  }
242
363
  }
243
364
 
365
+ // ── Output contract ──────────────────────────────────────────────────
366
+
367
+ /**
368
+ * Why the requested `output_contract` cannot run under the type this spawn
369
+ * resolved to, or `undefined` when the pairing is fine.
370
+ *
371
+ * The two non-default contracts each need a capability only one type has: a
372
+ * verdict is a claim about what already exists, which is the read-only
373
+ * researcher's job, and an artifact has to be written, which only a builder
374
+ * can do. The advisor takes no contract at all: it is a blocking consult that
375
+ * returns guidance in its own framing, and its child never sees a built system
376
+ * prompt or fork framing to render one into. That includes an explicit
377
+ * `report`, which is checked against the advisor before it is waved through as
378
+ * the default everywhere else: the caller asked for a shape the consult cannot
379
+ * produce, and only an omitted contract means it never asked.
380
+ *
381
+ * Mismatches are rejected rather than coerced. Silently promoting a verdict
382
+ * spawn to a builder would hand out write access the caller never asked for,
383
+ * and silently demoting one to a report would return prose where the caller
384
+ * expected pass/fail. Either way the caller is the only one who can say which
385
+ * half of its request was the mistake.
386
+ */
387
+ function outputContractError(
388
+ contract: SubagentOutputContract | undefined,
389
+ role: SubagentRole,
390
+ ): string | undefined {
391
+ if (contract === undefined) {
392
+ return undefined;
393
+ }
394
+ if (role === "advisor") {
395
+ return (
396
+ `output_contract "${contract}" does not apply to the advisor: it is a blocking consult that returns guidance in its own shape. ` +
397
+ 'Drop output_contract, or spawn role "researcher" with output_contract "verdict" to have work checked.'
398
+ );
399
+ }
400
+ if (contract === "report") {
401
+ return undefined;
402
+ }
403
+ if (contract === "verdict" && role !== "researcher") {
404
+ return (
405
+ `output_contract "verdict" is only available to researcher-typed subagents, and this spawn resolved to "${role}". ` +
406
+ 'Spawn it with role "researcher" to get an evidence-backed PASS/FAIL check, or drop output_contract to get a report.'
407
+ );
408
+ }
409
+ if (contract === "artifact" && role !== "builder") {
410
+ return (
411
+ `output_contract "artifact" is only available to builder-typed subagents, and this spawn resolved to "${role}". ` +
412
+ 'Spawn it with role "builder" so it can actually produce the artifact, or drop output_contract to get a report.'
413
+ );
414
+ }
415
+ return undefined;
416
+ }
417
+
418
+ // ── Role resolution ──────────────────────────────────────────────────
419
+
420
+ /**
421
+ * What to tell the parent about how its `role` was read, or `undefined` when
422
+ * the requested role was a type name and nothing needs saying.
423
+ *
424
+ * The parent chose the role, so a silent substitution is the one outcome to
425
+ * avoid: an alias and a persona fallback both change what the child can do,
426
+ * and only the parent can decide whether that is what it wanted.
427
+ */
428
+ function resolvedRoleNote(resolved: ResolvedSubagentRole): string | undefined {
429
+ if (resolved.alias) {
430
+ return `Role "${resolved.alias}" is an older name for "${resolved.role}"; the subagent runs with the ${resolved.role} toolset.`;
431
+ }
432
+ if (resolved.personaText) {
433
+ return (
434
+ `Role "${resolved.personaText}" is not a subagent type, so it was used as a persona and the subagent runs as a researcher: ` +
435
+ 'read-only, no shell. If the task has to write files or run commands, re-spawn it with role "builder".'
436
+ );
437
+ }
438
+ return undefined;
439
+ }
440
+
441
+ // ── Repeat-spawn guard ───────────────────────────────────────────────
442
+
443
+ /**
444
+ * The result to return instead of spawning when this objective has already run
445
+ * too often inside {@link LOOP_GUARD_WINDOW_MS}, or `undefined` when the spawn
446
+ * should proceed.
447
+ *
448
+ * Two shapes of repetition are held, and they are told apart because the caller
449
+ * has to do something different about each. Completed runs mean an answer
450
+ * exists, so the caller is pointed at it. In-flight runs mean copies of this
451
+ * work are executing right now with nothing to show yet, which is the runaway
452
+ * loop the guard is for: a burst issued faster than anything can finish is
453
+ * invisible to the completed counts for its whole duration. Runs that ended
454
+ * without an answer count as neither, so a retry after a failure is never held.
455
+ *
456
+ * Not an error: the caller is being handed what its earlier runs produced and
457
+ * cost, and can still spawn by passing `confirm_repeat: true`. The spawn
458
+ * history can only advise, so a failed read lets the spawn through.
459
+ */
460
+ function repeatSpawnGuardResult(
461
+ parentConversationId: string,
462
+ objective: string,
463
+ ): ToolExecutionResult | undefined {
464
+ let recent: RecentSimilarSpawns;
465
+ try {
466
+ recent = countRecentSimilarSpawns({
467
+ parentConversationId,
468
+ normalizedObjective: normalizeSpawnObjective(objective),
469
+ sinceMs: Date.now() - LOOP_GUARD_WINDOW_MS,
470
+ });
471
+ } catch (err) {
472
+ log.warn(
473
+ { err, conversationId: parentConversationId },
474
+ "Repeat-spawn guard could not read spawn history",
475
+ );
476
+ return undefined;
477
+ }
478
+
479
+ // The spawn being asked for is itself part of the limit, so a window whose
480
+ // runs already fill the allowance is what trips the guard. Completed runs are
481
+ // checked first: when both shapes are present, an answer that already exists
482
+ // is the more actionable thing to hand back.
483
+ let scope: string;
484
+ let tally: SimilarSpawnTally;
485
+ if (recent.conversation.count >= LOOP_GUARD_CONVERSATION_LIMIT) {
486
+ scope = "in this conversation";
487
+ tally = recent.conversation;
488
+ } else if (recent.assistant.count >= LOOP_GUARD_ASSISTANT_LIMIT) {
489
+ scope = "across this assistant";
490
+ tally = recent.assistant;
491
+ } else if (
492
+ recent.conversation.inFlight >= LOOP_GUARD_IN_FLIGHT_CONVERSATION_LIMIT
493
+ ) {
494
+ return inFlightGuardResult(
495
+ recent.conversation.inFlight,
496
+ "this conversation",
497
+ );
498
+ } else if (
499
+ recent.assistant.inFlight >= LOOP_GUARD_IN_FLIGHT_ASSISTANT_LIMIT
500
+ ) {
501
+ return inFlightGuardResult(recent.assistant.inFlight, "this assistant");
502
+ } else {
503
+ return undefined;
504
+ }
505
+
506
+ const hours = LOOP_GUARD_WINDOW_MS / 3_600_000;
507
+ const cost =
508
+ tally.estimatedCost > 0
509
+ ? `, at a total cost of about $${tally.estimatedCost.toFixed(2)}`
510
+ : "";
511
+ return {
512
+ content:
513
+ `${tally.count} near-identical subagents already completed ${scope} in the last ${hours} hours${cost}. ` +
514
+ "Repeating an objective rarely returns a different answer: read what the earlier run produced with subagent_read, " +
515
+ "or narrow the objective to what is actually still missing. " +
516
+ "If the repetition is intentional, call subagent_spawn again with confirm_repeat: true.",
517
+ isError: false,
518
+ };
519
+ }
520
+
521
+ /**
522
+ * The result handed back when copies of this objective are still executing.
523
+ * Says explicitly that there is nothing to read yet, so the caller waits for
524
+ * the running work instead of being sent after results that do not exist.
525
+ */
526
+ function inFlightGuardResult(
527
+ inFlight: number,
528
+ scope: string,
529
+ ): ToolExecutionResult {
530
+ return {
531
+ content:
532
+ `${inFlight} near-identical subagents are already running in ${scope}, and none of them has returned yet. ` +
533
+ "Another copy would repeat work that is in progress: wait for the running ones to report back, " +
534
+ "or narrow the objective to the part they are not covering. " +
535
+ "If the repetition is intentional, call subagent_spawn again with confirm_repeat: true.",
536
+ isError: false,
537
+ };
538
+ }
539
+
244
540
  // ── Advisor consult ──────────────────────────────────────────────────
245
541
 
246
542
  /**
@@ -248,15 +544,20 @@ export async function executeSubagentSpawn(
248
544
  * consult and return its guidance as the tool result.
249
545
  *
250
546
  * Inherits the parent transcript (sanitized), frames it as advice via
251
- * `buildAdvisorSystem`, runs tool-less on `llm.advisorProfile` (unless the
252
- * caller passed an explicit `inference_profile`), and is bounded by a
253
- * progress-aware deadline: an idle window (`ADVISOR_IDLE_TIMEOUT_MS`) reset on
254
- * every streamed token so a reasoning model isn't killed mid-thought, plus an
255
- * absolute `ADVISOR_MAX_TIMEOUT_MS` backstop. If either ceiling is hit, the
256
- * partial guidance produced so far is recovered and returned with a "may be cut
257
- * off" note rather than discarded. Degrades to a benign non-error notice on any
258
- * other failure (including the depth-limit rejection when a subagent itself
259
- * calls the advisor).
547
+ * `buildAdvisorSystem`, and runs on `llm.advisorProfile` (unless the caller
548
+ * passed an explicit `inference_profile`) under both the advisor role allowlist
549
+ * and `denySideEffectTools`, so the only tools it can reach are the first-party
550
+ * built-in readers. It is bounded on two axes, because the consult holds up the
551
+ * user-facing turn while it runs: a progress-aware deadline (an idle window,
552
+ * `ADVISOR_IDLE_TIMEOUT_MS`, reset on every streamed token and every tool event
553
+ * so a reasoning or reading model isn't killed mid-consult, plus an absolute
554
+ * `ADVISOR_MAX_TIMEOUT_MS` backstop), and a ceiling of `ADVISOR_MAX_TOOL_CALLS`
555
+ * tool calls, since tool activity re-arms the idle window and a reading consult
556
+ * would otherwise be bounded only by the backstop. Whichever ceiling is hit, the
557
+ * partial guidance produced so far is recovered and returned with a note saying
558
+ * what cut it short, rather than discarded. Degrades to a benign non-error
559
+ * notice on any other failure (including the depth-limit rejection when a
560
+ * subagent itself calls the advisor).
260
561
  */
261
562
  async function runAdvisorConsult(args: {
262
563
  context: ToolContext;
@@ -269,6 +570,11 @@ async function runAdvisorConsult(args: {
269
570
  const { context, label, objective, sendToClient, requestedOverrideProfile } =
270
571
  args;
271
572
 
573
+ /** Set when the consult is stopped for reading past its tool ceiling. */
574
+ let stoppedForToolCap = false;
575
+ /** Appended to the guidance when the consult did not run as asked for. */
576
+ let profileNote: string | undefined;
577
+
272
578
  try {
273
579
  const parentConversation = findConversation(context.conversationId);
274
580
  if (!parentConversation) {
@@ -313,24 +619,76 @@ async function runAdvisorConsult(args: {
313
619
 
314
620
  // Default to the stronger advisor profile when the caller did not pin one;
315
621
  // an explicit `inference_profile` wins (already forced upstream).
316
- const advisorProfile = getConfig().llm.advisorProfile;
317
- const overrideProfile = requestedOverrideProfile ?? advisorProfile;
622
+ const config = getConfig();
623
+ let overrideProfile = requestedOverrideProfile ?? config.llm.advisorProfile;
624
+ // The advisor carries read tools, so a profile the catalog states cannot
625
+ // call them is handed a surface it can never use and answers from the
626
+ // transcript alone. Fall back to the call site's own default and say so
627
+ // alongside the guidance, the way a regular spawn reports it. The check is
628
+ // unconditional, matching the tools it protects, and only a catalog `false`
629
+ // redirects, so a model the catalog has never heard of is left alone.
630
+ if (
631
+ overrideProfile !== undefined &&
632
+ profileSupportsTools(overrideProfile, config) === false
633
+ ) {
634
+ profileNote = `profile "${overrideProfile}" is not verified for tool calling; the advisor ran on the default profile instead.`;
635
+ // Clearing this is what lands the consult on the subagentSpawn default:
636
+ // the child runs under that call site, so the resolver picks it anyway.
637
+ // Naming it would select the same model but register as an override,
638
+ // which usage attribution reports as a conversation pin, filing a consult
639
+ // that merely fell back against a pin nobody set.
640
+ overrideProfile = undefined;
641
+ }
318
642
  const forceOverrideProfile = overrideProfile !== undefined;
319
643
 
320
- // Progress-aware deadline: reset on every streamed token so the consult
321
- // isn't killed mid-thought, with an absolute backstop. Combine it with the
322
- // caller's own signal.
644
+ // Progress-aware deadline: reset on every sign of forward progress so the
645
+ // consult isn't killed mid-thought or mid-read, with an absolute backstop.
646
+ // Combine it with the caller's own signal.
323
647
  const deadline = createConsultDeadline({
324
648
  idleMs: ADVISOR_IDLE_TIMEOUT_MS,
325
649
  maxMs: ADVISOR_MAX_TIMEOUT_MS,
326
650
  });
327
- const signal = context.signal
328
- ? AbortSignal.any([context.signal, deadline.signal])
329
- : deadline.signal;
330
- // Every streamed chunk (thinking or text) counts as progress and resets the
331
- // idle window, then forwards to the caller's stream sink if one is present.
332
- const onText = (chunk: string): void => {
651
+ // Tool ceiling, on its own controller so the abort is attributable: the
652
+ // consult is stopped for reading too much, which reads back to the caller
653
+ // differently from running out of time.
654
+ const toolCap = new AbortController();
655
+ const signal = AbortSignal.any(
656
+ context.signal
657
+ ? [context.signal, deadline.signal, toolCap.signal]
658
+ : [deadline.signal, toolCap.signal],
659
+ );
660
+ // Count the child's tool calls off its own event stream. Each executed call
661
+ // emits exactly one `tool_use_start`, enveloped by the manager as a
662
+ // `subagent_event` on its way to the parent's client. The budget is spent
663
+ // in full before the abort fires: the call that trips it is the one past
664
+ // `ADVISOR_MAX_TOOL_CALLS`, so the consult keeps every result inside the
665
+ // budget and loses only the call it was starting.
666
+ let toolCalls = 0;
667
+ const countingSendToClient = (msg: AssistantEvent): void => {
668
+ sendToClient(msg);
669
+ if (!isAdvisorToolCallEvent(msg) || toolCap.signal.aborted) {
670
+ return;
671
+ }
672
+ toolCalls += 1;
673
+ if (toolCalls > ADVISOR_MAX_TOOL_CALLS) {
674
+ stoppedForToolCap = true;
675
+ log.warn(
676
+ { conversationId: context.conversationId, toolCalls },
677
+ "Advisor consult exceeded its tool ceiling; returning guidance so far",
678
+ );
679
+ toolCap.abort();
680
+ }
681
+ };
682
+ // Streamed tokens AND tool activity both count as progress. A consult that
683
+ // opens a file emits no token while the read runs, so token-only progress
684
+ // would let the idle window lapse on a healthy advisor and abort it before
685
+ // the model ever sees its tool result. The absolute backstop is untouched,
686
+ // so a stalled tool cannot extend the consult past `ADVISOR_MAX_TIMEOUT_MS`.
687
+ const onProgress = (): void => {
333
688
  deadline.recordProgress();
689
+ };
690
+ // Streamed chunks additionally forward to the caller's stream sink.
691
+ const onText = (chunk: string): void => {
334
692
  context.onOutput?.(chunk);
335
693
  };
336
694
 
@@ -349,39 +707,74 @@ async function runAdvisorConsult(args: {
349
707
  sendResultToUser: false,
350
708
  role: "advisor",
351
709
  fork: true,
710
+ // The advisor's read-only guarantee cannot rest on tool NAMES. A
711
+ // workspace tool may register under `file_read` (registerWorkspaceTools
712
+ // stashes the built-in and installs its own implementation), and a
713
+ // name-only role allowlist would hand the advisor that implementation
714
+ // to execute. The owner-aware read-only gate that closes this comes
715
+ // from `denySideEffects` on the advisor's registry entry, so every
716
+ // path that projects the role applies it, this spawn and the
717
+ // `tools list --agent advisor` preview alike.
718
+ //
719
+ // The advisor is a ROLE, not an `LLMCallSiteEnum` value, so its usage
720
+ // lands under `subagentSpawn` like any other subagent. This is what
721
+ // makes advisor consults separable from regular forks in telemetry.
722
+ spawnMode: "advisor_consult",
352
723
  parentMessages: sanitizedMessages,
353
724
  systemPromptOverride: buildAdvisorSystem(parentSystemPrompt),
354
725
  ...(overrideProfile ? { overrideProfile } : {}),
355
726
  ...(forceOverrideProfile ? { forceOverrideProfile: true } : {}),
727
+ // A consult is delegated work of the invoking turn, so its spend
728
+ // attributes to the same firing.
729
+ ...(context.cronRunId ? { cronRunId: context.cronRunId } : {}),
356
730
  ...(context.toolUseId ? { parentToolUseId: context.toolUseId } : {}),
357
731
  },
358
- sendToClient,
359
- { signal, onText },
732
+ countingSendToClient,
733
+ { signal, onText, onProgress },
360
734
  );
361
735
 
362
736
  const trimmed = advice.trim();
363
737
  return {
364
- content:
738
+ content: withAdvisorNote(
365
739
  trimmed.length > 0 ? trimmed : "(advisor returned no guidance)",
740
+ profileNote,
741
+ ),
366
742
  isError: false,
367
743
  };
368
744
  } finally {
369
745
  deadline.dispose();
370
746
  }
371
747
  } catch (err) {
372
- // Timed out mid-generation: salvage whatever guidance the advisor had
748
+ // Cut short mid-generation: salvage whatever guidance the advisor had
373
749
  // written rather than throwing it away. Partial strategic advice is far
374
750
  // more useful to the agent than an "unavailable" notice — especially on a
375
- // slow reasoning profile that needs most of the window to think.
751
+ // slow reasoning profile that needs most of the window to think. The note
752
+ // names which ceiling stopped it, because "it ran out of time" and "it was
753
+ // still reading" call for different follow-ups from the caller.
376
754
  if (err instanceof SubagentAbortedError) {
377
755
  const partial = err.partialText.trim();
378
756
  if (partial.length > 0) {
379
757
  log.warn(
380
- { conversationId: context.conversationId },
381
- "Advisor consult timed out; returning partial guidance",
758
+ { conversationId: context.conversationId, stoppedForToolCap },
759
+ "Advisor consult was cut short; returning partial guidance",
382
760
  );
383
761
  return {
384
- content: `${partial}\n\n_(The advisor reached its time limit while still writing — the guidance above may be cut off.)_`,
762
+ content: withAdvisorNote(
763
+ partial,
764
+ stoppedForToolCap
765
+ ? `The advisor used its full budget of ${ADVISOR_MAX_TOOL_CALLS} tool calls and answered with what it had read, so the guidance above may be incomplete.`
766
+ : "The advisor reached its time limit while still writing, so the guidance above may be cut off.",
767
+ profileNote,
768
+ ),
769
+ isError: false,
770
+ };
771
+ }
772
+ if (stoppedForToolCap) {
773
+ return {
774
+ content: withAdvisorNote(
775
+ `(advisor used its full budget of ${ADVISOR_MAX_TOOL_CALLS} tool calls without writing any guidance: narrow the question, or point it at the specific file or decision you want checked)`,
776
+ profileNote,
777
+ ),
385
778
  isError: false,
386
779
  };
387
780
  }
@@ -396,6 +789,35 @@ async function runAdvisorConsult(args: {
396
789
  }
397
790
  }
398
791
 
792
+ /**
793
+ * Whether a parent-bound event is a child tool call starting. The manager
794
+ * envelopes every child event as `subagent_event`, so the consult reads the
795
+ * inner event to see what the advisor is doing.
796
+ */
797
+ function isAdvisorToolCallEvent(msg: AssistantEvent): boolean {
798
+ if (msg.type !== "subagent_event") {
799
+ return false;
800
+ }
801
+ const inner = (msg as { event?: { type?: string } }).event;
802
+ return inner?.type === "tool_use_start";
803
+ }
804
+
805
+ /**
806
+ * The guidance with any notes about how the consult actually ran appended
807
+ * below it. Notes are italicized asides so the guidance itself stays the
808
+ * result; nothing is appended when there is nothing to say.
809
+ */
810
+ function withAdvisorNote(
811
+ guidance: string,
812
+ ...notes: (string | undefined)[]
813
+ ): string {
814
+ const present = notes.filter((note): note is string => Boolean(note));
815
+ if (present.length === 0) {
816
+ return guidance;
817
+ }
818
+ return `${guidance}\n\n${present.map((note) => `_(${note})_`).join("\n")}`;
819
+ }
820
+
399
821
  /**
400
822
  * Append the in-flight assistant turn (persisted this turn before the advisor
401
823
  * tool ran) to an in-memory message snapshot, unless the snapshot already ends