@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
@@ -0,0 +1,543 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, join, relative } from "node:path";
3
+
4
+ import { minimatch } from "minimatch";
5
+ import { RE2JS } from "re2js";
6
+
7
+ import { RiskLevel } from "../../permissions/types.js";
8
+ import { registerTool } from "../registry.js";
9
+ import {
10
+ isDeniedBasename,
11
+ sandboxPolicy,
12
+ } from "../shared/filesystem/path-policy.js";
13
+ import type {
14
+ ToolContext,
15
+ ToolDefinition,
16
+ ToolExecutionResult,
17
+ } from "../types.js";
18
+
19
+ // Directory names that are never worth searching and would otherwise dominate
20
+ // the file budget (dependencies, VCS metadata).
21
+ const IGNORED_DIRS = new Set(["node_modules", ".git"]);
22
+
23
+ // Defensive caps so a search over a huge tree cannot read the whole disk into
24
+ // memory. These bound the walk regardless of `max_results`.
25
+ const MAX_FILES_SCANNED = 20_000;
26
+ const MAX_TOTAL_BYTES = 256 * 1024 * 1024; // 256 MiB
27
+ const MAX_FILE_BYTES = 8 * 1024 * 1024; // skip files larger than 8 MiB
28
+
29
+ // Hard cap on directory entries visited during the walk, independent of
30
+ // wall-clock time. `MAX_FILES_SCANNED`/`MAX_TOTAL_BYTES` only count files that
31
+ // are actually READ, so a pathological tree full of oversized, unreadable,
32
+ // permission-denied, or glob-filtered files (which never increment those
33
+ // counters) could otherwise traverse unbounded. This bounds the readdir/stat
34
+ // work even when every individual operation is fast.
35
+ const MAX_ENTRIES_TRAVERSED = 200_000;
36
+
37
+ // Display-only cap on emitted output lines (ripgrep-style long-line bound). The
38
+ // pattern is matched against the FULL line, so a token anywhere on a long line
39
+ // is found; this cap only bounds the PRINTED line text (matched and context
40
+ // lines) so a single pathological line (e.g. a minified bundle on one line)
41
+ // can't emit a multi-megabyte output line. Normal code/log lines are well under
42
+ // the cap and print verbatim.
43
+ const MAX_DISPLAY_LINE_LENGTH = 2000;
44
+
45
+ // Output-byte budget: when `context_lines` is large and many nearby lines
46
+ // match, the surrounding block is appended for every match, so a single large
47
+ // file with many matches could allocate hundreds of MB before the post-tool
48
+ // truncation hook runs. Stop accumulating once this budget is exceeded and
49
+ // report the result as truncated.
50
+ const MAX_OUTPUT_BYTES = 4 * 1024 * 1024; // 4 MiB
51
+
52
+ // Wall-clock deadline for the whole search. The synchronous scan never yields,
53
+ // so the promise-based tool timeout / abort signal can't fire mid-scan. Checking
54
+ // this deadline (and the abort signal) once per line bounds how long a search
55
+ // over a very large tree can block the event loop and lets an external abort
56
+ // stop the walk promptly.
57
+ const MAX_SEARCH_MS = 10_000; // 10 s
58
+
59
+ const DEFAULT_MAX_RESULTS = 200;
60
+ const DEFAULT_CONTEXT_LINES = 0;
61
+ // Clamp `context_lines` so a single match cannot request unbounded surrounding
62
+ // context (which would blow past the output budget on its own).
63
+ const MAX_CONTEXT_LINES = 20;
64
+
65
+ /** Heuristic binary-file detection: a NUL byte in the leading bytes. */
66
+ function looksBinary(buf: Buffer): boolean {
67
+ const len = Math.min(buf.length, 8000);
68
+ for (let i = 0; i < len; i++) {
69
+ if (buf[i] === 0) return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ export const codeSearchTool = {
75
+ name: "code_search",
76
+ description:
77
+ "Search file contents for a regular-expression pattern across a directory tree on your own machine, returning matching `path:line: text` lines. Read-only — never modifies files. Skips node_modules, .git, and binary files. Use this to grep across files without a shell.",
78
+ category: "filesystem",
79
+ executionTarget: "sandbox",
80
+ defaultRiskLevel: RiskLevel.Low,
81
+
82
+ input_schema: {
83
+ type: "object",
84
+ properties: {
85
+ pattern: {
86
+ type: "string",
87
+ description:
88
+ "Regular-expression pattern to search for in file contents",
89
+ },
90
+ path: {
91
+ type: "string",
92
+ description:
93
+ "Directory to search under (defaults to the working directory)",
94
+ },
95
+ glob: {
96
+ type: "string",
97
+ description:
98
+ "Only search files whose path (relative to the search root) matches this glob, e.g. '*.ts' or 'src/**'",
99
+ },
100
+ case_insensitive: {
101
+ type: "boolean",
102
+ description: "Match the pattern case-insensitively",
103
+ },
104
+ context_lines: {
105
+ type: "number",
106
+ description:
107
+ "Number of lines of context to include before and after each match (default 0)",
108
+ },
109
+ max_results: {
110
+ type: "number",
111
+ description: "Maximum number of matches to return (default 200)",
112
+ },
113
+ activity: {
114
+ type: "string",
115
+ description:
116
+ "Brief non-technical explanation of what you are doing and why, shown as a status update.",
117
+ },
118
+ },
119
+ required: ["pattern", "activity"],
120
+ },
121
+
122
+ async execute(
123
+ input: Record<string, unknown>,
124
+ context: ToolContext,
125
+ ): Promise<ToolExecutionResult> {
126
+ const pattern = input.pattern;
127
+ if (!pattern || typeof pattern !== "string") {
128
+ return {
129
+ content: "Error: pattern is required and must be a non-empty string",
130
+ isError: true,
131
+ };
132
+ }
133
+
134
+ const rawPath =
135
+ typeof input.path === "string" && input.path.length > 0
136
+ ? input.path
137
+ : context.workingDir;
138
+
139
+ const pathCheck = sandboxPolicy(rawPath, context.workingDir);
140
+ if (!pathCheck.ok) {
141
+ return {
142
+ content: `Error: ${pathCheck.error}. To search outside the workspace, use the host_bash tool instead.`,
143
+ isError: true,
144
+ };
145
+ }
146
+ const root = pathCheck.resolved;
147
+
148
+ const caseInsensitive = input.case_insensitive === true;
149
+ // Match with the linear-time RE2 engine (via re2js) instead of V8's
150
+ // backtracking RegExp. RE2 guarantees linear-time matching, so a
151
+ // user/subagent-supplied pattern cannot trigger catastrophic backtracking
152
+ // and block the synchronous scan. The trade-off is that RE2 rejects
153
+ // backreferences and lookarounds; compile() throws on those, surfaced as a
154
+ // clean error below.
155
+ let regex: RE2JS;
156
+ try {
157
+ regex = RE2JS.compile(
158
+ pattern,
159
+ caseInsensitive ? RE2JS.CASE_INSENSITIVE : 0,
160
+ );
161
+ } catch (err) {
162
+ return {
163
+ content: `Error: invalid or unsupported pattern "${pattern}": ${
164
+ err instanceof Error ? err.message : String(err)
165
+ }. The RE2 engine does not support backreferences or lookarounds.`,
166
+ isError: true,
167
+ };
168
+ }
169
+
170
+ const glob =
171
+ typeof input.glob === "string" && input.glob.length > 0
172
+ ? input.glob
173
+ : undefined;
174
+
175
+ const contextLines =
176
+ typeof input.context_lines === "number" && input.context_lines > 0
177
+ ? Math.min(Math.floor(input.context_lines), MAX_CONTEXT_LINES)
178
+ : DEFAULT_CONTEXT_LINES;
179
+
180
+ const maxResults =
181
+ typeof input.max_results === "number" && input.max_results > 0
182
+ ? Math.floor(input.max_results)
183
+ : DEFAULT_MAX_RESULTS;
184
+
185
+ // Validate the search root before walking so a missing/unreadable/typo'd
186
+ // path surfaces a hard error instead of being swallowed like an unreadable
187
+ // child directory and returning a false "No matches found". Mirrors the
188
+ // NOT_FOUND / NOT_A_DIRECTORY reporting in file_list.
189
+ let rootStat: import("node:fs").Stats;
190
+ try {
191
+ rootStat = statSync(root);
192
+ } catch {
193
+ return {
194
+ content: `Error: path not found: ${rawPath}`,
195
+ isError: true,
196
+ };
197
+ }
198
+
199
+ // Start the wall-clock deadline before walking so a pathological regex
200
+ // (or an external abort) can stop the synchronous scan mid-flight instead
201
+ // of blocking the event loop until the whole tree is exhausted.
202
+ const searchStart = Date.now();
203
+
204
+ const lines: string[] = [];
205
+ let matchCount = 0;
206
+ let truncated = false;
207
+ // Set when the output-byte budget (not the max-results cap) stopped the
208
+ // accumulation, so the truncation note can explain why.
209
+ let outputBudgetHit = false;
210
+ // Set when the wall-clock deadline (or an abort signal) stopped the scan,
211
+ // so the result can report that the search timed out and is incomplete —
212
+ // distinct from the scan-cap and output-budget truncation notes.
213
+ let timedOut = false;
214
+ // Set when the search stopped because it reached the `max_results` cap (as
215
+ // opposed to a scan/traversal cap). Only this cause is helped by raising
216
+ // `max_results`, so the matched-branch truncation note keys off this flag to
217
+ // avoid suggesting a larger `max_results` when a scan cap stopped the walk.
218
+ let maxResultsHit = false;
219
+ // Set when a file was skipped mid-walk because it exceeds MAX_FILE_BYTES.
220
+ // Unlike the truncation flags this does NOT halt the walk — other files may
221
+ // still match — but it does mark the overall result incomplete, since the
222
+ // skipped file could have contained the pattern. Kept separate from
223
+ // `truncated` so the zero-match invariant ("truncated => scan cap/timeout")
224
+ // stays intact.
225
+ let skippedLargeFile = false;
226
+ let filesScanned = 0;
227
+ let totalBytes = 0;
228
+ // Number of directory entries visited during the walk (files + subdirs),
229
+ // bounded by MAX_ENTRIES_TRAVERSED so a pathological tree can't run the
230
+ // synchronous readdir/stat work unbounded even when no file is ever read.
231
+ let entriesTraversed = 0;
232
+ // Set when an EXPLICIT file root (not a child discovered mid-walk) can't be
233
+ // read — e.g. EACCES/EPERM. A child file's read failure is optional and
234
+ // silently skipped, but an unreadable explicit root means nothing was
235
+ // searched, so we surface it as an error instead of a false "No matches".
236
+ let rootReadError: string | null = null;
237
+ // Running size of the accumulated output (lines joined by "\n") so we can
238
+ // stop before allocating an unbounded result. Each pushed line contributes
239
+ // its UTF-8 byte length plus one byte for the joining newline.
240
+ let outputBytes = 0;
241
+
242
+ // Bound a single emitted line's length for display only. Matching always
243
+ // runs against the full line; this just keeps the PRINTED text readable and
244
+ // prevents one pathological line (minified bundle, huge JSON/log line) from
245
+ // emitting a multi-megabyte output line. Normal lines pass through verbatim.
246
+ const truncateForDisplay = (text: string): string =>
247
+ text.length > MAX_DISPLAY_LINE_LENGTH
248
+ ? `${text.slice(0, MAX_DISPLAY_LINE_LENGTH)} …[line truncated]`
249
+ : text;
250
+
251
+ // Append an output line, tracking its byte cost. Returns false once the
252
+ // output-byte budget is exhausted so callers can stop accumulating.
253
+ const pushLine = (line: string): boolean => {
254
+ outputBytes += Buffer.byteLength(line, "utf8") + 1;
255
+ lines.push(line);
256
+ if (outputBytes > MAX_OUTPUT_BYTES) {
257
+ truncated = true;
258
+ outputBudgetHit = true;
259
+ return false;
260
+ }
261
+ return true;
262
+ };
263
+
264
+ // Scan a single regular file for matches. Applies the denied-basename guard
265
+ // and per-file size caps. Returns nothing; mutates the shared accumulators.
266
+ const scanFile = (full: string, isExplicitRoot = false): void => {
267
+ if (truncated) return;
268
+
269
+ // Honor the wall-clock deadline / abort signal before doing any stat/size
270
+ // work, not just inside the per-line loop. A tree full of oversized or
271
+ // unreadable files (which never reach the per-line loop) could otherwise
272
+ // run far past the advertised deadline.
273
+ if (Date.now() - searchStart > MAX_SEARCH_MS || context.signal?.aborted) {
274
+ truncated = true;
275
+ timedOut = true;
276
+ return;
277
+ }
278
+
279
+ // Never read files the assistant is forbidden from touching, even if a
280
+ // broad pattern would otherwise match them. Reuses the same denylist as
281
+ // file_read/file_write so the policies stay in sync.
282
+ if (isDeniedBasename(full)) return;
283
+
284
+ const rel = relative(root, full);
285
+ // matchBase lets a slash-free pattern like "*.ts" match files at any
286
+ // depth (against the basename); patterns with slashes match the full
287
+ // relative path. dot so dotfiles aren't silently excluded. When the root
288
+ // is a single file, `rel` is empty, so match against the basename.
289
+ const globTarget = rel.length > 0 ? rel : basename(full);
290
+ if (
291
+ glob &&
292
+ !minimatch(globTarget, glob, { matchBase: true, dot: true })
293
+ ) {
294
+ return;
295
+ }
296
+
297
+ if (filesScanned >= MAX_FILES_SCANNED) {
298
+ truncated = true;
299
+ return;
300
+ }
301
+
302
+ let size: number;
303
+ try {
304
+ size = statSync(full).size;
305
+ } catch {
306
+ return;
307
+ }
308
+ if (size > MAX_FILE_BYTES) {
309
+ // Skip just this file (others may still match), but record that the
310
+ // search is incomplete so the result doesn't read as a definitive miss.
311
+ skippedLargeFile = true;
312
+ return;
313
+ }
314
+ if (totalBytes + size > MAX_TOTAL_BYTES) {
315
+ truncated = true;
316
+ return;
317
+ }
318
+
319
+ let buf: Buffer;
320
+ try {
321
+ buf = readFileSync(full);
322
+ } catch (err) {
323
+ // A child file discovered mid-walk is optional — skip it silently. But
324
+ // an explicit file root that can't be read was never searched, so record
325
+ // the failure for the caller to surface as an error.
326
+ if (isExplicitRoot) {
327
+ const code = (err as NodeJS.ErrnoException)?.code;
328
+ rootReadError = `Error: failed to read file (${code ?? "unreadable"}): ${rawPath}`;
329
+ }
330
+ return;
331
+ }
332
+ filesScanned++;
333
+ totalBytes += buf.length;
334
+ if (looksBinary(buf)) return;
335
+
336
+ // Display path: when searching a single file at the root, `rel` is empty;
337
+ // fall back to the basename so output stays readable.
338
+ const display = rel.length > 0 ? rel : basename(full);
339
+ const fileLines = buf.toString("utf8").split("\n");
340
+ for (let i = 0; i < fileLines.length; i++) {
341
+ // Bound the total event-loop block: check the wall-clock deadline and
342
+ // the abort signal before every regex test (the per-line Date.now() and
343
+ // .aborted reads are negligible next to a catastrophic-backtracking
344
+ // test). When either trips, mark the search timed out / aborted and
345
+ // stop the whole walk so partial results are still reported.
346
+ if (
347
+ Date.now() - searchStart > MAX_SEARCH_MS ||
348
+ context.signal?.aborted
349
+ ) {
350
+ truncated = true;
351
+ timedOut = true;
352
+ return;
353
+ }
354
+ // Match against the FULL line so a token that appears past any display
355
+ // cap (e.g. after column 2000 on a long log/JSON/minified line) is still
356
+ // found. RE2's linear-time matching makes this safe — there is no
357
+ // backtracking blowup to bound, so no need to slice before matching.
358
+ const line = fileLines[i];
359
+ // Unanchored partial match anywhere in the line — the RE2 equivalent of
360
+ // RegExp.prototype.test(). The compiled pattern is stateless per call.
361
+ if (!regex.test(line)) continue;
362
+ if (matchCount >= maxResults) {
363
+ truncated = true;
364
+ maxResultsHit = true;
365
+ return;
366
+ }
367
+ // Count the match before emitting its lines: a single match whose output
368
+ // alone exceeds the output-byte budget would otherwise leave matchCount
369
+ // at 0, making the zero-match branch report a false "no matches / scan
370
+ // cap" instead of a truncated result that acknowledges the match.
371
+ matchCount++;
372
+ const lineNo = i + 1;
373
+ if (contextLines > 0) {
374
+ const start = Math.max(0, i - contextLines);
375
+ const end = Math.min(fileLines.length - 1, i + contextLines);
376
+ for (let j = start; j <= end; j++) {
377
+ const sep = j === i ? ":" : "-";
378
+ if (
379
+ !pushLine(
380
+ `${display}:${j + 1}${sep} ${truncateForDisplay(fileLines[j])}`,
381
+ )
382
+ )
383
+ return;
384
+ }
385
+ if (!pushLine("--")) return;
386
+ } else {
387
+ if (
388
+ !pushLine(
389
+ `${display}:${lineNo}: ${truncateForDisplay(fileLines[i])}`,
390
+ )
391
+ )
392
+ return;
393
+ }
394
+ }
395
+ };
396
+
397
+ const walk = (dir: string, isExplicitRoot = false): void => {
398
+ if (truncated) return;
399
+ // Bound the traversal itself, not just file reads: a deep/wide tree of
400
+ // directories (or many entries that never get read) could otherwise run
401
+ // the synchronous readdir/stat work far past the advertised deadline.
402
+ if (Date.now() - searchStart > MAX_SEARCH_MS || context.signal?.aborted) {
403
+ truncated = true;
404
+ timedOut = true;
405
+ return;
406
+ }
407
+ let entries: import("node:fs").Dirent[];
408
+ try {
409
+ entries = readdirSync(dir, { withFileTypes: true });
410
+ } catch (err) {
411
+ // A child directory discovered mid-walk is optional — skip it silently.
412
+ // But an explicit directory root that can't be read (e.g. EACCES/EPERM)
413
+ // was never searched, so record the failure for the caller to surface
414
+ // as an error instead of a false "No matches found".
415
+ if (isExplicitRoot) {
416
+ const code = (err as NodeJS.ErrnoException)?.code;
417
+ rootReadError = `Error: failed to read directory (${code ?? "unreadable"}): ${rawPath}`;
418
+ }
419
+ return;
420
+ }
421
+ for (const entry of entries) {
422
+ if (truncated) return;
423
+ // Per-entry deadline/abort check (the Date.now()/.aborted reads are
424
+ // negligible) plus a hard entry cap independent of wall-clock time so a
425
+ // pathological tree is bounded even when every operation is fast.
426
+ if (
427
+ Date.now() - searchStart > MAX_SEARCH_MS ||
428
+ context.signal?.aborted
429
+ ) {
430
+ truncated = true;
431
+ timedOut = true;
432
+ return;
433
+ }
434
+ entriesTraversed++;
435
+ if (entriesTraversed > MAX_ENTRIES_TRAVERSED) {
436
+ truncated = true;
437
+ return;
438
+ }
439
+ const full = join(dir, entry.name);
440
+ if (entry.isDirectory()) {
441
+ if (IGNORED_DIRS.has(entry.name)) continue;
442
+ walk(full);
443
+ continue;
444
+ }
445
+ if (!entry.isFile()) continue;
446
+ scanFile(full);
447
+ }
448
+ };
449
+
450
+ if (rootStat.isDirectory()) {
451
+ walk(root, true);
452
+ // An explicit directory root that statSync sees but readdirSync can't read
453
+ // (EACCES/EPERM) was never searched — surface a hard error instead of a
454
+ // false "No matches found", mirroring the unreadable explicit-file root.
455
+ if (rootReadError) {
456
+ return { content: rootReadError, isError: true };
457
+ }
458
+ } else if (rootStat.isFile()) {
459
+ // A regular-file root is a valid single-file search. Unlike an oversized
460
+ // file skipped mid-walk (where other files may still match), an oversized
461
+ // explicit root means nothing was searched at all — surface a hard error
462
+ // instead of falling through to a false "No matches found".
463
+ if (rootStat.size > MAX_FILE_BYTES) {
464
+ return {
465
+ content: `Error: file too large to search (${rootStat.size} bytes): ${rawPath}`,
466
+ isError: true,
467
+ };
468
+ }
469
+ scanFile(root, true);
470
+ if (rootReadError) {
471
+ return { content: rootReadError, isError: true };
472
+ }
473
+ } else {
474
+ return {
475
+ content: `Error: path not found: ${rawPath}`,
476
+ isError: true,
477
+ };
478
+ }
479
+
480
+ if (matchCount === 0) {
481
+ // With zero matches, `truncated` can only have been set by a scan cap
482
+ // (MAX_FILES_SCANNED / MAX_TOTAL_BYTES / MAX_ENTRIES_TRAVERSED) or the
483
+ // wall-clock deadline, never the max-results path. In either case the tree
484
+ // was only partially scanned, so a definitive "No matches found" would be
485
+ // a false negative.
486
+ if (timedOut) {
487
+ return {
488
+ content: `Search timed out after ${MAX_SEARCH_MS / 1000}s before finding any match for /${pattern}/${caseInsensitive ? "i" : ""} under ${basename(root)}. Results are incomplete — narrow your pattern, glob, or path and search again.`,
489
+ isError: false,
490
+ status: "truncated",
491
+ };
492
+ }
493
+ if (truncated) {
494
+ return {
495
+ content: `Search stopped early after hitting a scan cap before finding any match for /${pattern}/${caseInsensitive ? "i" : ""} under ${basename(root)}. Results are incomplete — narrow your glob or path and search again.`,
496
+ isError: false,
497
+ status: "truncated",
498
+ };
499
+ }
500
+ if (skippedLargeFile) {
501
+ return {
502
+ content: `No matches found for /${pattern}/${caseInsensitive ? "i" : ""} under ${basename(root)}, but one or more files were skipped because they exceed the ${Math.round(MAX_FILE_BYTES / (1024 * 1024))} MiB per-file size limit, so results may be incomplete.`,
503
+ isError: false,
504
+ status: "truncated",
505
+ };
506
+ }
507
+ return {
508
+ content: `No matches found for /${pattern}/${caseInsensitive ? "i" : ""} under ${basename(root)}`,
509
+ isError: false,
510
+ };
511
+ }
512
+
513
+ let content = lines.join("\n");
514
+ if (truncated) {
515
+ // Distinguish the truncation cause in priority order so the note doesn't
516
+ // mislead. Only `maxResultsHit` is helped by raising `max_results`; a
517
+ // scan/traversal cap (MAX_FILES_SCANNED / MAX_TOTAL_BYTES /
518
+ // MAX_ENTRIES_TRAVERSED) is not, so it gets its own note.
519
+ if (timedOut) {
520
+ content += `\n\n[Search timed out after ${MAX_SEARCH_MS / 1000}s. Results are incomplete. Narrow your pattern, glob, or path to see more.]`;
521
+ } else if (outputBudgetHit) {
522
+ content += `\n\n[Output capped at ${Math.round(MAX_OUTPUT_BYTES / (1024 * 1024))} MiB. Narrow your pattern, glob, or path, or reduce context_lines, to see more.]`;
523
+ } else if (maxResultsHit) {
524
+ content += `\n\n[Results truncated at ${maxResults} matches. Narrow your pattern, glob, or path to see more.]`;
525
+ } else {
526
+ content += `\n\n[Search stopped early after hitting a scan/traversal cap — some files weren't searched. Narrow your glob or path (raising max_results won't help).]`;
527
+ }
528
+ }
529
+ if (skippedLargeFile) {
530
+ // Independent of the truncation reasons above: note any files skipped for
531
+ // exceeding the per-file size limit so the result isn't read as complete.
532
+ content += `\n\n[One or more files were skipped because they exceed the ${Math.round(MAX_FILE_BYTES / (1024 * 1024))} MiB per-file size limit. Results may be incomplete.]`;
533
+ }
534
+
535
+ return {
536
+ content,
537
+ isError: false,
538
+ ...(truncated || skippedLargeFile ? { status: "truncated" } : {}),
539
+ };
540
+ },
541
+ } satisfies ToolDefinition;
542
+
543
+ registerTool(codeSearchTool);
@@ -1,3 +1,4 @@
1
+ import { isPluginDisabled } from "../plugins/disabled-state.js";
1
2
  import { getLogger } from "../util/logger.js";
2
3
  import { coreAppProxyTools } from "./apps/definitions.js";
3
4
  import { registerAppTools } from "./apps/registry.js";
@@ -548,6 +549,25 @@ export function getMcpToolDefinitions(): Tool[] {
548
549
  );
549
550
  }
550
551
 
552
+ /**
553
+ * Return tool definitions for all currently registered plugin-origin tools.
554
+ * Used by the session resolver to dynamically pick up plugin tools that were
555
+ * registered after session creation — e.g. a plugin installed at runtime and
556
+ * activated on a subsequent turn (see `plugins/mtime-cache.ts`). Mirrors
557
+ * {@link getMcpToolDefinitions} so a plugin install behaves like `mcp reload`.
558
+ */
559
+ export function getPluginToolDefinitions(): Tool[] {
560
+ return Array.from(tools.values()).filter((t) => {
561
+ const owner = ownersByName.get(t.name);
562
+ if (owner?.kind !== "plugin") return false;
563
+ // Filter out tools contributed by disabled plugins at read time so
564
+ // `assistant plugins disable <name>` takes effect on the next turn
565
+ // without a daemon restart. Mirrors the `.disabled` sentinel filtering
566
+ // in `getHooksFor` (plugins/registry.ts).
567
+ return !isPluginDisabled(owner.id);
568
+ });
569
+ }
570
+
551
571
  /**
552
572
  * Return MCP tools grouped by their owning server ID. Each entry contains
553
573
  * the server ID and the tool definitions registered by that server.
@@ -826,6 +846,20 @@ export function getWorkspaceToolNames(): string[] {
826
846
  .map((t) => t.name);
827
847
  }
828
848
 
849
+ /**
850
+ * Return tool definitions for all currently registered workspace-origin
851
+ * tools. Used by the conversation tool resolver to re-read workspace tools
852
+ * from the registry each turn, the same way {@link getMcpToolDefinitions}
853
+ * lets a conversation pick up MCP tools registered after it was created —
854
+ * here so reconciled edits under `<workspaceDir>/tools/` are picked up
855
+ * without recreating the conversation.
856
+ */
857
+ export function getWorkspaceToolDefinitions(): Tool[] {
858
+ return Array.from(tools.values()).filter(
859
+ (t) => ownersByName.get(t.name)?.kind === "workspace",
860
+ );
861
+ }
862
+
829
863
  /**
830
864
  * Return the names of core tools currently stripped via workspace
831
865
  * `.removed` sentinels — i.e. names where the stash holds an entry but
@@ -968,6 +1002,10 @@ export async function initializeTools(): Promise<void> {
968
1002
  // Workspace tools land after the core snapshot above so they're never
969
1003
  // baked into the test-reset baseline.
970
1004
  //
1005
+ // `loadWorkspaceTools` is idempotent: this is the first reconcile, and
1006
+ // conversation reads re-run it later to pick up on-disk edits without a
1007
+ // restart (see workspace-tools/loader.ts).
1008
+ //
971
1009
  // Imported dynamically because the loader imports back from this module
972
1010
  // (registerWorkspaceTools / removeCoreToolViaWorkspace); a static import
973
1011
  // here would create a registry ↔ loader cycle.
@@ -20,6 +20,16 @@ export type PathFailureReason = "not_absolute" | "out_of_bounds" | "denied";
20
20
  */
21
21
  const DENIED_BASENAMES = new Set([".backup.key", "backup.key"]);
22
22
 
23
+ /**
24
+ * Whether a path's basename is on the denylist of files the assistant must
25
+ * never read or write. Shared so callers that walk the filesystem (e.g.
26
+ * `code_search`) apply the same denylist as `sandboxPolicy`/`hostPolicy`,
27
+ * keeping the three in sync.
28
+ */
29
+ export function isDeniedBasename(path: string): boolean {
30
+ return DENIED_BASENAMES.has(basename(path));
31
+ }
32
+
23
33
  export type PathResult =
24
34
  | { ok: true; resolved: string }
25
35
  | { ok: false; reason: PathFailureReason; error: string };
@@ -138,10 +148,7 @@ export function sandboxPolicy(
138
148
 
139
149
  // Check both the logical path and the symlink-resolved path so a symlink
140
150
  // with a non-denied name pointing at a denied file is still caught.
141
- if (
142
- DENIED_BASENAMES.has(basename(resolved)) ||
143
- DENIED_BASENAMES.has(basename(realResolved))
144
- ) {
151
+ if (isDeniedBasename(resolved) || isDeniedBasename(realResolved)) {
145
152
  return {
146
153
  ok: false,
147
154
  reason: "denied",
@@ -168,7 +175,7 @@ export function hostPolicy(rawPath: string): PathResult {
168
175
  error: `path must be absolute for host file access: ${rawPath}`,
169
176
  };
170
177
  }
171
- if (DENIED_BASENAMES.has(basename(rawPath))) {
178
+ if (isDeniedBasename(rawPath)) {
172
179
  return {
173
180
  ok: false,
174
181
  reason: "denied",