@vellumai/assistant 0.10.2 → 0.10.3-dev.202606252046.9075fd5

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 (546) hide show
  1. package/bun.lock +20 -0
  2. package/docs/workspace-tools.md +42 -33
  3. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  5. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  6. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +44 -0
  7. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  9. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +28 -0
  12. package/node_modules/@vellumai/service-contracts/src/channels.ts +41 -0
  13. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/openapi.yaml +196 -56
  15. package/package.json +4 -1
  16. package/scripts/test.sh +36 -15
  17. package/src/__tests__/actor-token-service.test.ts +36 -14
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  19. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  21. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  22. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  23. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  24. package/src/__tests__/approval-cascade.test.ts +2 -0
  25. package/src/__tests__/assistant-attachments.test.ts +42 -0
  26. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  27. package/src/__tests__/btw-routes.test.ts +2 -0
  28. package/src/__tests__/build-persisted-content.test.ts +2 -0
  29. package/src/__tests__/call-controller.test.ts +19 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  31. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  32. package/src/__tests__/channel-guardian.test.ts +164 -78
  33. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  34. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  35. package/src/__tests__/code-search-tool.test.ts +585 -0
  36. package/src/__tests__/compaction-events.test.ts +2 -0
  37. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  38. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  39. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  40. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  41. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  43. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  44. package/src/__tests__/config-loader-backfill.test.ts +123 -10
  45. package/src/__tests__/config-schema.test.ts +1 -0
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  47. package/src/__tests__/consult-deadline.test.ts +60 -0
  48. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  49. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  50. package/src/__tests__/contacts-relay-reads.test.ts +19 -24
  51. package/src/__tests__/contacts-write.test.ts +0 -2
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  54. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  55. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  56. package/src/__tests__/conversation-agent-loop.test.ts +134 -0
  57. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  58. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  59. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  61. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  62. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  63. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  64. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  65. package/src/__tests__/conversation-pairing.test.ts +2 -0
  66. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  67. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  68. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  69. package/src/__tests__/conversation-queue.test.ts +91 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  72. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  73. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  74. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  75. package/src/__tests__/conversation-surfaces-task-progress.test.ts +29 -0
  76. package/src/__tests__/conversation-title-service.test.ts +2 -0
  77. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  78. package/src/__tests__/conversation-usage.test.ts +2 -0
  79. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  80. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  81. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  82. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  83. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  84. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  85. package/src/__tests__/db-test-helpers.ts +5 -4
  86. package/src/__tests__/delete-propagation.test.ts +5 -3
  87. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  88. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  89. package/src/__tests__/dm-backfill.test.ts +6 -4
  90. package/src/__tests__/dm-persistence.test.ts +2 -0
  91. package/src/__tests__/emit-signal-routing-intent.test.ts +7 -6
  92. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  93. package/src/__tests__/exploration-drift-hook.test.ts +3 -2
  94. package/src/__tests__/filing-service.test.ts +2 -0
  95. package/src/__tests__/guardian-binding-drift-heal.test.ts +104 -19
  96. package/src/__tests__/guardian-dispatch.test.ts +140 -1
  97. package/src/__tests__/guardian-expiry-notifier.test.ts +282 -0
  98. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  99. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  100. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  101. package/src/__tests__/heartbeat-service.test.ts +2 -0
  102. package/src/__tests__/helpers/channel-test-adapter.ts +46 -19
  103. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  104. package/src/__tests__/helpers/mock-logger.ts +1 -0
  105. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  106. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  107. package/src/__tests__/host-bash-routes.test.ts +31 -41
  108. package/src/__tests__/host-browser-routes.test.ts +26 -32
  109. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  110. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  111. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  112. package/src/__tests__/http-user-message-parity.test.ts +290 -8
  113. package/src/__tests__/inbound-invite-redemption.test.ts +115 -10
  114. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  115. package/src/__tests__/invite-redemption-service.test.ts +471 -53
  116. package/src/__tests__/invite-routes-http.test.ts +34 -0
  117. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  118. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  119. package/src/__tests__/llm-request-log-error-payload.test.ts +71 -9
  120. package/src/__tests__/llm-usage-store.test.ts +25 -0
  121. package/src/__tests__/mcp-config-secret-boundary.test.ts +392 -0
  122. package/src/__tests__/mcp-health-check.test.ts +2 -1
  123. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  124. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  125. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  126. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  127. package/src/__tests__/mtime-cache.test.ts +275 -5
  128. package/src/__tests__/native-web-search.test.ts +2 -0
  129. package/src/__tests__/non-member-access-request.test.ts +191 -17
  130. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  131. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  132. package/src/__tests__/notification-deep-link.test.ts +6 -0
  133. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  134. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  135. package/src/__tests__/openai-provider.test.ts +22 -12
  136. package/src/__tests__/openai-responses-provider.test.ts +12 -2
  137. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  138. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  139. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  140. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  141. package/src/__tests__/persona-resolver.test.ts +75 -45
  142. package/src/__tests__/plugin-bootstrap.test.ts +21 -83
  143. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  144. package/src/__tests__/plugin-pipeline.test.ts +96 -0
  145. package/src/__tests__/plugin-route-contribution.test.ts +6 -19
  146. package/src/__tests__/plugin-tool-contribution.test.ts +5 -20
  147. package/src/__tests__/plugin-types.test.ts +2 -4
  148. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  149. package/src/__tests__/process-message-display-content.test.ts +2 -0
  150. package/src/__tests__/provider-error-scenarios.test.ts +5 -4
  151. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  152. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  153. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  154. package/src/__tests__/reaction-persistence.test.ts +51 -4
  155. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  156. package/src/__tests__/registry.test.ts +4 -1
  157. package/src/__tests__/relay-server.test.ts +758 -32
  158. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -12
  159. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  160. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  161. package/src/__tests__/settings-routes.test.ts +32 -0
  162. package/src/__tests__/skills.test.ts +44 -0
  163. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  164. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +79 -0
  165. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  166. package/src/__tests__/stt-hints.test.ts +49 -15
  167. package/src/__tests__/subagent-detail.test.ts +27 -0
  168. package/src/__tests__/subagent-disposal.test.ts +65 -0
  169. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  170. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  171. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  172. package/src/__tests__/subagent-role-registry.test.ts +24 -6
  173. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  174. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  175. package/src/__tests__/subagent-tools.test.ts +398 -1
  176. package/src/__tests__/suggestion-routes.test.ts +2 -0
  177. package/src/__tests__/thread-backfill.test.ts +3 -3
  178. package/src/__tests__/title-generate-hook.test.ts +2 -0
  179. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  180. package/src/__tests__/tool-executor.test.ts +16 -11
  181. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  182. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  183. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  184. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  185. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +12 -12
  186. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  187. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  188. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  189. package/src/__tests__/twilio-routes.test.ts +96 -0
  190. package/src/__tests__/ui-file-upload-surface.test.ts +86 -0
  191. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  192. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  193. package/src/__tests__/voice-invite-redemption.test.ts +216 -20
  194. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  195. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  196. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +208 -0
  197. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  198. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  199. package/src/__tests__/workspace-migration-remove-hooks.test.ts +14 -35
  200. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  201. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  202. package/src/agent/loop-exclusive-tool.test.ts +154 -0
  203. package/src/agent/loop-native-web-search.test.ts +200 -0
  204. package/src/agent/loop.ts +164 -1
  205. package/src/api/constants/sse-replay.ts +41 -0
  206. package/src/api/events/conversation-notice.ts +26 -0
  207. package/src/api/index.ts +19 -1
  208. package/src/api/responses/llm-request-log-entry.ts +29 -0
  209. package/src/api/responses/subagent-detail.ts +17 -0
  210. package/src/api/surfaces.ts +39 -3
  211. package/src/approvals/guardian-channel-delivery.ts +30 -0
  212. package/src/approvals/guardian-expiry-notifier.ts +148 -0
  213. package/src/approvals/guardian-request-resolvers.ts +17 -15
  214. package/src/calls/__tests__/relay-setup-router.test.ts +267 -17
  215. package/src/calls/call-domain.ts +3 -3
  216. package/src/calls/guardian-dispatch.ts +14 -9
  217. package/src/calls/inbound-trust-reader.ts +24 -2
  218. package/src/calls/media-stream-server.ts +21 -0
  219. package/src/calls/relay-access-wait.ts +6 -6
  220. package/src/calls/relay-server.ts +188 -51
  221. package/src/calls/relay-setup-router.ts +47 -17
  222. package/src/calls/relay-verification.ts +4 -4
  223. package/src/calls/stt-hints.ts +9 -12
  224. package/src/calls/twilio-routes.ts +14 -4
  225. package/src/channels/types.ts +10 -20
  226. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  227. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  228. package/src/cli/commands/cache.ts +194 -181
  229. package/src/cli/commands/contacts.ts +10 -7
  230. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  231. package/src/cli/commands/db/status.ts +37 -1
  232. package/src/cli/commands/mcp.ts +252 -218
  233. package/src/cli/commands/memory/__tests__/worker.test.ts +432 -0
  234. package/src/cli/commands/memory/index.ts +2 -0
  235. package/src/cli/commands/memory/worker.ts +242 -0
  236. package/src/cli/commands/plugins.ts +125 -13
  237. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  238. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  239. package/src/cli/lib/__tests__/publish-plugin.test.ts +404 -0
  240. package/src/cli/lib/list-installed-plugins.ts +179 -1
  241. package/src/cli/lib/publish-plugin.ts +633 -0
  242. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  243. package/src/config/__tests__/sync-gated-profiles.test.ts +16 -10
  244. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  245. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  246. package/src/config/bundled-skills/subagent/SKILL.md +17 -2
  247. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  248. package/src/config/call-site-defaults.ts +0 -6
  249. package/src/config/feature-flag-registry.json +5 -13
  250. package/src/config/llm-resolver.ts +0 -3
  251. package/src/config/loader.ts +38 -5
  252. package/src/config/prune-seeded-callsite-defaults.ts +110 -0
  253. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  254. package/src/config/schemas/call-site-catalog.ts +0 -7
  255. package/src/config/schemas/heartbeat.ts +2 -5
  256. package/src/config/schemas/llm.ts +3 -12
  257. package/src/config/schemas/memory-lifecycle.ts +12 -0
  258. package/src/config/schemas/memory-v3.ts +7 -0
  259. package/src/config/schemas/memory.ts +4 -0
  260. package/src/config/schemas/timeouts.ts +8 -0
  261. package/src/config/seed-inference-profiles.ts +30 -34
  262. package/src/config/skills.ts +27 -5
  263. package/src/config/sync-gated-profiles.ts +12 -16
  264. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +128 -0
  265. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  266. package/src/contacts/__tests__/member-write-relay.test.ts +226 -0
  267. package/src/contacts/contact-store.ts +27 -237
  268. package/src/contacts/contacts-write.ts +18 -55
  269. package/src/contacts/gateway-channel-read.ts +51 -0
  270. package/src/contacts/guardian-delivery-reader.ts +223 -0
  271. package/src/contacts/member-write-relay.ts +183 -0
  272. package/src/contacts/types.ts +3 -15
  273. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +35 -0
  274. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  275. package/src/daemon/assistant-attachments.ts +27 -4
  276. package/src/daemon/conversation-agent-loop.ts +28 -0
  277. package/src/daemon/conversation-notices.ts +60 -0
  278. package/src/daemon/conversation-process.ts +35 -16
  279. package/src/daemon/conversation-surfaces.ts +111 -38
  280. package/src/daemon/conversation-tool-setup.ts +71 -30
  281. package/src/daemon/conversation.ts +23 -1
  282. package/src/daemon/disk-pressure-guard.ts +12 -2
  283. package/src/daemon/event-loop-watchdog.ts +28 -1
  284. package/src/daemon/external-plugins-bootstrap.ts +17 -41
  285. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  287. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -2
  288. package/src/daemon/handlers/__tests__/config-channels.test.ts +220 -0
  289. package/src/daemon/handlers/config-a2a.ts +6 -14
  290. package/src/daemon/handlers/config-channels.ts +67 -26
  291. package/src/daemon/handlers/conversations.ts +77 -0
  292. package/src/daemon/lifecycle.ts +16 -0
  293. package/src/daemon/mcp-reload-service.ts +10 -0
  294. package/src/daemon/memory-v2-startup.test.ts +72 -0
  295. package/src/daemon/memory-v2-startup.ts +87 -19
  296. package/src/daemon/message-types/conversations.ts +2 -0
  297. package/src/daemon/message-types/surfaces.ts +12 -12
  298. package/src/daemon/server.ts +0 -4
  299. package/src/daemon/shutdown-handlers.ts +20 -0
  300. package/src/daemon/tool-setup-types.ts +9 -0
  301. package/src/heartbeat/heartbeat-service.ts +5 -0
  302. package/src/home/relationship-state-writer.ts +5 -0
  303. package/src/hooks/hook-loader.ts +341 -0
  304. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  305. package/src/ipc/assistant-server.ts +2 -2
  306. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -0
  307. package/src/mcp/client.ts +15 -1
  308. package/src/mcp/mcp-auth-orchestrator.ts +6 -1
  309. package/src/mcp/mcp-header-store.ts +134 -0
  310. package/src/mcp/mcp-oauth-provider.ts +19 -8
  311. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  312. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  313. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  314. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  315. package/src/memory/compaction-ledger-store.ts +107 -0
  316. package/src/memory/conversation-crud.ts +129 -61
  317. package/src/memory/db-connection.ts +22 -3
  318. package/src/memory/db-init.ts +37 -503
  319. package/src/memory/db-singleton.ts +6 -4
  320. package/src/memory/jobs-worker.ts +104 -0
  321. package/src/memory/llm-request-log-store.ts +26 -1
  322. package/src/memory/llm-usage-store.ts +48 -20
  323. package/src/memory/memory-retrospective-job.ts +14 -8
  324. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +152 -56
  325. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  326. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  327. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  328. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +297 -0
  329. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  330. package/src/memory/migrations/run-migrations.ts +106 -10
  331. package/src/memory/migrations/validate-migration-state.ts +105 -67
  332. package/src/memory/prompt-override.ts +129 -0
  333. package/src/memory/schema/contacts.ts +6 -2
  334. package/src/memory/schema/conversations.ts +37 -0
  335. package/src/memory/schema/infrastructure.ts +20 -0
  336. package/src/memory/steps.ts +1294 -0
  337. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  338. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  339. package/src/memory/v2/cli-command-store.ts +75 -38
  340. package/src/memory/v2/prompts/consolidation.ts +13 -82
  341. package/src/memory/v2/prompts/router.ts +21 -93
  342. package/src/memory/v2/skill-store.ts +68 -31
  343. package/src/memory/watchdog-events-store.ts +87 -0
  344. package/src/memory/worker-control.ts +204 -0
  345. package/src/memory/worker-process.ts +89 -0
  346. package/src/messaging/channel-binding-metadata.ts +31 -0
  347. package/src/messaging/provider-types.ts +8 -0
  348. package/src/messaging/providers/slack/binding-metadata.ts +60 -0
  349. package/src/notifications/__tests__/broadcaster.test.ts +8 -8
  350. package/src/notifications/__tests__/connected-channels.test.ts +86 -0
  351. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  352. package/src/notifications/__tests__/destination-resolver.test.ts +151 -0
  353. package/src/notifications/broadcaster.ts +8 -1
  354. package/src/notifications/decision-engine.ts +15 -7
  355. package/src/notifications/destination-resolver.ts +53 -25
  356. package/src/notifications/emit-signal.ts +34 -15
  357. package/src/onboarding/checkin-event.test.ts +222 -0
  358. package/src/onboarding/checkin-event.ts +321 -0
  359. package/src/onboarding/schedule-checkin.ts +190 -0
  360. package/src/permissions/question-prompter.test.ts +1 -1
  361. package/src/permissions/question-prompter.ts +7 -4
  362. package/src/plugin-api/index.ts +12 -12
  363. package/src/plugin-api/types.ts +12 -14
  364. package/src/plugin-api/vision-support.test.ts +28 -4
  365. package/src/plugin-api/vision-support.ts +66 -31
  366. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +2 -2
  367. package/src/plugins/defaults/empty-response/hooks/stop.ts +2 -2
  368. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +4 -3
  369. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +2 -2
  370. package/src/plugins/defaults/history-repair/hooks/stop.ts +2 -2
  371. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +2 -2
  372. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  373. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +12 -13
  374. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +14 -22
  375. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  376. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +2 -2
  377. package/src/plugins/defaults/image-recovery/hooks/stop.ts +2 -2
  378. package/src/plugins/defaults/index.ts +0 -35
  379. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +2 -2
  380. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +2 -2
  381. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +2 -2
  382. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +33 -3
  384. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +48 -4
  385. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +4 -8
  386. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +2 -2
  387. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +2 -2
  388. package/src/plugins/defaults/memory-v3-shadow/injector.ts +43 -15
  389. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  390. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  391. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +77 -13
  392. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +12 -11
  393. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +2 -2
  394. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +2 -2
  395. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -2
  396. package/src/plugins/defaults/title-generate/hooks/stop.ts +2 -2
  397. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +2 -2
  398. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +2 -2
  399. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +2 -2
  400. package/src/plugins/disabled-state.ts +31 -0
  401. package/src/plugins/external-plugin-loader.ts +2 -2
  402. package/src/plugins/mtime-cache.ts +186 -330
  403. package/src/plugins/pipeline.ts +111 -13
  404. package/src/plugins/registry.ts +59 -16
  405. package/src/plugins/surface-import.ts +121 -0
  406. package/src/plugins/types.ts +7 -9
  407. package/src/prompts/persona-resolver.ts +43 -11
  408. package/src/providers/anthropic/client.ts +5 -0
  409. package/src/providers/call-site-routing.ts +45 -0
  410. package/src/providers/model-catalog.ts +16 -0
  411. package/src/providers/openai/__tests__/api-error-normalization.test.ts +321 -0
  412. package/src/providers/openai/api-error-normalization.ts +270 -0
  413. package/src/providers/openai/chat-completions-provider.ts +37 -83
  414. package/src/providers/openai/responses-provider.ts +50 -46
  415. package/src/providers/openrouter/client.ts +5 -0
  416. package/src/providers/provider-send-message.ts +10 -0
  417. package/src/providers/ratelimit.ts +10 -0
  418. package/src/providers/registry.ts +1 -1
  419. package/src/providers/retry.ts +10 -0
  420. package/src/providers/types.ts +22 -0
  421. package/src/providers/usage-tracking.ts +10 -0
  422. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  423. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +184 -0
  424. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  425. package/src/runtime/__tests__/local-principal-trust.test.ts +162 -0
  426. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  427. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +402 -123
  428. package/src/runtime/access-request-helper.ts +18 -39
  429. package/src/runtime/actor-trust-resolver.ts +46 -19
  430. package/src/runtime/anchored-guardian.test.ts +109 -0
  431. package/src/runtime/anchored-guardian.ts +86 -0
  432. package/src/runtime/assistant-event-hub.ts +1 -1
  433. package/src/runtime/assistant-stream-state.ts +9 -2
  434. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  435. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  436. package/src/runtime/channel-verification-service.ts +56 -31
  437. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  438. package/src/runtime/guardian-vellum-migration.ts +69 -8
  439. package/src/runtime/invite-redemption-service.ts +213 -187
  440. package/src/runtime/local-actor-identity.test.ts +108 -0
  441. package/src/runtime/local-actor-identity.ts +85 -13
  442. package/src/runtime/local-principal-trust.ts +52 -0
  443. package/src/runtime/member-verdict-cache.ts +0 -0
  444. package/src/runtime/pending-interactions.ts +11 -1
  445. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  446. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  447. package/src/runtime/routes/__tests__/contact-routes.test.ts +305 -0
  448. package/src/runtime/routes/__tests__/global-search-routes.test.ts +92 -0
  449. package/src/runtime/routes/__tests__/http-adapter-actor-header.test.ts +129 -0
  450. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +188 -0
  451. package/src/runtime/routes/browser-routes.ts +1 -1
  452. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +13 -5
  453. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  454. package/src/runtime/routes/contact-routes.ts +48 -57
  455. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  456. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  457. package/src/runtime/routes/conversation-query-routes.ts +72 -0
  458. package/src/runtime/routes/conversation-routes.ts +84 -85
  459. package/src/runtime/routes/events-routes.ts +2 -2
  460. package/src/runtime/routes/global-search-routes.ts +3 -1
  461. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  462. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  463. package/src/runtime/routes/host-bash-routes.ts +5 -4
  464. package/src/runtime/routes/host-browser-routes.ts +9 -11
  465. package/src/runtime/routes/host-cu-routes.ts +5 -4
  466. package/src/runtime/routes/host-file-routes.ts +5 -4
  467. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  468. package/src/runtime/routes/http-adapter.ts +16 -1
  469. package/src/runtime/routes/identity-routes.ts +3 -2
  470. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  471. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  472. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -59
  473. package/src/runtime/routes/inbound-stages/background-dispatch.ts +13 -5
  474. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  475. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  476. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  477. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  478. package/src/runtime/routes/index.ts +2 -0
  479. package/src/runtime/routes/llm-context-normalization.ts +83 -0
  480. package/src/runtime/routes/mcp-auth-routes.ts +171 -19
  481. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  482. package/src/runtime/routes/migration-routes.ts +4 -1
  483. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  484. package/src/runtime/routes/settings-routes.ts +8 -3
  485. package/src/runtime/routes/subagents-routes.ts +5 -0
  486. package/src/runtime/routes/surface-action-routes.ts +42 -56
  487. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  488. package/src/runtime/services/conversation-serializer.ts +13 -58
  489. package/src/runtime/tool-grant-request-helper.ts +3 -3
  490. package/src/runtime/trust-verdict-consumer.ts +111 -40
  491. package/src/runtime/verification-outbound-actions.ts +18 -18
  492. package/src/signals/user-message.ts +16 -0
  493. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  494. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  495. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +22 -32
  496. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  497. package/src/subagent/index.ts +1 -1
  498. package/src/subagent/manager.ts +254 -33
  499. package/src/subagent/types.ts +11 -4
  500. package/src/telemetry/types.ts +34 -1
  501. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  502. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  503. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  504. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  505. package/src/tools/executor.ts +4 -4
  506. package/src/tools/filesystem/search.ts +543 -0
  507. package/src/tools/registry.ts +38 -0
  508. package/src/tools/shared/filesystem/path-policy.ts +12 -5
  509. package/src/tools/subagent/consult-deadline.ts +49 -0
  510. package/src/tools/subagent/spawn.ts +234 -5
  511. package/src/tools/tool-approval-handler.ts +1 -1
  512. package/src/tools/tool-defaults.ts +9 -2
  513. package/src/tools/tool-manifest.ts +3 -0
  514. package/src/tools/types.ts +131 -25
  515. package/src/tools/workspace-tools/loader.ts +348 -244
  516. package/src/util/errors.ts +26 -1
  517. package/src/util/logger.ts +9 -0
  518. package/src/util/platform.ts +19 -0
  519. package/src/util/telemetry-db-path.ts +24 -0
  520. package/src/workflows/library.test.ts +140 -0
  521. package/src/workflows/library.ts +82 -28
  522. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  523. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  524. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  525. package/src/workspace/migrations/048-remove-workspace-hooks.ts +14 -66
  526. package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +134 -0
  527. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  528. package/src/workspace/migrations/registry.ts +4 -0
  529. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  530. package/src/daemon/workspace-tools-watcher.ts +0 -328
  531. package/src/memory/migrations/registry.ts +0 -573
  532. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  533. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  534. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  535. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -153
  536. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  537. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  538. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  539. package/src/plugins/defaults/advisor/config.ts +0 -21
  540. package/src/plugins/defaults/advisor/consult.ts +0 -93
  541. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  542. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  543. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  544. package/src/plugins/defaults/advisor/package.json +0 -14
  545. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -65
  546. package/src/providers/openai/__tests__/api-error-detail.test.ts +0 -120
