@vellumai/assistant 0.10.2-dev.202606250318.5e7cfb0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (430) hide show
  1. package/bun.lock +0 -20
  2. package/docs/workspace-tools.md +33 -42
  3. package/eslint-rules/cli-no-daemon-internals.js +0 -6
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +0 -31
  5. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +0 -44
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +0 -14
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +0 -17
  8. package/node_modules/@vellumai/service-contracts/package.json +0 -1
  9. package/node_modules/@vellumai/service-contracts/src/index.ts +0 -1
  10. package/openapi.yaml +0 -155
  11. package/package.json +1 -4
  12. package/scripts/test.sh +15 -36
  13. package/src/__tests__/actor-token-service.test.ts +14 -36
  14. package/src/__tests__/agent-loop-override-profile.test.ts +0 -1
  15. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -2
  16. package/src/__tests__/agent-wake-override-profile.test.ts +0 -2
  17. package/src/__tests__/annotate-activity-metadata.test.ts +0 -2
  18. package/src/__tests__/annotate-risk-options.test.ts +0 -2
  19. package/src/__tests__/approval-cascade.test.ts +0 -2
  20. package/src/__tests__/assistant-attachments.test.ts +0 -42
  21. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  22. package/src/__tests__/btw-routes.test.ts +0 -2
  23. package/src/__tests__/build-persisted-content.test.ts +0 -2
  24. package/src/__tests__/call-controller.test.ts +0 -19
  25. package/src/__tests__/channel-guardian.test.ts +58 -94
  26. package/src/__tests__/channel-reply-delivery.test.ts +0 -2
  27. package/src/__tests__/compaction-events.test.ts +0 -2
  28. package/src/__tests__/compaction.benchmark.test.ts +0 -2
  29. package/src/__tests__/compactor-call-site-logging.test.ts +0 -2
  30. package/src/__tests__/compactor-low-watermark-cut.test.ts +0 -2
  31. package/src/__tests__/compactor-preserved-tail-count.test.ts +0 -2
  32. package/src/__tests__/compactor-summary-call-truncation.test.ts +0 -2
  33. package/src/__tests__/compactor-web-search-strip.test.ts +0 -2
  34. package/src/__tests__/config-loader-backfill.test.ts +10 -123
  35. package/src/__tests__/config-schema.test.ts +0 -1
  36. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +29 -31
  37. package/src/__tests__/contacts-relay-reads.test.ts +15 -13
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -2
  39. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +0 -2
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -2
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  42. package/src/__tests__/conversation-agent-loop.test.ts +0 -134
  43. package/src/__tests__/conversation-analysis-routes.test.ts +0 -2
  44. package/src/__tests__/conversation-app-control-lifecycle.test.ts +0 -2
  45. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -2
  46. package/src/__tests__/conversation-history-web-search.test.ts +0 -2
  47. package/src/__tests__/conversation-load-history-repair.test.ts +0 -2
  48. package/src/__tests__/conversation-load-history-stripped.test.ts +0 -2
  49. package/src/__tests__/conversation-pairing.test.ts +0 -2
  50. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +0 -2
  51. package/src/__tests__/conversation-process-callsite.test.ts +0 -2
  52. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -2
  53. package/src/__tests__/conversation-queue.test.ts +0 -91
  54. package/src/__tests__/conversation-routes-guardian-reply.test.ts +0 -14
  55. package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -14
  56. package/src/__tests__/conversation-slash-queue.test.ts +0 -2
  57. package/src/__tests__/conversation-slash-unknown.test.ts +0 -2
  58. package/src/__tests__/conversation-speed-override.test.ts +0 -2
  59. package/src/__tests__/conversation-surfaces-task-progress.test.ts +0 -29
  60. package/src/__tests__/conversation-title-service.test.ts +0 -2
  61. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -47
  62. package/src/__tests__/conversation-usage.test.ts +0 -2
  63. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -2
  64. package/src/__tests__/conversation-workspace-injection.test.ts +0 -2
  65. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -2
  66. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  67. package/src/__tests__/db-migration-rollback.test.ts +171 -205
  68. package/src/__tests__/db-test-helpers.ts +4 -5
  69. package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -4
  70. package/src/__tests__/disk-pressure-guard.test.ts +0 -41
  71. package/src/__tests__/dm-persistence.test.ts +0 -2
  72. package/src/__tests__/emit-signal-routing-intent.test.ts +5 -10
  73. package/src/__tests__/events-dev-bypass-actor.test.ts +1 -7
  74. package/src/__tests__/exploration-drift-hook.test.ts +2 -3
  75. package/src/__tests__/filing-service.test.ts +0 -2
  76. package/src/__tests__/guardian-binding-drift-heal.test.ts +10 -75
  77. package/src/__tests__/guardian-dispatch.test.ts +1 -95
  78. package/src/__tests__/guardian-outbound-http.test.ts +0 -13
  79. package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -2
  80. package/src/__tests__/heartbeat-service.test.ts +0 -2
  81. package/src/__tests__/helpers/channel-test-adapter.ts +7 -1
  82. package/src/__tests__/host-app-control-routes.test.ts +30 -24
  83. package/src/__tests__/host-bash-routes.test.ts +41 -31
  84. package/src/__tests__/host-browser-routes.test.ts +32 -26
  85. package/src/__tests__/host-cu-routes-targeted.test.ts +33 -25
  86. package/src/__tests__/host-file-routes-targeted.test.ts +52 -40
  87. package/src/__tests__/host-transfer-routes-targeted.test.ts +43 -31
  88. package/src/__tests__/http-user-message-parity.test.ts +8 -290
  89. package/src/__tests__/inbound-invite-redemption.test.ts +0 -28
  90. package/src/__tests__/inbound-slack-persistence.test.ts +0 -2
  91. package/src/__tests__/invite-redemption-service.test.ts +0 -198
  92. package/src/__tests__/llm-context-normalization.test.ts +0 -105
  93. package/src/__tests__/llm-request-log-error-payload.test.ts +9 -71
  94. package/src/__tests__/llm-usage-store.test.ts +0 -25
  95. package/src/__tests__/mcp-health-check.test.ts +1 -2
  96. package/src/__tests__/media-stream-server-integration.test.ts +0 -127
  97. package/src/__tests__/memory-retrieval-hook.test.ts +0 -2
  98. package/src/__tests__/messaging-send-tool.test.ts +0 -2
  99. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  100. package/src/__tests__/mtime-cache.test.ts +5 -146
  101. package/src/__tests__/native-web-search.test.ts +0 -2
  102. package/src/__tests__/non-member-access-request.test.ts +17 -189
  103. package/src/__tests__/notification-broadcaster.test.ts +0 -4
  104. package/src/__tests__/notification-decision-recipient-context.test.ts +32 -33
  105. package/src/__tests__/notification-deep-link.test.ts +0 -6
  106. package/src/__tests__/notification-guardian-path.test.ts +0 -19
  107. package/src/__tests__/openai-provider.test.ts +12 -22
  108. package/src/__tests__/openai-responses-provider.test.ts +2 -12
  109. package/src/__tests__/outbound-slack-persistence.test.ts +0 -2
  110. package/src/__tests__/pending-interactions-resolved-event.test.ts +4 -7
  111. package/src/__tests__/persistence-secret-redaction.test.ts +0 -2
  112. package/src/__tests__/plugin-bootstrap.test.ts +73 -3
  113. package/src/__tests__/plugin-route-contribution.test.ts +17 -4
  114. package/src/__tests__/plugin-tool-contribution.test.ts +18 -3
  115. package/src/__tests__/plugin-types.test.ts +2 -0
  116. package/src/__tests__/process-message-background-slack.test.ts +0 -2
  117. package/src/__tests__/process-message-display-content.test.ts +0 -2
  118. package/src/__tests__/provider-error-scenarios.test.ts +4 -5
  119. package/src/__tests__/provider-usage-tracking.test.ts +0 -39
  120. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -2
  121. package/src/__tests__/registry.test.ts +1 -4
  122. package/src/__tests__/relay-server.test.ts +25 -694
  123. package/src/__tests__/runtime-attachment-metadata.test.ts +1 -0
  124. package/src/__tests__/secret-ingress-http.test.ts +0 -14
  125. package/src/__tests__/send-endpoint-busy.test.ts +8 -30
  126. package/src/__tests__/skills.test.ts +0 -44
  127. package/src/__tests__/slack-inbound-verification.test.ts +2 -47
  128. package/src/__tests__/stt-hints.test.ts +13 -44
  129. package/src/__tests__/subagent-detail.test.ts +0 -27
  130. package/src/__tests__/subagent-disposal.test.ts +0 -65
  131. package/src/__tests__/subagent-notify-parent.test.ts +0 -2
  132. package/src/__tests__/subagent-role-registry.test.ts +2 -7
  133. package/src/__tests__/subagent-spawn-tool-fork.test.ts +0 -2
  134. package/src/__tests__/subagent-tools.test.ts +0 -2
  135. package/src/__tests__/suggestion-routes.test.ts +0 -2
  136. package/src/__tests__/title-generate-hook.test.ts +0 -2
  137. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -2
  138. package/src/__tests__/tool-executor.test.ts +11 -16
  139. package/src/__tests__/tool-preview-lifecycle.test.ts +0 -2
  140. package/src/__tests__/tool-result-metadata-plumbing.test.ts +0 -2
  141. package/src/__tests__/tool-start-timestamp.test.ts +0 -2
  142. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  143. package/src/__tests__/twilio-routes.test.ts +0 -96
  144. package/src/__tests__/ui-file-upload-surface.test.ts +0 -86
  145. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  146. package/src/__tests__/voice-invite-redemption.test.ts +0 -33
  147. package/src/__tests__/web-search-backend-failure.test.ts +0 -2
  148. package/src/__tests__/workspace-migration-remove-hooks.test.ts +35 -14
  149. package/src/__tests__/workspace-tool-loader.test.ts +2 -195
  150. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  151. package/src/agent/loop.ts +0 -56
  152. package/src/api/index.ts +1 -19
  153. package/src/api/responses/llm-request-log-entry.ts +0 -29
  154. package/src/api/responses/subagent-detail.ts +0 -17
  155. package/src/api/surfaces.ts +3 -39
  156. package/src/approvals/guardian-request-resolvers.ts +11 -1
  157. package/src/calls/__tests__/relay-setup-router.test.ts +4 -262
  158. package/src/calls/call-domain.ts +3 -3
  159. package/src/calls/guardian-dispatch.ts +8 -10
  160. package/src/calls/inbound-trust-reader.ts +1 -17
  161. package/src/calls/media-stream-server.ts +0 -21
  162. package/src/calls/relay-server.ts +50 -167
  163. package/src/calls/relay-setup-router.ts +7 -37
  164. package/src/calls/relay-verification.ts +4 -4
  165. package/src/calls/stt-hints.ts +12 -9
  166. package/src/calls/twilio-routes.ts +4 -14
  167. package/src/channels/types.ts +20 -10
  168. package/src/cli/commands/__tests__/cache.test.ts +1 -8
  169. package/src/cli/commands/cache.ts +181 -194
  170. package/src/cli/commands/db/__tests__/repair.test.ts +5 -6
  171. package/src/cli/commands/db/status.ts +1 -37
  172. package/src/cli/commands/mcp.ts +218 -252
  173. package/src/cli/commands/memory/index.ts +0 -2
  174. package/src/cli/commands/plugins.ts +3 -75
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -102
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +1 -160
  177. package/src/cli/lib/list-installed-plugins.ts +1 -179
  178. package/src/config/__tests__/sync-gated-profiles.test.ts +3 -11
  179. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +17 -27
  180. package/src/config/bundled-skills/contacts/tools/contact-search.ts +3 -13
  181. package/src/config/bundled-skills/subagent/SKILL.md +1 -1
  182. package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
  183. package/src/config/feature-flag-registry.json +13 -5
  184. package/src/config/loader.ts +5 -38
  185. package/src/config/schemas/__tests__/memory-v3.test.ts +0 -1
  186. package/src/config/schemas/memory-lifecycle.ts +0 -12
  187. package/src/config/schemas/memory-v3.ts +0 -7
  188. package/src/config/schemas/memory.ts +0 -4
  189. package/src/config/schemas/timeouts.ts +0 -8
  190. package/src/config/seed-inference-profiles.ts +11 -21
  191. package/src/config/skills.ts +5 -27
  192. package/src/config/sync-gated-profiles.ts +13 -12
  193. package/src/contacts/contacts-write.ts +0 -3
  194. package/src/daemon/assistant-attachments.ts +4 -27
  195. package/src/daemon/conversation-agent-loop.ts +0 -28
  196. package/src/daemon/conversation-process.ts +16 -35
  197. package/src/daemon/conversation-surfaces.ts +38 -111
  198. package/src/daemon/conversation-tool-setup.ts +16 -50
  199. package/src/daemon/conversation.ts +1 -13
  200. package/src/daemon/disk-pressure-guard.ts +2 -12
  201. package/src/daemon/event-loop-watchdog.ts +1 -28
  202. package/src/daemon/external-plugins-bootstrap.ts +34 -4
  203. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -25
  204. package/src/daemon/handlers/config-a2a.ts +14 -6
  205. package/src/daemon/handlers/config-channels.ts +22 -78
  206. package/src/daemon/handlers/conversations.ts +0 -77
  207. package/src/daemon/lifecycle.ts +0 -4
  208. package/src/daemon/mcp-reload-service.ts +0 -10
  209. package/src/daemon/memory-v2-startup.test.ts +0 -72
  210. package/src/daemon/memory-v2-startup.ts +19 -87
  211. package/src/daemon/message-types/conversations.ts +0 -2
  212. package/src/daemon/message-types/surfaces.ts +12 -12
  213. package/src/daemon/server.ts +4 -0
  214. package/src/daemon/shutdown-handlers.ts +0 -20
  215. package/src/daemon/tool-setup-types.ts +0 -9
  216. package/src/daemon/workspace-tools-watcher.ts +328 -0
  217. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  218. package/src/ipc/assistant-server.ts +2 -2
  219. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  220. package/src/mcp/client.ts +1 -15
  221. package/src/mcp/mcp-auth-orchestrator.ts +1 -6
  222. package/src/mcp/mcp-oauth-provider.ts +8 -19
  223. package/src/memory/__tests__/memory-retrospective-job.test.ts +0 -8
  224. package/src/memory/conversation-crud.ts +0 -38
  225. package/src/memory/db-connection.ts +3 -22
  226. package/src/memory/db-init.ts +502 -36
  227. package/src/memory/db-singleton.ts +4 -6
  228. package/src/memory/jobs-worker.ts +0 -58
  229. package/src/memory/llm-request-log-store.ts +1 -26
  230. package/src/memory/llm-usage-store.ts +20 -48
  231. package/src/memory/memory-retrospective-job.ts +8 -9
  232. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +56 -130
  233. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  234. package/src/memory/migrations/registry.ts +573 -0
  235. package/src/memory/migrations/run-migrations.ts +6 -90
  236. package/src/memory/migrations/validate-migration-state.ts +66 -101
  237. package/src/memory/schema/conversations.ts +0 -9
  238. package/src/memory/schema/infrastructure.ts +0 -20
  239. package/src/memory/v2/__tests__/cli-command-store.test.ts +0 -25
  240. package/src/memory/v2/__tests__/skill-store.test.ts +0 -80
  241. package/src/memory/v2/cli-command-store.ts +38 -75
  242. package/src/memory/v2/prompts/consolidation.ts +82 -13
  243. package/src/memory/v2/prompts/router.ts +93 -21
  244. package/src/memory/v2/skill-store.ts +31 -68
  245. package/src/notifications/__tests__/broadcaster.test.ts +8 -16
  246. package/src/notifications/__tests__/decision-engine.test.ts +9 -78
  247. package/src/notifications/broadcaster.ts +1 -8
  248. package/src/notifications/decision-engine.ts +7 -15
  249. package/src/notifications/destination-resolver.ts +24 -68
  250. package/src/notifications/emit-signal.ts +14 -39
  251. package/src/permissions/question-prompter.test.ts +1 -1
  252. package/src/permissions/question-prompter.ts +4 -7
  253. package/src/plugin-api/index.ts +6 -6
  254. package/src/plugin-api/types.ts +5 -3
  255. package/src/plugin-api/vision-support.test.ts +4 -28
  256. package/src/plugin-api/vision-support.ts +31 -66
  257. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -161
  258. package/src/plugins/defaults/advisor/consult.ts +6 -110
  259. package/src/plugins/defaults/advisor/steering.ts +2 -14
  260. package/src/plugins/defaults/advisor/tools/advisor.ts +5 -32
  261. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +1 -2
  262. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +7 -47
  263. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +11 -10
  264. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +20 -12
  265. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +11 -42
  266. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +3 -33
  267. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +4 -48
  268. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +8 -4
  269. package/src/plugins/defaults/memory-v3-shadow/injector.ts +15 -43
  270. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +2 -11
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -77
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +11 -12
  273. package/src/plugins/mtime-cache.ts +291 -76
  274. package/src/plugins/pipeline.ts +13 -111
  275. package/src/plugins/types.ts +2 -0
  276. package/src/providers/anthropic/client.ts +0 -5
  277. package/src/providers/call-site-routing.ts +0 -4
  278. package/src/providers/model-catalog.ts +0 -16
  279. package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
  280. package/src/providers/openai/chat-completions-provider.ts +83 -37
  281. package/src/providers/openai/responses-provider.ts +46 -50
  282. package/src/providers/openrouter/client.ts +0 -5
  283. package/src/providers/provider-send-message.ts +0 -4
  284. package/src/providers/ratelimit.ts +0 -4
  285. package/src/providers/retry.ts +0 -4
  286. package/src/providers/types.ts +0 -9
  287. package/src/providers/usage-tracking.ts +0 -4
  288. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +3 -335
  289. package/src/runtime/access-request-helper.ts +39 -19
  290. package/src/runtime/actor-trust-resolver.ts +2 -2
  291. package/src/runtime/assistant-event-hub.ts +1 -1
  292. package/src/runtime/assistant-stream-state.ts +2 -9
  293. package/src/runtime/auth/require-bound-guardian.ts +11 -21
  294. package/src/runtime/channel-verification-service.ts +31 -56
  295. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  296. package/src/runtime/guardian-vellum-migration.ts +7 -66
  297. package/src/runtime/invite-redemption-service.ts +187 -198
  298. package/src/runtime/local-actor-identity.ts +11 -76
  299. package/src/runtime/pending-interactions.ts +1 -11
  300. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +5 -56
  301. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  302. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +0 -187
  303. package/src/runtime/routes/browser-routes.ts +1 -1
  304. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +5 -13
  305. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  306. package/src/runtime/routes/contact-routes.ts +32 -8
  307. package/src/runtime/routes/conversation-cli-routes.ts +5 -4
  308. package/src/runtime/routes/conversation-list-routes.ts +7 -4
  309. package/src/runtime/routes/conversation-query-routes.ts +0 -72
  310. package/src/runtime/routes/conversation-routes.ts +85 -84
  311. package/src/runtime/routes/events-routes.ts +2 -2
  312. package/src/runtime/routes/global-search-routes.ts +1 -3
  313. package/src/runtime/routes/guardian-action-routes.ts +5 -4
  314. package/src/runtime/routes/host-app-control-routes.ts +4 -5
  315. package/src/runtime/routes/host-bash-routes.ts +4 -5
  316. package/src/runtime/routes/host-browser-routes.ts +11 -9
  317. package/src/runtime/routes/host-cu-routes.ts +4 -5
  318. package/src/runtime/routes/host-file-routes.ts +4 -5
  319. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  320. package/src/runtime/routes/http-adapter.ts +1 -1
  321. package/src/runtime/routes/identity-routes.ts +2 -3
  322. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  323. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +5 -97
  324. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +49 -61
  325. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -16
  326. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  327. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +8 -21
  328. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +3 -14
  329. package/src/runtime/routes/index.ts +0 -2
  330. package/src/runtime/routes/llm-context-normalization.ts +0 -83
  331. package/src/runtime/routes/mcp-auth-routes.ts +19 -171
  332. package/src/runtime/routes/migration-rollback-routes.ts +3 -4
  333. package/src/runtime/routes/migration-routes.ts +1 -4
  334. package/src/runtime/routes/subagents-routes.ts +0 -5
  335. package/src/runtime/routes/surface-action-routes.ts +56 -42
  336. package/src/runtime/services/__tests__/conversation-serializer.test.ts +0 -1
  337. package/src/runtime/services/conversation-serializer.ts +9 -7
  338. package/src/runtime/tool-grant-request-helper.ts +3 -3
  339. package/src/runtime/trust-verdict-consumer.ts +9 -85
  340. package/src/runtime/verification-outbound-actions.ts +18 -18
  341. package/src/signals/user-message.ts +0 -16
  342. package/src/subagent/manager.ts +0 -9
  343. package/src/subagent/types.ts +3 -3
  344. package/src/telemetry/types.ts +1 -34
  345. package/src/telemetry/usage-telemetry-reporter.test.ts +2 -3
  346. package/src/telemetry/usage-telemetry-reporter.ts +3 -87
  347. package/src/tools/ask-question/ask-question-tool.test.ts +0 -29
  348. package/src/tools/ask-question/ask-question-tool.ts +0 -13
  349. package/src/tools/executor.ts +4 -4
  350. package/src/tools/registry.ts +0 -18
  351. package/src/tools/shared/filesystem/path-policy.ts +5 -12
  352. package/src/tools/tool-approval-handler.ts +1 -1
  353. package/src/tools/tool-defaults.ts +2 -9
  354. package/src/tools/tool-manifest.ts +0 -3
  355. package/src/tools/types.ts +2 -17
  356. package/src/tools/workspace-tools/loader.ts +244 -348
  357. package/src/util/errors.ts +1 -26
  358. package/src/util/platform.ts +0 -5
  359. package/src/workflows/library.test.ts +0 -140
  360. package/src/workflows/library.ts +28 -82
  361. package/src/workspace/migrations/017-seed-persona-dirs.ts +34 -3
  362. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +24 -3
  363. package/src/workspace/migrations/048-remove-workspace-hooks.ts +66 -14
  364. package/src/workspace/migrations/registry.ts +0 -2
  365. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +0 -91
  366. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +0 -48
  367. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +0 -28
  368. package/node_modules/@vellumai/service-contracts/src/channels.ts +0 -41
  369. package/src/__tests__/code-search-tool.test.ts +0 -585
  370. package/src/__tests__/guardian-expiry-notifier.test.ts +0 -282
  371. package/src/__tests__/mcp-config-secret-boundary.test.ts +0 -390
  372. package/src/__tests__/plugin-pipeline.test.ts +0 -96
  373. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +0 -102
  374. package/src/__tests__/steer-on-enqueue-question.test.ts +0 -181
  375. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +0 -208
  376. package/src/agent/loop-exclusive-tool.test.ts +0 -150
  377. package/src/api/constants/sse-replay.ts +0 -41
  378. package/src/api/events/conversation-notice.ts +0 -26
  379. package/src/approvals/guardian-channel-delivery.ts +0 -30
  380. package/src/approvals/guardian-expiry-notifier.ts +0 -148
  381. package/src/cli/commands/memory/__tests__/worker.test.ts +0 -302
  382. package/src/cli/commands/memory/worker.ts +0 -175
  383. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +0 -143
  384. package/src/config/prune-seeded-callsite-defaults.ts +0 -110
  385. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -129
  386. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +0 -312
  387. package/src/contacts/__tests__/member-write-relay.test.ts +0 -202
  388. package/src/contacts/guardian-delivery-reader.ts +0 -223
  389. package/src/contacts/member-write-relay.ts +0 -189
  390. package/src/daemon/conversation-notices.ts +0 -60
  391. package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -225
  392. package/src/hooks/hook-loader.ts +0 -341
  393. package/src/mcp/mcp-header-store.ts +0 -134
  394. package/src/memory/__tests__/301-create-watchdog-events.test.ts +0 -110
  395. package/src/memory/__tests__/prompt-override.test.ts +0 -192
  396. package/src/memory/__tests__/watchdog-events-store.test.ts +0 -161
  397. package/src/memory/migrations/300-add-processing-started-at.ts +0 -30
  398. package/src/memory/migrations/301-create-watchdog-events.ts +0 -45
  399. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +0 -224
  400. package/src/memory/prompt-override.ts +0 -129
  401. package/src/memory/steps.ts +0 -573
  402. package/src/memory/watchdog-events-store.ts +0 -87
  403. package/src/memory/worker-control.ts +0 -118
  404. package/src/memory/worker-process.ts +0 -72
  405. package/src/notifications/__tests__/connected-channels.test.ts +0 -114
  406. package/src/notifications/__tests__/destination-resolver.test.ts +0 -256
  407. package/src/onboarding/checkin-event.test.ts +0 -222
  408. package/src/onboarding/checkin-event.ts +0 -321
  409. package/src/onboarding/schedule-checkin.ts +0 -190
  410. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  411. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  412. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  413. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +0 -146
  414. package/src/plugins/surface-import.ts +0 -121
  415. package/src/providers/openai/__tests__/api-error-normalization.test.ts +0 -321
  416. package/src/providers/openai/api-error-normalization.ts +0 -270
  417. package/src/runtime/__tests__/channel-verification-service.test.ts +0 -133
  418. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +0 -181
  419. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +0 -66
  420. package/src/runtime/__tests__/local-principal-trust.test.ts +0 -164
  421. package/src/runtime/anchored-guardian.test.ts +0 -156
  422. package/src/runtime/anchored-guardian.ts +0 -135
  423. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +0 -99
  424. package/src/runtime/local-principal-trust.ts +0 -52
  425. package/src/runtime/routes/__tests__/contact-routes.test.ts +0 -212
  426. package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -93
  427. package/src/runtime/routes/onboarding-checkin-routes.ts +0 -86
  428. package/src/tools/filesystem/search.ts +0 -543
  429. package/src/util/telemetry-db-path.ts +0 -24
  430. package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +0 -134
