@vellumai/assistant 0.8.7 → 0.8.8

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 (387) hide show
  1. package/Dockerfile +20 -4
  2. package/docker-entrypoint.sh +4 -2
  3. package/docker-init-apt-root.sh +3 -1
  4. package/docker-kata-apt-env.sh +3 -1
  5. package/docker-kata-runtime-family.sh +12 -0
  6. package/docs/architecture/memory.md +1 -1
  7. package/docs/plugins.md +75 -79
  8. package/examples/plugins/echo/README.md +6 -12
  9. package/examples/plugins/echo/register.ts +0 -41
  10. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  11. package/openapi.yaml +3381 -348
  12. package/package.json +1 -1
  13. package/scripts/generate-openapi.ts +68 -41
  14. package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
  15. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  16. package/src/__tests__/agent-loop.test.ts +37 -87
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  18. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  19. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  20. package/src/__tests__/anthropic-provider.test.ts +95 -2
  21. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  22. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  23. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  24. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  25. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  26. package/src/__tests__/btw-routes.test.ts +62 -3
  27. package/src/__tests__/build-persisted-content.test.ts +184 -0
  28. package/src/__tests__/catalog-files.test.ts +1 -1
  29. package/src/__tests__/clawhub-files.test.ts +1 -1
  30. package/src/__tests__/compaction-pipeline.test.ts +1 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  32. package/src/__tests__/config-watcher.test.ts +1 -1
  33. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  34. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -2
  35. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
  37. package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
  38. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  39. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  40. package/src/__tests__/conversation-pairing.test.ts +4 -31
  41. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +26 -5
  43. package/src/__tests__/conversation-queue.test.ts +2 -0
  44. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  45. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +170 -229
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
  48. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  49. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  50. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  51. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  52. package/src/__tests__/conversation-title-service.test.ts +135 -2
  53. package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
  54. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  55. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  56. package/src/__tests__/dm-persistence.test.ts +5 -1
  57. package/src/__tests__/empty-response-hook.test.ts +304 -0
  58. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  59. package/src/__tests__/gemini-image-service.test.ts +13 -0
  60. package/src/__tests__/helpers/mock-provider.ts +110 -0
  61. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  62. package/src/__tests__/history-repair-hook.test.ts +1 -0
  63. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  64. package/src/__tests__/identity-routes.test.ts +248 -7
  65. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  66. package/src/__tests__/injector-background-turn.test.ts +2 -8
  67. package/src/__tests__/injector-chain.test.ts +106 -270
  68. package/src/__tests__/injector-disk-pressure.test.ts +3 -12
  69. package/src/__tests__/injector-document-comments.test.ts +2 -2
  70. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  71. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  72. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  73. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  74. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  75. package/src/__tests__/llm-usage-store.test.ts +223 -1
  76. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  77. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  78. package/src/__tests__/native-web-search.test.ts +191 -0
  79. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  80. package/src/__tests__/openai-image-service.test.ts +17 -0
  81. package/src/__tests__/openai-provider.test.ts +31 -1
  82. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  83. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  84. package/src/__tests__/pipeline-runner.test.ts +29 -39
  85. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  86. package/src/__tests__/plugin-bootstrap.test.ts +13 -28
  87. package/src/__tests__/plugin-registry.test.ts +0 -27
  88. package/src/__tests__/plugin-types.test.ts +2 -125
  89. package/src/__tests__/process-message-display-content.test.ts +6 -2
  90. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  91. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  92. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  93. package/src/__tests__/schedule-routes.test.ts +603 -2
  94. package/src/__tests__/schedule-store.test.ts +41 -0
  95. package/src/__tests__/schedule-tools.test.ts +35 -0
  96. package/src/__tests__/server-history-render.test.ts +314 -1
  97. package/src/__tests__/skillssh-files.test.ts +1 -1
  98. package/src/__tests__/system-prompt.test.ts +20 -0
  99. package/src/__tests__/task-scheduler.test.ts +162 -1
  100. package/src/__tests__/terminal-tools.test.ts +6 -1
  101. package/src/__tests__/title-generate-hook.test.ts +319 -0
  102. package/src/__tests__/tool-error-hook.test.ts +278 -0
  103. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  104. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  106. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  107. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  108. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  109. package/src/__tests__/usage-routes.test.ts +285 -1
  110. package/src/__tests__/user-plugin-loader.test.ts +2 -2
  111. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  112. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  113. package/src/agent/loop.ts +346 -442
  114. package/src/api/events/assistant-thinking-delta.ts +33 -0
  115. package/src/api/events/tool-output-chunk.ts +45 -0
  116. package/src/api/events/tool-use-preview-start.ts +32 -0
  117. package/src/api/events/trace-event.ts +69 -0
  118. package/src/api/index.ts +48 -13
  119. package/src/api/responses/conversation-message.ts +368 -0
  120. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  121. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  122. package/src/cli/commands/notifications.ts +112 -60
  123. package/src/config/assistant-feature-flags.ts +22 -11
  124. package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
  125. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  126. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  127. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  128. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  129. package/src/config/bundled-skills/document-editor/SKILL.md +1 -1
  130. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  131. package/src/config/feature-flag-cache.ts +3 -3
  132. package/src/config/feature-flag-registry.json +35 -3
  133. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  134. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  135. package/src/config/schemas/llm.ts +1 -0
  136. package/src/config/schemas/memory-v2.ts +8 -0
  137. package/src/config/schemas/memory-v3.ts +8 -0
  138. package/src/config/schemas/platform.ts +8 -0
  139. package/src/config/seed-inference-profiles.ts +2 -2
  140. package/src/config/skills.ts +13 -0
  141. package/src/context/compactor.ts +1 -1
  142. package/src/context/strip-injections.ts +122 -0
  143. package/src/context/token-estimator.ts +23 -0
  144. package/src/context/tool-result-truncation.ts +0 -23
  145. package/src/context/window-manager.ts +3 -6
  146. package/src/credential-execution/executable-discovery.ts +16 -0
  147. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  148. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  149. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  150. package/src/daemon/assistant-attachments.ts +1 -1
  151. package/src/daemon/config-watcher.ts +2 -2
  152. package/src/daemon/context-overflow-reducer.ts +0 -1
  153. package/src/daemon/conversation-agent-loop-handlers.ts +605 -153
  154. package/src/daemon/conversation-agent-loop.ts +281 -760
  155. package/src/daemon/conversation-history.ts +5 -4
  156. package/src/daemon/conversation-lifecycle.ts +3 -4
  157. package/src/daemon/conversation-messaging.ts +7 -6
  158. package/src/daemon/conversation-process.ts +11 -16
  159. package/src/daemon/conversation-runtime-assembly.ts +130 -347
  160. package/src/daemon/conversation-slash.ts +6 -25
  161. package/src/daemon/conversation-surfaces.ts +222 -4
  162. package/src/daemon/conversation-tool-setup.ts +2 -29
  163. package/src/daemon/conversation.ts +32 -14
  164. package/src/daemon/external-plugins-bootstrap.ts +9 -10
  165. package/src/daemon/handlers/config-a2a.ts +51 -36
  166. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  167. package/src/daemon/handlers/config-telegram.ts +16 -2
  168. package/src/daemon/handlers/shared.ts +156 -84
  169. package/src/daemon/handlers/skills.ts +39 -10
  170. package/src/daemon/lifecycle.ts +4 -0
  171. package/src/daemon/message-types/apps.ts +1 -29
  172. package/src/daemon/message-types/messages.ts +9 -57
  173. package/src/daemon/message-types/skills.ts +2 -0
  174. package/src/daemon/message-types/surfaces.ts +136 -3
  175. package/src/daemon/now-scratchpad.ts +21 -0
  176. package/src/daemon/orphan-reaper.test.ts +210 -0
  177. package/src/daemon/orphan-reaper.ts +240 -0
  178. package/src/daemon/persist-unsendable-image.ts +117 -0
  179. package/src/daemon/process-message.ts +1 -3
  180. package/src/daemon/trace-emitter.ts +6 -4
  181. package/src/daemon/trust-context.ts +19 -0
  182. package/src/daemon/wake-target-adapter.ts +3 -1
  183. package/src/home/home-greeting-cache.ts +24 -1
  184. package/src/ipc/gateway-client.test.ts +2 -2
  185. package/src/ipc/gateway-client.ts +3 -3
  186. package/src/media/gemini-image-service.ts +15 -0
  187. package/src/media/openai-image-service.ts +14 -0
  188. package/src/media/types.ts +34 -0
  189. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  190. package/src/memory/auth-fallback-events-store.ts +94 -0
  191. package/src/memory/conversation-title-service.ts +65 -41
  192. package/src/memory/db-init.ts +4 -0
  193. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  194. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  195. package/src/memory/jobs-store.ts +33 -0
  196. package/src/memory/jobs-worker.ts +31 -4
  197. package/src/memory/llm-usage-store.ts +224 -50
  198. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  199. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  200. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  201. package/src/memory/migrations/index.ts +2 -0
  202. package/src/memory/pkb/autoinject.ts +61 -0
  203. package/src/memory/pkb/context.ts +50 -0
  204. package/src/memory/pkb/types.ts +14 -0
  205. package/src/memory/schedule-attribution-sql.ts +104 -0
  206. package/src/memory/schema/infrastructure.ts +16 -0
  207. package/src/memory/usage-grouped-buckets.ts +6 -1
  208. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
  209. package/src/memory/v2/consolidation-job.ts +1 -1
  210. package/src/memory/v3/__tests__/health.test.ts +16 -0
  211. package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
  212. package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
  213. package/src/memory/v3/__tests__/router.test.ts +101 -29
  214. package/src/memory/v3/__tests__/selector.test.ts +93 -27
  215. package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
  216. package/src/memory/v3/health.ts +0 -0
  217. package/src/memory/v3/llm-retry.ts +32 -0
  218. package/src/memory/v3/orchestrate.ts +26 -14
  219. package/src/memory/v3/provider-blocks.ts +15 -5
  220. package/src/memory/v3/router.ts +48 -42
  221. package/src/memory/v3/selector.ts +57 -42
  222. package/src/memory/v3/shadow-plugin.ts +47 -15
  223. package/src/memory/v3/types.ts +8 -0
  224. package/src/notifications/conversation-pairing.ts +8 -15
  225. package/src/notifications/decision-engine.ts +6 -3
  226. package/src/notifications/home-feed-side-effect.ts +12 -1
  227. package/src/permissions/prompter.ts +4 -0
  228. package/src/plugin-api/constants.ts +4 -0
  229. package/src/plugin-api/index.ts +8 -1
  230. package/src/plugin-api/types.ts +151 -1
  231. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  232. package/src/plugins/defaults/empty-response/register.ts +8 -13
  233. package/src/plugins/defaults/index.ts +1 -15
  234. package/src/plugins/defaults/injectors/register.ts +243 -74
  235. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -0
  236. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  237. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  238. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  239. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  240. package/src/plugins/defaults/title-generate/package.json +1 -1
  241. package/src/plugins/defaults/title-generate/register.ts +18 -18
  242. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  243. package/src/plugins/defaults/tool-error/package.json +1 -1
  244. package/src/plugins/defaults/tool-error/register.ts +9 -21
  245. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  246. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  247. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  248. package/src/plugins/pipeline.ts +6 -18
  249. package/src/plugins/registry.ts +8 -25
  250. package/src/plugins/types.ts +43 -474
  251. package/src/proactive-artifact/aux-message-injector.ts +3 -3
  252. package/src/proactive-artifact/job.test.ts +7 -12
  253. package/src/prompts/__tests__/system-prompt.test.ts +36 -0
  254. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -0
  255. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  256. package/src/prompts/templates/system-sections.ts +15 -0
  257. package/src/providers/anthropic/client.ts +37 -29
  258. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  259. package/src/providers/openai/chat-completions-provider.ts +44 -0
  260. package/src/providers/openrouter/client.ts +1 -0
  261. package/src/providers/placeholder-sentinels.ts +35 -0
  262. package/src/runtime/__tests__/agent-wake.test.ts +5 -1
  263. package/src/runtime/agent-wake.ts +2 -2
  264. package/src/runtime/assistant-event-hub.ts +36 -6
  265. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  266. package/src/runtime/http-router.ts +16 -21
  267. package/src/runtime/http-types.ts +16 -70
  268. package/src/runtime/pending-interactions.ts +1 -0
  269. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  270. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  271. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  272. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  273. package/src/runtime/routes/app-management-routes.ts +6 -117
  274. package/src/runtime/routes/app-routes.ts +13 -15
  275. package/src/runtime/routes/attachment-routes.ts +26 -15
  276. package/src/runtime/routes/avatar-routes.ts +26 -0
  277. package/src/runtime/routes/btw-routes.ts +29 -23
  278. package/src/runtime/routes/consolidation-routes.ts +120 -20
  279. package/src/runtime/routes/conversation-query-routes.ts +2 -0
  280. package/src/runtime/routes/conversation-routes.ts +358 -184
  281. package/src/runtime/routes/documents-routes.ts +4 -0
  282. package/src/runtime/routes/domain-routes.ts +51 -37
  283. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  284. package/src/runtime/routes/events-routes.ts +28 -34
  285. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  286. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  287. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  288. package/src/runtime/routes/identity-routes.ts +208 -17
  289. package/src/runtime/routes/image-generation-routes.ts +40 -2
  290. package/src/runtime/routes/index.ts +2 -0
  291. package/src/runtime/routes/integrations/a2a.ts +12 -10
  292. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  293. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  294. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  295. package/src/runtime/routes/integrations/telegram.ts +6 -0
  296. package/src/runtime/routes/integrations/twilio.ts +42 -0
  297. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  298. package/src/runtime/routes/log-export-routes.ts +8 -0
  299. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  300. package/src/runtime/routes/memory-v3-routes.ts +50 -28
  301. package/src/runtime/routes/oauth-apps.ts +66 -12
  302. package/src/runtime/routes/oauth-providers.ts +44 -5
  303. package/src/runtime/routes/platform-routes.ts +81 -5
  304. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  305. package/src/runtime/routes/playground/force-compact.ts +1 -1
  306. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  307. package/src/runtime/routes/schedule-routes.ts +152 -42
  308. package/src/runtime/routes/secret-routes.ts +14 -2
  309. package/src/runtime/routes/skills-routes.ts +43 -14
  310. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  311. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  312. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  313. package/src/runtime/routes/tts-routes.ts +35 -0
  314. package/src/runtime/routes/types.ts +66 -8
  315. package/src/runtime/routes/usage-routes.ts +47 -39
  316. package/src/runtime/routes/webhook-routes.ts +41 -2
  317. package/src/runtime/routes/workspace-routes.ts +4 -0
  318. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  319. package/src/runtime/services/analyze-conversation.ts +2 -2
  320. package/src/schedule/schedule-store.ts +20 -1
  321. package/src/schedule/schedule-usage-store.ts +83 -0
  322. package/src/schedule/scheduler.ts +12 -5
  323. package/src/skills/catalog-files.ts +2 -2
  324. package/src/skills/catalog-install.ts +3 -0
  325. package/src/skills/categories-cache.ts +118 -0
  326. package/src/skills/clawhub-files.ts +1 -2
  327. package/src/skills/skillssh-files.ts +1 -2
  328. package/src/telemetry/types.ts +29 -1
  329. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  330. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  331. package/src/tools/executor.ts +1 -53
  332. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  333. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  334. package/src/tools/network/web-search-error.test.ts +248 -0
  335. package/src/tools/network/web-search-error.ts +267 -0
  336. package/src/tools/network/web-search.ts +207 -48
  337. package/src/tools/schedule/create.ts +2 -0
  338. package/src/tools/terminal/safe-env.ts +10 -1
  339. package/src/tools/ui-surface/definitions.ts +9 -1
  340. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  341. package/src/tts/provider-catalog.ts +76 -1
  342. package/src/util/mutex.ts +47 -0
  343. package/src/workspace/git-service.ts +1 -42
  344. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  345. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  346. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +93 -0
  347. package/src/workspace/migrations/registry.ts +6 -0
  348. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  349. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  350. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  351. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  352. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  353. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  354. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  355. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  356. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  357. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  358. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  359. package/src/gallery/default-gallery.ts +0 -1359
  360. package/src/gallery/gallery-manifest.ts +0 -28
  361. package/src/home/feature-gate.ts +0 -22
  362. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  363. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  364. package/src/plugins/defaults/injectors/package.json +0 -15
  365. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  366. package/src/plugins/defaults/llm-call/package.json +0 -15
  367. package/src/plugins/defaults/llm-call/register.ts +0 -45
  368. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  369. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  370. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  371. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  372. package/src/plugins/defaults/persistence/package.json +0 -15
  373. package/src/plugins/defaults/persistence/register.ts +0 -38
  374. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  375. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  376. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  377. package/src/plugins/defaults/token-estimate/package.json +0 -15
  378. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  379. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  380. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  381. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  382. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  383. package/src/plugins/defaults/tool-execute/package.json +0 -15
  384. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  385. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  386. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  387. package/src/skills/category-inference.ts +0 -111