@@ -24,6 +24,7 @@ import { wrapWithCallSiteRouting } from "../providers/call-site-routing.js";
24
24
  import { resolveDefaultProvider } from "../providers/connection-resolution.js";
25
25
  import { RateLimitProvider } from "../providers/ratelimit.js";
26
26
  import { listProviders } from "../providers/registry.js";
27
+ import type { Message, TextContent } from "../providers/types.js";
27
28
  import { createAbortReason } from "../util/abort-reasons.js";
28
29
  import { ProviderNotConfiguredError } from "../util/errors.js";
29
30
  import { getLogger } from "../util/logger.js";
@@ -58,6 +59,38 @@ export function mergeSkillIds(
58
59
  return [...new Set([...roleSkillIds, ...(configSkillIds ?? [])])];
59
60
  }
60
61
 
62
+ // ── Final-text extraction helper ────────────────────────────────────────
63
+
64
+ /**
65
+ * Concatenate the `text` blocks of the conversation's trailing assistant
66
+ * message. Used by `spawnAndAwait` to return the child's final synthesis to
67
+ * the awaiting caller. Returns an empty string when the conversation has no
68
+ * assistant message or the final assistant message carries no text blocks
69
+ * (e.g. it ended on a tool_use).
70
+ */
71
+ function extractFinalAssistantText(messages: Message[]): string {
72
+ for (let i = messages.length - 1; i >= 0; i--) {
73
+ const message = messages[i];
74
+ if (message.role !== "assistant") continue;
75
+ return message.content
76
+ .filter((block): block is TextContent => block.type === "text")
77
+ .map((block) => block.text)
78
+ .join("");
79
+ }
80
+ return "";
81
+ }
82
+
83
+ /**
84
+ * Pull the user-visible text out of a streaming delta event, or null for any
85
+ * other event type. Used by the synchronous `onText` tap to forward
86
+ * `assistant_text_delta` / `assistant_thinking_delta` chunks to the caller.
87
+ */
88
+ function extractDeltaText(msg: ServerMessage): string | null {
89
+ if (msg.type === "assistant_text_delta") return msg.text;
90
+ if (msg.type === "assistant_thinking_delta") return msg.thinking;
91
+ return null;
92
+ }
93
+
61
94
  // ── Default subagent system prompt ──────────────────────────────────────