@@ -1,42 +1,32 @@
1
1
  /**
2
- * Per-surface mtime cache for user plugins (discovery + tools).
2
+ * Per-surface mtime cache for user plugins.
3
3
  *
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.
4
+ * Instead of caching whole `Plugin` objects, this module caches individual
5
+ * hooks and tools keyed by their source file's mtime. This means:
11
6
  *
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.
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.
17
14
  *
18
15
  * The cache is populated at boot by `loadUserPlugins()` and read on every
19
- * `getHooksFor` / `getAllTools` call.
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.
20
19
  */
21
20
 
22
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
21
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
23
22
  import { join } from "node:path";
24
23
 
24
+ import { getConfig } from "../config/loader.js";
25
25
  import { registerShutdownHook } from "../daemon/shutdown-registry.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";
26
+ import { HOOKS } from "../plugin-api/constants.js";
38
27
  import type {
39
28
  PluginHookFn,
29
+ PluginInitContext,
40
30
  PluginShutdownContext,
41
31
  } from "../plugin-api/types.js";
42
32
  import {
@@ -46,19 +36,14 @@ import {
46
36
  import { finalizeTool } from "../tools/tool-defaults.js";
47
37
  import type { Tool, ToolDefinition } from "../tools/types.js";
48
38
  import { getLogger } from "../util/logger.js";
49
- import { getWorkspacePluginsDir } from "../util/platform.js";
39
+ import { getWorkspaceDir, getWorkspacePluginsDir } from "../util/platform.js";
50
40
  import { APP_VERSION } from "../version.js";
51
41
  import {
52
42
  deriveToolName,
43
+ importDefault,
53
44
  listSurfaceDir,
54
45
  parsePluginManifest,
55
46
  } from "./external-plugin-loader.js";
56
- import {
57
- clearSurfaceImportInflight,
58
- getMtime,
59
- importWithTimeout,
60
- setSurfaceImportTimeout,
61
- } from "./surface-import.js";
62
47
 
63
48
  // Re-export for type compat — consumers that import PluginHookFn from
64
49
  // the mtime cache module still resolve.
@@ -66,6 +51,13 @@ export type { PluginHookFn } from "./types.js";
66
51
 
67
52
  const log = getLogger("plugin-mtime-cache");
68
53
 
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
+
69
61
  /**
70
62
  * Cached install-date timestamps per plugin directory, so `scanPlugins`
71
63
  * doesn't re-read `install-meta.json` on every turn. Populated on first
@@ -138,6 +130,16 @@ function getInstallDate(pluginDir: string): number {
138
130
 
139
131
  // ─── Cache entries ───────────────────────────────────────────────────────────
140
132
 
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
+
141
143
  /**
142
144
  * A cached tool plus the mtime of its source file. When the on-disk mtime
143
145
  * changes, the tool is re-imported, the old tool is unregistered from the
@@ -153,12 +155,25 @@ interface CachedTool {
153
155
 
154
156
  // ─── Internal state ──────────────────────────────────────────────────────────
155
157
 
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
+
156
164
  /**
157
165
  * Cached tools keyed by `${pluginName}/${toolName}`. The key includes the
158
166
  * plugin name so tools from different plugins don't collide.
159
167
  */
160
168
  const toolCache = new Map<string, CachedTool>();
161
169
 
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
+
162
177
  /**
163
178
  * Plugin directories discovered at boot, in discovery order. Maps directory
164
179
  * path to the plugin's scope-stripped manifest name so eviction can find
@@ -174,19 +189,94 @@ const discoveredPluginDirs = new Map<string, string>();
174
189
  */
175
190
  const disabledPluginDirs = new Set<string>();
176
191
 
177
- // ─── Hook reads ──────────────────────────────────────────────────────────────
192
+ // ─── Mtime helpers ───────────────────────────────────────────────────────────
178
193
 
179
194
  /**
180
- * Get all hooks for a given event name from user plugins and standalone
181
- * workspace hooks. Refreshes plugin discovery first, then delegates the actual
182
- * hook resolution to the hook loader. Plugin hooks run in install-date order,
183
- * the workspace hook runs last.
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
+ }
214
+
215
+ /**
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).
184
222
  */
185
223
  export async function getUserHooksFor<TCtx = unknown>(
186
224
  hookName: string,
187
225
  ): Promise<PluginHookFn<TCtx>[]> {
188
226
  await scanPlugins();
189
- return collectUserHooks<TCtx>(hookName, discoveredPluginDirs);
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;
190
280
  }
191
281
 
192
282
  // ─── Tool cache ──────────────────────────────────────────────────────────────
@@ -236,7 +326,10 @@ async function reconcilePluginTools(
236
326
  }
237
327
 
238
328
  try {
239
- const toolSpec = await importWithTimeout<ToolDefinition>(file.path);
329
+ const toolSpec = await importWithTimeout<ToolDefinition>(
330
+ file.path,
331
+ importTimeoutMs,
332
+ );
240
333
  if (
241
334
  toolSpec === undefined ||
242
335
  toolSpec === null ||
@@ -377,13 +470,17 @@ async function evictPlugin(
377
470
  pluginDir: string,
378
471
  pluginName: string,
379
472
  ): Promise<void> {
380
- // Evict hooks (owned by the hook loader).
381
- evictHooksForOwner(pluginName);
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
+ }
382
480
 
383
481
  // Evict tools.
384
- const toolPrefix = `${pluginName}/`;
385
482
  for (const key of toolCache.keys()) {
386
- if (key.startsWith(toolPrefix)) {
483
+ if (key.startsWith(hookPrefix)) {
387
484
  toolCache.delete(key);
388
485
  }
389
486
  }
@@ -397,30 +494,83 @@ async function evictPlugin(
397
494
  }
398
495
 
399
496
  /**
400
- * Evict all plugin-owned cache entries (when the plugins directory is gone
401
- * entirely). Standalone workspace hooks are preserved by the hook loader:
402
- * they live outside the plugins directory, so the absence of any plugin must
403
- * not evict them.
497
+ * Evict all cache entries (when the plugins directory is gone entirely).
404
498
  */
405
499
  async function evictAll(): Promise<void> {
406
- clearPluginHooks();
500
+ hookCache.clear();
407
501
  toolCache.clear();
408
502
  discoveredPluginDirs.clear();
409
503
  installDateCache.clear();
410
504
  disabledPluginDirs.clear();
411
505
  }
412
506
 
507
+ // ─── Import dedup ────────────────────────────────────────────────────────────
508
+
509
+ /**
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.
513
+ */
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`,
533
+ );
534
+ return undefined;
535
+ }
536
+ return result as T;
537
+ } finally {
538
+ if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
539
+ }
540
+ }
541
+
542
+ /**
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.
549
+ */
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
+ }
556
+ try {
557
+ return (await promise) as T;
558
+ } finally {
559
+ inflight.delete(filePath);
560
+ }
561
+ }
562
+
413
563
  // ─── Boot population ─────────────────────────────────────────────────────────
414
564
 
415
565
  /**
416
- * Plugins (and the workspace-hooks pseudo-owner) that were fully activated at
417
- * boot (tools registered + init hook run). Used by the shutdown hook to tear
418
- * down only what was actually brought up.
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.
419
569
  */
420
570
  const activatedPlugins: Array<{ name: string }> = [];
421
571
 
422
572
  /**
423
- * Populate the caches at boot by scanning the plugins directory once,
573
+ * Populate the cache at boot by scanning the plugins directory once,
424
574
  * importing all surfaces, registering tools into the tool registry,
425
575
  * running `init` hooks, and installing a shutdown hook.
426
576
  *
@@ -437,7 +587,7 @@ export async function populateCacheAtBoot(
437
587
  opts: { importTimeoutMs?: number } = {},
438
588
  ): Promise<void> {
439
589
  if (opts.importTimeoutMs !== undefined) {
440
- setSurfaceImportTimeout(opts.importTimeoutMs);
590
+ importTimeoutMs = opts.importTimeoutMs;
441
591
  }
442
592
 
443
593
  await scanPlugins();
@@ -448,7 +598,28 @@ export async function populateCacheAtBoot(
448
598
 
449
599
  for (const [pluginDir, pluginName] of discoveredPluginDirs) {
450
600
  // Pre-import all hooks so the first turn doesn't pay the import cost.
451
- await preImportHooksDir(join(pluginDir, "hooks"), pluginName);
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
+ }
452
623
 
453
624
  // Register user plugin tools into the global tool registry so
454
625
  // `getAllTools()` and `getTool()` can find them. Tools were already
@@ -472,33 +643,38 @@ export async function populateCacheAtBoot(
472
643
  }
473
644
 
474
645
  // Run the `init` hook if present.
475
- await runInitHook(pluginName);
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
+ }
476
665
 
477
666
  activatedPlugins.push({ name: pluginName });
478
667
  }
479
668
 
480
- // Activate standalone workspace hooks under `<workspace>/hooks/`. These
481
- // carry no package.json, no tools, and no install-date ordering — just hook
482
- // files. Pre-import them and run their `init` hook so a workspace-wide
483
- // `init`/`shutdown` lifecycle works the same way a plugin's does. Only
484
- // register for teardown when at least one hook file is actually present, so
485
- // an empty/absent directory adds no shutdown work.
486
- if (hasWorkspaceHooks()) {
487
- await preImportWorkspaceHooks();
488
- await runInitHook(WORKSPACE_HOOKS_OWNER);
489
- activatedPlugins.push({ name: WORKSPACE_HOOKS_OWNER });
490
- }
491
-
492
- // Register a single shutdown hook that walks all activated owners in reverse
493
- // order, unregistering tools and running shutdown hooks.
669
+ // Register a single shutdown hook that walks all activated user plugins
670
+ // in reverse order, unregistering tools and running shutdown hooks.
494
671
  const shutdownSnapshot = [...activatedPlugins];
495
672
  registerShutdownHook("user-plugins", async (reason) => {
496
673
  for (let i = shutdownSnapshot.length - 1; i >= 0; i--) {
497
674
  const { name } = shutdownSnapshot[i]!;
498
675
 
499
676
  // Unregister tools before running shutdown so onShutdown sees a
500
- // clean model-visible surface. (No-op for the workspace-hooks owner,
501
- // which registers no tools.)
677
+ // clean model-visible surface.
502
678
  try {
503
679
  unregisterPluginTools(name);
504
680
  } catch (err) {
@@ -509,11 +685,30 @@ export async function populateCacheAtBoot(
509
685
  }
510
686
 
511
687
  // Run the `shutdown` hook if present.
512
- await runShutdownHook(name, shutdownContext, reason);
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
+ }
513
699
  }
514
700
  });
515
701
  }
516
702
 
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
+
517
712
  // ─── Test hooks ──────────────────────────────────────────────────────────────
518
713
 
519
714
  /**
@@ -527,15 +722,35 @@ export function resetPluginCacheForTests(): void {
527
722
  "resetPluginCacheForTests may only be called in test environments",
528
723
  );
529
724
  }
530
- resetHookCacheForTests();
531
- clearSurfaceImportInflight();
725
+ hookCache.clear();
532
726
  toolCache.clear();
727
+ inflight.clear();
533
728
  discoveredPluginDirs.clear();
534
729
  installDateCache.clear();
535
730
  activatedPlugins.length = 0;
536
731
  disabledPluginDirs.clear();
537
732
  }
538
733
 
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
+
539
754
  /**
540
755
  * Test-only: inspect the tool cache.
541
756
  */
@@ -4,10 +4,9 @@
4
4
  * A "hook" is a named lifecycle event (`user-prompt-submit`, `post-tool-use`,
5
5
  * ...) that every registered plugin may handle. The runner walks each plugin's
6
6
  * hook for a given event in registration order, threading a context value
7
- * through the chain so hooks can observe and transform it. Each hook receives
8
- * an isolated draft of the current context. A hook either mutates the draft in
9
- * place (returning `void`) or returns a partial context whose fields are merged
10
- * onto the draft. Failed hook drafts are discarded.
7
+ * through the chain so hooks can observe and transform it. A hook either
8
+ * mutates the context in place (returning `void`) or returns a partial
9
+ * context whose fields are merged onto the threaded value.
11
10
  *
12
11
  * `getHooksFor` is now async — it pulls user-land hooks from the mtime
13
12
  * cache (filesystem-as-truth) and default plugin hooks from the registry
@@ -17,131 +16,34 @@
17
16
  */
18
17
 
19
18
  import type { HookName } from "../plugin-api/constants.js";
20
- import { getLogger } from "../util/logger.js";
21
19
  import { getHooksFor } from "./registry.js";
22
- import type { PluginHookFn } from "./types.js";
23
20
 
24
21
  // ─── Hook runner ────────────────────────────────────────────────────────────
25
22
 
26
- const log = getLogger("plugin-pipeline");
27
-
28
- function isPluginLogger(value: unknown): value is {
29
- info: unknown;
30
- warn: unknown;
31
- error: unknown;
32
- debug: unknown;
33
- } {
34
- return (
35
- value !== null &&
36
- typeof value === "object" &&
37
- typeof (value as { info?: unknown }).info === "function" &&
38
- typeof (value as { warn?: unknown }).warn === "function" &&
39
- typeof (value as { error?: unknown }).error === "function" &&
40
- typeof (value as { debug?: unknown }).debug === "function"
41
- );
42
- }
43
-
44
- function isPlainObject(value: object): boolean {
45
- const prototype = Object.getPrototypeOf(value);
46
- return prototype === Object.prototype || prototype === null;
47
- }
48
-
49
- function cloneHookValue<T>(value: T, seen = new WeakMap<object, unknown>()): T {
50
- if (value === null || typeof value !== "object") return value;
51
- if (value instanceof Error || isPluginLogger(value)) return value;
52
-
53
- const existing = seen.get(value);
54
- if (existing !== undefined) return existing as T;
55
-
56
- if (Array.isArray(value)) {
57
- const copy: unknown[] = [];
58
- seen.set(value, copy);
59
- for (const item of value) {
60
- copy.push(cloneHookValue(item, seen));
61
- }
62
- return copy as T;
63
- }
64
-
65
- if (value instanceof Date) {
66
- return new Date(value.getTime()) as T;
67
- }
68
-
69
- if (value instanceof Map) {
70
- const copy = new Map();
71
- seen.set(value, copy);
72
- for (const [key, mapValue] of value) {
73
- copy.set(cloneHookValue(key, seen), cloneHookValue(mapValue, seen));
74
- }
75
- return copy as T;
76
- }
77
-
78
- if (value instanceof Set) {
79
- const copy = new Set();
80
- seen.set(value, copy);
81
- for (const item of value) {
82
- copy.add(cloneHookValue(item, seen));
83
- }
84
- return copy as T;
85
- }
86
-
87
- if (!isPlainObject(value)) return value;
88
-
89
- const copy: Record<PropertyKey, unknown> = {};
90
- seen.set(value, copy);
91
- for (const key of Reflect.ownKeys(value)) {
92
- copy[key] = cloneHookValue(
93
- (value as Record<PropertyKey, unknown>)[key],
94
- seen,
95
- );
96
- }
97
- return copy as T;
98
- }
99
-
100
23
  /**
101
24
  * Execute a hook chain: walk every registered plugin's hook for `name` in
102
25
  * registration order, threading `initialCtx` through each. Hooks may either
103
- * mutate their draft context in place (returning `void`) or return a partial
104
- * context whose fields are merged onto the draft — keys the hook returns
105
- * overwrite the running context, every other field is preserved. If a hook
106
- * throws, its draft is discarded and the next hook receives the last
107
- * successfully committed context. The final context after the chain settles is
108
- * returned.
26
+ * mutate the context in place (returning `void`) or return a partial context
27
+ * whose fields are merged onto the threaded value — keys the hook returns
28
+ * overwrite the running context, every other field is preserved. The final
29
+ * context after the chain settles is returned.
109
30
  *
110
31
  * @param name The hook identifier — pick one from {@link HOOKS}.
111
32
  * @param initialCtx Context the first hook receives.
112
33
  * @returns The final context after the chain settles. Same reference as
113
- * `initialCtx` when no plugin registers `name`.
34
+ * `initialCtx` when no plugin registers `name`, and when every
35
+ * chained hook returns `void` (mutation-in-place style).
114
36
  */
115
37
  export async function runHook<TCtx>(
116
38
  name: HookName,
117
39
  initialCtx: TCtx,
118
40
  ): Promise<TCtx> {
119
- let hooks: PluginHookFn<TCtx>[];
120
- try {
121
- hooks = await getHooksFor<TCtx>(name);
122
- } catch (err) {
123
- log.error(
124
- { err, hookName: name },
125
- "plugin hook discovery failed — proceeding without hooks",
126
- );
127
- return initialCtx;
128
- }
129
-
41
+ const hooks = await getHooksFor<TCtx>(name);
130
42
  let active = initialCtx;
131
43
  for (const hook of hooks) {
132
- const draft = cloneHookValue(active);
133
- try {
134
- const result = await hook(draft);
135
- if (result !== undefined) {
136
- active = { ...draft, ...result };
137
- } else {
138
- active = draft;
139
- }
140
- } catch (err) {
141
- log.error(
142
- { err, hookName: name },
143
- "plugin hook failed — proceeding with current context",
144
- );
44
+ const result = await hook(active);
45
+ if (result !== undefined) {
46
+ active = { ...active, ...result };
145
47
  }
146
48
  }
147
49
  return active;
@@ -47,6 +47,8 @@ export interface PluginManifest {
47
47
  * own version at load time.
48
48
  */
49
49
  version: string;
50
+ /** Credential keys the plugin needs resolved before `init()` runs. */
51
+ requiresCredential?: string[];
50
52
  /**
51
53
  * Assistant feature-flag keys that must all be enabled for this plugin to
52
54
  * activate. Checked by `bootstrapPlugins` via `isAssistantFeatureFlagEnabled`