@@ -1,29 +1,25 @@
1
1
  /**
2
- * Tests for the plugin-driven runtime-injection chain (PR 21 of the
3
- * `agent-plugin-system` plan).
2
+ * Tests for the static runtime-injection chain.
4
3
  *
5
4
  * Covers:
6
5
  *
7
- * 1. The ten default injectors registered by `defaultInjectorsPlugin` come
8
- * back from `getInjectors()` in the documented order
9
- * (disk-pressure-warningworkspace-context → unified-turn-context →
10
- * pkb-contextpkb-remindermemory-v2-staticnow-md
6
+ * 1. The default injectors ({@link defaultInjectors}) are listed in the
7
+ * documented order (disk-pressure-warning workspace-context
8
+ * background-turnunified-turn-context → pkb-context → pkb-reminder →
9
+ * memory-v2-staticnow-mdactive-documentsdocument-comments
11
10
  * subagent-status → slack-messages → thread-focus).
12
- * 2. A third-party-registered injector at `order: 25` slots between
13
- * `unified-turn-context` (order 20) and `pkb` (order 30), proving the
14
- * extensibility contract.
15
- * 3. `composeInjectorChain` concatenates non-null blocks with a blank-line
16
- * separator and yields an empty string when every injector opts out — the
17
- * latter matches pre-PR behavior for the golden-path conversation state
18
- * (all defaults return `null` in this PR).
19
- * 4. `applyRuntimeInjections` with an empty `turnContext` chain leaves
20
- * `blocks.injectorChainBlock` undefined, preserving the existing snapshot
21
- * for conversations that don't opt into the chain.
22
- * 5. `applyRuntimeInjections` surfaces the composed chain output on
23
- * `blocks.injectorChainBlock` when a third-party injector contributes
24
- * content.
11
+ * 2. The assembled {@link injectorChain} sorts the defaults together with the
12
+ * memory-v3 injector by ascending `order`, so memory-v3 (order 1000) lands
13
+ * last.
14
+ * 3. `composeInjectorChain` yields an empty string when every injector opts out
15
+ * the golden-path conversation state where all defaults return `null`.
16
+ * 4. `applyRuntimeInjections` splices each default injector's block into the
17
+ * correct position in the per-turn message array, and gates blocks by
18
+ * injection mode.
25
19
  */