62
95
 
63
96
  function buildSubagentSystemPrompt(
@@ -110,6 +143,19 @@ interface ManagedSubagent {
110
143
  * the release to the TTL sweep rather than tearing down mid-drain.
111
144
  */
112
145
  hadEnqueuedMessages?: boolean;
146
+ /**
147
+ * Set on the synchronous `spawnAndAwait` path. When true, `runSubagent`
148
+ * skips the terminal parent-injection (`notifyParentTerminal`) — the awaiting
149
+ * caller receives the child's final text directly, so re-injecting a
150
+ * "read the result" notification into the parent would be redundant noise.
151
+ */
152
+ synchronous?: boolean;
153
+ /**
154
+ * Optional text tap for the synchronous path. When set, `wrappedSendToClient`
155
+ * forwards each `assistant_text_delta` / `assistant_thinking_delta` chunk to
156
+ * this callback IN ADDITION to the normal `subagent_event` envelope.
157
+ */
158
+ onText?: (chunk: string) => void;
113
159
  }
114
160
 
115
161
  export interface SubagentNotificationInfo {
@@ -121,6 +167,21 @@ export interface SubagentNotificationInfo {
121
167
  objective?: string;
122
168
  }
123
169
 
170
+ /**
171
+ * Thrown by `spawnAndAwait` when the run is aborted (e.g. an external timeout)
172
+ * before reaching a terminal `completed` state. Carries `partialText` — the
173
+ * child's trailing assistant text captured at the moment of abort — so a caller
174
+ * that times out a long generation can still surface the partial result instead
175
+ * of discarding it. Extends `Error` with the same legacy message, so callers
176
+ * that only inspect `.message` keep working.
177
+ */
178
+ export class SubagentAbortedError extends Error {
179
+ constructor(readonly partialText: string) {
180
+ super("Subagent run aborted before completion.");
181
+ this.name = "SubagentAbortedError";
182
+ }
183
+ }
184
+
124
185
  export class SubagentManager {
125
186
  /** subagentId → ManagedSubagent */
126
187
  private subagents = new Map<string, ManagedSubagent>();
@@ -145,6 +206,30 @@ export class SubagentManager {
145
206
  config: Omit<SubagentConfig, "id">,
146
207
  parentSendToClient: (msg: ServerMessage) => void,
147
208
  ): Promise<string> {
209
+ const { subagentId } = await this.setUpSubagent(config, parentSendToClient);
210
+
211
+ // ── Kick off the agent loop (fire-and-forget) ───────────────────
212
+ this.runSubagent(subagentId, config.objective).catch((err) => {
213
+ log.error({ subagentId, err }, "Subagent run failed unexpectedly");
214
+ });
215
+
216
+ return subagentId;
217
+ }
218
+
219
+ // ── Internal: shared spawn setup ──────────────────────────────────────
220
+
221
+ /**
222
+ * Perform all spawn-time setup shared by `spawn` and `spawnAndAwait`:
223
+ * enforce the depth limit, resolve role/provider/system prompt, construct
224
+ * the child Conversation, register it, and emit the `subagent_spawned`
225
+ * event. Does NOT start the agent loop — the caller decides whether to run
226
+ * fire-and-forget (`spawn`) or awaited (`spawnAndAwait`).
227
+ */
228
+ private async setUpSubagent(
229
+ config: Omit<SubagentConfig, "id">,
230
+ parentSendToClient: (msg: ServerMessage) => void,
231
+ opts?: { synchronous?: boolean; onText?: (chunk: string) => void },
232
+ ): Promise<{ subagentId: string; managed: ManagedSubagent }> {
148
233
  // ── Limit checks ────────────────────────────────────────────────
149
234
 
150
235
  // Depth check: prevent subagents from spawning nested subagents.
@@ -159,17 +244,20 @@ export class SubagentManager {
159
244
 
160
245
  // ── Resolve role ─────────────────────────────────────────────────
161
246
  const isFork = config.fork === true;
162
- let role: SubagentRole = (config.role as SubagentRole) ?? "general";
247
+ const role: SubagentRole = (config.role as SubagentRole) ?? "general";
163
248
  if (isFork && role !== "general") {
249
+ // A context-inheriting subagent normally keeps the parent's `general`
250
+ // role so its KV cache stays aligned with the parent conversation. An
251
+ // explicit non-general role opts out of that alignment on purpose
252
+ // (e.g. the advisor role running on a stronger profile), so honor it.
164
253
  log.warn(
165
254
  {
166
255
  requestedRole: role,
167
256
  parentConversationId: config.parentConversationId,
168
257
  label: config.label,
169
258
  },
170
- "Fork requested with non-general role — forcing general to preserve KV cache alignment",
259
+ "Fork requested with non-general role — caller opted out of parent KV-cache alignment",
171
260
  );
172
- role = "general";
173
261
  }
174
262
  if (!SUBAGENT_ROLE_REGISTRY[role]) {
175
263
  throw new Error(
@@ -219,19 +307,21 @@ export class SubagentManager {
219
307
 
220
308
  let systemPrompt: string;
221
309
  if (isFork) {
222
- // Forks use the parent's system prompt directly — no subagent preamble.
223
- if (config.parentSystemPrompt) {
224
- systemPrompt = config.parentSystemPrompt;
225
- } else {
226
- const resolved = parentConversation?.getCurrentSystemPrompt();
227
- if (!resolved) {
228
- throw new Error(
229
- "Fork spawn requires a parent system prompt but neither config.parentSystemPrompt " +
230
- "nor findConversation yielded one.",
231
- );
232
- }
233
- systemPrompt = resolved;
310
+ // Forks default to the parent's system prompt verbatim — no subagent
311
+ // preamble — so the KV cache stays aligned with the parent. An explicit
312
+ // `systemPromptOverride` opts out of that alignment and takes precedence
313
+ // (e.g. the advisor role framing the inherited context as advice).
314
+ const resolved =
315
+ config.systemPromptOverride ??
316
+ config.parentSystemPrompt ??
317
+ parentConversation?.getCurrentSystemPrompt();
318
+ if (!resolved) {
319
+ throw new Error(
320
+ "Fork spawn requires a parent system prompt but neither config.parentSystemPrompt " +
321
+ "nor findConversation yielded one.",
322
+ );
234
323
  }
324
+ systemPrompt = resolved;
235
325
  } else {
236
326
  systemPrompt =
237
327
  config.systemPromptOverride ??
@@ -279,11 +369,20 @@ export class SubagentManager {
279
369
  conversation: null! as Conversation,
280
370
  state,
281
371
  parentSendToClient,
372
+ ...(opts?.synchronous ? { synchronous: true } : {}),
373
+ ...(opts?.onText ? { onText: opts.onText } : {}),
282
374
  };
283
375
 
284
376
  // Wrap sendToClient to envelope all events with the subagent ID.
285
377
  // Reads from managed.parentSendToClient so reconnects are picked up.
286
378
  const wrappedSendToClient = (msg: ServerMessage): void => {
379
+ // Tap streaming text/thinking deltas for the synchronous caller (if any),
380
+ // in addition to the normal envelope below. Reads from managed.onText so
381
+ // the synchronous path can forward chunks without altering event routing.
382
+ if (managed.onText) {
383
+ const text = extractDeltaText(msg);
384
+ if (text) managed.onText(text);
385
+ }
287
386
  managed.parentSendToClient({
288
387
  type: "subagent_event",
289
388
  subagentId,
@@ -298,7 +397,16 @@ export class SubagentManager {
298
397
  systemPrompt,
299
398
  wrappedSendToClient,
300
399
  workingDir,
301
- { maxTokens, cacheTtl: "5m" },
400
+ {
401
+ maxTokens,
402
+ cacheTtl: "5m",
403
+ // The advisor consult runs tool-less for CLIENT tools but should ground
404
+ // its guidance with provider-native web search when the resolved
405
+ // provider supports it. This is a server tool the provider runs itself,
406
+ // so it stays one-shot — no client tool surfaced, allowlist unchanged.
407
+ // Other roles keep the default (no native search appended).
408
+ ...(role === "advisor" ? { enableNativeWebSearch: true } : {}),
409
+ },
302
410
  );
303
411
 
304
412
  // Mark conversation as having no direct client — it routes through parent.
@@ -325,16 +433,19 @@ export class SubagentManager {
325
433
  conversation.setAssistantId(parentConversation.assistantId);
326
434
  }
327
435
 
328
- if (isFork) {
329
- // Force the fork to use the parent's system prompt as-is without dynamic rebuild.
330
- // This ensures KV cache alignment with the parent conversation.
436
+ if (isFork && !config.systemPromptOverride) {
437
+ // A verbatim-prompt fork pins the parent's system prompt as-is, skipping
438
+ // the dynamic rebuild so the KV cache stays aligned with the parent. A
439
+ // fork that supplies its own override prompt opts out of that alignment,
440
+ // so leave `hasSystemPromptOverride` at its default.
331
441
  conversation.hasSystemPromptOverride = true;
332
442
  }
333
443
 
334
- // Apply role-based tool filter if the role defines one.
335
- // Skip for forks general role has allowedTools: undefined, and forks
336
- // should have the same tool access as the parent.
337
- if (!isFork && roleConfig.allowedTools) {
444
+ // Apply the role's tool allowlist when one is defined. The `general` role
445
+ // has `allowedTools: undefined`, so default forks (which keep the general
446
+ // role) are unaffected; a fork carrying an explicit role gets its
447
+ // allowlist applied like any other subagent.
448
+ if (roleConfig.allowedTools) {
338
449
  conversation.setSubagentAllowedTools(new Set(roleConfig.allowedTools));
339
450
  }
340
451
 
@@ -393,12 +504,65 @@ export class SubagentManager {
393
504
  "Subagent spawned",
394
505
  );
395
506
 
396
- // ── Kick off the agent loop (fire-and-forget) ───────────────────
397
- this.runSubagent(subagentId, config.objective).catch((err) => {
398
- log.error({ subagentId, err }, "Subagent run failed unexpectedly");
399
- });
507
+ return { subagentId, managed };
508
+ }
400
509
 
401
- return subagentId;
510
+ // ── Spawn and await (synchronous) ─────────────────────────────────────
511
+
512
+ /**
513
+ * Spawn a subagent and AWAIT its run, resolving to the child's final
514
+ * assistant text. Unlike `spawn` (fire-and-forget), the caller blocks until
515
+ * the child reaches a terminal state and receives the text directly — so the
516
+ * terminal parent-injection (`notifyParentTerminal`) is skipped on this path.
517
+ *
518
+ * `opts.signal` aborts the underlying run when triggered (e.g. an external
519
+ * timeout). `opts.onText` receives each streaming text/thinking chunk in
520
+ * addition to the normal `subagent_event` envelope.
521
+ */
522
+ async spawnAndAwait(
523
+ config: Omit<SubagentConfig, "id">,
524
+ parentSendToClient: (msg: ServerMessage) => void,
525
+ opts?: { signal?: AbortSignal; onText?: (chunk: string) => void },
526
+ ): Promise<string> {
527
+ const { subagentId, managed } = await this.setUpSubagent(
528
+ config,
529
+ parentSendToClient,
530
+ { synchronous: true, ...(opts?.onText ? { onText: opts.onText } : {}) },
531
+ );
532
+
533
+ // Wire the external signal to abort the child conversation. If the signal
534
+ // is already aborted, abort immediately so the run rejects promptly.
535
+ const signal = opts?.signal;
536
+ const onAbort = (): void => {
537
+ // Route through the manager abort path so the subagent is marked terminal
538
+ // ("aborted") and broadcast as such. A bare conversation.abort() leaves
539
+ // status non-terminal, so runSubagent's success branch would record the
540
+ // run as "completed" once runAgentLoop resolves the consumed cancellation.
541
+ // Suppress the parent notification: the awaiting caller observes the abort
542
+ // as a thrown rejection, so a "do NOT re-spawn" injection would be
543
+ // redundant noise.
544
+ this.abort(subagentId, managed.parentSendToClient, undefined, {
545
+ suppressNotification: true,
546
+ });
547
+ };
548
+ if (signal) {
549
+ if (signal.aborted) onAbort();
550
+ else signal.addEventListener("abort", onAbort, { once: true });
551
+ }
552
+
553
+ try {
554
+ const finalText = await this.runSubagent(subagentId, config.objective);
555
+ // Surface aborts as a rejection so the caller's timeout path is
556
+ // observable — but carry the partial text on the error so a caller that
557
+ // timed out a long generation (e.g. the advisor consult) can still
558
+ // surface what was produced instead of throwing it away.
559
+ if (signal?.aborted) {
560
+ throw new SubagentAbortedError(finalText);
561
+ }
562
+ return finalText;
563
+ } finally {
564
+ signal?.removeEventListener("abort", onAbort);
565
+ }
402
566
  }
403
567
 
404
568
  // ── Internal: run the subagent ────────────────────────────────────────
@@ -406,14 +570,31 @@ export class SubagentManager {
406
570
  private async runSubagent(
407
571
  subagentId: string,
408
572
  objective: string,
409
- ): Promise<void> {
573
+ ): Promise<string> {
410
574
  const managed = this.subagents.get(subagentId);
411
- if (!managed) return;
575
+ if (!managed) return "";
412
576
 
413
577
  // Capture the live conversation — it is non-null at this point because
414
578
  // spawn() sets it before firing runSubagent.
415
579
  const conversation = managed.conversation!;
416
580
 
581
+ // The child's trailing assistant text, captured after runAgentLoop resolves
582
+ // (before the `finally` releases the conversation). Returned to the
583
+ // synchronous `spawnAndAwait` caller; the fire-and-forget `spawn` caller
584
+ // ignores it.
585
+ let finalText = "";
586
+
587
+ // Aborted before the run started (e.g. an already-aborted signal on the
588
+ // synchronous spawnAndAwait path): the subagent is already terminal. Do not
589
+ // start the agent loop or reset status back to "running" — but still release
590
+ // the conversation, exactly as the post-run `finally` does for a terminal
591
+ // run. The loop never started, so no messages were enqueued; this matches
592
+ // the finally's non-deferred release branch.
593
+ if (TERMINAL_STATUSES.has(managed.state.status)) {
594
+ this.releaseConversation(managed);
595
+ return finalText;
596
+ }
597
+
417
598
  // Read the current parent sender so reconnects are picked up.
418
599
  const getSender = () => managed.parentSendToClient;
419
600
 
@@ -441,7 +622,16 @@ export class SubagentManager {
441
622
  // the fork tends to continue the parent conversation instead of
442
623
  // pivoting to the task — the inherited context is louder than a bare
443
624
  // objective buried after 100k+ tokens of chat history.
444
- const message = managed.state.isFork
625
+ //
626
+ // The advisor consult is the exception: it is a fork, but its
627
+ // `systemPromptOverride` already frames the inherited context as advice
628
+ // ("you are a senior advisor … do not write its final deliverable"), so
629
+ // the generic "complete this task and return your findings" wrapper would
630
+ // fight that framing. The advisor's objective is already the bare advice
631
+ // request (`advisorRequestText()`), so it is sent uncontested.
632
+ const useForkFraming =
633
+ managed.state.isFork && managed.state.config.role !== "advisor";
634
+ const message = useForkFraming
445
635
  ? [
446
636
  "⎯⎯⎯ FORK TASK ⎯⎯⎯",
447
637
  "You have been forked from the parent conversation to execute a specific task.",
@@ -466,6 +656,9 @@ export class SubagentManager {
466
656
  });
467
657
 
468
658
  // Agent loop completed successfully.
659
+ // Capture the trailing assistant text before any release nulls the
660
+ // conversation reference. The fire-and-forget caller ignores the return.
661
+ finalText = extractFinalAssistantText(conversation.messages);
469
662
  // Copy usage stats from the conversation before sending status (which includes usage).
470
663
  managed.state.usage = { ...conversation.usageStats };
471
664
  // Only update state + notify if still non-terminal (guards against abort race).
@@ -476,7 +669,12 @@ export class SubagentManager {
476
669
  log.info({ subagentId }, "Subagent completed");
477
670
 
478
671
  // Notify the parent conversation so the LLM can call subagent_read.
479
- this.notifyParentTerminal(managed, "completed");
672
+ // Skipped on the synchronous path — the awaiting caller receives the
673
+ // final text directly, so re-injecting a "read the result" prompt
674
+ // would be redundant noise in the parent.
675
+ if (!managed.synchronous) {
676
+ this.notifyParentTerminal(managed, "completed");
677
+ }
480
678
  }
481
679
  } catch (err) {
482
680
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -489,10 +687,22 @@ export class SubagentManager {
489
687
  // Only update status if not already terminal (e.g. aborted).
490
688
  if (!TERMINAL_STATUSES.has(managed.state.status)) {
491
689
  this.setStatus(subagentId, "failed", getSender(), errorMsg);
492
- this.notifyParentTerminal(managed, "failed");
690
+ // Skip terminal parent-injection on the synchronous path — the failure
691
+ // surfaces to the awaiting caller as a rejected promise instead.
692
+ if (!managed.synchronous) {
693
+ this.notifyParentTerminal(managed, "failed");
694
+ }
493
695
  }
494
696
 
495
697
  log.error({ subagentId, err }, "Subagent failed");
698
+
699
+ // Surface the failure to the synchronous caller. The fire-and-forget
700
+ // path has no awaiter, so re-throwing there only feeds the `.catch()`
701
+ // logger in `spawn` — harmless but noisy — so it is confined to the
702
+ // synchronous path.
703
+ if (managed.synchronous) {
704
+ throw err;
705
+ }
496
706
  } finally {
497
707
  // Release the heavyweight Conversation — output is already persisted in DB.
498
708
  // drainQueue is async: it awaits buildPassthroughBatch (which awaits
@@ -516,6 +726,8 @@ export class SubagentManager {
516
726
  this.releaseConversation(managed);
517
727
  }
518
728
  }
729
+
730
+ return finalText;
519
731
  }
520
732
 
521
733
  // ── Abort ─────────────────────────────────────────────────────────────
@@ -553,6 +765,15 @@ export class SubagentManager {
553
765
  ),
554
766
  );
555
767
  managed.state.completedAt = Date.now();
768
+ // Capture the conversation's latest usage before emitting the terminal
769
+ // status. `subagent_status_changed` ships `state.usage`, and the abort path
770
+ // (unlike the completion/failure paths, which sync at agent-loop exit) would
771
+ // otherwise send the {0,0,0} init usage — zeroing the client's token counts
772
+ // even though those tokens were already spent. `usageStats` accrues per LLM
773
+ // turn (see conversation-usage.ts), so this is the most recent total.
774
+ if (managed.conversation) {
775
+ managed.state.usage = { ...managed.conversation.usageStats };
776
+ }
556
777
  if (parentSendToClient) {
557
778
  // Route the status update through the stored parent sender so the
558
779
  // owning conversation's UI chip updates, even when the abort comes from a
@@ -116,7 +116,8 @@ export type SubagentRole =
116
116
  | "researcher"
117
117
  | "coder"
118
118
  | "planner"
119
- | "investigator";
119
+ | "investigator"
120
+ | "advisor";
120
121
 
121
122
  export interface SubagentRoleConfig {
122
123
  /**
@@ -180,7 +181,7 @@ export const SUBAGENT_ROLE_REGISTRY: Record<SubagentRole, SubagentRoleConfig> =
180
181
  },
181
182
  investigator: {
182
183
  allowedTools: [
183
- "bash",
184
+ "code_search",
184
185
  "file_read",
185
186
  "file_list",
186
187
  "web_search",
@@ -191,11 +192,17 @@ export const SUBAGENT_ROLE_REGISTRY: Record<SubagentRole, SubagentRoleConfig> =
191
192
  skillIds: [],
192
193
  systemPromptPreamble: [
193
194
  "You are an investigation-focused subagent for root-cause analysis: debugging, log forensics, and tracing behavior across code.",
194
- "Your shell access is for read-only investigation (grep, find, reading files and logs) do not modify files or system state.",
195
- "Working method: read whole files instead of many small line-range slices; prefer broad searches (e.g. grep -rn across a directory) over one-symbol-at-a-time queries.",
195
+ "You have read-only investigation tools only — there is no shell. Use code_search to search file contents across directories, file_list to enumerate paths, and file_read to read whole files and logs. You cannot modify files or system state.",
196
+ "Working method: read whole files instead of many small line-range slices; prefer broad code_search queries across a directory over one-symbol-at-a-time queries.",
196
197
  "Send notify_parent (urgency 'important') as soon as each finding is confirmed, so progress survives interruption.",
197
198
  "Your final message must be a compact root-cause report with these sections: Symptom, Root cause, Evidence (file:line references), Suggested fix, Open questions.",
198
199
  "If you approach context limits, stop investigating and produce the report from what you have — a partial report delivered is worth more than a complete investigation lost.",
199
200
  ].join(" "),
200
201
  },
202
+ advisor: {
203
+ allowedTools: [],
204
+ skillIds: [],
205
+ systemPromptPreamble:
206
+ "You are a read-only senior advisor consulted for a one-shot strategic review. Read the inherited conversation, then return focused, high-leverage guidance in a single response. You have no tools — you cannot search, read files, or run commands — so reason from the context you were given.",
207
+ },
201
208
  };
@@ -391,6 +391,38 @@ export interface SkillLoadedTelemetryEvent extends ModelTelemetryEventBase {
391
391
  conversation_id: string | null;
392
392
  }
393
393
 
394
+ /**
395
+ * Watchdog health event — one per watchdog check firing. The daemon's
396
+ * watchdog observes liveness/health signals (event-loop block, stream-idle
397
+ * stalls, restarts, ...) and emits one event per check firing.
398
+ *
399
+ * Deliberately minimal and forward-compatible, mirroring the platform
400
+ * `WatchdogTelemetryEventSerializer`:
401
+ *
402
+ * - `check_name` — which watchdog check fired. Open string set (not a
403
+ * closed enum) so the daemon can introduce a new check without a
404
+ * coordinated serializer release; it is the primary group-by dimension
405
+ * downstream. The infrastructure admin chart filters this to
406
+ * `event_loop_blocked`.
407
+ * - `value` — the single measured magnitude for the check (block ms, idle
408
+ * ms, ...). Nullable: not every check carries a scalar. The platform
409
+ * coerces ints to float, so the daemon need not distinguish.
410
+ * - `detail` — open JSON bag for any extra fields the daemon attaches
411
+ * (reason codes, secondary numbers, a human message) without a
412
+ * platform-coordinated schema change. Null when the daemon attaches
413
+ * nothing. Bounded server-side (4096 bytes serialized); an oversize bag
414
+ * rejects only the single event, never the batch.
415
+ *
416
+ * Metadata only — no conversation content. Dedupe downstream on
417
+ * `daemon_event_id` (the daemon retries a batch on transient POST failure).
418
+ */
419
+ export interface WatchdogTelemetryEvent extends TelemetryEventBase {
420
+ type: "watchdog";
421
+ check_name: string;
422
+ value: number | null;
423
+ detail: Record<string, unknown> | null;
424
+ }
425
+
394
426
  /** Discriminated union of all telemetry event types. */
395
427
  export type TelemetryEvent =
396
428
  | LlmUsageTelemetryEvent
@@ -399,4 +431,5 @@ export type TelemetryEvent =
399
431
  | OnboardingTelemetryEvent
400
432
  | AuthFallbackTelemetryEvent
401
433
  | ToolExecutedTelemetryEvent
402
- | SkillLoadedTelemetryEvent;
434
+ | SkillLoadedTelemetryEvent
435
+ | WatchdogTelemetryEvent;
@@ -1547,11 +1547,11 @@ describe("UsageTelemetryReporter", () => {
1547
1547
  // No HTTP call should have been made
1548
1548
  expect(mockFetch).not.toHaveBeenCalled();
1549
1549
 
1550
- // All 7 timestamp watermarks should have been advanced, and all 7 ID
1550
+ // All 8 timestamp watermarks should have been advanced, and all 8 ID
1551
1551
  // watermarks pinned to the high-sorting sentinel (a truthy value keeps
1552
1552
  // the compound-cursor branch active while closing its same-millisecond
1553
1553
  // arm against opt-out rows).
1554
- expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(14);
1554
+ expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(16);
1555
1555
 
1556
1556
  const calls = mockSetMemoryCheckpoint.mock.calls;
1557
1557
  const keys = calls.map((c) => c[0]);
@@ -1563,6 +1563,7 @@ describe("UsageTelemetryReporter", () => {
1563
1563
  "auth_fallback",
1564
1564
  "tool_executed",
1565
1565
  "skill_loaded",
1566
+ "watchdog",
1566
1567
  ];
1567
1568
  for (const eventType of eventTypes) {
1568
1569
  expect(keys).toContain(`telemetry:${eventType}:last_reported_at`);