@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
@@ -1,34 +1,41 @@
1
1
  /**
2
- * Per-surface mtime cache for user plugins.
2
+ * Per-surface mtime cache for user plugins (discovery + tools).
3
3
  *
4
- * Instead of caching whole `Plugin` objects, this module caches individual
5
- * hooks and tools keyed by their source file's mtime. This means:
4
+ * Instead of caching whole `Plugin` objects, the user-plugin system caches
5
+ * individual surfaces keyed by their source file's mtime. This module owns
6
+ * plugin **discovery** (which plugin directories exist, in what order) and the
7
+ * **tool** cache; the **hook** cache and every hook operation live in
8
+ * `../hooks/hook-loader.ts`. This module is the boot orchestrator: it scans the
9
+ * plugins directory, registers tools, and drives hook pre-import / init /
10
+ * shutdown by handing the discovered directories to the hook loader.
6
11
  *
7
- * - A changed hook file triggers a re-import of just that hook, not a full
8
- * plugin rebuild.
9
- * - The same machinery extends to workspace-driven hooks and tools in the
10
- * future (PR B), since each surface is cached independently.
11
- * - Plugins are never "registered" as a unit we just register their tools
12
- * and hooks into the global registries, then cache-bust them using mtime
13
- * on reads.
12
+ * - A changed tool file triggers a re-import of just that tool, not a full
13
+ * plugin rebuild. Added and removed surface files are picked up live, since
14
+ * discovery is by directory listing.
15
+ * - Plugins are never "registered" as a unit — we register their tools into
16
+ * the global tool registry and cache-bust them using mtime on reads.
14
17
  *
15
18
  * The cache is populated at boot by `loadUserPlugins()` and read on every
16
- * `getHooksFor` / `getAllTools` call. When a surface file's mtime changes,
17
- * the next read detects the mismatch, re-imports the file, and swaps the
18
- * cached entry.
19
+ * `getHooksFor` / `getAllTools` call.
19
20
  */
20
21
 
21
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
22
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
22
23
  import { join } from "node:path";
23
24
 
24
- import { getConfig } from "../config/loader.js";
25
25
  import { registerShutdownHook } from "../daemon/shutdown-registry.js";
