@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d

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 (367) hide show
  1. package/docs/workspace-tools.md +42 -33
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  5. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  8. package/openapi.yaml +74 -1
  9. package/package.json +1 -1
  10. package/scripts/test.sh +36 -15
  11. package/src/__tests__/actor-token-service.test.ts +36 -14
  12. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  13. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  14. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  15. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  16. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  17. package/src/__tests__/approval-cascade.test.ts +2 -0
  18. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  19. package/src/__tests__/btw-routes.test.ts +2 -0
  20. package/src/__tests__/build-persisted-content.test.ts +2 -0
  21. package/src/__tests__/call-controller.test.ts +19 -0
  22. package/src/__tests__/channel-guardian.test.ts +94 -58
  23. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  24. package/src/__tests__/compaction-events.test.ts +2 -0
  25. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  26. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  27. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  28. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  29. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  30. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  31. package/src/__tests__/computer-use-tools.test.ts +13 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +5 -1
  33. package/src/__tests__/config-schema.test.ts +1 -0
  34. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  35. package/src/__tests__/contacts-relay-reads.test.ts +13 -15
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  37. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  38. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  41. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  42. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  44. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  46. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  47. package/src/__tests__/conversation-pairing.test.ts +2 -0
  48. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  51. package/src/__tests__/conversation-queue.test.ts +91 -0
  52. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  53. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  57. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
  58. package/src/__tests__/conversation-title-service.test.ts +2 -0
  59. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  60. package/src/__tests__/conversation-usage.test.ts +2 -0
  61. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  64. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  65. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  66. package/src/__tests__/db-test-helpers.ts +5 -4
  67. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  68. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  69. package/src/__tests__/dm-persistence.test.ts +2 -0
  70. package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
  71. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  72. package/src/__tests__/filing-service.test.ts +2 -0
  73. package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
  74. package/src/__tests__/guardian-dispatch.test.ts +95 -1
  75. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  76. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  77. package/src/__tests__/heartbeat-service.test.ts +2 -0
  78. package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
  79. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  80. package/src/__tests__/host-bash-routes.test.ts +31 -41
  81. package/src/__tests__/host-browser-routes.test.ts +26 -32
  82. package/src/__tests__/host-cu-proxy.test.ts +299 -0
  83. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  84. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  85. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  86. package/src/__tests__/http-user-message-parity.test.ts +167 -8
  87. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  88. package/src/__tests__/invite-redemption-service.test.ts +43 -0
  89. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  90. package/src/__tests__/llm-usage-store.test.ts +25 -0
  91. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  92. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  93. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  94. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  95. package/src/__tests__/native-web-search.test.ts +2 -0
  96. package/src/__tests__/non-member-access-request.test.ts +189 -17
  97. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  98. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  99. package/src/__tests__/notification-deep-link.test.ts +6 -0
  100. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  101. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  102. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  103. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  104. package/src/__tests__/plugin-bootstrap.test.ts +3 -73
  105. package/src/__tests__/plugin-route-contribution.test.ts +4 -17
  106. package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
  107. package/src/__tests__/plugin-types.test.ts +0 -2
  108. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  109. package/src/__tests__/process-message-display-content.test.ts +2 -0
  110. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  111. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  112. package/src/__tests__/registry.test.ts +3 -0
  113. package/src/__tests__/relay-server.test.ts +694 -25
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  115. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  116. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  117. package/src/__tests__/skills.test.ts +44 -0
  118. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  119. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
  120. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  121. package/src/__tests__/stt-hints.test.ts +44 -13
  122. package/src/__tests__/subagent-detail.test.ts +27 -0
  123. package/src/__tests__/subagent-disposal.test.ts +65 -0
  124. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  125. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  126. package/src/__tests__/subagent-tools.test.ts +2 -0
  127. package/src/__tests__/suggestion-routes.test.ts +2 -0
  128. package/src/__tests__/title-generate-hook.test.ts +2 -0
  129. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  130. package/src/__tests__/tool-executor.test.ts +16 -11
  131. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  132. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  133. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  134. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  135. package/src/__tests__/twilio-routes.test.ts +96 -0
  136. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  137. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  138. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  139. package/src/agent/loop-exclusive-tool.test.ts +150 -0
  140. package/src/agent/loop.ts +56 -0
  141. package/src/api/constants/sse-replay.ts +41 -0
  142. package/src/api/index.ts +6 -0
  143. package/src/api/responses/llm-request-log-entry.ts +25 -0
  144. package/src/api/responses/subagent-detail.ts +17 -0
  145. package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
  146. package/src/calls/call-domain.ts +3 -3
  147. package/src/calls/guardian-dispatch.ts +10 -8
  148. package/src/calls/inbound-trust-reader.ts +17 -1
  149. package/src/calls/media-stream-server.ts +21 -0
  150. package/src/calls/relay-server.ts +167 -50
  151. package/src/calls/relay-setup-router.ts +37 -7
  152. package/src/calls/relay-verification.ts +4 -4
  153. package/src/calls/stt-hints.ts +9 -12
  154. package/src/calls/twilio-routes.ts +14 -4
  155. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  156. package/src/cli/commands/cache.ts +194 -181
  157. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  158. package/src/cli/commands/db/status.ts +37 -1
  159. package/src/cli/commands/mcp.ts +252 -218
  160. package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
  161. package/src/cli/commands/memory/index.ts +2 -0
  162. package/src/cli/commands/memory/worker.ts +175 -0
  163. package/src/cli/commands/plugins.ts +75 -3
  164. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  165. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  166. package/src/cli/lib/list-installed-plugins.ts +179 -1
  167. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  168. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
  169. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  170. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  171. package/src/config/feature-flag-registry.json +0 -8
  172. package/src/config/loader.ts +36 -5
  173. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  174. package/src/config/schemas/memory-lifecycle.ts +12 -0
  175. package/src/config/schemas/memory-v3.ts +7 -0
  176. package/src/config/schemas/memory.ts +4 -0
  177. package/src/config/schemas/timeouts.ts +8 -0
  178. package/src/config/seed-inference-profiles.ts +14 -5
  179. package/src/config/skills.ts +27 -5
  180. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  181. package/src/contacts/contacts-write.ts +3 -0
  182. package/src/contacts/guardian-delivery-reader.ts +223 -0
  183. package/src/daemon/conversation-agent-loop.ts +9 -0
  184. package/src/daemon/conversation-process.ts +39 -17
  185. package/src/daemon/conversation-surfaces.ts +8 -0
  186. package/src/daemon/conversation-tool-setup.ts +49 -16
  187. package/src/daemon/conversation.ts +21 -2
  188. package/src/daemon/disk-pressure-guard.ts +12 -2
  189. package/src/daemon/event-loop-watchdog.ts +28 -1
  190. package/src/daemon/external-plugins-bootstrap.ts +4 -34
  191. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
  192. package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
  193. package/src/daemon/handlers/config-a2a.ts +6 -14
  194. package/src/daemon/handlers/config-channels.ts +78 -22
  195. package/src/daemon/handlers/conversations.ts +77 -0
  196. package/src/daemon/host-cu-proxy.ts +102 -11
  197. package/src/daemon/lifecycle.ts +4 -0
  198. package/src/daemon/memory-v2-startup.test.ts +72 -0
  199. package/src/daemon/memory-v2-startup.ts +87 -19
  200. package/src/daemon/server.ts +0 -4
  201. package/src/daemon/shutdown-handlers.ts +20 -0
  202. package/src/daemon/tool-setup-types.ts +9 -0
  203. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  204. package/src/ipc/assistant-server.ts +2 -2
  205. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  206. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  207. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  208. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  209. package/src/memory/conversation-crud.ts +38 -0
  210. package/src/memory/db-connection.ts +22 -3
  211. package/src/memory/db-init.ts +36 -502
  212. package/src/memory/db-singleton.ts +6 -4
  213. package/src/memory/jobs-worker.ts +58 -0
  214. package/src/memory/llm-usage-store.ts +48 -20
  215. package/src/memory/memory-retrospective-job.ts +9 -8
  216. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
  217. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
  218. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
  219. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  220. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  221. package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
  222. package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
  223. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
  224. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  225. package/src/memory/migrations/run-migrations.ts +90 -6
  226. package/src/memory/migrations/schema-introspection.ts +14 -0
  227. package/src/memory/migrations/validate-migration-state.ts +101 -66
  228. package/src/memory/prompt-override.ts +129 -0
  229. package/src/memory/schema/conversations.ts +9 -0
  230. package/src/memory/schema/infrastructure.ts +20 -0
  231. package/src/memory/steps.ts +573 -0
  232. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  233. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  234. package/src/memory/v2/cli-command-store.ts +75 -38
  235. package/src/memory/v2/prompts/consolidation.ts +13 -82
  236. package/src/memory/v2/prompts/router.ts +21 -93
  237. package/src/memory/v2/skill-store.ts +68 -31
  238. package/src/memory/watchdog-events-store.ts +87 -0
  239. package/src/memory/worker-control.ts +118 -0
  240. package/src/memory/worker-process.ts +72 -0
  241. package/src/notifications/__tests__/broadcaster.test.ts +16 -8
  242. package/src/notifications/__tests__/connected-channels.test.ts +114 -0
  243. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  244. package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
  245. package/src/notifications/broadcaster.ts +8 -1
  246. package/src/notifications/decision-engine.ts +15 -7
  247. package/src/notifications/destination-resolver.ts +68 -24
  248. package/src/notifications/emit-signal.ts +39 -14
  249. package/src/onboarding/checkin-event.test.ts +220 -0
  250. package/src/onboarding/checkin-event.ts +321 -0
  251. package/src/onboarding/schedule-checkin.ts +190 -0
  252. package/src/permissions/question-prompter.test.ts +1 -1
  253. package/src/permissions/question-prompter.ts +7 -4
  254. package/src/plugin-api/index.ts +6 -6
  255. package/src/plugin-api/types.ts +3 -5
  256. package/src/plugin-api/vision-support.test.ts +28 -4
  257. package/src/plugin-api/vision-support.ts +66 -31
  258. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
  259. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
  260. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
  261. package/src/plugins/defaults/advisor/consult.ts +110 -6
  262. package/src/plugins/defaults/advisor/context-pack.ts +288 -0
  263. package/src/plugins/defaults/advisor/steering.ts +14 -2
  264. package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
  265. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  266. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
  267. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
  268. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  269. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  270. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
  273. package/src/plugins/mtime-cache.ts +7 -2
  274. package/src/plugins/types.ts +0 -2
  275. package/src/providers/anthropic/client.ts +5 -0
  276. package/src/providers/call-site-routing.ts +4 -0
  277. package/src/providers/model-catalog.ts +16 -0
  278. package/src/providers/openai/responses-provider.ts +5 -0
  279. package/src/providers/openrouter/client.ts +5 -0
  280. package/src/providers/provider-send-message.ts +4 -0
  281. package/src/providers/ratelimit.ts +4 -0
  282. package/src/providers/retry.ts +4 -0
  283. package/src/providers/types.ts +9 -0
  284. package/src/providers/usage-tracking.ts +4 -0
  285. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  286. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
  287. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  288. package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
  289. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
  290. package/src/runtime/access-request-helper.ts +19 -39
  291. package/src/runtime/actor-trust-resolver.ts +2 -2
  292. package/src/runtime/anchored-guardian.test.ts +156 -0
  293. package/src/runtime/anchored-guardian.ts +135 -0
  294. package/src/runtime/assistant-event-hub.ts +1 -1
  295. package/src/runtime/assistant-stream-state.ts +9 -2
  296. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  297. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  298. package/src/runtime/channel-verification-service.ts +56 -31
  299. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  300. package/src/runtime/guardian-vellum-migration.ts +66 -7
  301. package/src/runtime/invite-redemption-service.ts +50 -18
  302. package/src/runtime/local-actor-identity.ts +76 -11
  303. package/src/runtime/local-principal-trust.ts +52 -0
  304. package/src/runtime/pending-interactions.ts +11 -1
  305. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  306. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  307. package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
  308. package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
  309. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
  310. package/src/runtime/routes/browser-routes.ts +1 -1
  311. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  312. package/src/runtime/routes/contact-routes.ts +8 -32
  313. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  314. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  315. package/src/runtime/routes/conversation-routes.ts +74 -81
  316. package/src/runtime/routes/events-routes.ts +2 -2
  317. package/src/runtime/routes/global-search-routes.ts +3 -1
  318. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  319. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  320. package/src/runtime/routes/host-bash-routes.ts +5 -4
  321. package/src/runtime/routes/host-browser-routes.ts +9 -11
  322. package/src/runtime/routes/host-cu-routes.ts +5 -4
  323. package/src/runtime/routes/host-file-routes.ts +5 -4
  324. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  325. package/src/runtime/routes/http-adapter.ts +1 -1
  326. package/src/runtime/routes/identity-routes.ts +3 -2
  327. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  328. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
  331. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  332. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  333. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  334. package/src/runtime/routes/index.ts +2 -0
  335. package/src/runtime/routes/llm-context-normalization.ts +71 -0
  336. package/src/runtime/routes/mcp-auth-routes.ts +38 -15
  337. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  338. package/src/runtime/routes/migration-routes.ts +4 -1
  339. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  340. package/src/runtime/routes/subagents-routes.ts +5 -0
  341. package/src/runtime/routes/surface-action-routes.ts +51 -55
  342. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  343. package/src/runtime/services/conversation-serializer.ts +7 -9
  344. package/src/runtime/tool-grant-request-helper.ts +3 -3
  345. package/src/runtime/trust-verdict-consumer.ts +85 -9
  346. package/src/runtime/verification-outbound-actions.ts +18 -18
  347. package/src/signals/user-message.ts +16 -0
  348. package/src/subagent/manager.ts +9 -0
  349. package/src/telemetry/types.ts +34 -1
  350. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  351. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  352. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  353. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  354. package/src/tools/computer-use/definitions.ts +8 -2
  355. package/src/tools/executor.ts +4 -4
  356. package/src/tools/registry.ts +18 -0
  357. package/src/tools/tool-approval-handler.ts +1 -1
  358. package/src/tools/tool-defaults.ts +9 -2
  359. package/src/tools/types.ts +17 -2
  360. package/src/tools/workspace-tools/loader.ts +348 -244
  361. package/src/util/platform.ts +5 -0
  362. package/src/util/telemetry-db-path.ts +24 -0
  363. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  364. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  365. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  366. package/src/daemon/workspace-tools-watcher.ts +0 -328
  367. package/src/memory/migrations/registry.ts +0 -573
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Personal-memory gating for the advisor context pack: NOW.md and PKB must only
3
+ * reach the advisor when the turn's trust admits personal memory (and, for
4
+ * NOW.md, when the scratchpad-injection toggle is on) — the same policy the
5
+ * runtime memory injectors apply. Without it, a low-risk advisor consult on a
6
+ * remote/trusted-contact turn could forward private content the main agent
7
+ * would never receive.
8
+ *
9
+ * Mocks are isolated to this file (the test runner runs each file in its own
10
+ * process), so the broad module stubs here don't leak into other suites.
11
+ */
12
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
13
+
14
+ let personalAllowed = false;
15
+ let scratchpadEnabled = true;
16
+ let gateArg: unknown = null;
17
+
18
+ mock.module("../../../../daemon/trust-context.js", () => ({
19
+ isPersonalMemoryAllowed: (trust: unknown) => {
20
+ gateArg = trust;
21
+ return personalAllowed;
22
+ },
23
+ }));
24
+ mock.module("../../../../daemon/now-scratchpad.js", () => ({
25
+ readNowScratchpad: () => "NOW-CONTENT",
26
+ }));
27
+ mock.module("../../../../memory/pkb/context.js", () => ({
28
+ readPkbContext: () => "PKB-CONTENT",
29
+ }));
30
+ mock.module("../../../../config/loader.js", () => ({
31
+ getConfig: () => ({
32
+ memory: {
33
+ retrieval: { scratchpadInjection: { enabled: scratchpadEnabled } },
34
+ },
35
+ llm: {},
36
+ }),
37
+ }));
38
+ // Keep every other section empty so the assertions isolate NOW.md / PKB.
39
+ mock.module("../../../../daemon/conversation-workspace.js", () => ({
40
+ resolveWorkspaceTopLevelContext: () => null,
41
+ }));
42
+ mock.module("../../../../daemon/conversation-runtime-assembly.js", () => ({
43
+ buildActiveDocuments: () => null,
44
+ }));
45
+ mock.module("../../../../runtime/capabilities.js", () => ({
46
+ resolveCapabilities: () => ({ canAccessMemory: false }),
47
+ }));
48
+ mock.module("../../../../config/skills.js", () => ({
49
+ loadSkillCatalog: () => [],
50
+ }));
51
+
52
+ const { buildAdvisorContext } = await import("../context-pack.js");
53
+
54
+ const sources = {
55
+ conversationId: "c1",
56
+ workingDir: "/tmp",
57
+ // A remote, non-guardian per-turn snapshot — the case the live-state read
58
+ // could have wrongly elevated.
59
+ trustClass: "unknown" as const,
60
+ sourceChannel: "telegram",
61
+ transcript: [],
62
+ allowedToolNames: new Set<string>(),
63
+ };
64
+
65
+ beforeEach(() => {
66
+ personalAllowed = false;
67
+ scratchpadEnabled = true;
68
+ gateArg = null;
69
+ });
70
+
71
+ describe("advisor context pack — personal-memory gating", () => {
72
+ test("withholds NOW.md and PKB when personal memory is disallowed", async () => {
73
+ personalAllowed = false;
74
+ const ctx = (await buildAdvisorContext(sources)) ?? "";
75
+ expect(ctx).not.toContain("NOW-CONTENT");
76
+ expect(ctx).not.toContain("PKB-CONTENT");
77
+ });
78
+
79
+ test("includes NOW.md and PKB when allowed and the scratchpad toggle is on", async () => {
80
+ personalAllowed = true;
81
+ scratchpadEnabled = true;
82
+ const ctx = await buildAdvisorContext(sources);
83
+ expect(ctx).toContain("NOW-CONTENT");
84
+ expect(ctx).toContain("PKB-CONTENT");
85
+ });
86
+
87
+ test("withholds NOW.md when the scratchpad toggle is off, PKB still allowed", async () => {
88
+ personalAllowed = true;
89
+ scratchpadEnabled = false;
90
+ const ctx = (await buildAdvisorContext(sources)) ?? "";
91
+ expect(ctx).not.toContain("NOW-CONTENT");
92
+ expect(ctx).toContain("PKB-CONTENT");
93
+ });
94
+
95
+ test("feeds the gate the per-turn trust snapshot, not live conversation state", async () => {
96
+ personalAllowed = true;
97
+ await buildAdvisorContext(sources);
98
+ // The gate must see exactly the snapshot threaded from ToolContext —
99
+ // trustClass + executionChannel — so a concurrent live-trust change can't
100
+ // elevate this invocation.
101
+ expect(gateArg).toEqual({
102
+ sourceChannel: "telegram",
103
+ trustClass: "unknown",
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type { Message } from "../../../../providers/types.js";
4
+ import { buildAdvisorContext, deriveRecallQuery } from "../context-pack.js";
5
+
6
+ const userMsg = (t: string): Message => ({
7
+ role: "user",
8
+ content: [{ type: "text", text: t }],
9
+ });
10
+
11
+ describe("deriveRecallQuery", () => {
12
+ test("returns the most recent user message text", () => {
13
+ const query = deriveRecallQuery([
14
+ userMsg("the original task"),
15
+ { role: "assistant", content: [{ type: "text", text: "ok" }] },
16
+ userMsg("the latest question"),
17
+ ]);
18
+ expect(query).toBe("the latest question");
19
+ });
20
+
21
+ test("returns null when there is no user text", () => {
22
+ expect(
23
+ deriveRecallQuery([
24
+ { role: "assistant", content: [{ type: "text", text: "hi" }] },
25
+ ]),
26
+ ).toBeNull();
27
+ expect(deriveRecallQuery([])).toBeNull();
28
+ });
29
+ });
30
+
31
+ describe("buildAdvisorContext", () => {
32
+ test("lists the agent's available tools, skipping the advisor itself", async () => {
33
+ const context = await buildAdvisorContext({
34
+ conversationId: "ctx-1",
35
+ workingDir: "/tmp/does-not-exist",
36
+ allowedToolNames: new Set(["bash", "advisor", "read_file"]),
37
+ trustClass: "unknown",
38
+ transcript: [userMsg("hi")],
39
+ });
40
+
41
+ expect(context).toContain("## Available tools");
42
+ expect(context).toContain("- bash");
43
+ expect(context).toContain("- read_file");
44
+ // The advisor advises; it never tells the agent to consult itself.
45
+ expect(context).not.toContain("- advisor");
46
+ });
47
+
48
+ test("omits the tools section when no tools are available", async () => {
49
+ const context = await buildAdvisorContext({
50
+ conversationId: "ctx-2",
51
+ workingDir: "/tmp/does-not-exist",
52
+ allowedToolNames: new Set(),
53
+ trustClass: "unknown",
54
+ transcript: [],
55
+ });
56
+ // Other sources (e.g. the skills catalog) may still contribute, but with no
57
+ // allowed tools the tools section must not appear.
58
+ if (context !== null) expect(context).not.toContain("## Available tools");
59
+ });
60
+ });
@@ -16,7 +16,11 @@ import {
16
16
  getConfiguredProvider,
17
17
  userMessage,
18
18
  } from "../../../providers/provider-send-message.js";
19
- import type { Message } from "../../../providers/types.js";
19
+ import type {
20
+ Message,
21
+ ProviderEvent,
22
+ ToolDefinition,
23
+ } from "../../../providers/types.js";
20
24
  import { ADVISOR_CONFIG } from "./config.js";
21
25
  import { advisorRequestText, buildAdvisorSystem } from "./steering.js";
22
26
  import { toAdvisorMessages } from "./transcript.js";
@@ -26,6 +30,26 @@ import { toAdvisorMessages } from "./transcript.js";
26
30
  // on via `llm.advisorProfile`, which we float above the call-site layers.
27
31
  const ADVISOR_CALL_SITE: LLMCallSite = "advisor";
28
32
 
33
+ /**
34
+ * The single tool the consult may attach: a `web_search`-named tool that
35
+ * provider-native search (Anthropic/OpenAI) substitutes for its server-side
36
+ * web tool. Only passed when `provider.supportsNativeWebSearch` is true, so the
37
+ * provider runs the search itself and returns results inline — no agent loop,
38
+ * which keeps the consult a one-shot completion.
39
+ */
40
+ const ADVISOR_WEB_SEARCH_TOOL: ToolDefinition = {
41
+ name: "web_search",
42
+ description:
43
+ "Search the web for current information to ground your guidance.",
44
+ input_schema: {
45
+ type: "object",
46
+ properties: {
47
+ query: { type: "string", description: "The search query." },
48
+ },
49
+ required: ["query"],
50
+ },
51
+ };
52
+
29
53
  /**
30
54
  * Resolve the routing override for the advisor consult. When the workspace has
31
55
  * set `llm.advisorProfile`, force it above the call-site layers so it is
@@ -45,7 +69,22 @@ function advisorOverride(): {
45
69
  export interface ConsultParams {
46
70
  systemPrompt: string | null;
47
71
  messages: ReadonlyArray<Message>;
72
+ /**
73
+ * The agent's runtime context (available tools and skills, workspace/project
74
+ * context, recalled memory), gathered by the tool from its `ToolContext`.
75
+ * Embedded in the advisor's system prompt so its advice is grounded in what
76
+ * the agent can actually do. Omitted/null when nothing could be gathered.
77
+ */
78
+ runtimeContext?: string | null;
48
79
  signal?: AbortSignal;
80
+ /**
81
+ * Optional sink for the advisor's live activity as it generates: incremental
82
+ * advice text, the reasoning summary (when surfaced), and a note per web
83
+ * search. Wiring this to the tool's `onOutput` surfaces the consult live as
84
+ * `tool_output_chunk` while the advisor is still working; the complete
85
+ * guidance is still returned as the resolved string. See `advisorActivitySink`.
86
+ */
87
+ onText?: (chunk: string) => void;
49
88
  }
50
89
 
51
90
  /** Combine the caller's signal with a consult timeout. */
@@ -54,6 +93,55 @@ function withTimeout(signal: AbortSignal | undefined, ms: number): AbortSignal {
54
93
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
55
94
  }
56
95
 
96
+ /**
97
+ * Build the streaming sink for a consult: forward the advisor's live activity
98
+ * to `onText` so the tool-output drawer streams throughout the consult instead
99
+ * of sitting silent until the final advice lands.
100
+ *
101
+ * The consult searches the web (up to 5×) and reasons over full context before
102
+ * writing its guidance. Forwarding the visible advice text alone would leave
103
+ * the drawer blank for that whole prefix, so the sink also surfaces the
104
+ * reasoning summary (when the model emits one) and a one-line note per web
105
+ * search — a success note with the query, or a failure note when the search
106
+ * errors. The complete guidance is still returned by `consultAdvisor`; the
107
+ * renderer swaps it in once the tool result arrives.
108
+ */
109
+ function advisorActivitySink(
110
+ onText: (chunk: string) => void,
111
+ ): (event: ProviderEvent) => void {
112
+ return (event) => {
113
+ switch (event.type) {
114
+ case "text_delta":
115
+ if (event.text) onText(event.text);
116
+ break;
117
+ case "thinking_delta":
118
+ if (event.thinking) onText(event.thinking);
119
+ break;
120
+ case "server_tool_start":
121
+ if (event.name === "web_search") onText("\n🔎 Searching the web…\n");
122
+ break;
123
+ case "server_tool_complete": {
124
+ const rawQuery = event.resolvedInput?.["query"];
125
+ const query = typeof rawQuery === "string" ? rawQuery.trim() : "";
126
+ if (event.isError) {
127
+ // A failed search (e.g. `query_too_long`, `max_uses_exceeded`) must
128
+ // not be announced as a success — the advisor proceeds without it.
129
+ onText(
130
+ query
131
+ ? `\n⚠️ Web search failed: ${query}\n`
132
+ : "\n⚠️ Web search failed.\n",
133
+ );
134
+ } else if (query) {
135
+ onText(`\n🔎 Searched: ${query}\n`);
136
+ }
137
+ break;
138
+ }
139
+ default:
140
+ break;
141
+ }
142
+ };
143
+ }
144
+
57
145
  /**
58
146
  * Returns the advisor's guidance text, or a short benign notice when the
59
147
  * advisor can't run. Callers should surface the string as a non-error tool
@@ -73,17 +161,33 @@ export async function consultAdvisor(params: ConsultParams): Promise<string> {
73
161
  }
74
162
 
75
163
  // Append the consult instruction as the final user turn, then run a
76
- // tool-less completion through the resolved provider. No `max_tokens` is
77
- // set, so the resolver applies the profile's normal output budget rather
78
- // than an advisor-specific cap.
164
+ // completion through the resolved provider. No `max_tokens` is set, so the
165
+ // resolver applies the profile's normal output budget rather than an
166
+ // advisor-specific cap.
79
167
  const messages: Message[] = [...history, userMessage(advisorRequestText())];
80
168
 
169
+ // Give the advisor live web access when — and only when — the resolved
170
+ // provider runs search server-side (provider-native). Passing a `web_search`
171
+ // tool to a non-native provider would surface a client tool call this
172
+ // one-shot consult cannot execute, so we gate strictly on the capability and
173
+ // otherwise keep the consult tool-less.
174
+ const webEnabled = provider.supportsNativeWebSearch === true;
175
+
176
+ const { onText } = params;
81
177
  const response = await provider.sendMessage(messages, {
82
- systemPrompt: buildAdvisorSystem(params.systemPrompt),
178
+ systemPrompt: buildAdvisorSystem(
179
+ params.systemPrompt,
180
+ params.runtimeContext,
181
+ ),
182
+ ...(webEnabled ? { tools: [ADVISOR_WEB_SEARCH_TOOL] } : {}),
183
+ // Stream the consult's activity live (advice text, reasoning summary, and a
184
+ // note per web search) so the drawer isn't blank while the advisor searches
185
+ // and reasons before writing its guidance. See `advisorActivitySink`.
186
+ onEvent: onText ? advisorActivitySink(onText) : undefined,
83
187
  config: {
84
188
  callSite: ADVISOR_CALL_SITE,
85
189
  ...override,
86
- tool_choice: { type: "none" },
190
+ tool_choice: webEnabled ? { type: "auto" } : { type: "none" },
87
191
  },
88
192
  signal: withTimeout(params.signal, ADVISOR_CONFIG.timeoutMs),
89
193
  });
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Assemble the runtime context the advisor needs to make grounded
3
+ * recommendations — the same situational awareness the executing agent has:
4
+ * - the tools available to it this turn,
5
+ * - the skills it can load,
6
+ * - the loaded workspace / project context, NOW.md, PKB, and open documents,
7
+ * - and relevant memory pulled through the recall search.
8
+ *
9
+ * The advisor already receives the agent's transcript and system prompt; this
10
+ * adds the situational context that lives *outside* the prompt (tools and
11
+ * skills are passed to the model as a separate catalog, not inlined) plus a
12
+ * fresh, task-focused memory recall.
13
+ *
14
+ * Personal-memory surfaces are gated to the same policy the main agent's
15
+ * memory injectors apply: the recall search honors `canAccessMemory` (like the
16
+ * `recall` tool), and NOW.md / PKB honor `isPersonalMemoryAllowed` (plus the
17
+ * scratchpad-injection toggle for NOW.md). The advisor tool is low-risk and can
18
+ * run on remote/trusted-contact turns, so without these gates it could forward
19
+ * private content the main agent itself would not receive.
20
+ *
21
+ * Every section is best-effort: each source is wrapped so a failure or empty
22
+ * result drops just that section, never the consult. Daemon- and memory-side
23
+ * modules are pulled in via dynamic `import()` so this plugin module — loaded
24
+ * at bootstrap through `defaults/index.ts` — never forms a static import cycle
25
+ * with them. The result is a single string injected into the advisor's system
26
+ * prompt (see `buildAdvisorSystem`), or `null` when nothing could be gathered.
27
+ */
28
+
29
+ import type { ChannelId } from "../../../channels/types.js";
30
+ import type { TrustContext } from "../../../daemon/trust-context.js";
31
+ import type { Message } from "../../../providers/types.js";
32
+ import type { TrustClass } from "../../../runtime/actor-trust-resolver.js";
33
+
34
+ export interface AdvisorContextSources {
35
+ conversationId: string;
36
+ workingDir: string;
37
+ /** The live tool set the executor sees this turn (`ToolContext.allowedToolNames`). */
38
+ allowedToolNames?: ReadonlySet<string>;
39
+ /**
40
+ * Trust class of the turn's actor, from the per-turn `ToolContext.trustClass`
41
+ * snapshot. Gates the memory recall and (with {@link sourceChannel}) the
42
+ * personal-memory surfaces.
43
+ */
44
+ trustClass: TrustClass;
45
+ /**
46
+ * Channel the turn originates on, from the per-turn `ToolContext.executionChannel`
47
+ * snapshot. Combined with {@link trustClass} to evaluate personal-memory
48
+ * access exactly as the injectors do, off the same per-turn snapshot rather
49
+ * than the mutable live conversation trust.
50
+ */
51
+ sourceChannel?: string;
52
+ /** The captured transcript, used to derive the recall query. */
53
+ transcript: ReadonlyArray<Message>;
54
+ signal?: AbortSignal;
55
+ }
56
+
57
+ /** Cap a block so the assembled context never balloons the consult prompt. */
58
+ function truncate(text: string, max: number): string {
59
+ const trimmed = text.trim();
60
+ return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max)}…`;
61
+ }
62
+
63
+ /** First sentence (or a capped prefix) of a tool/skill description. */
64
+ function summarize(description: string | undefined, max = 160): string {
65
+ if (!description) return "";
66
+ const firstSentence = description.split(/(?<=[.!?])\s/)[0] ?? description;
67
+ return truncate(firstSentence, max);
68
+ }
69
+
70
+ /** Pull the most recent user-authored text to seed the memory recall query. */
71
+ export function deriveRecallQuery(
72
+ transcript: ReadonlyArray<Message>,
73
+ ): string | null {
74
+ for (let i = transcript.length - 1; i >= 0; i--) {
75
+ const message = transcript[i];
76
+ if (message.role !== "user") continue;
77
+ const text = message.content
78
+ .map((block) => (block.type === "text" ? block.text : ""))
79
+ .join(" ")
80
+ .trim();
81
+ if (text.length > 0) return truncate(text, 500);
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /** `## Available tools` — the live tool set the agent can act with this turn. */
87
+ async function buildToolsSection(
88
+ allowedToolNames: ReadonlySet<string> | undefined,
89
+ ): Promise<string | null> {
90
+ if (!allowedToolNames || allowedToolNames.size === 0) return null;
91
+ try {
92
+ const { getTool } = await import("../../../tools/registry.js");
93
+ const lines: string[] = [];
94
+ for (const name of [...allowedToolNames].sort()) {
95
+ // The advisor advises; it never recommends consulting itself.
96
+ if (name === "advisor") continue;
97
+ const summary = summarize(getTool(name)?.description);
98
+ lines.push(summary ? `- ${name} — ${summary}` : `- ${name}`);
99
+ }
100
+ if (lines.length === 0) return null;
101
+ return `## Available tools (what the agent can do)\n${lines.join("\n")}`;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /** `## Available skills` — the skills the agent can load via `skill_load`. */
108
+ async function buildSkillsSection(): Promise<string | null> {
109
+ try {
110
+ const { loadSkillCatalog } = await import("../../../config/skills.js");
111
+ const catalog = loadSkillCatalog();
112
+ if (catalog.length === 0) return null;
113
+ const lines = catalog.slice(0, 60).map((skill) => {
114
+ const summary = summarize(skill.description);
115
+ const when = skill.activationHints?.length
116
+ ? ` (use when: ${truncate(skill.activationHints.join("; "), 120)})`
117
+ : "";
118
+ const label = skill.displayName || skill.name || skill.id;
119
+ return `- ${label} (${skill.id})${summary ? ` — ${summary}` : ""}${when}`;
120
+ });
121
+ const more =
122
+ catalog.length > 60 ? `\n- …and ${catalog.length - 60} more` : "";
123
+ return `## Available skills (load with skill_load)\n${lines.join("\n")}${more}`;
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Whether personal-memory surfaces (NOW.md, PKB) may be exposed to the advisor
131
+ * — the same `isPersonalMemoryAllowed` gate the runtime memory injectors apply.
132
+ *
133
+ * Derived from the per-turn trust snapshot (`ToolContext.trustClass` /
134
+ * `executionChannel`, threaded in via {@link AdvisorContextSources}), NOT the
135
+ * live `findConversation().trustContext`: that conversation state is mutable
136
+ * and a concurrent guardian/meta command could flip it to guardian mid-flight,
137
+ * granting a remote/non-guardian turn access its own snapshot was never given.
138
+ * Fail-closed: if the gate can't be resolved, returns false.
139
+ */
140
+ async function personalMemoryAllowedForAdvisor(
141
+ trustClass: TrustClass,
142
+ sourceChannel: string | undefined,
143
+ ): Promise<boolean> {
144
+ try {
145
+ const { isPersonalMemoryAllowed } =
146
+ await import("../../../daemon/trust-context.js");
147
+ // `isPersonalMemoryAllowed` reads only `sourceChannel` + `trustClass`; build
148
+ // a minimal trust context from the per-turn snapshot. The channel may be
149
+ // absent (local/internal turns), which the gate treats as non-remote.
150
+ const snapshot = {
151
+ sourceChannel: sourceChannel as ChannelId | undefined,
152
+ trustClass,
153
+ } as TrustContext;
154
+ return isPersonalMemoryAllowed(snapshot);
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ /** `## Workspace & project context` — the loaded environment around the agent. */
161
+ async function buildWorkspaceSection(
162
+ sources: AdvisorContextSources,
163
+ ): Promise<string | null> {
164
+ const { conversationId } = sources;
165
+ const parts: string[] = [];
166
+
167
+ // The `<workspace>` directory listing is not personal memory — the agent's
168
+ // own file tools already operate in this cwd — so it is surfaced ungated, the
169
+ // same way the workspace-context injector does.
170
+ try {
171
+ const { resolveWorkspaceTopLevelContext } =
172
+ await import("../../../daemon/conversation-workspace.js");
173
+ const workspace = resolveWorkspaceTopLevelContext(conversationId);
174
+ if (workspace) parts.push(truncate(workspace, 2500));
175
+ } catch {
176
+ /* best-effort */
177
+ }
178
+
179
+ // NOW.md and PKB are personal-memory surfaces. Gate them behind the same
180
+ // `isPersonalMemoryAllowed` policy (and, for NOW.md, the scratchpad-injection
181
+ // toggle) the runtime injectors use, evaluated off the per-turn trust
182
+ // snapshot, so a low-risk advisor consult cannot forward private content the
183
+ // main agent would never receive.
184
+ if (
185
+ await personalMemoryAllowedForAdvisor(
186
+ sources.trustClass,
187
+ sources.sourceChannel,
188
+ )
189
+ ) {
190
+ try {
191
+ const [{ readNowScratchpad }, { getConfig }] = await Promise.all([
192
+ import("../../../daemon/now-scratchpad.js"),
193
+ import("../../../config/loader.js"),
194
+ ]);
195
+ if (getConfig().memory.retrieval.scratchpadInjection.enabled) {
196
+ const now = readNowScratchpad();
197
+ if (now) parts.push(`NOW.md scratchpad:\n${truncate(now, 1500)}`);
198
+ }
199
+ } catch {
200
+ /* best-effort */
201
+ }
202
+
203
+ try {
204
+ const { readPkbContext } = await import("../../../memory/pkb/context.js");
205
+ const pkb = readPkbContext();
206
+ if (pkb) parts.push(truncate(pkb, 1500));
207
+ } catch {
208
+ /* best-effort */
209
+ }
210
+ }
211
+
212
+ try {
213
+ const { buildActiveDocuments } =
214
+ await import("../../../daemon/conversation-runtime-assembly.js");
215
+ const docs = buildActiveDocuments(conversationId);
216
+ if (docs && docs.length > 0) {
217
+ const titles = docs
218
+ .slice(0, 20)
219
+ .map((doc) => `- ${doc.title} (${doc.wordCount} words)`)
220
+ .join("\n");
221
+ parts.push(`Open documents:\n${titles}`);
222
+ }
223
+ } catch {
224
+ /* best-effort */
225
+ }
226
+
227
+ if (parts.length === 0) return null;
228
+ return `## Workspace & project context\n${parts.join("\n\n")}`;
229
+ }
230
+
231
+ /** `## Relevant memory (recall)` — a fresh, task-focused recall search. */
232
+ async function buildMemorySection(
233
+ sources: AdvisorContextSources,
234
+ ): Promise<string | null> {
235
+ try {
236
+ const { resolveCapabilities } =
237
+ await import("../../../runtime/capabilities.js");
238
+ // Recall reads sensitive local context; honor the same trust gate the
239
+ // `recall` tool applies. Non-guardian turns get no fresh recall here.
240
+ if (!resolveCapabilities(sources.trustClass).canAccessMemory) return null;
241
+
242
+ const query = deriveRecallQuery(sources.transcript);
243
+ if (!query) return null;
244
+
245
+ const [{ runDeterministicRecallSearch }, { getConfig }] = await Promise.all(
246
+ [
247
+ import("../../../memory/context-search/search.js"),
248
+ import("../../../config/loader.js"),
249
+ ],
250
+ );
251
+
252
+ const { evidence } = await runDeterministicRecallSearch(
253
+ { query, max_results: 8 },
254
+ {
255
+ workingDir: sources.workingDir,
256
+ conversationId: sources.conversationId,
257
+ config: getConfig(),
258
+ signal: sources.signal,
259
+ },
260
+ );
261
+ if (evidence.length === 0) return null;
262
+
263
+ const lines = evidence.slice(0, 8).map((item) => {
264
+ const excerpt = truncate(item.excerpt, 220);
265
+ return `- [${item.source}] ${item.title} (${item.locator}): ${excerpt}`;
266
+ });
267
+ return `## Relevant memory (recall: "${truncate(query, 120)}")\n${lines.join("\n")}`;
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Gather the advisor's runtime context block, or `null` if nothing is
275
+ * available. Sections run concurrently; each is independently best-effort.
276
+ */
277
+ export async function buildAdvisorContext(
278
+ sources: AdvisorContextSources,
279
+ ): Promise<string | null> {
280
+ const sections = await Promise.all([
281
+ buildToolsSection(sources.allowedToolNames),
282
+ buildSkillsSection(),
283
+ buildWorkspaceSection(sources),
284
+ buildMemorySection(sources),
285
+ ]);
286
+ const present = sections.filter((s): s is string => s !== null);
287
+ return present.length > 0 ? present.join("\n\n") : null;
288
+ }
@@ -33,9 +33,15 @@ export function stripSteering(systemPrompt: string | null): string | null {
33
33
  * System prompt for the advisor sub-call. Frames the advisor's role and, for
34
34
  * context, quotes the executor's own system prompt (as the advisor tool does —
35
35
  * the advisor sees the system prompt as context about the executor's task).
36
+ *
37
+ * `runtimeContext`, when present, carries the agent's situational context that
38
+ * lives outside its system prompt — available tools and skills, workspace /
39
+ * project context, and recalled memory (see `buildAdvisorContext`) — so the
40
+ * advisor can ground its recommendations in what the agent can actually do.
36
41
  */
37
42
  export function buildAdvisorSystem(
38
43
  originalSystemPrompt: string | null,
44
+ runtimeContext?: string | null,
39
45
  ): string {
40
46
  const base = `You are a senior advisor consulted by another AI agent working on a task — most often at the planning stage, before it starts building, but sometimes partway through. The entire conversation above is the agent's working context: its task or goal, every tool call it has made, and every result it has seen. The agent has paused to consult you because you bring a second, independent perspective it cannot get from inside its own reasoning loop. Your job is to maximize its odds of completing the task correctly and efficiently.
41
47
 
@@ -54,8 +60,14 @@ How to advise:
54
60
  - Stay in your lane. Advise the agent; do not role-play as it, write its final deliverable, or take its next action for it. If the agent is already on the right track, confirm it and sharpen the plan rather than manufacturing objections.
55
61
 
56
62
  Write as much as the guidance genuinely needs, and no more.`;
57
- if (!originalSystemPrompt) return base;
58
- return `${base}\n\nFor context, the agent is operating under this system prompt:\n<agent_system_prompt>\n${originalSystemPrompt}\n</agent_system_prompt>`;
63
+ let prompt = base;
64
+ if (originalSystemPrompt) {
65
+ prompt += `\n\nFor context, the agent is operating under this system prompt:\n<agent_system_prompt>\n${originalSystemPrompt}\n</agent_system_prompt>`;
66
+ }
67
+ if (runtimeContext) {
68
+ prompt += `\n\nThe agent's runtime context — the tools and skills available to it, the loaded workspace/project context, and relevant memory — follows. Ground your recommendations in what the agent can actually do and what is around it; reference specific tools, skills, files, or memory where relevant.\n<agent_runtime_context>\n${runtimeContext}\n</agent_runtime_context>`;
69
+ }
70
+ return prompt;
59
71
  }
60
72
 
61
73
  /**