@vellumai/assistant 0.8.8 → 0.8.9-staging.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 (360) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/examples/plugins/echo/README.md +61 -60
  4. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  5. package/examples/plugins/echo/hooks/stop.ts +16 -0
  6. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  7. package/examples/plugins/echo/package.json +1 -2
  8. package/examples/plugins/echo/src/emit.ts +19 -0
  9. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  10. package/openapi.yaml +235 -6
  11. package/package.json +2 -2
  12. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  13. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  14. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  15. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  16. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  17. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  18. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  19. package/src/__tests__/agent-loop.test.ts +441 -96
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  21. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  22. package/src/__tests__/anthropic-provider.test.ts +1 -1
  23. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  24. package/src/__tests__/app-control-flow.test.ts +1 -1
  25. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  26. package/src/__tests__/app-executors.test.ts +132 -0
  27. package/src/__tests__/approval-cascade.test.ts +5 -4
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  30. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  31. package/src/__tests__/channel-approvals.test.ts +1 -1
  32. package/src/__tests__/compaction-circuit.test.ts +258 -0
  33. package/src/__tests__/compaction-direct.test.ts +132 -0
  34. package/src/__tests__/compaction-events.test.ts +5 -5
  35. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  36. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  37. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
  38. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  39. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  40. package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
  41. package/src/__tests__/conversation-agent-loop.test.ts +297 -586
  42. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
  44. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  45. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  46. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
  47. package/src/__tests__/conversation-process-callsite.test.ts +14 -14
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
  49. package/src/__tests__/conversation-queue.test.ts +9 -9
  50. package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
  51. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  52. package/src/__tests__/conversation-slash-queue.test.ts +6 -4
  53. package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
  54. package/src/__tests__/conversation-speed-override.test.ts +10 -9
  55. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  56. package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
  57. package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
  59. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  61. package/src/__tests__/db-acp-history.test.ts +101 -0
  62. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  63. package/src/__tests__/empty-response-hook.test.ts +1 -1
  64. package/src/__tests__/file-write-tool.test.ts +63 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  66. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  67. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  68. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  69. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/heartbeat-service.test.ts +1 -0
  71. package/src/__tests__/history-repair-hook.test.ts +1 -1
  72. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  73. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  74. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  75. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  76. package/src/__tests__/injector-background-turn.test.ts +13 -23
  77. package/src/__tests__/injector-chain.test.ts +268 -44
  78. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  79. package/src/__tests__/injector-document-comments.test.ts +97 -114
  80. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  81. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  82. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  83. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  84. package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
  85. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  86. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  87. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  88. package/src/__tests__/plugin-bootstrap.test.ts +12 -23
  89. package/src/__tests__/plugin-registry.test.ts +3 -49
  90. package/src/__tests__/plugin-types.test.ts +0 -70
  91. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  92. package/src/__tests__/reaction-persistence.test.ts +1 -1
  93. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  94. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  95. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  96. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  97. package/src/__tests__/subagent-detail.test.ts +25 -7
  98. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  99. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  100. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  101. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  102. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  103. package/src/__tests__/title-generate-hook.test.ts +1 -1
  104. package/src/__tests__/tool-error-hook.test.ts +1 -1
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  106. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  107. package/src/acp/__tests__/agent-process.test.ts +161 -0
  108. package/src/acp/__tests__/client-handler.test.ts +40 -0
  109. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  110. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  111. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  112. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  113. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  114. package/src/acp/agent-process.ts +61 -1
  115. package/src/acp/auto-install.test.ts +280 -0
  116. package/src/acp/auto-install.ts +232 -0
  117. package/src/acp/client-handler.ts +31 -0
  118. package/src/acp/feature-gate.test.ts +48 -0
  119. package/src/acp/feature-gate.ts +34 -0
  120. package/src/acp/prepare-agent-env.ts +80 -27
  121. package/src/acp/resolve-agent.test.ts +225 -9
  122. package/src/acp/resolve-agent.ts +122 -17
  123. package/src/acp/resume-hint.ts +23 -0
  124. package/src/acp/session-manager.ts +507 -73
  125. package/src/agent/compaction-circuit.ts +60 -102
  126. package/src/agent/loop.ts +414 -248
  127. package/src/api/responses/conversation-message.ts +14 -1
  128. package/src/approvals/guardian-request-resolvers.ts +1 -1
  129. package/src/background-wake/next-wake.ts +1 -0
  130. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  131. package/src/cli/commands/plugins.ts +43 -37
  132. package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
  133. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  134. package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
  135. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
  136. package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
  137. package/src/cli/lib/install-from-github.ts +464 -55
  138. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  139. package/src/cli/lib/plugin-details.ts +409 -0
  140. package/src/cli/lib/plugin-marketplace.ts +197 -0
  141. package/src/cli/lib/search-plugins.ts +195 -29
  142. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  143. package/src/config/acp-defaults.test.ts +10 -0
  144. package/src/config/acp-defaults.ts +6 -0
  145. package/src/config/bundled-skills/acp/SKILL.md +85 -33
  146. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  147. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  148. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  149. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  150. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  151. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  152. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  153. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  154. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  155. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  156. package/src/config/bundled-tool-registry.ts +4 -0
  157. package/src/config/call-site-defaults.ts +0 -2
  158. package/src/config/feature-flag-registry.json +15 -6
  159. package/src/config/schemas/call-site-catalog.ts +0 -14
  160. package/src/config/schemas/heartbeat.ts +9 -0
  161. package/src/config/schemas/llm.ts +0 -2
  162. package/src/context/compactor.ts +22 -4
  163. package/src/context/strip-injections.ts +8 -2
  164. package/src/context/window-manager.ts +27 -13
  165. package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
  166. package/src/daemon/conversation-agent-loop.ts +175 -1055
  167. package/src/daemon/conversation-lifecycle.ts +11 -255
  168. package/src/daemon/conversation-process.ts +8 -136
  169. package/src/daemon/conversation-registry.ts +159 -0
  170. package/src/daemon/conversation-runtime-assembly.ts +293 -392
  171. package/src/daemon/conversation-store.ts +9 -90
  172. package/src/daemon/conversation-surfaces.ts +24 -8
  173. package/src/daemon/conversation-workspace.ts +17 -0
  174. package/src/daemon/conversation.ts +404 -57
  175. package/src/daemon/disk-pressure-policy.ts +0 -1
  176. package/src/daemon/external-plugins-bootstrap.ts +14 -19
  177. package/src/daemon/handlers/conversations.ts +3 -1
  178. package/src/daemon/handlers/skills.ts +4 -1
  179. package/src/daemon/host-proxy-preactivation.ts +1 -3
  180. package/src/daemon/lifecycle.ts +21 -7
  181. package/src/daemon/server.ts +2 -0
  182. package/src/daemon/wake-conversation-ops.ts +269 -0
  183. package/src/embedded/plugin-api.ts +2 -2
  184. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  185. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  186. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  187. package/src/heartbeat/heartbeat-service.ts +26 -0
  188. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  189. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  190. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  191. package/src/ipc/skill-routes/memory.ts +4 -2
  192. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  193. package/src/memory/conversation-crud.ts +29 -19
  194. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  195. package/src/memory/db-init.ts +2 -0
  196. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  197. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  198. package/src/memory/jobs-worker.ts +25 -1
  199. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  200. package/src/memory/migrations/index.ts +1 -0
  201. package/src/memory/schema/acp.ts +4 -0
  202. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  203. package/src/memory/v2/consolidation-job.ts +13 -4
  204. package/src/plugin-api/constants.ts +4 -0
  205. package/src/plugin-api/index.ts +6 -5
  206. package/src/plugin-api/types.ts +75 -0
  207. package/src/plugins/defaults/compaction/compact.ts +59 -0
  208. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  209. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  210. package/src/plugins/defaults/compaction/package.json +1 -2
  211. package/src/plugins/defaults/empty-response/package.json +0 -1
  212. package/src/plugins/defaults/history-repair/package.json +0 -1
  213. package/src/plugins/defaults/index.ts +135 -26
  214. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  215. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
  216. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  217. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  218. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  219. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  220. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
  221. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  222. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
  223. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  224. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  225. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
  226. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  227. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
  228. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
  229. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
  230. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
  231. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  235. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
  236. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
  237. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  238. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  239. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
  240. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
  241. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  242. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
  243. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  244. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
  245. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
  246. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  247. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
  248. package/src/plugins/defaults/title-generate/package.json +0 -1
  249. package/src/plugins/defaults/tool-error/package.json +0 -1
  250. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  251. package/src/plugins/pipeline.ts +6 -293
  252. package/src/plugins/registry.ts +9 -37
  253. package/src/plugins/types.ts +76 -381
  254. package/src/plugins/user-loader.ts +30 -127
  255. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  256. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
  257. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  258. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  259. package/src/runtime/agent-wake.ts +108 -209
  260. package/src/runtime/assistant-event-hub.ts +1 -1
  261. package/src/runtime/channel-approvals.ts +1 -1
  262. package/src/runtime/interactive-ui.ts +1 -1
  263. package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
  264. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  265. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  266. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  267. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  268. package/src/runtime/routes/acp-routes.test.ts +89 -25
  269. package/src/runtime/routes/acp-routes.ts +81 -29
  270. package/src/runtime/routes/approval-routes.ts +1 -1
  271. package/src/runtime/routes/browser-routes.ts +1 -1
  272. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  273. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  274. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  275. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  276. package/src/runtime/routes/conversation-routes.ts +28 -2
  277. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  278. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  279. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  280. package/src/runtime/routes/host-cu-routes.ts +1 -1
  281. package/src/runtime/routes/identity-routes.ts +18 -3
  282. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  283. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  284. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  285. package/src/runtime/routes/memory-v3-routes.ts +16 -6
  286. package/src/runtime/routes/playground/helpers.ts +1 -1
  287. package/src/runtime/routes/plugins-routes.ts +337 -35
  288. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  289. package/src/runtime/routes/work-items-routes.ts +2 -4
  290. package/src/runtime/services/conversation-serializer.ts +1 -1
  291. package/src/signals/cancel.ts +2 -4
  292. package/src/subagent/manager.ts +21 -5
  293. package/src/tools/acp/context.ts +20 -0
  294. package/src/tools/acp/list-agents.test.ts +8 -2
  295. package/src/tools/acp/spawn.test.ts +176 -195
  296. package/src/tools/acp/spawn.ts +37 -172
  297. package/src/tools/acp/steer.test.ts +105 -8
  298. package/src/tools/acp/steer.ts +48 -17
  299. package/src/tools/apps/executors.ts +166 -50
  300. package/src/tools/filesystem/write.ts +34 -0
  301. package/src/tools/subagent/spawn.ts +2 -4
  302. package/src/tools/ui-surface/definitions.ts +25 -5
  303. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  304. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  305. package/docs/plugins.md +0 -832
  306. package/examples/plugins/echo/register.ts +0 -143
  307. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  308. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  309. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  310. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  311. package/src/__tests__/pipeline-runner.test.ts +0 -554
  312. package/src/__tests__/plugin-external-api.test.ts +0 -68
  313. package/src/daemon/wake-target-adapter.ts +0 -253
  314. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  315. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  316. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  317. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  318. package/src/plugins/defaults/compaction/register.ts +0 -35
  319. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  320. package/src/plugins/defaults/empty-response/register.ts +0 -23
  321. package/src/plugins/defaults/history-repair/register.ts +0 -24
  322. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  323. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  324. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  325. package/src/plugins/defaults/title-generate/register.ts +0 -35
  326. package/src/plugins/defaults/tool-error/register.ts +0 -23
  327. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  328. package/src/plugins/external-api.ts +0 -104
  329. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  330. package/src/proactive-artifact/decision.test.ts +0 -226
  331. package/src/proactive-artifact/decision.ts +0 -165
  332. package/src/proactive-artifact/index.ts +0 -7
  333. package/src/proactive-artifact/job.test.ts +0 -962
  334. package/src/proactive-artifact/job.ts +0 -372
  335. package/src/proactive-artifact/message-copy.ts +0 -58
  336. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  337. package/src/proactive-artifact/trigger-state.ts +0 -123
  338. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  339. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  340. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  341. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  342. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
  343. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  344. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
  345. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  346. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  347. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  348. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  349. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  350. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  351. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  352. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  353. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  354. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  355. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  356. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  357. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
  358. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  359. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  360. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -33,6 +33,7 @@ import { NotFoundError } from "./errors.js";