26
- import { HOOKS } from "../plugin-api/constants.js";
27
- import type {
28
- PluginHookFn,
29
- PluginInitContext,
30
- PluginShutdownContext,
31
- } from "../plugin-api/types.js";
26
+ import {
27
+ clearPluginHooks,
28
+ collectUserHooks,
29
+ evictHooksForOwner,
30
+ hasWorkspaceHooks,
31
+ preImportHooksDir,
32
+ preImportWorkspaceHooks,
33
+ resetHookCacheForTests,
34
+ runInitHook,
35
+ runShutdownHook,
36
+ WORKSPACE_HOOKS_OWNER,
37
+ } from "../hooks/hook-loader.js";
38
+ import type { HookFunction, ShutdownContext } from "../plugin-api/types.js";
32
39
  import {
33
40
  registerPluginTools,
34
41
  unregisterPluginTools,
@@ -36,28 +43,26 @@ import {
36
43
  import { finalizeTool } from "../tools/tool-defaults.js";
37
44
  import type { Tool, ToolDefinition } from "../tools/types.js";
38
45
  import { getLogger } from "../util/logger.js";
39
- import { getWorkspaceDir, getWorkspacePluginsDir } from "../util/platform.js";
46
+ import { getWorkspacePluginsDir } from "../util/platform.js";
40
47
  import { APP_VERSION } from "../version.js";
41
48
  import {
42
49
  deriveToolName,
43
- importDefault,
44
50
  listSurfaceDir,
45
51
  parsePluginManifest,
46
52
  } from "./external-plugin-loader.js";
53
+ import {
54
+ clearSurfaceImportInflight,
55
+ getMtime,
56
+ importWithTimeout,
57
+ setSurfaceImportTimeout,
58
+ } from "./surface-import.js";
47
59
 
48
- // Re-export for type compat — consumers that import PluginHookFn from
60
+ // Re-export for type compat — consumers that import HookFunction from
49
61
  // the mtime cache module still resolve.
50
- export type { PluginHookFn } from "./types.js";
62
+ export type { HookFunction } from "./types.js";
51
63
 
52
64
  const log = getLogger("plugin-mtime-cache");
53
65
 
54
- /**
55
- * Import timeout for surface file imports. Set by `populateCacheAtBoot` from
56
- * the value passed by `loadUserPlugins`, and used by `getUserHooksFor` and
57
- * `reconcilePluginTools` for runtime re-imports. Defaults to 10s.
58
- */
59
- let importTimeoutMs = 10_000;
60
-
61
66
  /**
62
67
  * Cached install-date timestamps per plugin directory, so `scanPlugins`
63
68
  * doesn't re-read `install-meta.json` on every turn. Populated on first
@@ -130,16 +135,6 @@ function getInstallDate(pluginDir: string): number {
130
135
 
131
136
  // ─── Cache entries ───────────────────────────────────────────────────────────
132
137
 
133
- /**
134
- * A cached hook function plus the mtime of its source file. When the on-disk
135
- * mtime changes, the hook is re-imported and the entry is replaced.
136
- */
137
- interface CachedHook {
138
- readonly hook: PluginHookFn;
139
- /** mtimeMs of the source file this hook was imported from. */
140
- readonly sourceMtime: number;
141
- }
142
-
143
138
  /**
144
139
  * A cached tool plus the mtime of its source file. When the on-disk mtime
145
140
  * changes, the tool is re-imported, the old tool is unregistered from the
@@ -155,25 +150,12 @@ interface CachedTool {
155
150
 
156
151
  // ─── Internal state ──────────────────────────────────────────────────────────
157
152
 
158
- /**
159
- * Cached hooks keyed by `${pluginName}/${hookName}`. The key includes the
160
- * plugin name so hooks from different plugins don't collide.
161
- */
162
- const hookCache = new Map<string, CachedHook>();
163
-
164
153
  /**
165
154
  * Cached tools keyed by `${pluginName}/${toolName}`. The key includes the
166
155
  * plugin name so tools from different plugins don't collide.
167
156
  */
168
157
  const toolCache = new Map<string, CachedTool>();
169
158
 
170
- /**
171
- * In-flight import promises, keyed by file path. Prevents duplicate
172
- * `import()` calls when multiple readers request the same surface
173
- * concurrently.
174
- */
175
- const inflight = new Map<string, Promise<unknown>>();
176
-
177
159
  /**
178
160
  * Plugin directories discovered at boot, in discovery order. Maps directory
179
161
  * path to the plugin's scope-stripped manifest name so eviction can find
@@ -189,94 +171,19 @@ const discoveredPluginDirs = new Map<string, string>();
189
171
  */
190
172
  const disabledPluginDirs = new Set<string>();
191
173
 
192
- // ─── Mtime helpers ───────────────────────────────────────────────────────────
193
-
194
- /**
195
- * Get the mtimeMs of a file, or 0 if the file doesn't exist or can't be
196
- * stat'd.
197
- */
198
- function getMtime(filePath: string): number {
199
- try {
200
- return statSync(filePath).mtimeMs;
201
- } catch {
202
- return 0;
203
- }
204
- }
205
-
206
- // ─── Hook cache ──────────────────────────────────────────────────────────────
207
-
208
- /**
209
- * Cache key for a hook: `${pluginName}/${hookName}`.
210
- */
211
- function hookKey(pluginName: string, hookName: string): string {
212
- return `${pluginName}/${hookName}`;
213
- }
174
+ // ─── Hook reads ──────────────────────────────────────────────────────────────
214
175
 
215
176
  /**
216
- * Get all hooks for a given event name from user plugins, re-importing
217
- * any whose source files have changed since the cache was populated.
218
- *
219
- * Also scans for newly added plugins and hooks (via directory listing).
220
- * Deleted plugins/hooks are skipped naturally (their directories/files
221
- * no longer appear in the listing).
177
+ * Get all hooks for a given event name from user plugins and standalone
178
+ * workspace hooks. Refreshes plugin discovery first, then delegates the actual
179
+ * hook resolution to the hook loader. Plugin hooks run in install-date order,
180
+ * the workspace hook runs last.
222
181
  */
223
182
  export async function getUserHooksFor<TCtx = unknown>(
224
183
  hookName: string,
225
- ): Promise<PluginHookFn<TCtx>[]> {
184
+ ): Promise<HookFunction<TCtx>[]> {
226
185
  await scanPlugins();
227
-
228
- const out: PluginHookFn<TCtx>[] = [];
229
-
230
- for (const [pluginDir, pluginName] of discoveredPluginDirs) {
231
- const hooksDir = join(pluginDir, "hooks");
232
- const surfaceFiles = listSurfaceDir(hooksDir);
233
- const hookFile = surfaceFiles.find((f) => f.name === hookName);
234
- if (hookFile === undefined) continue;
235
-
236
- const key = hookKey(pluginName, hookName);
237
- const currentMtime = getMtime(hookFile.path);
238
-
239
- // Cache hit — same mtime.
240
- const cached = hookCache.get(key);
241
- if (
242
- cached !== undefined &&
243
- cached.sourceMtime === currentMtime &&
244
- currentMtime > 0
245
- ) {
246
- out.push(cached.hook as PluginHookFn<TCtx>);
247
- continue;
248
- }
249
-
250
- // Cache miss — re-import.
251
- if (currentMtime === 0) {
252
- // File was deleted — evict cache entry.
253
- hookCache.delete(key);
254
- continue;
255
- }
256
-
257
- try {
258
- const hook = await importWithTimeout<PluginHookFn>(
259
- hookFile.path,
260
- importTimeoutMs,
261
- );
262
- if (hook === undefined || typeof hook !== "function") {
263
- log.error(
264
- { plugin: pluginName, hook: hookName, path: hookFile.path },
265
- `hook ${hookName} default export must be a function (got ${typeof hook}) — skipping`,
266
- );
267
- continue;
268
- }
269
- hookCache.set(key, { hook, sourceMtime: currentMtime });
270
- out.push(hook as PluginHookFn<TCtx>);
271
- } catch (err) {
272
- log.error(
273
- { err, plugin: pluginName, hook: hookName, path: hookFile.path },
274
- `Failed to import hook ${hookName} from ${hookFile.path}`,
275
- );
276
- }
277
- }
278
-
279
- return out;
186
+ return collectUserHooks<TCtx>(hookName, discoveredPluginDirs);
280
187
  }
281
188
 
282
189
  // ─── Tool cache ──────────────────────────────────────────────────────────────
@@ -326,10 +233,7 @@ async function reconcilePluginTools(
326
233
  }
327
234
 
328
235
  try {
329
- const toolSpec = await importWithTimeout<ToolDefinition>(
330
- file.path,
331
- importTimeoutMs,
332
- );
236
+ const toolSpec = await importWithTimeout<ToolDefinition>(file.path);
333
237
  if (
334
238
  toolSpec === undefined ||
335
239
  toolSpec === null ||
@@ -416,6 +320,7 @@ async function scanPlugins(): Promise<void> {
416
320
  const manifest = await parsePluginManifest(pluginDir);
417
321
  const pluginName = manifest?.name ?? entry;
418
322
  if (discoveredPluginDirs.has(pluginDir)) {
323
+ await deactivatePlugin(pluginName);
419
324
  await evictPlugin(pluginDir, pluginName);
420
325
  }
421
326
  if (!disabledPluginDirs.has(pluginDir)) {
@@ -443,9 +348,10 @@ async function scanPlugins(): Promise<void> {
443
348
  await reconcilePluginTools(pluginDir, pluginName);
444
349
  }
445
350
 
446
- // Evict cache entries for deleted plugins.
351
+ // Deactivate and evict cache entries for deleted plugins.
447
352
  for (const [pluginDir, pluginName] of discoveredPluginDirs) {
448
353
  if (!currentDirs.has(pluginDir)) {
354
+ await deactivatePlugin(pluginName);
449
355
  await evictPlugin(pluginDir, pluginName);
450
356
  }
451
357
  }
@@ -459,6 +365,15 @@ async function scanPlugins(): Promise<void> {
459
365
  for (const [dir, name] of sorted) {
460
366
  discoveredPluginDirs.set(dir, name);
461
367
  }
368
+
369
+ // Activate any plugin not yet brought up. Idempotent: already-active plugins
370
+ // are skipped by the `activatedNames` guard, so steady-state scans (one per
371
+ // hook dispatch) cost only a membership check per plugin. Tools were imported
372
+ // into `toolCache` by `reconcilePluginTools` above, so they are ready to
373
+ // register here.
374
+ for (const [dir, name] of discoveredPluginDirs) {
375
+ await activatePlugin(dir, name);
376
+ }
462
377
  }
463
378
 
464
379
  /**
@@ -470,17 +385,13 @@ async function evictPlugin(
470
385
  pluginDir: string,
471
386
  pluginName: string,
472
387
  ): Promise<void> {
473
- // Evict hooks.
474
- const hookPrefix = `${pluginName}/`;
475
- for (const key of hookCache.keys()) {
476
- if (key.startsWith(hookPrefix)) {
477
- hookCache.delete(key);
478
- }
479
- }
388
+ // Evict hooks (owned by the hook loader).
389
+ evictHooksForOwner(pluginName);
480
390
 
481
391
  // Evict tools.
392
+ const toolPrefix = `${pluginName}/`;
482
393
  for (const key of toolCache.keys()) {
483
- if (key.startsWith(hookPrefix)) {
394
+ if (key.startsWith(toolPrefix)) {
484
395
  toolCache.delete(key);
485
396
  }
486
397
  }
@@ -494,187 +405,170 @@ async function evictPlugin(
494
405
  }
495
406
 
496
407
  /**
497
- * Evict all cache entries (when the plugins directory is gone entirely).
408
+ * Evict all plugin-owned cache entries (when the plugins directory is gone
409
+ * entirely). Standalone workspace hooks are preserved by the hook loader:
410
+ * they live outside the plugins directory, so the absence of any plugin must
411
+ * not evict them.
498
412
  */
499
413
  async function evictAll(): Promise<void> {
500
- hookCache.clear();
414
+ clearPluginHooks();
501
415
  toolCache.clear();
502
416
  discoveredPluginDirs.clear();
503
417
  installDateCache.clear();
504
418
  disabledPluginDirs.clear();
505
419
  }
506
420
 
507
- // ─── Import dedup ────────────────────────────────────────────────────────────
421
+ // ─── Activation lifecycle ────────────────────────────────────────────────────
508
422
 
509
423
  /**
510
- * Import a module's default export with a timeout. If the import doesn't
511
- * resolve within `timeoutMs`, logs a warning and returns `undefined` so
512
- * a hanging plugin module doesn't block daemon startup indefinitely.
424
+ * Plugins (and the workspace-hooks pseudo-owner) fully activated (tools
425
+ * registered + `init` hook run) within this process, in activation order. The
426
+ * process shutdown hook walks this list in reverse to tear everything down.
513
427
  */
514
- async function importWithTimeout<T>(
515
- filePath: string,
516
- timeoutMs: number,
517
- ): Promise<T | undefined> {
518
- let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
519
- try {
520
- const timeoutSentinel = Symbol("import-timeout");
521
- const importPromise = importWithDedup<T>(filePath);
522
- const timeoutPromise = new Promise<typeof timeoutSentinel>((resolve) => {
523
- timeoutHandle = setTimeout(() => resolve(timeoutSentinel), timeoutMs);
524
- });
525
- const result = await Promise.race([importPromise, timeoutPromise]);
526
- if (result === timeoutSentinel) {
527
- importPromise.catch(() => {
528
- /* swallow — late rejection from abandoned import */
529
- });
530
- log.warn(
531
- { filePath, timeoutMs },
532
- `Import timed out after ${timeoutMs}ms skipping surface`,
428
+ const activatedPlugins: Array<{ name: string }> = [];
429
+
430
+ /**
431
+ * Names in {@link activatedPlugins}, kept as a set for O(1) membership and —
432
+ * critically reserved *synchronously* at the top of `activatePlugin`. The
433
+ * per-turn hook dispatch reaches `scanPlugins` on every turn (sometimes
434
+ * concurrently), so the synchronous reservation is what prevents a second scan
435
+ * from double-activating a plugin while its async `init()` is still in flight.
436
+ */
437
+ const activatedNames = new Set<string>();
438
+
439
+ const shutdownContext: ShutdownContext = {
440
+ assistantVersion: APP_VERSION,
441
+ };
442
+
443
+ /**
444
+ * Activate a single discovered plugin: pre-import its hooks, register its tools
445
+ * into the global tool registry, and run its `init` hook. Idempotent — a plugin
446
+ * already activated (or mid-activation) is skipped. Never throws; per-surface
447
+ * failures are logged and the plugin still counts as activated so the shutdown
448
+ * hook tears down whatever came up (mirrors boot semantics).
449
+ *
450
+ * Called from `scanPlugins`, which runs both at boot and on every subsequent
451
+ * scan — so a plugin whose files appear at runtime (installed via the CLI or
452
+ * provisioned out-of-band) becomes live without a daemon restart.
453
+ */
454
+ async function activatePlugin(
455
+ pluginDir: string,
456
+ pluginName: string,
457
+ ): Promise<void> {
458
+ if (activatedNames.has(pluginName)) return;
459
+ // Reserve synchronously, before any await, so a re-entrant or concurrent
460
+ // scan observes this plugin as already handled.
461
+ activatedNames.add(pluginName);
462
+
463
+ // Pre-import all hooks so the first dispatch doesn't pay the import cost.
464
+ await preImportHooksDir(join(pluginDir, "hooks"), pluginName);
465
+
466
+ // Register this plugin's tools into the global tool registry so
467
+ // `getAllTools()` and `getTool()` can find them. Tools were already imported
468
+ // and cached by `reconcilePluginTools` during the scan.
469
+ const pluginTools = Array.from(toolCache.values())
470
+ .filter((c) => c.pluginName === pluginName)
471
+ .map((c) => c.tool);
472
+ if (pluginTools.length > 0) {
473
+ try {
474
+ registerPluginTools(pluginName, pluginTools);
475
+ log.info(
476
+ { plugin: pluginName, count: pluginTools.length },
477
+ "user plugin tools registered",
478
+ );
479
+ } catch (err) {
480
+ log.error(
481
+ { err, plugin: pluginName },
482
+ `Failed to register tools for user plugin ${pluginName}`,
533
483
  );
534
- return undefined;
535
484
  }
536
- return result as T;
537
- } finally {
538
- if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
539
485
  }
486
+
487
+ // Run the `init` hook if present.
488
+ await runInitHook(pluginName);
489
+
490
+ activatedPlugins.push({ name: pluginName });
540
491
  }
541
492
 
542
493
  /**
543
- * Import a module's default export, deduplicating concurrent imports for
544
- * the same file path. This prevents two readers from triggering duplicate
545
- * `import()` calls when they request the same surface simultaneously.
546
- *
547
- * Note: Bun caches `import()` by URL within a process, so the dedup is
548
- * primarily about avoiding redundant async work, not about cache-busting.
494
+ * Deactivate a plugin whose directory was removed or disabled at runtime:
495
+ * unregister its tools and run its `shutdown` hook. Must run *before*
496
+ * `evictPlugin` clears the hook cache, since the shutdown hook is read from it.
497
+ * Idempotent — a plugin that was never activated is a no-op.
549
498
  */
550
- async function importWithDedup<T>(filePath: string): Promise<T> {
551
- let promise = inflight.get(filePath);
552
- if (promise === undefined) {
553
- promise = importDefault<T>(filePath);
554
- inflight.set(filePath, promise);
555
- }
499
+ async function deactivatePlugin(pluginName: string): Promise<void> {
500
+ if (!activatedNames.has(pluginName)) return;
501
+ activatedNames.delete(pluginName);
502
+ const idx = activatedPlugins.findIndex((p) => p.name === pluginName);
503
+ if (idx >= 0) activatedPlugins.splice(idx, 1);
504
+
505
+ // Unregister tools before running shutdown so the model-visible surface is
506
+ // clean before teardown.
556
507
  try {
557
- return (await promise) as T;
558
- } finally {
559
- inflight.delete(filePath);
508
+ unregisterPluginTools(pluginName);
509
+ } catch (err) {
510
+ log.warn(
511
+ { err, plugin: pluginName },
512
+ "user plugin tool unregister failed (continuing)",
513
+ );
560
514
  }
515
+
516
+ await runShutdownHook(pluginName, shutdownContext, "plugin-removed");
561
517
  }
562
518
 
563
519
  // ─── Boot population ─────────────────────────────────────────────────────────
564
520
 
565
521
  /**
566
- * Plugins that were fully activated at boot (tools registered + init hook
567
- * run). Used by the shutdown hook to tear down only what was actually
568
- * brought up.
569
- */
570
- const activatedPlugins: Array<{ name: string }> = [];
571
-
572
- /**
573
- * Populate the cache at boot by scanning the plugins directory once,
574
- * importing all surfaces, registering tools into the tool registry,
575
- * running `init` hooks, and installing a shutdown hook.
522
+ * Populate the caches at boot by scanning the plugins directory once (which
523
+ * imports surfaces, registers tools, and runs `init` hooks via `activatePlugin`
524
+ * inside `scanPlugins`), activating standalone workspace hooks, and installing
525
+ * the process shutdown hook.
576
526
  *
577
527
  * This replaces the old `loadExternalPlugin` → `registerPlugin` →
578
528
  * `bootstrapPlugins` path for user plugins. Instead of registering whole
579
- * `Plugin` objects into the plugin registry, we register individual tools
580
- * into the tool registry and cache hooks by mtime. The `init` and
581
- * `shutdown` hooks are still run exactly once per boot, preserving the
582
- * activation lifecycle that plugins rely on.
529
+ * `Plugin` objects into the plugin registry, we register individual tools into
530
+ * the tool registry and cache hooks by mtime.
583
531
  *
584
- * Called by `loadUserPlugins()` during daemon startup.
532
+ * Called by `loadUserPlugins()` during daemon startup. After boot, the same
533
+ * `scanPlugins` → `activatePlugin`/`deactivatePlugin` reconciliation runs on
534
+ * every turn via `getUserHooksFor` (plugin hook dispatch), so plugins whose
535
+ * files appear or disappear at runtime are picked up without a restart.
585
536
  */
586
537
  export async function populateCacheAtBoot(
587
538
  opts: { importTimeoutMs?: number } = {},
588
539
  ): Promise<void> {
589
540
  if (opts.importTimeoutMs !== undefined) {
590
- importTimeoutMs = opts.importTimeoutMs;
541
+ setSurfaceImportTimeout(opts.importTimeoutMs);
591
542
  }
592
543
 
544
+ // Scans + activates every discovered plugin (tools registered + `init` run).
593
545
  await scanPlugins();
594
546
 
595
- const shutdownContext: PluginShutdownContext = {
596
- assistantVersion: APP_VERSION,
597
- };
598
-
599
- for (const [pluginDir, pluginName] of discoveredPluginDirs) {
600
- // Pre-import all hooks so the first turn doesn't pay the import cost.
601
- const hooksDir = join(pluginDir, "hooks");
602
- const hookFiles = listSurfaceDir(hooksDir);
603
- for (const file of hookFiles) {
604
- const key = hookKey(pluginName, file.name);
605
- const currentMtime = getMtime(file.path);
606
- if (currentMtime === 0) continue;
607
-
608
- try {
609
- const hook = await importWithTimeout<PluginHookFn>(
610
- file.path,
611
- importTimeoutMs,
612
- );
613
- if (hook !== undefined && typeof hook === "function") {
614
- hookCache.set(key, { hook, sourceMtime: currentMtime });
615
- }
616
- } catch (err) {
617
- log.error(
618
- { err, plugin: pluginName, hook: file.name, path: file.path },
619
- `Failed to pre-import hook ${file.name}`,
620
- );
621
- }
622
- }
623
-
624
- // Register user plugin tools into the global tool registry so
625
- // `getAllTools()` and `getTool()` can find them. Tools were already
626
- // imported and cached by `reconcilePluginTools` during `scanPlugins`.
627
- const pluginTools = Array.from(toolCache.values())
628
- .filter((c) => c.pluginName === pluginName)
629
- .map((c) => c.tool);
630
- if (pluginTools.length > 0) {
631
- try {
632
- registerPluginTools(pluginName, pluginTools);
633
- log.info(
634
- { plugin: pluginName, count: pluginTools.length },
635
- "user plugin tools registered",
636
- );
637
- } catch (err) {
638
- log.error(
639
- { err, plugin: pluginName },
640
- `Failed to register tools for user plugin ${pluginName}`,
641
- );
642
- }
643
- }
644
-
645
- // Run the `init` hook if present.
646
- const initHookEntry = hookCache.get(hookKey(pluginName, HOOKS.INIT));
647
- if (initHookEntry !== undefined) {
648
- try {
649
- const initContext: PluginInitContext = {
650
- config: getConfig().plugins?.[pluginName],
651
- credentials: {},
652
- logger: log.child({ plugin: pluginName }),
653
- pluginStorageDir: ensurePluginStorageDir(pluginName),
654
- assistantVersion: APP_VERSION,
655
- };
656
- await initHookEntry.hook(initContext);
657
- log.info({ plugin: pluginName }, "user plugin initialized");
658
- } catch (err) {
659
- log.error(
660
- { err, plugin: pluginName },
661
- `User plugin ${pluginName} init() failed — continuing`,
662
- );
663
- }
664
- }
665
-
666
- activatedPlugins.push({ name: pluginName });
547
+ // Activate standalone workspace hooks under `<workspace>/hooks/`. These
548
+ // carry no package.json, no tools, and no install-date ordering — just hook
549
+ // files. Pre-import them and run their `init` hook so a workspace-wide
550
+ // `init`/`shutdown` lifecycle works the same way a plugin's does. Only
551
+ // register for teardown when at least one hook file is actually present, so
552
+ // an empty/absent directory adds no shutdown work.
553
+ if (hasWorkspaceHooks()) {
554
+ await preImportWorkspaceHooks();
555
+ await runInitHook(WORKSPACE_HOOKS_OWNER);
556
+ activatedPlugins.push({ name: WORKSPACE_HOOKS_OWNER });
667
557
  }
668
558
 
669
- // Register a single shutdown hook that walks all activated user plugins
670
- // in reverse order, unregistering tools and running shutdown hooks.
671
- const shutdownSnapshot = [...activatedPlugins];
559
+ // Register a single shutdown hook that walks all activated owners in reverse
560
+ // order, unregistering tools and running shutdown hooks. It reads the live
561
+ // `activatedPlugins` array at teardown time, so plugins activated after boot
562
+ // (runtime installs) are torn down too.
672
563
  registerShutdownHook("user-plugins", async (reason) => {
673
- for (let i = shutdownSnapshot.length - 1; i >= 0; i--) {
674
- const { name } = shutdownSnapshot[i]!;
564
+ for (let i = activatedPlugins.length - 1; i >= 0; i--) {
565
+ const entry = activatedPlugins[i];
566
+ if (entry === undefined) continue;
567
+ const { name } = entry;
675
568
 
676
569
  // Unregister tools before running shutdown so onShutdown sees a
677
- // clean model-visible surface.
570
+ // clean model-visible surface. (No-op for the workspace-hooks owner,
571
+ // which registers no tools.)
678
572
  try {
679
573
  unregisterPluginTools(name);
680
574
  } catch (err) {
@@ -685,30 +579,11 @@ export async function populateCacheAtBoot(
685
579
  }
686
580
 
687
581
  // Run the `shutdown` hook if present.
688
- const shutdownHookEntry = hookCache.get(hookKey(name, HOOKS.SHUTDOWN));
689
- if (shutdownHookEntry !== undefined) {
690
- try {
691
- await shutdownHookEntry.hook(shutdownContext);
692
- } catch (err) {
693
- log.warn(
694
- { err, plugin: name, reason },
695
- "user plugin shutdown hook failed (continuing)",
696
- );
697
- }
698
- }
582
+ await runShutdownHook(name, shutdownContext, reason);
699
583
  }
700
584
  });
701
585
  }
702
586
 
703
- /**
704
- * Ensure `<workspaceDir>/plugins-data/<name>/` exists and return its path.
705
- */
706
- function ensurePluginStorageDir(pluginName: string): string {
707
- const dir = join(getWorkspaceDir(), "plugins-data", pluginName);
708
- mkdirSync(dir, { recursive: true });
709
- return dir;
710
- }
711
-
712
587
  // ─── Test hooks ──────────────────────────────────────────────────────────────
713
588
 
714
589
  /**
@@ -722,35 +597,16 @@ export function resetPluginCacheForTests(): void {
722
597
  "resetPluginCacheForTests may only be called in test environments",
723
598
  );
724
599
  }
725
- hookCache.clear();
600
+ resetHookCacheForTests();
601
+ clearSurfaceImportInflight();
726
602
  toolCache.clear();
727
- inflight.clear();
728
603
  discoveredPluginDirs.clear();
729
604
  installDateCache.clear();
730
605
  activatedPlugins.length = 0;
606
+ activatedNames.clear();
731
607
  disabledPluginDirs.clear();
732
608
  }
733
609
 
734
- /**
735
- * Test-only: inspect the hook cache.
736
- */
737
- export function _inspectHookCacheForTests(): Array<{
738
- key: string;
739
- sourceMtime: number;
740
- }> {
741
- const isTest =
742
- process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
743
- if (!isTest) {
744
- throw new Error(
745
- "_inspectHookCacheForTests may only be called in test environments",
746
- );
747
- }
748
- return Array.from(hookCache.entries()).map(([key, c]) => ({
749
- key,
750
- sourceMtime: c.sourceMtime,
751
- }));
752
- }
753
-
754
610
  /**
755
611
  * Test-only: inspect the tool cache.
756
612
  */