26
20
 
21
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
22
+ import { dirname, join } from "node:path";
27
23
  import { beforeEach, describe, expect, mock, test } from "bun:test";
28
24
 
29
25
  // This test exercises v1 PKB injection. `config.memory.v2.enabled`
@@ -44,20 +40,15 @@ mock.module("../config/loader.js", () => ({
44
40
 
45
41
  const { applyRuntimeInjections, composeInjectorChain } =
46
42
  await import("../daemon/conversation-runtime-assembly.js");
47
- const { DEFAULT_INJECTOR_ORDER, defaultInjectorsPlugin } =
43
+ const { DEFAULT_INJECTOR_ORDER, defaultInjectors } =
48
44
  await import("../plugins/defaults/injectors/register.js");
49
- import {
50
- getInjectors,
51
- registerPlugin,
52
- resetPluginRegistryForTests,
53
- } from "../plugins/registry.js";
54
- import type {
55
- InjectionBlock,
56
- Injector,
57
- Plugin,
58
- TurnContext,
59
- } from "../plugins/types.js";
45
+ const { getInjectorChain } =
46
+ await import("../plugins/defaults/memory-retrieval/injector-chain.js");
47
+ import { buildPkbReminder } from "../daemon/pkb-reminder-builder.js";
48
+ import { getPkbRoot } from "../memory/pkb/types.js";
49
+ import type { TurnContext } from "../plugins/types.js";
60
50
  import type { Message } from "../providers/types.js";
51
+ import { getWorkspacePromptPath } from "../util/platform.js";
61
52
 
62
53
  /** A fake TurnContext sufficient for driving `composeInjectorChain`. */
63
54
  function makeTurnContext(): TurnContext {
@@ -72,26 +63,45 @@ function makeTurnContext(): TurnContext {
72
63
  };
73
64
  }
74
65
 
75
- /** Build a tiny valid plugin wrapping an array of injectors. */
76
- function wrapInPlugin(name: string, injectors: Injector[]): Plugin {
77
- return {
78
- manifest: {
79
- name,
80
- version: "0.0.1",
81
- },
82
- injectors,
83
- };
66
+ // The pkb-context and pkb-reminder injectors both derive PKB-active state from
67
+ // the workspace itself — `readPkbContext()` returning content behind the
68
+ // personal-memory trust gate — rather than from a threaded flag. Seed the file
69
+ // with exactly the content the test expects so the `<knowledge_base>` block
70
+ // renders deterministically; clear it between tests so suites that assert the
71
+ // PKB injectors are absent stay unaffected.
72
+ function seedPkbContent(content: string): void {
73
+ const root = getPkbRoot();
74
+ mkdirSync(root, { recursive: true });
75
+ writeFileSync(join(root, "INDEX.md"), content, "utf-8");
76
+ }
77
+
78
+ function clearPkbContent(): void {
79
+ rmSync(getPkbRoot(), { recursive: true, force: true });
80
+ }
81
+
82
+ // The now-md injector sources NOW.md from the workspace itself — behind the
83
+ // personal-memory trust gate and the `scratchpadInjection` config toggle —
84
+ // rather than from a threaded option. Seed the file so the injector fires;
85
+ // clear it between tests so suites that assert NOW.md is absent stay
86
+ // unaffected.
87
+ function seedNowScratchpad(content: string): void {
88
+ const nowPath = getWorkspacePromptPath("NOW.md");
89
+ mkdirSync(dirname(nowPath), { recursive: true });
90
+ writeFileSync(nowPath, content, "utf-8");
91
+ }
92
+
93
+ function clearNowScratchpad(): void {
94
+ rmSync(getWorkspacePromptPath("NOW.md"), { force: true });
84
95
  }
85
96
 
86
97
  describe("injector chain", () => {
87
98
  beforeEach(() => {
88
- resetPluginRegistryForTests();
99
+ clearPkbContent();
100
+ clearNowScratchpad();
89
101
  });
90
102
 
91
- test("defaultInjectorsPlugin registers the defaults in the documented order", () => {
92
- registerPlugin(defaultInjectorsPlugin);
93
-
94
- const names = getInjectors().map((i) => i.name);
103
+ test("defaultInjectors lists the defaults in the documented order", () => {
104
+ const names = defaultInjectors.map((i) => i.name);
95
105
  expect(names).toEqual([
96
106
  "disk-pressure-warning",
97
107
  "workspace-context",
@@ -109,10 +119,8 @@ describe("injector chain", () => {
109
119
  ]);
110
120
  });
111
121
 
112
- test("default injector order constants match the registered order values", () => {
113
- registerPlugin(defaultInjectorsPlugin);
114
-
115
- const byName = new Map(getInjectors().map((i) => [i.name, i.order]));
122
+ test("default injector order constants match the listed order values", () => {
123
+ const byName = new Map(defaultInjectors.map((i) => [i.name, i.order]));
116
124
  expect(byName.get("disk-pressure-warning")).toBe(
117
125
  DEFAULT_INJECTOR_ORDER.diskPressureWarning,
118
126
  );
@@ -143,114 +151,38 @@ describe("injector chain", () => {
143
151
  expect(byName.get("thread-focus")).toBe(DEFAULT_INJECTOR_ORDER.threadFocus);
144
152
  });
145
153
 
146
- test("a third-party injector at order 25 slots between unified-turn-context (20) and pkb-context (30)", () => {
147
- registerPlugin(defaultInjectorsPlugin);
148
-
149
- const middleInjector: Injector = {
150
- name: "plugin-25",
151
- order: 25,
152
- async produce() {
153
- return null;
154
- },
155
- };
156
- registerPlugin(wrapInPlugin("third-party", [middleInjector]));
157
-
158
- const names = getInjectors().map((i) => i.name);
159
- expect(names).toEqual([
160
- "disk-pressure-warning", // 5
161
- "workspace-context", // 10
162
- "background-turn", // 15
163
- "unified-turn-context", // 20
164
- "plugin-25", // 25 — slots in
165
- "pkb-context", // 30
166
- "pkb-reminder", // 35
167
- "memory-v2-static", // 38
168
- "now-md", // 40
169
- "active-documents", // 45
170
- "document-comments", // 46
171
- "subagent-status", // 50
172
- "slack-messages", // 60
173
- "thread-focus", // 70
154
+ test("the injector chain sorts the defaults plus memory-v3 by ascending order", () => {
155
+ // The assembled chain merges the defaults with the memory-v3 injector and
156
+ // sorts by `order`, so memory-v3 (order 1000) sits last.
157
+ const chain = getInjectorChain();
158
+ const orders = chain.map((i) => i.order);
159
+ expect(orders).toEqual([...orders].sort((a, b) => a - b));
160
+ expect(chain[chain.length - 1]?.name).toBe("memory-v3-shadow");
161
+ expect(chain.map((i) => i.name)).toEqual([
162
+ ...defaultInjectors.map((i) => i.name),
163
+ "memory-v3-shadow",
174
164
  ]);
175
165
  });
176
166
 
177
167
  test("composeInjectorChain returns empty string when every injector opts out", async () => {
178
- // The default chain is the golden-path: all ten defaults return `null`
179
- // on an empty turn context, so the composed block is an empty string.
180
- registerPlugin(defaultInjectorsPlugin);
181
-
182
- const composed = await composeInjectorChain(makeTurnContext());
183
- expect(composed).toBe("");
184
- });
185
-
186
- test("composeInjectorChain returns empty string when registry is empty", async () => {
187
- // No plugins registered — the chain is a no-op and must return an empty
188
- // string (not throw, not undefined). Callers rely on this to treat the
189
- // chain as purely additive.
168
+ // The default chain is the golden-path: every default returns `null` on an
169
+ // empty turn context, so the composed block is an empty string.
190
170
  const composed = await composeInjectorChain(makeTurnContext());
191
171
  expect(composed).toBe("");
192
172
  });
193
173
 
194
- test("composeInjectorChain concatenates non-null blocks in order with blank-line separators", async () => {
195
- const first: Injector = {
196
- name: "a",
197
- order: 5,
198
- async produce(): Promise<InjectionBlock> {
199
- return { id: "a", text: "BLOCK_A" };
200
- },
201
- };
202
- const second: Injector = {
203
- name: "b",
204
- order: 15,
205
- async produce(): Promise<InjectionBlock> {
206
- return { id: "b", text: "BLOCK_B" };
207
- },
208
- };
209
- const skipped: Injector = {
210
- name: "c",
211
- order: 25,
212
- async produce() {
213
- return null;
214
- },
215
- };
216
- // Register the higher-order one first to prove the chain sorts by `order`
217
- // rather than registration order.
218
- registerPlugin(wrapInPlugin("higher", [second]));
219
- registerPlugin(wrapInPlugin("lower", [first]));
220
- registerPlugin(wrapInPlugin("opts-out", [skipped]));
221
-
222
- const composed = await composeInjectorChain(makeTurnContext());
223
- expect(composed).toBe("BLOCK_A\n\nBLOCK_B");
224
- });
225
-
226
- test("composeInjectorChain skips blocks with empty text", async () => {
227
- const emitEmpty: Injector = {
228
- name: "empty",
229
- order: 10,
230
- async produce(): Promise<InjectionBlock> {
231
- return { id: "empty", text: "" };
232
- },
233
- };
234
- const emitReal: Injector = {
235
- name: "real",
236
- order: 20,
237
- async produce(): Promise<InjectionBlock> {
238
- return { id: "real", text: "CONTENT" };
239
- },
240
- };
241
- registerPlugin(wrapInPlugin("plugin", [emitEmpty, emitReal]));
242
-
243
- const composed = await composeInjectorChain(makeTurnContext());
244
- expect(composed).toBe("CONTENT");
245
- });
174
+ // ── Integration tests ───────────────────────────────────────────────
175
+ //
176
+ // These assertions exercise the real per-turn injection pipeline with
177
+ // the static chain active, verifying that each default injector emits
178
+ // the expected content in the correct position in the final user-tail
179
+ // content.
246
180
 
247
181
  test("applyRuntimeInjections leaves injectorChainBlock undefined when defaults opt out", async () => {
248
- // Golden-path snapshot: with only default injectors (all returning
182
+ // Golden-path snapshot: with the static chain (all defaults returning
249
183
  // `null`), `applyRuntimeInjections` reports no chain output, so the
250
184
  // historical `blocks` shape is preserved byte-for-byte for any
251
- // conversation that doesn't involve third-party injectors.
252
- registerPlugin(defaultInjectorsPlugin);
253
-
185
+ // conversation that doesn't drive a known injector.
254
186
  const runMessages: Message[] = [
255
187
  { role: "user", content: [{ type: "text", text: "hello" }] },
256
188
  ];
@@ -265,68 +197,27 @@ describe("injector chain", () => {
265
197
  expect(result.messages).toEqual(runMessages);
266
198
  });
267
199
 
268
- test("applyRuntimeInjections surfaces third-party injector output on blocks.injectorChainBlock", async () => {
269
- registerPlugin(defaultInjectorsPlugin);
270
- registerPlugin(
271
- wrapInPlugin("third-party-25", [
272
- {
273
- name: "plugin-25",
274
- order: 25,
275
- async produce(): Promise<InjectionBlock> {
276
- return { id: "plugin-25", text: "THIRD_PARTY_BLOCK" };
277
- },
278
- },
279
- ]),
280
- );
281
-
200
+ test("applyRuntimeInjections without turnContext still runs the chain under a synthesized context", async () => {
201
+ // The static chain is the canonical injection path, so
202
+ // `applyRuntimeInjections` must drive it even when the caller doesn't
203
+ // pass a `turnContext`. Call sites that rely on option fields to opt
204
+ // into injections continue to work because the synthesized fallback
205
+ // exposes `injectionInputs` built from `options`.
282
206
  const runMessages: Message[] = [
283
207
  { role: "user", content: [{ type: "text", text: "hi" }] },
284
208
  ];
285
209
 
286
210
  const result = await applyRuntimeInjections(runMessages, {
287
- turnContext: makeTurnContext(),
211
+ unifiedTurnContext: "<turn_context>\nsynthesized\n</turn_context>",
288
212
  });
289
213
 
290
- expect(result.blocks.injectorChainBlock).toBe("THIRD_PARTY_BLOCK");
291
- });
292
-
293
- test("applyRuntimeInjections without turnContext still runs the chain under a synthesized context", async () => {
294
- // Post-G2.1 semantics: the default chain is the canonical injection
295
- // path, so `applyRuntimeInjections` must drive it even when the caller
296
- // doesn't pass a `turnContext`. Test/legacy call sites that rely on
297
- // option fields to opt into injections continue to work because the
298
- // synthesized fallback exposes `injectionInputs` built from `options`.
299
- registerPlugin(defaultInjectorsPlugin);
300
- registerPlugin(
301
- wrapInPlugin("third-party-25", [
302
- {
303
- name: "plugin-25",
304
- order: 25,
305
- async produce(): Promise<InjectionBlock> {
306
- return { id: "plugin-25", text: "THIRD_PARTY_BLOCK" };
307
- },
308
- },
309
- ]),
214
+ // The unified-turn-context injector fires even without a caller-supplied
215
+ // turnContext, proving the chain runs under the synthesized context.
216
+ expect(result.blocks.unifiedTurnContext).toBe(
217
+ "<turn_context>\nsynthesized\n</turn_context>",
310
218
  );
311
-
312
- const runMessages: Message[] = [
313
- { role: "user", content: [{ type: "text", text: "hi" }] },
314
- ];
315
-
316
- const result = await applyRuntimeInjections(runMessages, {});
317
-
318
- // Third-party injector runs even without a caller-supplied turnContext.
319
- expect(result.blocks.injectorChainBlock).toBe("THIRD_PARTY_BLOCK");
320
219
  });
321
220
 
322
- // ── Integration tests ───────────────────────────────────────────────
323
- //
324
- // These assertions exercise the real per-turn injection pipeline with
325
- // the default chain active, verifying that each default injector emits
326
- // the expected content and that a third-party injector registered at a
327
- // fractional `order` slots into the correct position in the final
328
- // user-tail content.
329
-
330
221
  test("golden-path: default chain injects workspace + unified-turn + PKB + NOW + subagent in the correct positions", async () => {
331
222
  // Canonical golden-path conversation state: full mode, non-Slack
332
223
  // channel, workspace context + unified-turn + PKB + NOW + subagent
@@ -335,14 +226,21 @@ describe("injector chain", () => {
335
226
  // [workspace] ← prepend order 10 (topmost)
336
227
  // [unified-turn] ← prepend order 20
337
228
  // [now-md] ← after-memory-prefix order 40 (highest order, closest to memory)
338
- // [pkb-reminder] ← after-memory-prefix order 35 (skipped when pkbActive=false)
229
+ // [pkb-reminder] ← after-memory-prefix order 35
339
230
  // [pkb-context] ← after-memory-prefix order 30
340
231
  // [user text]
341
232
  // [subagent] ← append order 50
342
233
  //
343
234
  // No memory prefix blocks in this scenario, so after-memory-prefix
344
- // lands right at the head of the user-text cluster.
345
- registerPlugin(defaultInjectorsPlugin);
235
+ // lands right at the head of the user-text cluster. The pkb-context and
236
+ // pkb-reminder injectors both fire off the seeded PKB content under the
237
+ // guardian trust on `makeTurnContext()` — pkb-context renders the seeded
238
+ // `<knowledge_base>` body, pkb-reminder the flat `<system_reminder>`
239
+ // (no graph handle is registered, so it has no search hints).
240
+ const pkbContent = "essentials of the project";
241
+ seedPkbContent(pkbContent);
242
+ const nowContent = "Current focus: shipping G2.1";
243
+ seedNowScratchpad(nowContent);
346
244
 
347
245
  const runMessages: Message[] = [
348
246
  { role: "user", content: [{ type: "text", text: "What next?" }] },
@@ -352,8 +250,6 @@ describe("injector chain", () => {
352
250
  "<workspace>\nRoot: /sandbox\nDirectories: src, lib\n</workspace>";
353
251
  const unifiedTurn =
354
252
  "<turn_context>\ncurrent_time: 2026-04-22\ninterface: macos\n</turn_context>";
355
- const pkbContent = "essentials of the project";
356
- const nowContent = "Current focus: shipping G2.1";
357
253
  const subagentBlock =
358
254
  '<active_subagents>\n- [running] "worker" (sub-1) | elapsed: 5s\n</active_subagents>';
359
255
 
@@ -361,9 +257,6 @@ describe("injector chain", () => {
361
257
  turnContext: makeTurnContext(),
362
258
  workspaceTopLevelContext: workspaceText,
363
259
  unifiedTurnContext: unifiedTurn,
364
- pkbContext: pkbContent,
365
- pkbActive: false, // disable reminder-branch to keep the snapshot small
366
- nowScratchpad: nowContent,
367
260
  subagentStatusBlock: subagentBlock,
368
261
  });
369
262
 
@@ -378,14 +271,17 @@ describe("injector chain", () => {
378
271
  // placement says it does.
379
272
  expect(texts[0]).toBe(workspaceText); // prepend order 10
380
273
  expect(texts[1]).toBe(unifiedTurn); // prepend order 20
381
- // NOW and PKB are both after-memory-prefix; NOW runs later so sits above PKB.
274
+ // NOW, pkb-reminder and pkb-context are all after-memory-prefix; higher
275
+ // order splices closer to the memory prefix, so NOW sits above the
276
+ // reminder, which sits above the knowledge_base.
382
277
  expect(texts[2]).toBe(
383
278
  `<NOW.md Always keep this up to date; keep under 10 lines>\n${nowContent}\n</NOW.md>`,
384
279
  );
385
- expect(texts[3]).toBe(`<knowledge_base>\n${pkbContent}\n</knowledge_base>`);
386
- expect(texts[4]).toBe("What next?"); // user's typed text
387
- expect(texts[5]).toBe(subagentBlock); // append order 50
388
- expect(texts).toHaveLength(6);
280
+ expect(texts[3]).toBe(buildPkbReminder([])); // pkb-reminder order 35
281
+ expect(texts[4]).toBe(`<knowledge_base>\n${pkbContent}\n</knowledge_base>`);
282
+ expect(texts[5]).toBe("What next?"); // user's typed text
283
+ expect(texts[6]).toBe(subagentBlock); // append order 50
284
+ expect(texts).toHaveLength(7);
389
285
 
390
286
  // Block metadata captures for DB persistence — one field per default
391
287
  // injector whose output the loader rehydrates from message metadata.
@@ -399,59 +295,6 @@ describe("injector chain", () => {
399
295
  );
400
296
  });
401
297
 
402
- test("third-party prepend injector at order 15 lands between workspace (10) and unified-turn-context (20) in the final message", async () => {
403
- // Proves the extensibility contract end-to-end: a plugin-registered
404
- // injector at `order: 15` with `placement: "prepend-user-tail"` slots
405
- // between the workspace prepend (order 10) and the unified-turn
406
- // prepend (order 20). Because descending-order application for
407
- // prepends puts the lowest-`order` injector topmost, workspace ends
408
- // up on top, then plugin@15, then unified-turn.
409
- registerPlugin(defaultInjectorsPlugin);
410
- registerPlugin(
411
- wrapInPlugin("third-party-15-prepend", [
412
- {
413
- name: "plugin-15",
414
- order: 15, // between workspace (10) and unified-turn (20)
415
- async produce(): Promise<InjectionBlock> {
416
- return {
417
- id: "plugin-15",
418
- text: "<plugin_block_15/>",
419
- placement: "prepend-user-tail",
420
- };
421
- },
422
- },
423
- ]),
424
- );
425
-
426
- const runMessages: Message[] = [
427
- { role: "user", content: [{ type: "text", text: "hi" }] },
428
- ];
429
-
430
- const workspaceText = "<workspace>\nRoot: /sandbox\n</workspace>";
431
- const unifiedTurn =
432
- "<turn_context>\ncurrent_time: 2026-04-22\n</turn_context>";
433
-
434
- const result = await applyRuntimeInjections(runMessages, {
435
- turnContext: makeTurnContext(),
436
- workspaceTopLevelContext: workspaceText,
437
- unifiedTurnContext: unifiedTurn,
438
- });
439
-
440
- const tail = result.messages[result.messages.length - 1];
441
- expect(tail.role).toBe("user");
442
- const texts = tail.content
443
- .filter((b): b is { type: "text"; text: string } => b.type === "text")
444
- .map((b) => b.text);
445
-
446
- // Descending-order application for prepends puts the lowest-`order`
447
- // injector topmost, so order 10 (workspace) ends up on top, then
448
- // plugin@15 below it, then unified-turn (order 20) below that.
449
- expect(texts[0]).toBe(workspaceText);
450
- expect(texts[1]).toBe("<plugin_block_15/>");
451
- expect(texts[2]).toBe(unifiedTurn);
452
- expect(texts[3]).toBe("hi");
453
- });
454
-
455
298
  test("slack-messages injector replaces runMessages when a chronological transcript is provided", async () => {
456
299
  // End-to-end verification for the `replace-run-messages` placement:
457
300
  // a Slack channel turn with a pre-rendered chronological transcript
@@ -459,8 +302,6 @@ describe("injector chain", () => {
459
302
  // after-memory/append placements run. Memory-prefix blocks from the
460
303
  // original tail are re-prepended onto the new tail so PKB / NOW
461
304
  // splices still find them.
462
- registerPlugin(defaultInjectorsPlugin);
463
-
464
305
  const originalRun: Message[] = [
465
306
  {
466
307
  role: "user",
@@ -522,8 +363,6 @@ describe("injector chain", () => {
522
363
  // opts out in minimal mode, so the tail should carry only the turn
523
364
  // context prepend plus any non-injector hardcoded content (none
524
365
  // here).
525
- registerPlugin(defaultInjectorsPlugin);
526
-
527
366
  const result = await applyRuntimeInjections(
528
367
  [
529
368
  {
@@ -536,9 +375,6 @@ describe("injector chain", () => {
536
375
  mode: "minimal",
537
376
  workspaceTopLevelContext: "<workspace>...</workspace>",
538
377
  unifiedTurnContext: "<turn_context>...</turn_context>",
539
- pkbContext: "kbody",
540
- pkbActive: true,
541
- nowScratchpad: "nowbody",
542
378
  subagentStatusBlock: "<active_subagents>...</active_subagents>",
543
379
  },
544
380
  );
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, test } from "bun:test";
1
+ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
4
  applyRuntimeInjections,
@@ -6,18 +6,14 @@ import {
6
6
  } from "../daemon/conversation-runtime-assembly.js";
7
7
  import {
8
8
  DEFAULT_INJECTOR_ORDER,
9
- defaultInjectorsPlugin,
9
+ defaultInjectors,
10
10
  DISK_PRESSURE_WARNING_PROMPT,
11
11
  } from "../plugins/defaults/injectors/register.js";
12
- import {
13
- registerPlugin,
14
- resetPluginRegistryForTests,
15
- } from "../plugins/registry.js";
16
12
  import type { Injector, TurnContext } from "../plugins/types.js";
17
13
  import type { Message } from "../providers/types.js";
18
14
 
19
15
  function findInjector(name: string): Injector {
20
- const injector = defaultInjectorsPlugin.injectors?.find(
16
+ const injector = defaultInjectors.find(
21
17
  (candidate) => candidate.name === name,
22
18
  );
23
19
  if (!injector) {
@@ -50,11 +46,6 @@ const diskPressureInjector = findInjector("disk-pressure-warning");
50
46
  const cleanupContext = { cleanupModeActive: true };
51
47
 
52
48
  describe("disk-pressure-warning injector", () => {
53
- beforeEach(() => {
54
- resetPluginRegistryForTests();
55
- registerPlugin(defaultInjectorsPlugin);
56
- });
57
-
58
49
  test("emits the exact cleanup prompt during disk pressure cleanup mode", async () => {
59
50
  const block = await diskPressureInjector.produce(
60
51
  makeContext({
@@ -8,12 +8,12 @@ mock.module("../documents/document-comments-store.js", () => ({
8
8
  listComments: (...args: unknown[]) => listCommentsMock(...args),
9
9
  }));
10
10
 
11
- const { DEFAULT_INJECTOR_ORDER, defaultInjectorsPlugin } =
11
+ const { DEFAULT_INJECTOR_ORDER, defaultInjectors } =
12
12
  await import("../plugins/defaults/injectors/register.js");
13
13
  import type { Injector, TurnContext } from "../plugins/types.js";
14
14
 
15
15
  function findInjector(name: string): Injector {
16
- const injector = defaultInjectorsPlugin.injectors?.find(
16
+ const injector = defaultInjectors.find(
17
17
  (candidate) => candidate.name === name,
18
18
  );
19
19
  if (!injector) {
@@ -12,7 +12,9 @@
12
12
  * search so the reminder-with-hints branch can resolve deterministically
13
13
  * when called.
14
14
  */
15
- import { beforeEach, describe, expect, mock, test } from "bun:test";
15
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
16
+ import { dirname, join } from "node:path";
17
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
16
18
 
17
19
  let v2Active = false;
18
20
 
@@ -20,7 +22,12 @@ const realLoader = await import("../config/loader.js");
20
22
 
21
23
  mock.module("../config/loader.js", () => ({
22
24
  ...realLoader,
23
- getConfig: () => ({ memory: { v2: { enabled: v2Active } } }),
25
+ getConfig: () => ({
26
+ memory: {
27
+ v2: { enabled: v2Active },
28
+ retrieval: { scratchpadInjection: { enabled: true } },
29
+ },
30
+ }),
24
31
  }));
25
32
 
26
33
  mock.module("../memory/pkb/pkb-search.js", () => ({
@@ -29,12 +36,10 @@ mock.module("../memory/pkb/pkb-search.js", () => ({
29
36
 
30
37
  const { applyRuntimeInjections } =
31
38
  await import("../daemon/conversation-runtime-assembly.js");
32
- const { defaultInjectorsPlugin } =
33
- await import("../plugins/defaults/injectors/register.js");
34
- const { registerPlugin, resetPluginRegistryForTests } =
35
- await import("../plugins/registry.js");
39
+ import { getPkbRoot } from "../memory/pkb/types.js";
36
40
  import type { TurnContext } from "../plugins/types.js";
37
41
  import type { Message } from "../providers/types.js";
42
+ import { getWorkspacePromptPath } from "../util/platform.js";
38
43
 
39
44
  function makeTurnContext(): TurnContext {
40
45
  return {
@@ -55,28 +60,37 @@ function tailTexts(messages: Message[]): string[] {
55
60
  .map((b) => b.text);
56
61
  }
57
62
 
58
- const PKB_CONTEXT = "essentials of the project";
59
- const NOW_CONTENT = "Current focus: shipping G2.1";
60
63
  const RUN_MESSAGES: Message[] = [
61
64
  { role: "user", content: [{ type: "text", text: "What next?" }] },
62
65
  ];
63
66
 
64
67
  describe("PKB injector v2 cutover behavior", () => {
68
+ // The pkb-reminder gate derives PKB-active state from the workspace
69
+ // (`readPkbContext()` returning content) behind the guardian trust on
70
+ // `makeTurnContext()`, so seed a default auto-injected PKB file rather than
71
+ // passing a flag.
65
72
  beforeEach(() => {
66
- resetPluginRegistryForTests();
67
- registerPlugin(defaultInjectorsPlugin);
68
73
  v2Active = false;
74
+ mkdirSync(getPkbRoot(), { recursive: true });
75
+ writeFileSync(
76
+ join(getPkbRoot(), "INDEX.md"),
77
+ "workspace knowledge index",
78
+ "utf-8",
79
+ );
80
+ // now-md sources NOW.md from the workspace behind the same guardian trust,
81
+ // so seed the file rather than passing a flag.
82
+ const nowPath = getWorkspacePromptPath("NOW.md");
83
+ mkdirSync(dirname(nowPath), { recursive: true });
84
+ writeFileSync(nowPath, "Current focus: shipping G2.1", "utf-8");
85
+ });
86
+ afterEach(() => {
87
+ rmSync(getPkbRoot(), { recursive: true, force: true });
88
+ rmSync(getWorkspacePromptPath("NOW.md"), { force: true });
69
89
  });
70
90
 
71
91
  test("v2 inactive → pkb-context, pkb-reminder, and now-md all produce blocks", async () => {
72
92
  const result = await applyRuntimeInjections(RUN_MESSAGES, {
73
93
  turnContext: makeTurnContext(),
74
- pkbContext: PKB_CONTEXT,
75
- pkbActive: true,
76
- pkbScopeId: "scope-default",
77
- pkbRoot: "/tmp/pkb",
78
- pkbConversation: { messages: [] },
79
- nowScratchpad: NOW_CONTENT,
80
94
  });
81
95
 
82
96
  const texts = tailTexts(result.messages);
@@ -89,12 +103,6 @@ describe("PKB injector v2 cutover behavior", () => {
89
103
  v2Active = true;
90
104
  const result = await applyRuntimeInjections(RUN_MESSAGES, {
91
105
  turnContext: makeTurnContext(),
92
- pkbContext: PKB_CONTEXT,
93
- pkbActive: true,
94
- pkbScopeId: "scope-default",
95
- pkbRoot: "/tmp/pkb",
96
- pkbConversation: { messages: [] },
97
- nowScratchpad: NOW_CONTENT,
98
106
  });
99
107
 
100
108
  const texts = tailTexts(result.messages);