33
33
  import {
34
34
  getCachedIntro,
35
35
  readWorkspaceGreetings,
36
+ readWorkspaceIdentityIntro,
36
37
  setCachedIntro,
37
38
  } from "./identity-intro-cache.js";
38
39
  import type { RouteDefinition } from "./types.js";
@@ -455,16 +456,30 @@ function getIdentityIntro(): IdentityIntroResponse {
455
456
  return identityIntroResponse(FALLBACK_GREETINGS, "fallback");
456
457
  }
457
458
 
458
- // 2. Cached LLM-generated greetings
459
+ // 2. Cached LLM-generated greetings (populated by background refresh)
459
460
  const cached = getCachedIntro();
460
461
  if (cached) {
461
462
  return identityIntroResponse(cached.greetings, "cache");
462
463
  }
463
464
 
464
- // 3. Trigger fresh generation without blocking the empty-state UI.
465
+ // 3. Identity intro tagline from `## Identity Intro` in IDENTITY.md
466
+ // (written during onboarding by BOOTSTRAP.md instructions)
467
+ const identityIntro = readWorkspaceIdentityIntro();
468
+ if (identityIntro) {
469
+ // Still trigger background generation so the next request gets
470
+ // LLM-generated greetings instead of the static tagline.
471
+ const refreshing = triggerEmptyStateGreetingGeneration();
472
+ return identityIntroResponse(
473
+ [identityIntro, ...FALLBACK_GREETINGS],
474
+ "workspace",
475
+ refreshing,
476
+ );
477
+ }
478
+
479
+ // 4. Trigger fresh generation without blocking the empty-state UI.
465
480
  const refreshing = triggerEmptyStateGreetingGeneration();
