@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
@@ -1,6 +1,12 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
2
 
3
+ import {
4
+ clearConversations,
5
+ setConversation,
6
+ } from "../daemon/conversation-registry.js";
3
7
  import { applyRuntimeInjections } from "../daemon/conversation-runtime-assembly.js";
8
+ import type { SurfaceData, SurfaceType } from "../daemon/message-protocol.js";
9
+ import { createApp } from "../memory/app-store.js";
4
10
  import type { Message } from "../providers/types.js";
5
11
 
6
12
  // ---------------------------------------------------------------------------
@@ -11,6 +17,46 @@ function userMsg(text: string): Message {
11
17
  return { role: "user", content: [{ type: "text", text }] };
12
18
  }
13
19
 
20
+ // `applyRuntimeInjections` synthesizes this conversation id when no
21
+ // `turnContext` is supplied, so the `workspace-context` injector resolves the
22
+ // live workspace block from the registry under this key.
23
+ const FALLBACK_CONVERSATION_ID = "runtime-assembly-fallback";
24
+
25
+ // Register the fallback conversation in the live registry so the runtime
26
+ // injectors resolve their blocks from it (the orchestrator no longer threads
27
+ // workspace or active-surface content as options).
28
+ function registerFallbackConversation(fields: Record<string, unknown>): void {
29
+ setConversation(FALLBACK_CONVERSATION_ID, {
30
+ conversationId: FALLBACK_CONVERSATION_ID,
31
+ workingDir: "/sandbox",
32
+ // Non-dirty empty workspace by default so the workspace-context injector
33
+ // skips both the filesystem rescan and the DB refresh unless a test
34
+ // explicitly seeds a block via `workspaceTopLevelContext`.
35
+ workspaceTopLevelContext: "",
36
+ workspaceTopLevelDirty: false,
37
+ ...fields,
38
+ } as never);
39
+ }
40
+
41
+ // Seed the live conversation registry with a pre-rendered top-level block. The
42
+ // cache is non-dirty with non-null content, so `resolveWorkspaceTopLevelContext`
43
+ // returns it verbatim without rescanning the filesystem.
44
+ function seedWorkspaceContext(text: string): void {
45
+ registerFallbackConversation({
46
+ workspaceTopLevelContext: text,
47
+ workspaceTopLevelDirty: false,
48
+ });
49
+ }
50
+
51
+ // Build the conversation surface-state map that `buildActiveSurfaceContext`
52
+ // reads to render the `<active_workspace>` block.
53
+ function makeSurfaceState(
54
+ surfaceId: string,
55
+ data: SurfaceData,
56
+ ): Map<string, { surfaceType: SurfaceType; data: SurfaceData }> {
57
+ return new Map([[surfaceId, { surfaceType: "dynamic_page", data }]]);
58
+ }
59
+
14
60
  // ---------------------------------------------------------------------------
15
61
  // Tests
16
62
  // ---------------------------------------------------------------------------
@@ -19,14 +65,21 @@ const sampleContext =
19
65
  "<workspace>\nRoot: /sandbox\nDirectories: src, lib, tests\n</workspace>";
20
66
 
21
67
  // The workspace-context default injector emits the workspace block as a
22
- // `prepend-user-tail` placement during `applyRuntimeInjections`. The suite
23
- // below exercises that end-to-end path.
68
+ // `prepend-user-tail` placement during `applyRuntimeInjections`. It sources
69
+ // the rendered block from the per-conversation workspace registry and
70
+ // (re)injects only when the block is absent from the working messages, so the
71
+ // suite seeds the registry and exercises that end-to-end path.
24
72
 