466
481
 
467
- // 4. Generic fallback only when generation is unavailable.
482
+ // 5. Generic fallback only when generation is unavailable.
468
483
  return identityIntroResponse(FALLBACK_GREETINGS, "fallback", refreshing);
469
484
  }
470
485
 
@@ -20,7 +20,7 @@ import {
20
20
  createApprovalConversationGenerator,
21
21
  createApprovalCopyGenerator,
22
22
  } from "../../daemon/approval-generators.js";
23
- import { findConversation } from "../../daemon/conversation-store.js";
23
+ import { findConversation } from "../../daemon/conversation-registry.js";
24
24
  import {
25
25
  canonicalizeTimeZone,
26
26
  resolveTurnTimezoneContext,
@@ -15,6 +15,7 @@
15
15
  import { randomUUID } from "node:crypto";
16
16
 
17
17
  import { loadConfig } from "../../config/loader.js";
18
+ import { findConversation } from "../../daemon/conversation-registry.js";
18
19
  import {
19
20
  getConversation,
20
21
  listActiveInferenceProfileSessions,
@@ -118,6 +119,11 @@ export async function setInferenceProfileSession({
118
119
  };
119
120
  }
120
121
  setConversationInferenceProfileSession(resolvedId, null, null, null);
122
+ findConversation(resolvedId)?.applyInferenceProfileState({
123
+ profile: null,
124
+ sessionId: null,
125
+ expiresAt: null,
126
+ });
121
127
  publishConversationInferenceProfileChanged(
122
128
  {
123
129
  conversationId: resolvedId,
@@ -188,6 +194,11 @@ export async function setInferenceProfileSession({
188
194
  newSessionId ?? null,
189
195
  newExpiresAt ?? null,
190
196
  );
197
+ findConversation(resolvedId)?.applyInferenceProfileState({
198
+ profile,
199
+ sessionId: newSessionId ?? null,
200
+ expiresAt: newExpiresAt ?? null,
201
+ });
191
202
 
192
203
  publishConversationInferenceProfileChanged(
193
204
  {
@@ -11,6 +11,7 @@
11
11
  * background sweeps in `http-server.ts`.
12
12
  */
13
13
 
14
+ import { findConversation } from "../../daemon/conversation-registry.js";
14
15
  import { clearExpiredInferenceProfiles } from "../../memory/conversation-crud.js";
15
16
  import { getLogger } from "../../util/logger.js";
16
17
  import { publishConversationInferenceProfileChanged } from "../sync/resource-sync-events.js";
@@ -37,6 +38,11 @@ export function tickInferenceProfileReaper(): void {
37
38
  const now = Date.now();
38
39
  const cleared = clearExpiredInferenceProfiles(now);
39
40
  for (const { conversationId } of cleared) {
41
+ findConversation(conversationId)?.applyInferenceProfileState({
42
+ profile: null,
43
+ sessionId: null,
44
+ expiresAt: null,
45
+ });
40
46
  publishConversationInferenceProfileChanged({
41
47
  conversationId,
42
48
  profile: null,
@@ -21,16 +21,26 @@ import { join } from "node:path";
21
21
  import { z } from "zod";
22
22
 
23
23
  import { getPageIndex } from "../../memory/v2/page-index.js";
24
- import { loadCore } from "../../memory/v3/core.js";
25
- import { computeV3Health, renderV3Health } from "../../memory/v3/health.js";
26
- import { type LeafRef, reconcileTree } from "../../memory/v3/reconcile.js";
27
- import { invalidateLanes } from "../../memory/v3/shadow-plugin.js";
24
+ import { loadCore } from "../../plugins/defaults/memory-v3-shadow/core.js";
25
+ import {
26
+ computeV3Health,
27
+ renderV3Health,
28
+ } from "../../plugins/defaults/memory-v3-shadow/health.js";
29
+ import {
30
+ type LeafRef,
31
+ reconcileTree,
32
+ } from "../../plugins/defaults/memory-v3-shadow/reconcile.js";
33
+ import { invalidateLanes } from "../../plugins/defaults/memory-v3-shadow/shadow-plugin.js";
28
34
  import {
29
35
  coreSlugs,
30
36
  loadLeafTree,
31
37
  resolveDataDir,
32
- } from "../../memory/v3/tree.js";
33
- import type { LeafPath, LeafTree, Slug } from "../../memory/v3/types.js";
38
+ } from "../../plugins/defaults/memory-v3-shadow/tree.js";
39
+ import type {
40
+ LeafPath,
41
+ LeafTree,
42
+ Slug,
43
+ } from "../../plugins/defaults/memory-v3-shadow/types.js";
34
44
  import { getLogger } from "../../util/logger.js";
35
45
  import { getWorkspaceDir } from "../../util/platform.js";
36
46
  import { ACTOR_PRINCIPALS, type RoutePolicy } from "../auth/route-policy.js";
@@ -7,9 +7,9 @@
7
7
  */
8
8
 
9
9
  import type { Conversation } from "../../../daemon/conversation.js";
10
+ import { findConversation } from "../../../daemon/conversation-registry.js";
10
11
  import {
11
12
  destroyActiveConversation,
12
- findConversation,
13
13
  getOrCreateConversation,
14
14
  } from "../../../daemon/conversation-store.js";
15
15
  import {
@@ -1,51 +1,66 @@
1
1
  /**
2
2
  * Route handlers for the assistant plugins surface.
3
3
  *
4
- * GET /v1/plugins — list installed plugins under `<workspaceDir>/plugins/`.
5
- * GET /v1/plugins/search — search the canonical GitHub catalog of installable plugins.
6
- * DELETE /v1/plugins/:name uninstall a plugin from `<workspaceDir>/plugins/<name>/`.
4
+ * GET /v1/plugins — list installed plugins under `<workspaceDir>/plugins/`.
5
+ * GET /v1/plugins/search — search the canonical GitHub catalog of installable plugins.
6
+ * GET /v1/plugins/:name resolve a single plugin's detail view (metadata + README).
7
+ * POST /v1/plugins/install — install a plugin by name from the canonical source.
8
+ * DELETE /v1/plugins/:name — uninstall a plugin from `<workspaceDir>/plugins/<name>/`.
7
9
  *
8
10
  * The read-only routes are projections over the same library functions
9
11
  * the CLI uses (`assistant plugins list`, `assistant plugins search`).
10
- * The DELETE route is symmetric to `assistant plugins uninstall` and
11
- * delegates to the same `uninstallPlugin` lib function. CLI / daemon /
12
- * web stay aligned on what an installed or available plugin looks like.
13
- *
14
- * Install is intentionally not exposed here. The CLI remains the
15
- * install surface because installation fetches from GitHub, applies
16
- * sanitization, and writes to disk — a heavier flow than a single
17
- * JSON request justifies right now.
12
+ * The install / uninstall routes are symmetric to `assistant plugins
13
+ * install` / `uninstall` and delegate to the same `installPlugin` /
14
+ * `uninstallPlugin` lib functions. CLI / daemon / web stay aligned on
15
+ * what an installed or available plugin looks like — mirroring the
16
+ * skills surface, which already exposes detail + install over HTTP.
18
17
  *
19
18
  * # Policy gating
20
19
  *
21
- * Every route declares `policyKey: "plugins"` + `requirePolicyEnforcement:
22
- * true`. The HTTP router enforces via `enforcePolicy()` against the
23
- * `plugins:GET` / `plugins/search:GET` / `plugins:DELETE` registry
24
- * entries in `runtime/auth/route-policy.ts`. The same registry is the
25
- * source of truth for the IPC path too: the IPC route adapter resolves
26
- * each route's policy and ships it in `get_route_schema`, which the
27
- * gateway's IPC proxy reads from its in-memory cache. Reads require
28
- * `settings.read`; uninstall requires `settings.write`.
20
+ * Reads require `settings.read`; install and uninstall require
21
+ * `settings.write`. The HTTP router enforces the per-route `policy`
22
+ * block below, and the IPC route adapter ships the same policy in
23
+ * `get_route_schema` so the gateway's IPC proxy stays in sync.
29
24
  */
30
25
 
31
26
  import { z } from "zod";
32
27
 
33
- import { InvalidPluginNameError } from "../../cli/lib/install-from-github.js";
28
+ import {
29
+ DEFAULT_PLUGIN_REF,
30
+ installPlugin,
31
+ InvalidPluginNameError,
32
+ PluginAlreadyInstalledError,
33
+ PluginNotFoundError,
34
+ PluginSourceUnavailableError,
35
+ } from "../../cli/lib/install-from-github.js";
34
36
  import {
35
37
  type InstalledPluginInfo,
36
38
  listInstalledPlugins,
37
39
  } from "../../cli/lib/list-installed-plugins.js";
40
+ import { getPluginCatalog } from "../../cli/lib/plugin-catalog-cache.js";
41
+ import {
42
+ getPluginDetails,
43
+ PluginDetailsNotFoundError,
44
+ } from "../../cli/lib/plugin-details.js";
38
45
  import {
46
+ assertValidSearchPattern,
47
+ filterPluginCatalog,
39
48
  InvalidSearchPatternError,
49
+ PluginCatalogUnavailableError,
40
50
  type PluginSearchMatch,
41
- searchPlugins,
42
51
  } from "../../cli/lib/search-plugins.js";
43
52
  import {
44
53
  PluginNotInstalledError,
45
54
  uninstallPlugin,
46
55
  } from "../../cli/lib/uninstall-plugin.js";
47
56
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
48
- import { BadRequestError, InternalError, NotFoundError } from "./errors.js";
57
+ import {
58
+ BadRequestError,
59
+ ConflictError,
60
+ InternalError,
61
+ NotFoundError,
62
+ ServiceUnavailableError,
63
+ } from "./errors.js";
49
64
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
50
65
 
51
66
  // ---------------------------------------------------------------------------
@@ -83,17 +98,43 @@ const pluginsListResponseSchema = z.object({
83
98
  plugins: z.array(pluginInfoSchema),
84
99
  });
85
100
 
101
+ const pluginMatchSourceSchema = z
102
+ .discriminatedUnion("kind", [
103
+ z.object({
104
+ kind: z.literal("first-party"),
105
+ }),
106
+ z.object({
107
+ kind: z.literal("github"),
108
+ repo: z
109
+ .string()
110
+ .describe("`owner/repo` of the external plugin repository."),
111
+ path: z
112
+ .string()
113
+ .optional()
114
+ .describe(
115
+ "Directory within the repo, when the plugin is not at the root.",
116
+ ),
117
+ ref: z.string().describe("Pinned git ref the plugin is fetched from."),
118
+ }),
119
+ ])
120
+ .describe(
121
+ "Origin of the match: a first-party in-repo plugin or a whitelisted external one.",
122
+ );
123
+
86
124
  const pluginSearchMatchSchema = z.object({
87
125
  name: z
88
126
  .string()
89
- .describe(
90
- "Directory name under `experimental/plugins/`. Matches `assistant plugins install <name>`.",
91
- ),
127
+ .describe("Install name. Matches `assistant plugins install <name>`."),
92
128
  path: z
93
129
  .string()
94
130
  .describe(
95
- "Repo-relative path of the match (e.g. `experimental/plugins/<name>`).",
131
+ "Human-readable origin: a repo-relative path (`experimental/plugins/<name>`) for first-party plugins or a `github:owner/repo@ref` locator for external ones.",
96
132
  ),
133
+ description: z
134
+ .string()
135
+ .optional()
136
+ .describe("Short description, when known (external entries only today)."),
137
+ source: pluginMatchSourceSchema,
97
138
  });
98
139
 
99
140
  const pluginsSearchResponseSchema = z.object({
@@ -119,6 +160,66 @@ const pluginUninstallResponseSchema = z.object({
119
160
  ),
120
161
  });
121
162
 
163
+ const pluginDetailsResponseSchema = z.object({
164
+ name: z
165
+ .string()
166
+ .describe("Install name. Matches `assistant plugins install <name>`."),
167
+ installed: z
168
+ .boolean()
169
+ .describe(
170
+ "Whether a copy is materialized under `<workspaceDir>/plugins/<name>/`.",
171
+ ),
172
+ description: z
173
+ .string()
174
+ .nullable()
175
+ .describe(
176
+ "Short description, best-effort across disk, manifest, and repo.",
177
+ ),
178
+ homepage: z.string().nullable().describe("Project homepage URL, when known."),
179
+ license: z
180
+ .string()
181
+ .nullable()
182
+ .describe("SPDX license expression, when known."),
183
+ version: z
184
+ .string()
185
+ .nullable()
186
+ .describe(
187
+ "Resolved version (installed copy first, then repo `package.json`).",
188
+ ),
189
+ source: pluginMatchSourceSchema,
190
+ readme: z
191
+ .string()
192
+ .nullable()
193
+ .describe("README markdown, or null when the plugin ships none."),
194
+ ref: z
195
+ .string()
196
+ .describe("Git ref the catalog metadata / README were resolved at."),
197
+ });
198
+
199
+ const pluginInstallRequestSchema = z.object({
200
+ name: z
201
+ .string()
202
+ .describe(
203
+ "Install name to resolve against the catalog (first-party directory or marketplace entry).",
204
+ ),
205
+ force: z
206
+ .boolean()
207
+ .optional()
208
+ .describe("Overwrite an existing install in place. Defaults to false."),
209
+ });
210
+
211
+ const pluginInstallResponseSchema = z.object({
212
+ ok: z.literal(true),
213
+ name: z.string().describe("Install name that was materialized."),
214
+ target: z
215
+ .string()
216
+ .describe("Absolute path the plugin was materialized into on the host."),
217
+ fileCount: z
218
+ .number()
219
+ .describe("Number of files written for the installed plugin."),
220
+ ref: z.string().describe("Git ref the plugin was fetched from."),
221
+ });
222
+
122
223
  // ---------------------------------------------------------------------------
123
224
  // Helpers
124
225
  // ---------------------------------------------------------------------------
@@ -149,6 +250,41 @@ function projectPlugin(entry: InstalledPluginInfo): PluginView {
149
250
  return view;
150
251
  }
151
252
 
253
+ /** Wire shape for a catalog match. Mirrors {@link pluginSearchMatchSchema}. */
254
+ interface PluginMatchView {
255
+ name: string;
256
+ path: string;
257
+ description?: string;
258
+ source:
259
+ | { kind: "first-party" }
260
+ | { kind: "github"; repo: string; path?: string; ref: string };
261
+ }
262
+
263
+ /**
264
+ * Re-pack a `readonly` lib match into a mutable wire object so the route
265
+ * serializer's `Record<string, unknown>` contract holds. The wire shape is
266
+ * identical to {@link PluginSearchMatch}.
267
+ */
268
+ function projectMatch(m: PluginSearchMatch): PluginMatchView {
269
+ const view: PluginMatchView = {
270
+ name: m.name,
271
+ path: m.path,
272
+ source:
273
+ m.source.kind === "github"
274
+ ? {
275
+ kind: "github",
276
+ repo: m.source.repo,
277
+ ref: m.source.ref,
278
+ ...(m.source.path !== undefined ? { path: m.source.path } : {}),
279
+ }
280
+ : { kind: "first-party" },
281
+ };
282
+ if (m.description !== undefined) {
283
+ view.description = m.description;
284
+ }
285
+ return view;
286
+ }
287
+
152
288
  function matchesQuery(plugin: PluginView, needle: string): boolean {
153
289
  const q = needle.toLowerCase();
154
290
  if (plugin.id.toLowerCase().includes(q)) return true;
@@ -180,7 +316,7 @@ function handleListPlugins({ queryParams = {} }: RouteHandlerArgs): {
180
316
  interface PluginsSearchResponse {
181
317
  query: string;
182
318
  ref: string;
183
- matches: PluginSearchMatch[];
319
+ matches: PluginMatchView[];
184
320
  }
185
321
 
186
322
  async function handleSearchPlugins({
@@ -189,25 +325,39 @@ async function handleSearchPlugins({
189
325
  // Empty string is a legitimate "match everything" query per the lib's
190
326
  // contract — accept it without forcing the caller to pick a sentinel.
191
327
  const query = queryParams.q ?? "";
192
- const ref = queryParams.ref?.trim() || undefined;
328
+ const ref = queryParams.ref?.trim() || DEFAULT_PLUGIN_REF;
193
329
 
194
330
  try {
195
- const result = await searchPlugins(
196
- { query, ref },
197
- { fetch: globalThis.fetch.bind(globalThis) },
198
- );
331
+ // Reject a malformed regex before any network I/O so a user typo is a
332
+ // cheap deterministic 400 — never a wasted GitHub request that could
333
+ // surface as 503 on a cold cache when upstream is rate-limited.
334
+ assertValidSearchPattern(query);
335
+ // The catalog is cached per ref (and served stale on upstream failure),
336
+ // so repeated searches don't re-hit GitHub's unauthenticated rate limit.
337
+ // Filtering by the query is an in-memory operation over that catalog.
338
+ const catalog = await getPluginCatalog(ref, {
339
+ fetch: globalThis.fetch.bind(globalThis),
340
+ });
341
+ const matches = filterPluginCatalog(catalog, query);
199
342
  // Re-pack `readonly` lib types into mutable copies so the route
200
343
  // serializer's `Record<string, unknown>` contract holds. The wire
201
344
  // shape is identical.
202
345
  return {
203
- query: result.query,
204
- ref: result.ref,
205
- matches: result.matches.map((m) => ({ name: m.name, path: m.path })),
346
+ query,
347
+ ref: catalog.ref,
348
+ matches: matches.map(projectMatch),
206
349
  };
207
350
  } catch (err) {
208
351
  if (err instanceof InvalidSearchPatternError) {
209
352
  throw new BadRequestError(err.message);
210
353
  }
354
+ // A rate-limited or unavailable upstream (with no cache to fall back on)
355
+ // is transient and retryable — surface it as 503 rather than a
356
+ // misleading 500 so the client can show a "temporarily unavailable"
357
+ // state and retry later.
358
+ if (err instanceof PluginCatalogUnavailableError) {
359
+ throw new ServiceUnavailableError(err.message);
360
+ }
211
361
  throw new InternalError(
212
362
  err instanceof Error ? err.message : "plugin catalog search failed",
213
363
  );
@@ -248,6 +398,87 @@ function handleUninstallPlugin({
248
398
  }
249
399
  }
250
400
 
401
+ // ---------------------------------------------------------------------------
402
+ // Handler — detail view
403
+ // ---------------------------------------------------------------------------
404
+
405
+ async function handleGetPluginDetails({
406
+ pathParams = {},
407
+ queryParams = {},
408
+ }: RouteHandlerArgs) {
409
+ const rawName = pathParams.name ?? "";
410
+ const ref = queryParams.ref?.trim() || undefined;
411
+
412
+ try {
413
+ return await getPluginDetails(
414
+ { name: rawName, ref },
415
+ { fetch: globalThis.fetch.bind(globalThis) },
416
+ );
417
+ } catch (err) {
418
+ if (err instanceof InvalidPluginNameError) {
419
+ throw new BadRequestError(err.message);
420
+ }
421
+ if (err instanceof PluginDetailsNotFoundError) {
422
+ throw new NotFoundError(err.message);
423
+ }
424
+ throw new InternalError(
425
+ err instanceof Error ? err.message : "plugin detail lookup failed",
426
+ );
427
+ }
428
+ }
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // Handler — install
432
+ // ---------------------------------------------------------------------------
433
+
434
+ async function handleInstallPlugin({ body = {} }: RouteHandlerArgs) {
435
+ const name = typeof body.name === "string" ? body.name : "";
436
+ if (!name) {
437
+ throw new BadRequestError("`name` is required");
438
+ }
439
+ const force = typeof body.force === "boolean" ? body.force : undefined;
440
+
441
+ // The ref is pinned to the curated `DEFAULT_PLUGIN_REF` rather than taken
442
+ // from the request: a caller-supplied ref would let any `settings.write`
443
+ // principal install from an unreviewed revision (a PR branch, fork ref,
444
+ // ...) whose marketplace manifest or first-party plugin directory could
445
+ // carry attacker code that the loader then dynamically imports. Installs
446
+ // over HTTP therefore only ever resolve against the reviewed catalog on
447
+ // the default ref. Operators who need another ref use the local CLI's
448
+ // `assistant plugins install --ref`.
449
+ try {
450
+ const result = await installPlugin(
451
+ { name, ref: DEFAULT_PLUGIN_REF, force },
452
+ { fetch: globalThis.fetch.bind(globalThis) },
453
+ );
454
+ return {
455
+ ok: true as const,
456
+ name: result.name,
457
+ target: result.target,
458
+ fileCount: result.fileCount,
459
+ ref: result.ref,
460
+ };
461
+ } catch (err) {
462
+ if (err instanceof InvalidPluginNameError) {
463
+ throw new BadRequestError(err.message);
464
+ }
465
+ if (err instanceof PluginAlreadyInstalledError) {
466
+ throw new ConflictError(err.message);
467
+ }
468
+ if (err instanceof PluginNotFoundError) {
469
+ throw new NotFoundError(err.message);
470
+ }
471
+ // A rate-limited or otherwise temporarily-down GitHub source is
472
+ // retryable, so surface 503 rather than a misleading 500.
473
+ if (err instanceof PluginSourceUnavailableError) {
474
+ throw new ServiceUnavailableError(err.message);
475
+ }
476
+ throw new InternalError(
477
+ err instanceof Error ? err.message : "plugin install failed",
478
+ );
479
+ }
480
+ }
481
+
251
482
  // ---------------------------------------------------------------------------
252
483
  // Route definitions
253
484
  // ---------------------------------------------------------------------------
@@ -305,6 +536,77 @@ export const ROUTES: RouteDefinition[] = [
305
536
  responseBody: pluginsSearchResponseSchema,
306
537
  handler: handleSearchPlugins,
307
538
  },
539
+ {
540
+ operationId: "plugins_install",
541
+ endpoint: "plugins/install",
542
+ method: "POST",
543
+ policy: {
544
+ requiredScopes: ["settings.write"],
545
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
546
+ },
547
+ summary: "Install a plugin",
548
+ description:
549
+ "Install a plugin by name from the canonical source — a whitelisted `experimental/plugins/marketplace.json` entry, else the first-party `experimental/plugins/<name>/` convention. Always resolves against the curated default git ref (no caller-supplied ref): installing from an unreviewed revision would bypass the marketplace/first-party curation boundary and let attacker-controlled code be loaded. Materializes the plugin under `<workspaceDir>/plugins/<name>/`; the assistant must be restarted to load it. Mirrors the CLI's `assistant plugins install <name>`. An already-installed name without `force` returns 409; a name that resolves to nothing returns 404. Sibling to `POST /v1/skills/install`.",
550
+ tags: ["plugins"],
551
+ requestBody: pluginInstallRequestSchema,
552
+ responseBody: pluginInstallResponseSchema,
553
+ additionalResponses: {
554
+ "400": {
555
+ description:
556
+ "The request body was missing `name` or the name failed sanitization.",
557
+ },
558
+ "404": {
559
+ description:
560
+ "No plugin resolves to the given name at the requested ref.",
561
+ },
562
+ "409": {
563
+ description:
564
+ "A plugin with the same name is already installed and `force` was not set.",
565
+ },
566
+ },
567
+ handler: handleInstallPlugin,
568
+ },
569
+ {
570
+ operationId: "plugins_get",
571
+ endpoint: "plugins/:name",
572
+ method: "GET",
573
+ policy: {
574
+ requiredScopes: ["settings.read"],
575
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
576
+ },
577
+ summary: "Get a plugin's detail view",
578
+ description:
579
+ "Resolve a single plugin's tracked metadata (description, homepage, license, version, source) plus its README markdown. Unions the locally installed copy, the marketplace manifest, and the plugin's repository at the pinned ref — preferring the installed copy. Names that are neither installed nor present in the catalog return 404. Powers the web plugin detail page; mirrors `GET /v1/skills/:id`.",
580
+ tags: ["plugins"],
581
+ pathParams: [
582
+ {
583
+ name: "name",
584
+ type: "string",
585
+ description:
586
+ "Install name. Must match the kebab-case name accepted by `assistant plugins install`.",
587
+ },
588
+ ],
589
+ queryParams: [
590
+ {
591
+ name: "ref",
592
+ schema: { type: "string" },
593
+ description:
594
+ "Optional git ref to read catalog metadata / README at. Defaults to the CLI's `DEFAULT_PLUGIN_REF`.",
595
+ },
596
+ ],
597
+ responseBody: pluginDetailsResponseSchema,
598
+ additionalResponses: {
599
+ "400": {
600
+ description:
601
+ "The plugin name failed sanitization (e.g. contained slashes, dots, or uppercase letters).",
602
+ },
603
+ "404": {
604
+ description:
605
+ "No installed copy and no catalog entry claims the given name.",
606
+ },
607
+ },
608
+ handler: handleGetPluginDetails,
609
+ },
308
610
  {
309
611
  operationId: "plugins_uninstall",
310
612
  endpoint: "plugins/:name",
@@ -18,8 +18,8 @@ import type { Conversation } from "../../daemon/conversation.js";
18
18
  import {
19
19
  findConversation,
20
20
  findConversationBySurfaceId,
21
- getOrCreateConversation,
22
- } from "../../daemon/conversation-store.js";
21
+ } from "../../daemon/conversation-registry.js";
22
+ import { getOrCreateConversation } from "../../daemon/conversation-store.js";
23
23
  import { rawGet } from "../../memory/raw-query.js";
24
24
 
25
25
  /**
@@ -60,6 +60,7 @@ export async function resolveSurfaceConversation(
60
60
  `%"surfaceId":"${escaped}"%`,
61
61
  );
62
62
  if (!row) return undefined;
63
- if (conversationId && conversationId !== row.conversation_id) return undefined;
63
+ if (conversationId && conversationId !== row.conversation_id)
64
+ return undefined;
64
65
  return await getOrCreateConversation(row.conversation_id);
65
66
  }
@@ -8,10 +8,8 @@
8
8
  import { z } from "zod";
9
9
 
10
10
  import type { Conversation } from "../../daemon/conversation.js";
11
- import {
12
- findConversation,
13
- getOrCreateConversation,
14
- } from "../../daemon/conversation-store.js";
11
+ import { findConversation } from "../../daemon/conversation-registry.js";
12
+ import { getOrCreateConversation } from "../../daemon/conversation-store.js";
15
13
  import type { ServerMessage } from "../../daemon/message-protocol.js";
16
14
  import { getMessages } from "../../memory/conversation-crud.js";
17
15
  import { check, classifyRisk } from "../../permissions/checker.js";
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { parseChannelId } from "../../channels/types.js";
11
11
  import { getConfig } from "../../config/loader.js";
12
- import { findConversation } from "../../daemon/conversation-store.js";
12
+ import { findConversation } from "../../daemon/conversation-registry.js";
13
13
  import { normalizeConversationType } from "../../daemon/message-types/shared.js";
14
14
  import {
15
15
  type AttentionState,
@@ -11,10 +11,8 @@ import { readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
 
13
13
  import { getIsContainerized } from "../config/env-registry.js";
14
- import {
15
- findConversation,
16
- touchConversation,
17
- } from "../daemon/conversation-store.js";
14
+ import { findConversation } from "../daemon/conversation-registry.js";
15
+ import { touchConversation } from "../daemon/conversation-store.js";
18
16
  import { getSubagentManager } from "../subagent/index.js";
19
17
  import { createAbortReason } from "../util/abort-reasons.js";
20
18
  import { getLogger } from "../util/logger.js";