25
73
  describe("applyRuntimeInjections — workspace top-level context", () => {
26
- test("injects workspace context when provided", async () => {
74
+ afterEach(() => {
75
+ clearConversations();
76
+ });
77
+
78
+ test("injects workspace context when registered", async () => {
79
+ seedWorkspaceContext(sampleContext);
27
80
  const messages: Message[] = [userMsg("Hello")];
28
81
  const { messages: result } = await applyRuntimeInjections(messages, {
29
- workspaceTopLevelContext: sampleContext,
82
+ conversationId: FALLBACK_CONVERSATION_ID,
30
83
  });
31
84
 
32
85
  expect(result).toHaveLength(1);
@@ -35,21 +88,43 @@ describe("applyRuntimeInjections — workspace top-level context", () => {
35
88
  expect((result[0].content[1] as { text: string }).text).toBe("Hello");
36
89
  });
37
90
 
38
- test("does not inject when workspace context is null", async () => {
91
+ test("does not inject when no workspace context is registered", async () => {
39
92
  const messages: Message[] = [userMsg("Hello")];
40
93
  const { messages: result } = await applyRuntimeInjections(messages, {
41
- workspaceTopLevelContext: null,
94
+ conversationId: FALLBACK_CONVERSATION_ID,
42
95
  });
43
96
 
44
97
  expect(result).toHaveLength(1);
45
98
  expect(result[0].content).toHaveLength(1);
46
99
  });
47
100
 
101
+ test("does not re-inject when the workspace block is already present", async () => {
102
+ // GIVEN the registry holds a workspace block AND the working messages
103
+ // already carry that block (a normal cached turn, post-injection).
104
+ seedWorkspaceContext(sampleContext);
105
+ const messages: Message[] = [userMsg(sampleContext), userMsg("Hello")];
106
+
107
+ // WHEN injections are applied
108
+ const { messages: result } = await applyRuntimeInjections(messages, {
109
+ conversationId: FALLBACK_CONVERSATION_ID,
110
+ });
111
+
112
+ // THEN presence detection skips the block to keep the prefix stable.
113
+ expect(result).toHaveLength(2);
114
+ expect(result[1].content).toHaveLength(1);
115
+ expect((result[1].content[0] as { text: string }).text).toBe("Hello");
116
+ });
117
+
48
118
  test("workspace context appears before active surface context in content", async () => {
119
+ registerFallbackConversation({
120
+ workspaceTopLevelContext: sampleContext,
121
+ workspaceTopLevelDirty: false,
122
+ currentActiveSurfaceId: "sf_1",
123
+ surfaceState: makeSurfaceState("sf_1", { html: "<div>test</div>" }),
124
+ });
49
125
  const messages: Message[] = [userMsg("Hello")];
50
126
  const { messages: result } = await applyRuntimeInjections(messages, {
51
- activeSurface: { surfaceId: "sf_1", html: "<div>test</div>" },
52
- workspaceTopLevelContext: sampleContext,
127
+ conversationId: FALLBACK_CONVERSATION_ID,
53
128
  });
54
129
 
55
130
  // Workspace is injected last (in applyRuntimeInjections order) so it
@@ -64,15 +139,21 @@ describe("applyRuntimeInjections — workspace top-level context", () => {
64
139
  });
65
140
 
66
141
  test("app-backed active surface tells the model to load app-builder with the right argument", async () => {
142
+ const app = createApp({
143
+ name: "Example App",
144
+ schemaJson: "{}",
145
+ htmlDefinition: "<div>test</div>",
146
+ });
147
+ registerFallbackConversation({
148
+ currentActiveSurfaceId: "sf_1",
149
+ surfaceState: makeSurfaceState("sf_1", {
150
+ html: "<div>test</div>",
151
+ appId: app.id,
152
+ }),
153
+ });
67
154
  const messages: Message[] = [userMsg("Edit this app")];
68
155
  const { messages: result } = await applyRuntimeInjections(messages, {
69
- activeSurface: {
70
- surfaceId: "sf_1",
71
- html: "<div>test</div>",
72
- appId: "app-1",
73
- appName: "Example App",
74
- },
75
- workspaceTopLevelContext: null,
156
+ conversationId: FALLBACK_CONVERSATION_ID,
76
157
  });
77
158
 
78
159
  const activeWorkspaceText = (result[0].content[0] as { text: string }).text;
@@ -82,10 +163,15 @@ describe("applyRuntimeInjections — workspace top-level context", () => {
82
163
  });
83
164
 
84
165
  describe("applyRuntimeInjections — minimal mode skips workspace blocks", () => {
166
+ afterEach(() => {
167
+ clearConversations();
168
+ });
169
+
85
170
  test("minimal mode skips workspace top-level context", async () => {
171
+ seedWorkspaceContext(sampleContext);
86
172
  const messages: Message[] = [userMsg("Hello")];
87
173
  const { messages: result } = await applyRuntimeInjections(messages, {
88
- workspaceTopLevelContext: sampleContext,
174
+ conversationId: FALLBACK_CONVERSATION_ID,
89
175
  mode: "minimal",
90
176
  });
91
177
 
@@ -95,9 +181,13 @@ describe("applyRuntimeInjections — minimal mode skips workspace blocks", () =>
95
181
  });
96
182
 
97
183
  test("minimal mode skips active surface context", async () => {
184
+ registerFallbackConversation({
185
+ currentActiveSurfaceId: "sf_1",
186
+ surfaceState: makeSurfaceState("sf_1", { html: "<div>test</div>" }),
187
+ });
98
188
  const messages: Message[] = [userMsg("Hello")];
99
189
  const { messages: result } = await applyRuntimeInjections(messages, {
100
- activeSurface: { surfaceId: "sf_1", html: "<div>test</div>" },
190
+ conversationId: FALLBACK_CONVERSATION_ID,
101
191
  mode: "minimal",
102
192
  });
103
193
 
@@ -107,10 +197,15 @@ describe("applyRuntimeInjections — minimal mode skips workspace blocks", () =>
107
197
  });
108
198
 
109
199
  test("full mode (default) still includes workspace blocks", async () => {
200
+ registerFallbackConversation({
201
+ workspaceTopLevelContext: sampleContext,
202
+ workspaceTopLevelDirty: false,
203
+ currentActiveSurfaceId: "sf_1",
204
+ surfaceState: makeSurfaceState("sf_1", { html: "<div>test</div>" }),
205
+ });
110
206
  const messages: Message[] = [userMsg("Hello")];
111
207
  const { messages: result } = await applyRuntimeInjections(messages, {
112
- workspaceTopLevelContext: sampleContext,
113
- activeSurface: { surfaceId: "sf_1", html: "<div>test</div>" },
208
+ conversationId: FALLBACK_CONVERSATION_ID,
114
209
  });
115
210
 
116
211
  expect(result[0].content).toHaveLength(3);
@@ -156,6 +156,7 @@ mock.module("../memory/retriever.js", () => ({
156
156
  mock.module("../context/window-manager.js", () => ({
157
157
  ContextWindowManager: class {
158
158
  constructor() {}
159
+ updateConfig() {}
159
160
  shouldCompact() {
160
161
  return { needed: false, estimatedTokens: 0 };
161
162
  }
@@ -221,10 +222,11 @@ mock.module("../agent/loop.js", () => ({
221
222
  getActiveModel() {
222
223
  return undefined;
223
224
  }
224
- async run(
225
- messages: Message[],
226
- onEvent: (event: AgentEvent) => void | Promise<void>,
227
- ): Promise<AgentLoopRunResult> {
225
+ async run(options: {
226
+ messages: Message[];
227
+ onEvent: (event: AgentEvent) => void | Promise<void>;
228
+ }): Promise<AgentLoopRunResult> {
229
+ const { messages, onEvent } = options;
228
230
  const history = await new Promise<Message[]>((resolve) => {
229
231
  pendingRuns.push({ resolve, messages, onEvent });
230
232
  });
@@ -157,6 +157,7 @@ mock.module("../memory/retriever.js", () => ({
157
157
  mock.module("../context/window-manager.js", () => ({
158
158
  ContextWindowManager: class {
159
159
  constructor() {}
160
+ updateConfig() {}
160
161
  shouldCompact() {
161
162
  return { needed: false, estimatedTokens: 0 };
162
163
  }
@@ -245,10 +246,11 @@ mock.module("../agent/loop.js", () => ({
245
246
  getActiveModel() {
246
247
  return undefined;
247
248
  }
248
- async run(
249
- messages: Message[],
250
- onEvent: (event: AgentEvent) => void,
251
- ): Promise<Message[]> {
249
+ async run(options: {
250
+ messages: Message[];
251
+ onEvent: (event: AgentEvent) => void;
252
+ }): Promise<Message[]> {
253
+ const { messages, onEvent } = options;
252
254
  // Prime the assistant row anchor — production code emits this from
253
255
  // `AgentLoop.run` just before `provider.sendMessage`.
254
256
  await onEvent({ type: "llm_call_started" });
@@ -173,6 +173,7 @@ mock.module("../memory/retriever.js", () => ({
173
173
  mock.module("../context/window-manager.js", () => ({
174
174
  ContextWindowManager: class {
175
175
  constructor() {}
176
+ updateConfig() {}
176
177
  shouldCompact() {
177
178
  return { needed: false, estimatedTokens: 0 };
178
179
  }
@@ -198,11 +199,11 @@ let lastAgentLoopConfig: Partial<AgentLoopConfig> | undefined;
198
199
  mock.module("../agent/loop.js", () => ({
199
200
  AgentLoop: class {
200
201
  compactionCircuit = new CompactionCircuit("test-conv");
201
- constructor(
202
- _provider: unknown,
203
- _systemPrompt: string,
204
- options?: { config?: Partial<AgentLoopConfig> },
205
- ) {
202
+ constructor(options?: {
203
+ provider?: unknown;
204
+ systemPrompt?: string;
205
+ config?: Partial<AgentLoopConfig>;
206
+ }) {
206
207
  lastAgentLoopConfig = options?.config;
207
208
  }
208
209
  getToolTokenBudget() {
@@ -214,10 +215,10 @@ mock.module("../agent/loop.js", () => ({
214
215
  getActiveModel() {
215
216
  return undefined;
216
217
  }
217
- async run(
218
- _messages: Message[],
219
- _onEvent: (event: AgentEvent) => void,
220
- ): Promise<Message[]> {
218
+ async run(_options: {
219
+ messages: Message[];
220
+ onEvent: (event: AgentEvent) => void;
221
+ }): Promise<Message[]> {
221
222
  return [];
222
223
  }
223
224
  },
@@ -339,8 +339,8 @@ describe("GET /v1/conversation-starters", () => {
339
339
 
340
340
  test("does not re-enqueue for invalid items when within cooldown period", async () => {
341
341
  const now = Date.now();
342
- // Generation happened 30 seconds ago (within the 60s cooldown)
343
- const recentGenAt = now - 30_000;
342
+ // Last attempt 30 seconds ago (within the 5-minute cooldown)
343
+ const recentAttemptAt = now - 30_000;
344
344
  insertStarter({
345
345
  label: "Let me check calendar",
346
346
  prompt: "Let me check what Alice has today.",
@@ -353,9 +353,13 @@ describe("GET /v1/conversation-starters", () => {
353
353
  category: "productivity",
354
354
  createdAt: now - 1,
355
355
  });
356
+ setCheckpoint(
357
+ "conversation_starters:last_attempt_at:default",
358
+ String(recentAttemptAt),
359
+ );
356
360
  setCheckpoint(
357
361
  "conversation_starters:last_gen_at:default",
358
- String(recentGenAt),
362
+ String(recentAttemptAt),
359
363
  );
360
364
  setCheckpoint("conversation_starters:item_count_at_last_gen:default", "1");
361
365
  insertMemoryItem();
@@ -375,8 +379,8 @@ describe("GET /v1/conversation-starters", () => {
375
379
 
376
380
  test("re-enqueues for invalid items after cooldown expires", async () => {
377
381
  const now = Date.now();
378
- // Generation happened 90 seconds ago (past the 60s cooldown)
379
- const oldGenAt = now - 90_000;
382
+ // Last attempt 6 minutes ago (past the 5-minute cooldown)
383
+ const oldAttemptAt = now - 6 * 60_000;
380
384
  insertStarter({
381
385
  label: "Let me check calendar",
382
386
  prompt: "Let me check what Alice has today.",
@@ -389,9 +393,13 @@ describe("GET /v1/conversation-starters", () => {
389
393
  category: "productivity",
390
394
  createdAt: now - 1,
391
395
  });
396
+ setCheckpoint(
397
+ "conversation_starters:last_attempt_at:default",
398
+ String(oldAttemptAt),
399
+ );
392
400
  setCheckpoint(
393
401
  "conversation_starters:last_gen_at:default",
394
- String(oldGenAt),
402
+ String(oldAttemptAt),
395
403
  );
396
404
  setCheckpoint("conversation_starters:item_count_at_last_gen:default", "1");
397
405
  insertMemoryItem();
@@ -140,6 +140,7 @@ mock.module("../memory/retrieval-budget.js", () => ({
140
140
  mock.module("../context/window-manager.js", () => ({
141
141
  ContextWindowManager: class {
142
142
  constructor() {}
143
+ updateConfig() {}
143
144
  shouldCompact() {
144
145
  return { needed: false, estimatedTokens: 0 };
145
146
  }
@@ -176,10 +177,11 @@ mock.module("../agent/loop.js", () => ({
176
177
  getActiveModel() {
177
178
  return undefined;
178
179
  }
179
- async run(
180
- messages: Message[],
181
- onEvent: (event: AgentEvent) => void,
182
- ): Promise<Message[]> {
180
+ async run(options: {
181
+ messages: Message[];
182
+ onEvent: (event: AgentEvent) => void;
183
+ }): Promise<Message[]> {
184
+ const { messages, onEvent } = options;
183
185
  const assistantMessage: Message = {
184
186
  role: "assistant",
185
187
  content: [{ type: "text", text: "ok" }],
@@ -198,6 +200,7 @@ mock.module("../agent/loop.js", () => ({
198
200
  }));
199
201
 
200
202
  import { Conversation } from "../daemon/conversation.js";
203
+ import { refreshWorkspaceTopLevelContextIfNeeded } from "../daemon/conversation-workspace.js";
201
204
 
202
205
  // ---------------------------------------------------------------------------
203
206
  // Helpers
@@ -249,7 +252,7 @@ describe("Conversation workspace cache state", () => {
249
252
  });
250
253
 
251
254
  test("refreshWorkspaceTopLevelContextIfNeeded populates context and clears dirty", () => {
252
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
255
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
253
256
 
254
257
  expect(conversation.isWorkspaceTopLevelDirty()).toBe(false);
255
258
  expect(conversation.getWorkspaceTopLevelContext()).not.toBeNull();
@@ -265,10 +268,10 @@ describe("Conversation workspace cache state", () => {
265
268
  });
266
269
 
267
270
  test("refreshWorkspaceTopLevelContextIfNeeded no-ops when not dirty and cache exists", () => {
268
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
271
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
269
272
  const first = conversation.getWorkspaceTopLevelContext();
270
273
 
271
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
274
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
272
275
  const second = conversation.getWorkspaceTopLevelContext();
273
276
 
274
277
  // Same reference — no recomputation
@@ -276,7 +279,7 @@ describe("Conversation workspace cache state", () => {
276
279
  });
277
280
 
278
281
  test("markWorkspaceTopLevelDirty sets dirty flag", () => {
279
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
282
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
280
283
  expect(conversation.isWorkspaceTopLevelDirty()).toBe(false);
281
284
 
282
285
  conversation.markWorkspaceTopLevelDirty();
@@ -284,10 +287,10 @@ describe("Conversation workspace cache state", () => {
284
287
  });
285
288
 
286
289
  test("refresh after marking dirty produces fresh context", () => {
287
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
290
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
288
291
 
289
292
  conversation.markWorkspaceTopLevelDirty();
290
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
293
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
291
294
 
292
295
  expect(conversation.getWorkspaceTopLevelContext()).not.toBeNull();
293
296
  expect(conversation.getWorkspaceTopLevelContext()!).toContain(
@@ -299,7 +302,7 @@ describe("Conversation workspace cache state", () => {
299
302
  test("renders client-reported host env when set on the conversation", () => {
300
303
  conversation.hostHomeDir = "/Users/alice";
301
304
  conversation.hostUsername = "alice";
302
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
305
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
303
306
 
304
307
  const block = conversation.getWorkspaceTopLevelContext();
305
308
  expect(block).not.toBeNull();
@@ -309,7 +312,7 @@ describe("Conversation workspace cache state", () => {
309
312
 
310
313
  test("falls back to daemon os info when client host env is absent", async () => {
311
314
  const { homedir, userInfo } = await import("node:os");
312
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
315
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
313
316
 
314
317
  const block = conversation.getWorkspaceTopLevelContext();
315
318
  expect(block).not.toBeNull();
@@ -320,7 +323,7 @@ describe("Conversation workspace cache state", () => {
320
323
  test("re-renders with updated host env after marking dirty", () => {
321
324
  conversation.hostHomeDir = "/Users/alice";
322
325
  conversation.hostUsername = "alice";
323
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
326
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
324
327
  expect(conversation.getWorkspaceTopLevelContext()!).toContain(
325
328
  "Host home directory: /Users/alice",
326
329
  );
@@ -328,7 +331,7 @@ describe("Conversation workspace cache state", () => {
328
331
  conversation.hostHomeDir = "/Users/bob";
329
332
  conversation.hostUsername = "bob";
330
333
  conversation.markWorkspaceTopLevelDirty();
331
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
334
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
332
335
 
333
336
  const block = conversation.getWorkspaceTopLevelContext();
334
337
  expect(block).not.toBeNull();
@@ -344,7 +347,7 @@ describe("Conversation workspace cache state", () => {
344
347
  // Simulate a macOS turn populating host env.
345
348
  conversation.hostHomeDir = "/Users/alice";
346
349
  conversation.hostUsername = "alice";
347
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
350
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
348
351
  expect(conversation.getWorkspaceTopLevelContext()!).toContain(
349
352
  "Host home directory: /Users/alice",
350
353
  );
@@ -355,7 +358,7 @@ describe("Conversation workspace cache state", () => {
355
358
  conversation.hostHomeDir = undefined;
356
359
  conversation.hostUsername = undefined;
357
360
  conversation.markWorkspaceTopLevelDirty();
358
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
361
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
359
362
 
360
363
  const block = conversation.getWorkspaceTopLevelContext();
361
364
  expect(block).not.toBeNull();
@@ -381,7 +384,7 @@ describe("Conversation workspace cache state", () => {
381
384
  expect(conversation.hostUsername).toBe("alice");
382
385
  expect(conversation.isWorkspaceTopLevelDirty()).toBe(true);
383
386
 
384
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
387
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
385
388
  const block = conversation.getWorkspaceTopLevelContext();
386
389
  expect(block!).toContain("Host home directory: /Users/alice");
387
390
  expect(block!).toContain("Host username: alice");
@@ -455,7 +458,7 @@ describe("Conversation workspace cache state", () => {
455
458
  hostUsername: "alice",
456
459
  });
457
460
  // Render once so the dirty flag clears.
458
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
461
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
459
462
  expect(conversation.isWorkspaceTopLevelDirty()).toBe(false);
460
463
 
461
464
  // Re-apply the same values — dirty flag should remain false so we don't
@@ -476,7 +479,7 @@ describe("Conversation workspace cache state", () => {
476
479
  hostHomeDir: "/Users/alice",
477
480
  hostUsername: "alice",
478
481
  });
479
- conversation.refreshWorkspaceTopLevelContextIfNeeded();
482
+ refreshWorkspaceTopLevelContextIfNeeded(conversation);
480
483
  expect(conversation.isWorkspaceTopLevelDirty()).toBe(false);
481
484
 
482
485
  // New values — should mark dirty so the next render picks them up.
@@ -500,7 +503,7 @@ describe("Conversation workspace cache state", () => {
500
503
 
501
504
  try {
502
505
  const tempConversation = makeConversation(workspaceRoot);
503
- tempConversation.refreshWorkspaceTopLevelContextIfNeeded();
506
+ refreshWorkspaceTopLevelContextIfNeeded(tempConversation);
504
507
 
505
508
  expect(tempConversation.getWorkspaceTopLevelContext()!).toContain(
506
509
  `Current conversation attachments: conversations/${legacyDirName}/attachments/`,
@@ -144,7 +144,7 @@ mock.module("../memory/conversation-crud.js", () => ({
144
144
  getMessageById: () => null,
145
145
  getLastUserTimestampBefore: () => 0,
146
146
  setLastNotifiedInferenceProfile: () => {},
147
- getConversationOverrideProfileFromRow: () => undefined,
147
+ resolveOverrideProfile: () => undefined,
148
148
  updateMessageMetadata: () => {},
149
149
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
150
150
  updateMessageContent: mock(() => {}),
@@ -183,6 +183,7 @@ mock.module("../memory/retrieval-budget.js", () => ({
183
183
  }));
184
184
  mock.module("../context/window-manager.js", () => ({
185
185
  ContextWindowManager: class {
186
+ updateConfig() {}
186
187
  shouldCompact() {
187
188
  return { needed: false, estimatedTokens: 0 };
188
189
  }
@@ -244,10 +245,11 @@ mock.module("../agent/loop.js", () => ({
244
245
  getActiveModel() {
245
246
  return undefined;
246
247
  }
247
- async run(
248
- messages: Message[],
249
- onEvent: (event: AgentEvent) => void,
250
- ): Promise<AgentLoopRunResult> {
248
+ async run(options: {
249
+ messages: Message[];
250
+ onEvent: (event: AgentEvent) => void;
251
+ }): Promise<AgentLoopRunResult> {
252
+ const { messages, onEvent } = options;
251
253
  // Prime the assistant row anchor — production code emits this from
252
254
  // `AgentLoop.run` just before `provider.sendMessage`.
253
255
  await onEvent({ type: "llm_call_started" });
@@ -295,6 +297,14 @@ mock.module("../memory/canonical-guardian-store.js", () => ({
295
297
  }));
296
298
 
297
299
  import { Conversation } from "../daemon/conversation.js";
300
+ import {
301
+ clearConversations,
302
+ findConversation,
303
+ removeSubagentConversation,
304
+ setConversation,
305
+ setSubagentConversation,
306
+ } from "../daemon/conversation-registry.js";
307
+ import { resolveWorkspaceTopLevelContext } from "../daemon/conversation-workspace.js";
298
308
  import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
299
309
 
300
310
  function makeConversation(): Conversation {
@@ -309,7 +319,7 @@ function makeConversation(): Conversation {
309
319
  };
310
320
  },
311
321
  };
312
- return new Conversation(
322
+ const conversation = new Conversation(
313
323
  "conv-1",
314
324
  provider,
315
325
  "system prompt",
@@ -317,6 +327,10 @@ function makeConversation(): Conversation {
317
327
  "/tmp",
318
328
  { maxTokens: 4096 },
319
329
  );
330
+ // Mirror production: top-level conversations are registered in the store, so
331
+ // the workspace injector can resolve them by id via the conversation registry.
332
+ setConversation(conversation.conversationId, conversation);
333
+ return conversation;
320
334
  }
321
335
 
322
336
  function messageText(message: Message): string {
@@ -335,6 +349,7 @@ describe("Conversation workspace injection", () => {
335
349
  runCalls = [];
336
350
  agentLoopScript = () => {};
337
351
  scanCallCount = 0;
352
+ clearConversations();
338
353
  resetPluginRegistryAndRegisterDefaults();
339
354
  });
340
355
 
@@ -417,11 +432,58 @@ describe("Conversation workspace injection", () => {
417
432
  });
418
433
  });
419
434
 
435
+ describe("Conversation workspace injection — subagents", () => {
436
+ beforeEach(() => {
437
+ scanCallCount = 0;
438
+ clearConversations();
439
+ resetPluginRegistryAndRegisterDefaults();
440
+ });
441
+
442
+ test("workspace context resolves for subagent conversations not in the store", () => {
443
+ const provider = {
444
+ name: "mock",
445
+ async sendMessage(): Promise<ProviderResponse> {
446
+ return {
447
+ content: [],
448
+ model: "mock",
449
+ usage: { inputTokens: 0, outputTokens: 0 },
450
+ stopReason: "end_turn",
451
+ };
452
+ },
453
+ };
454
+ const subagentId = "subagent-conv-1";
455
+ const conversation = new Conversation(
456
+ subagentId,
457
+ provider,
458
+ "system prompt",
459
+ () => {},
460
+ "/tmp",
461
+ { maxTokens: 4096 },
462
+ );
463
+ setSubagentConversation(subagentId, conversation);
464
+
465
+ try {
466
+ // Subagents live only in the manager's index, never in the
467
+ // eviction-managed store, so the store lookup must not see them.
468
+ expect(findConversation(subagentId)).toBeUndefined();
469
+
470
+ // The per-conversation workspace lookup still resolves them, so subagent
471
+ // turns retain workspace grounding.
472
+ const context = resolveWorkspaceTopLevelContext(subagentId);
473
+ expect(context).not.toBeNull();
474
+ expect(context).toContain("Root: /tmp");
475
+ } finally {
476
+ removeSubagentConversation(subagentId, conversation);
477
+ }
478
+ });
479
+ });
480
+
420
481
  describe("Conversation workspace dirty-refresh E2E", () => {
421
482
  beforeEach(() => {
422
483
  runCalls = [];
423
484
  agentLoopScript = () => {};
424
485
  scanCallCount = 0;
486
+ clearConversations();
425
487
  resetPluginRegistryAndRegisterDefaults();
426
488
  });
427
489