@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
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Default `stop` hook: when the model yields a turn with no tool calls, decide
3
+ * whether to let the turn end or re-query the model with a nudge.
4
+ *
5
+ * Two cases warrant a nudge:
6
+ *
7
+ * 1. **Refusal stop.** The provider returned `stopReason === "refusal"` with no
8
+ * visible text (Anthropic's safety classifier zeroed the response). Nudged
9
+ * even on the first model call of the run — a refusal there guarantees no
10
+ * organic text exists yet, so without intervening the loop would persist an
11
+ * empty assistant bubble to the user. Uses `REFUSAL_NUDGE_TEXT`.
12
+ * 2. **Empty turn after tool use.** The turn produced no visible text, follows
13
+ * at least one prior assistant turn this run, and no earlier turn this run
14
+ * already delivered visible text. Uses `NUDGE_TEXT`.
15
+ *
16
+ * Every other case leaves the decision at `"stop"` (the model said its piece,
17
+ * or there is nothing to nudge about). The retry cap is owned by the agent
18
+ * loop: this hook always asks to continue when a nudge is warranted, and the
19
+ * loop stops anyway once the run's nudge budget is spent.
20
+ *
21
+ * Both prior-turn signals are derived from the current response cycle — the
22
+ * messages after the last genuine user prompt (a user turn that isn't purely
23
+ * tool results). Scoping this way keeps prior conversation turns from polluting
24
+ * the signals, and deriving the boundary from history content rather than an
25
+ * index means mid-run compaction (which rewrites the array in place) can't
26
+ * invalidate it. A prior assistant turn this cycle implies a completed tool-use
27
+ * iteration (an empty turn nudges-and-continues without pushing an assistant
28
+ * message), so "a prior assistant turn exists" is the equivalent of "this is
29
+ * not the first model call".
30
+ *
31
+ * Defaults register before any user plugin, so this hook runs at the front of
32
+ * the `stop` chain — later hooks see (and may override) its decision.
33
+ */
34
+
35
+ import type { PluginHookFn, StopContext } from "@vellumai/plugin-api";
36
+
37
+ import type { ContentBlock, Message } from "../../../../providers/types.js";
38
+
39
+ /**
40
+ * Canonical nudge text for an empty turn after tool use. Must stay verbatim so
41
+ * a plugin that wraps the default sees a stable string.
42
+ *
43
+ * Wire-compat note: this is shown to the LLM, not the user. Edits here affect
44
+ * model behavior but not end-user UX directly.
45
+ */
46
+ export const NUDGE_TEXT =
47
+ "<system_notice>Your previous response was empty. You must respond to the user with a summary of what you found or did. Do not use any tools — just respond with text.</system_notice>";
48
+
49
+ /**
50
+ * Refusal-specific nudge. Used when the provider stops with `"refusal"` and no
51
+ * visible text — i.e. the safety classifier zeroed the response. Kept distinct
52
+ * from `NUDGE_TEXT` so the model gets context-appropriate guidance (no "summary
53
+ * of what you found or did" — there is no tool trail to summarize on a refusal).
54
+ *
55
+ * Wire-compat note: this is shown to the LLM, not the user. Edits here affect
56
+ * retry behavior but not end-user UX directly.
57
+ */
58
+ export const REFUSAL_NUDGE_TEXT =
59
+ '<system_notice>Your previous response was empty because the upstream provider returned stop_reason="refusal". Please answer the user\'s last message directly with a plain-text response. Do not use any tools — just respond with text.</system_notice>';
60
+
61
+ function hasVisibleText(content: ReadonlyArray<ContentBlock>): boolean {
62
+ return content.some(
63
+ (block) => block.type === "text" && block.text.trim().length > 0,
64
+ );
65
+ }
66
+
67
+ function isAssistantTurn(message: Message): boolean {
68
+ return message.role === "assistant";
69
+ }
70
+
71
+ /** A user-role message carrying only tool results, not a fresh prompt. */
72
+ function isToolResultMessage(message: Message): boolean {
73
+ return (
74
+ message.role === "user" &&
75
+ message.content.length > 0 &&
76
+ message.content.every((block) => block.type === "tool_result")
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Messages belonging to the current response cycle: everything after the last
82
+ * genuine user prompt. Falls back to the whole history when none is found.
83
+ */
84
+ function currentCycleMessages(
85
+ messages: ReadonlyArray<Message>,
86
+ ): ReadonlyArray<Message> {
87
+ for (let i = messages.length - 1; i >= 0; i--) {
88
+ const message = messages[i];
89
+ if (message.role === "user" && !isToolResultMessage(message)) {
90
+ return messages.slice(i + 1);
91
+ }
92
+ }
93
+ return messages;
94
+ }
95
+
96
+ const stop: PluginHookFn<StopContext> = async (ctx) => {
97
+ const turnHasVisibleText = hasVisibleText(ctx.responseContent);
98
+
99
+ const appendNudge = (text: string): void => {
100
+ ctx.messages.push({ role: "user", content: [{ type: "text", text }] });
101
+ ctx.decision = "continue";
102
+ };
103
+
104
+ if (ctx.stopReason === "refusal" && !turnHasVisibleText) {
105
+ appendNudge(REFUSAL_NUDGE_TEXT);
106
+ return;
107
+ }
108
+
109
+ const cycleMessages = currentCycleMessages(ctx.messages);
110
+ const priorAssistantTurns = cycleMessages.filter(isAssistantTurn);
111
+ const hadPriorAssistantTurn = priorAssistantTurns.length > 0;
112
+ const priorAssistantHadVisibleText = priorAssistantTurns.some((message) =>
113
+ hasVisibleText(message.content),
114
+ );
115
+
116
+ const isEmptyTurnAfterTools =
117
+ !turnHasVisibleText &&
118
+ hadPriorAssistantTurn &&
119
+ !priorAssistantHadVisibleText;
120
+
121
+ if (isEmptyTurnAfterTools) {
122
+ appendNudge(NUDGE_TEXT);
123
+ }
124
+ };
125
+
126
+ export default stop;
@@ -1,19 +1,14 @@
1
1
  /**
2
- * Default `emptyResponse` plugin.
2
+ * Default `empty-response` plugin.
3
3
  *
4
- * The plugin's middleware is a passthrough it calls `next(args)` and returns
5
- * the result unchanged. The actual decision lives in the terminal handler in
6
- * `./terminal.ts`, which is wired in as the pipeline's `terminal` argument by
7
- * the `runPipeline` call site in `agent/loop.ts`. This separation matters: the
8
- * default plugin is registered before any user plugin (defaults load first in
9
- * `bootstrapPlugins()`), which puts it at the OUTERMOST position of the onion
10
- * chain. If the default middleware were to decide directly without calling
11
- * `next`, it would shadow every later-registered plugin. Routing through
12
- * `next(args)` lets user middleware participate normally.
4
+ * Contributes a `stop` hook that re-queries the model when a turn yields with
5
+ * no tool calls but came back empty (or as a provider refusal). The decision
6
+ * logic lives in `./hooks/stop.ts`. Defaults register before user plugins, so
7
+ * this runs at the front of the `stop` hook chain.
13
8
  */
14
9
 
15
10
  import { type Plugin } from "../../types.js";
16
- import emptyResponse from "./middlewares/emptyResponse.js";
11
+ import stop from "./hooks/stop.js";
17
12
  import pkg from "./package.json" with { type: "json" };
18
13
 
19
14
  /** Singleton plugin — the registry rejects duplicate registrations by name. */
@@ -22,7 +17,7 @@ export const defaultEmptyResponsePlugin: Plugin = {
22
17
  name: pkg.name,
23
18
  version: pkg.version,
24
19
  },
25
- middleware: {
26
- emptyResponse,
20
+ hooks: {
21
+ stop,
27
22
  },
28
23
  };
@@ -23,22 +23,15 @@
23
23
  * registration work.
24
24
  */
25
25
 
26
- import { memoryV3ShadowPlugin } from "../../memory/v3/shadow-plugin.js";
27
26
  import { registerPlugin, resetPluginRegistryForTests } from "../registry.js";
28
27
  import { type Plugin, PluginExecutionError } from "../types.js";
29
28
  import { defaultCircuitBreakerPlugin } from "./circuit-breaker/register.js";
30
29
  import { defaultCompactionPlugin } from "./compaction/register.js";
31
30
  import { defaultEmptyResponsePlugin } from "./empty-response/register.js";
32
31
  import { defaultHistoryRepairPlugin } from "./history-repair/register.js";
33
- import { defaultInjectorsPlugin } from "./injectors/register.js";
34
- import { defaultLlmCallPlugin } from "./llm-call/register.js";
35
- import { defaultMemoryRetrievalPlugin } from "./memory-retrieval/register.js";
36
32
  import { defaultOverflowReducePlugin } from "./overflow-reduce/register.js";
37
- import { defaultPersistencePlugin } from "./persistence/register.js";
38
33
  import { defaultTitleGeneratePlugin } from "./title-generate/register.js";
39
- import { defaultTokenEstimatePlugin } from "./token-estimate/register.js";
40
34
  import { defaultToolErrorPlugin } from "./tool-error/register.js";
41
- import { defaultToolExecutePlugin } from "./tool-execute/register.js";
42
35
  import { defaultToolResultTruncatePlugin } from "./tool-result-truncate/register.js";
43
36
 
44
37
  /**
@@ -53,21 +46,14 @@ import { defaultToolResultTruncatePlugin } from "./tool-result-truncate/register
53
46
  */
54
47
  function getAllDefaultPlugins(): readonly Plugin[] {
55
48
  return [
56
- defaultLlmCallPlugin,
57
- defaultToolExecutePlugin,
58
49
  defaultToolResultTruncatePlugin,
59
50
  defaultEmptyResponsePlugin,
60
51
  defaultToolErrorPlugin,
61
- defaultMemoryRetrievalPlugin,
62
- defaultInjectorsPlugin,
63
- defaultTokenEstimatePlugin,
64
52
  defaultOverflowReducePlugin,
65
53
  defaultHistoryRepairPlugin,
66
54
  defaultCompactionPlugin,
67
55
  defaultCircuitBreakerPlugin,
68
- defaultPersistencePlugin,
69
56
  defaultTitleGeneratePlugin,
70
- memoryV3ShadowPlugin,
71
57
  ];
72
58
  }
73
59
 
@@ -99,7 +85,7 @@ export function registerDefaultPlugins(): void {
99
85
  * so integration tests that exercise the full agent loop have a
100
86
  * production-parity plugin stack. Use this in `beforeEach` of tests that
101
87
  * dispatch through pipelines with a terminal that assumes the default
102
- * plugin handles every op (e.g. persistence, overflowReduce).
88
+ * plugin handles every op (e.g. overflowReduce).
103
89
  *
104
90
  * Tests that specifically need an empty registry (pipeline-unit tests, the
105
91
  * plugin-registry tests themselves) should continue to call
@@ -32,35 +32,47 @@
32
32
  * closer to the memory prefix themselves. For appends, ascending `order` is
33
33
  * the natural left-to-right append sequence. The runtime-injection applier
34
34
  * sorts and applies blocks declaratively so this invariant holds even when
35
- * third-party injectors slot additional blocks at fractional order values.
36
- *
37
- * Third-party plugins may register additional {@link Injector}s at any
38
- * `order` value; the registry's `getInjectors()` returns all injectors
39
- * sorted ascending, so a plugin-registered injector at `order: 25`
40
- * reliably slots between `unified-turn-context` (20) and `pkb` (30).
41
- *
42
- * This module only builds and exports the `Plugin` object; the defaults
43
- * aggregator in `plugins/defaults/index.ts` registers it centrally, either
44
- * explicitly from `daemon/external-plugins-bootstrap.ts` or lazily via the
45
- * registry's default registrar the first time a query reads the registry.
35
+ * injectors slot additional blocks at fractional order values.
36
+ *
37
+ * This module exports the default injectors as a plain ordered array
38
+ * ({@link defaultInjectors}). The chain assembler in
39
+ * `plugins/defaults/memory-retrieval/injector-chain.ts` sorts them by `order`
40
+ * (alongside the memory-v3 injector) into the single sequence
41
+ * `applyRuntimeInjections` walks each turn — injection is not a plugin
42
+ * contribution, so injectors are imported directly rather than aggregated
43
+ * through the registry.
46
44
  */
47
45
 
48
46
  import { resolve } from "node:path";
49
47
 
50
48
  import { getConfig } from "../../../config/loader.js";
49
+ import type { InjectionMatcher } from "../../../context/strip-injections.js";
50
+ import { readNowScratchpad } from "../../../daemon/now-scratchpad.js";
51
51
  import { getInContextPkbPaths } from "../../../daemon/pkb-context-tracker.js";
52
52
  import { buildPkbReminder } from "../../../daemon/pkb-reminder-builder.js";
53
+ import {
54
+ resolveTrustClass,
55
+ type TrustContext,
56
+ } from "../../../daemon/trust-context.js";
53
57
  import { listComments } from "../../../documents/document-comments-store.js";
58
+ import { getLiveGraphMemory } from "../../../memory/graph/conversation-graph-memory.js";
59
+ import { getPkbAutoInjectList } from "../../../memory/pkb/autoinject.js";
60
+ import { readPkbContext } from "../../../memory/pkb/context.js";
54
61
  import { searchPkbFiles } from "../../../memory/pkb/pkb-search.js";
62
+ import { getPkbRoot, PKB_WORKSPACE_SCOPE } from "../../../memory/pkb/types.js";
63
+ import {
64
+ readMemoryV2StaticContent,
65
+ shouldExposePersonalMemory,
66
+ } from "../../../memory/v2/static-context.js";
67
+ import type { Message } from "../../../providers/types.js";
55
68
  import { getLogger } from "../../../util/logger.js";
69
+ import { getSandboxWorkingDir } from "../../../util/platform.js";
56
70
  import {
57
71
  type InjectionBlock,
58
72
  type Injector,
59
- type Plugin,
60
73
  type TurnContext,
61
74
  type TurnInjectionInputs,
62
75
  } from "../../types.js";
63
- import pkg from "./package.json" with { type: "json" };
64
76
 
65
77
  const pkbReminderLog = getLogger("pkb-reminder");
66
78
 
@@ -79,7 +91,7 @@ const PKB_HINT_ARCHIVE_THRESHOLD = 0.7;
79
91
  * and any future integration code — can assert ordering without re-deriving
80
92
  * the constants.
81
93
  *
82
- * Gaps of 10 between slots leave room for third-party injectors to slot in
94
+ * Gaps of 10 between slots leave room for future injectors to slot in
83
95
  * at granular positions (e.g. `25` between unified-turn-context and pkb)
84
96
  * without renumbering the defaults.
85
97
  */
@@ -241,20 +253,33 @@ const unifiedTurnContextInjector: Injector = {
241
253
  *
242
254
  * Gating:
243
255
  * - `mode === "full"`.
244
- * - Non-null, non-empty `pkbContext`.
256
+ * - The personal-memory trust gate admits the actor and the workspace has
257
+ * PKB content (see {@link readGatedPkbContext}).
258
+ * - The `<knowledge_base>` block is not already present in the turn's working
259
+ * messages. The big block is injected once and then persists in history, so
260
+ * it only needs (re)injecting on the first turn and right after compaction
261
+ * strips it — both of which leave the working messages without the block.
262
+ * Skipping when it is present keeps the conversation prefix stable for
263
+ * Anthropic's prefix caching and avoids a duplicate splice.
245
264
  */
246
265
  const pkbContextInjector: Injector = {
247
266
  name: "pkb-context",
248
267
  order: DEFAULT_INJECTOR_ORDER.pkbContext,
249
- async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
268
+ async produce(
269
+ ctx: TurnContext,
270
+ runMessages?: Message[],
271
+ ): Promise<InjectionBlock | null> {
250
272
  const inputs = readInjectionInputs(ctx);
251
273
  const mode = inputs.mode ?? "full";
252
274
  if (mode !== "full") return null;
253
275
  if (isPkbInjectionSilencedByV2()) return null;
254
- if (!inputs.pkbContext) return null;
276
+ const content = readGatedPkbContext(ctx.trust);
277
+ if (!content) return null;
278
+ if (hasInjectedUserTextBlock(runMessages, KNOWLEDGE_BASE_BLOCK_PREFIXES))
279
+ return null;
255
280
  return {
256
281
  id: "pkb-context",
257
- text: buildPkbContextBlock(inputs.pkbContext),
282
+ text: buildPkbContextBlock(content),
258
283
  placement: "after-memory-prefix",
259
284
  };
260
285
  },
@@ -271,18 +296,21 @@ const pkbContextInjector: Injector = {
271
296
  *
272
297
  * Gating:
273
298
  * - `mode === "full"`.
274
- * - `pkbActive === true`.
299
+ * - PKB is active for the turn (see {@link isPkbActive}).
275
300
  */
276
301
  const pkbReminderInjector: Injector = {
277
302
  name: "pkb-reminder",
278
303
  order: DEFAULT_INJECTOR_ORDER.pkbReminder,
279
- async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
304
+ async produce(
305
+ ctx: TurnContext,
306
+ runMessages?: Message[],
307
+ ): Promise<InjectionBlock | null> {
280
308
  const inputs = readInjectionInputs(ctx);
281
309
  const mode = inputs.mode ?? "full";
282
310
  if (mode !== "full") return null;
283
- if (!inputs.pkbActive) return null;
311
+ if (!isPkbActive(ctx.trust)) return null;
284
312
  if (isPkbInjectionSilencedByV2()) return null;
285
- const reminder = await buildPkbReminderWithHints(inputs);
313
+ const reminder = await buildPkbReminderWithHints(ctx, runMessages);
286
314
  return {
287
315
  id: "pkb-reminder",
288
316
  text: reminder,
@@ -291,6 +319,125 @@ const pkbReminderInjector: Injector = {
291
319
  },
292
320
  };
293
321
 
322
+ /**
323
+ * Whether personal-memory content (PKB, NOW.md) may be surfaced this turn: the
324
+ * trust gate admits the actor (guardian-class, or an internal/local flow). All
325
+ * memory-domain injectors share this gate so they apply identical exposure
326
+ * rules without it being threaded in from the agent loop.
327
+ */
328
+ function isPersonalMemoryAllowed(trust: TrustContext): boolean {
329
+ return shouldExposePersonalMemory({
330
+ sourceChannel: trust.sourceChannel,
331
+ isTrustedActor: resolveTrustClass(trust) === "guardian",
332
+ });
333
+ }
334
+
335
+ /**
336
+ * Read the auto-injected PKB content for the turn, gated behind the
337
+ * personal-memory trust gate. Returns the content string when the gate admits
338
+ * the actor and the workspace has PKB content, otherwise `null`. Both the gate
339
+ * and the content are sourced from the turn's trust context and the PKB files
340
+ * directly, so the memory-domain injectors source their own inputs rather than
341
+ * having them threaded in from the agent loop.
342
+ */
343
+ function readGatedPkbContext(trust: TrustContext): string | null {
344
+ return isPersonalMemoryAllowed(trust) ? readPkbContext() : null;
345
+ }
346
+
347
+ /**
348
+ * Read the NOW.md scratchpad content for the turn, gated behind the
349
+ * personal-memory trust gate and the `scratchpadInjection` config toggle.
350
+ * Returns the trimmed content when both gates admit and the file is non-empty,
351
+ * otherwise `null`. Sourced from the trust context and the NOW.md file directly
352
+ * so the `now-md` injector owns its input rather than having it threaded in.
353
+ */
354
+ function readGatedNowScratchpad(trust: TrustContext): string | null {
355
+ if (!isPersonalMemoryAllowed(trust)) return null;
356
+ if (!getConfig().memory.retrieval.scratchpadInjection.enabled) return null;
357
+ return readNowScratchpad();
358
+ }
359
+
360
+ /**
361
+ * Read the v2 static memory content for the turn, gated behind the
362
+ * personal-memory trust gate. Returns the content (essentials/threads/recent/
363
+ * buffer concatenated) when the gate admits and v2 memory is enabled,
364
+ * otherwise `null`. {@link readMemoryV2StaticContent} self-gates on the v2
365
+ * flag + config, so the `memory-v2-static` injector owns its input rather than
366
+ * having it threaded in from the agent loop.
367
+ */
368
+ function readGatedMemoryV2Static(trust: TrustContext): string | null {
369
+ return isPersonalMemoryAllowed(trust) ? readMemoryV2StaticContent() : null;
370
+ }
371
+
372
+ /**
373
+ * Whether PKB is active for the turn: the personal-memory trust gate admits
374
+ * the actor and the workspace has PKB content to surface.
375
+ */
376
+ function isPkbActive(trust: TrustContext): boolean {
377
+ return readGatedPkbContext(trust) !== null;
378
+ }
379
+
380
+ /** Block prefixes that mark a persisted `<knowledge_base>` injection. */
381
+ const KNOWLEDGE_BASE_BLOCK_PREFIXES = [
382
+ "<knowledge_base>",
383
+ "<pkb>", // backward-compat: pre-rename history
384
+ ] as const;
385
+
386
+ /** Block prefixes that mark a persisted NOW.md injection. */
387
+ const NOW_MD_BLOCK_PREFIXES = [
388
+ "<NOW.md Always keep this up to date",
389
+ "<now_scratchpad>", // backward-compat: pre-rename history
390
+ ] as const;
391
+
392
+ /**
393
+ * Matchers that mark a persisted `memory-v2-static` injection. Uses the
394
+ * `{ prefix, suffix }` wrapper shape (not a bare prefix) so user-authored text
395
+ * merely starting with `<info>\n` is never mistaken for an injection — matching
396
+ * the full-wrapper requirement the compaction strip uses for this block.
397
+ *
398
+ * The static block is wrapped in `<info>…</info>` today, but rows persisted
399
+ * before that switch rehydrate verbatim as `<memory>…</memory>` (see
400
+ * `conversation-lifecycle`), so the legacy wrapper counts as present too.
401
+ * Matching `<memory>` cannot wrongly skip a needed injection: the static block
402
+ * is only (re)injected on the first turn (empty history) and right after
403
+ * compaction (which strips both wrappers), and on neither is a `<memory>` block
404
+ * present — the dynamic activation `<memory>` block only survives on normal
405
+ * cached turns, which is exactly when this injector must skip anyway.
406
+ */
407
+ const MEMORY_V2_STATIC_BLOCK_MATCHERS: readonly InjectionMatcher[] = [
408
+ { prefix: "<info>\n", suffix: "\n</info>" },
409
+ { prefix: "<memory>\n", suffix: "\n</memory>" },
410
+ ];
411
+
412
+ /**
413
+ * Whether a block matching any of the given matchers is already present in the
414
+ * turn's working messages. Mirrors `stripUserTextBlocksByPrefix` (a
415
+ * user-message text block whose content matches a bare-prefix or a
416
+ * `{ prefix, suffix }` wrapper matcher), so presence detection stays in
417
+ * lockstep with what compaction strips: a block is present here exactly when
418
+ * compaction would strip it.
419
+ */
420
+ function hasInjectedUserTextBlock(
421
+ runMessages: Message[] | undefined,
422
+ matchers: readonly InjectionMatcher[],
423
+ ): boolean {
424
+ if (!runMessages) return false;
425
+ return runMessages.some(
426
+ (message) =>
427
+ message.role === "user" &&
428
+ message.content.some(
429
+ (block) =>
430
+ block.type === "text" &&
431
+ matchers.some((m) =>
432
+ typeof m === "string"
433
+ ? block.text.startsWith(m)
434
+ : block.text.startsWith(m.prefix) &&
435
+ block.text.endsWith(m.suffix),
436
+ ),
437
+ ),
438
+ );
439
+ }
440
+
294
441
  /**
295
442
  * Render the PKB context block — wraps the raw content in
296
443
  * `<knowledge_base>...</knowledge_base>` while escaping any closing tags
@@ -305,38 +452,40 @@ function buildPkbContextBlock(content: string): string {
305
452
  }
306
453
 
307
454
  /**
308
- * Build the PKB `<system_reminder>` text. When a dense query vector plus
309
- * enough scope metadata is available, run the hybrid PKB search to
455
+ * Build the PKB `<system_reminder>` text. When a dense query vector and the
456
+ * turn's working messages are available, run the hybrid PKB search to
310
457
  * surface up to three relevance hints; fall back to the flat static
311
458
  * reminder on empty results or any error.
459
+ *
460
+ * The dense/sparse query pair is read off the conversation's live graph
461
+ * handle ({@link getLiveGraphMemory}) — the memory-retrieval hook records it
462
+ * there during the turn's retrieval. In-context PKB paths are computed from
463
+ * the turn's working messages (`runMessages`, supplied by the injector chain)
464
+ * resolved against the workspace working directory, so the reminder sources
465
+ * its inputs itself rather than having them threaded through the agent loop.
312
466
  */
313
467
  async function buildPkbReminderWithHints(
314
- inputs: TurnInjectionInputs,
468
+ ctx: TurnContext,
469
+ runMessages?: Message[],
315
470
  ): Promise<string> {
316
471
  let hints: string[] = [];
317
- const queryVector = inputs.pkbQueryVector;
318
- if (
319
- queryVector &&
320
- queryVector.length > 0 &&
321
- inputs.pkbScopeId &&
322
- inputs.pkbConversation &&
323
- inputs.pkbRoot
324
- ) {
472
+ const graphMemory = getLiveGraphMemory(ctx.conversationId);
473
+ const queryVector = graphMemory?.pkbQueryVector;
474
+ if (queryVector && queryVector.length > 0 && runMessages) {
325
475
  try {
476
+ const pkbRoot = getPkbRoot();
326
477
  const results = await searchPkbFiles(
327
478
  queryVector,
328
- inputs.pkbSparseVector,
479
+ graphMemory?.pkbSparseVector,
329
480
  8,
330
- [inputs.pkbScopeId],
481
+ [PKB_WORKSPACE_SCOPE],
331
482
  );
332
- const workingDir = inputs.pkbWorkingDir ?? inputs.pkbRoot;
333
483
  const inContext = getInContextPkbPaths(
334
- inputs.pkbConversation,
335
- inputs.pkbAutoInjectList ?? [],
336
- inputs.pkbRoot,
337
- workingDir,
484
+ { messages: runMessages },
485
+ getPkbAutoInjectList(pkbRoot),
486
+ pkbRoot,
487
+ getSandboxWorkingDir(),
338
488
  );
339
- const pkbRoot = inputs.pkbRoot;
340
489
  // Gate on `denseScore` (cosine, [0, 1]) so the quality bar is stable
341
490
  // regardless of whether sparse was provided. Rank by `hybridScore`
342
491
  // (RRF) when available — that captures the sparse signal for
@@ -395,18 +544,30 @@ async function buildPkbReminderWithHints(
395
544
  * the memory prefix so `now-md` (40) splices after it.
396
545
  *
397
546
  * Gating:
398
- * - `mode === "full"`.
399
- * - `memoryV2Static` is a non-null, non-empty string.
547
+ * - `mode === "full"` (skipped in minimal mode).
548
+ * - The personal-memory trust gate admits the actor and v2 static memory has
549
+ * content (see {@link readGatedMemoryV2Static}).
550
+ * - The `<info>` block is not already present in the turn's working messages.
551
+ * Like `<knowledge_base>`, the block is injected once and then persists in
552
+ * history, so it only needs (re)injecting on the first turn and right after
553
+ * compaction strips it — both of which leave the working messages without
554
+ * the block. Skipping when it is present keeps the conversation prefix
555
+ * stable for Anthropic's prefix caching and avoids a duplicate splice.
400
556
  */
401
557
  const memoryV2StaticInjector: Injector = {
402
558
  name: "memory-v2-static",
403
559
  order: DEFAULT_INJECTOR_ORDER.memoryV2Static,
404
- async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
560
+ async produce(
561
+ ctx: TurnContext,
562
+ runMessages?: Message[],
563
+ ): Promise<InjectionBlock | null> {
405
564
  const inputs = readInjectionInputs(ctx);
406
565
  const mode = inputs.mode ?? "full";
407
566
  if (mode !== "full") return null;
408
- const content = inputs.memoryV2Static;
567
+ const content = readGatedMemoryV2Static(ctx.trust);
409
568
  if (!content) return null;
569
+ if (hasInjectedUserTextBlock(runMessages, MEMORY_V2_STATIC_BLOCK_MATCHERS))
570
+ return null;
410
571
  return {
411
572
  id: "memory-v2-static",
412
573
  text: buildMemoryV2StaticBlock(content),
@@ -438,17 +599,30 @@ function buildMemoryV2StaticBlock(content: string): string {
438
599
  *
439
600
  * Gating:
440
601
  * - `mode === "full"` (skipped in minimal mode).
441
- * - `nowScratchpad` is a non-null, non-empty string.
602
+ * - The personal-memory trust gate admits the actor, the `scratchpadInjection`
603
+ * config toggle is on, and NOW.md has content (see
604
+ * {@link readGatedNowScratchpad}).
605
+ * - The NOW.md block is not already present in the turn's working messages.
606
+ * Like `<knowledge_base>`, the block is injected once and then persists in
607
+ * history, so it only needs (re)injecting on the first turn and right after
608
+ * compaction strips it — both of which leave the working messages without
609
+ * the block. Skipping when it is present keeps the conversation prefix
610
+ * stable for Anthropic's prefix caching and avoids a duplicate splice.
442
611
  */
443
612
  const nowMdInjector: Injector = {
444
613
  name: "now-md",
445
614
  order: DEFAULT_INJECTOR_ORDER.nowMd,
446
- async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
615
+ async produce(
616
+ ctx: TurnContext,
617
+ runMessages?: Message[],
618
+ ): Promise<InjectionBlock | null> {
447
619
  const inputs = readInjectionInputs(ctx);
448
620
  const mode = inputs.mode ?? "full";
449
621
  if (mode !== "full") return null;
450
- const content = inputs.nowScratchpad;
622
+ const content = readGatedNowScratchpad(ctx.trust);
451
623
  if (!content) return null;
624
+ if (hasInjectedUserTextBlock(runMessages, NOW_MD_BLOCK_PREFIXES))
625
+ return null;
452
626
  const text = `<NOW.md Always keep this up to date; keep under 10 lines>\n${content}\n</NOW.md>`;
453
627
  return {
454
628
  id: "now-md",
@@ -675,31 +849,26 @@ const threadFocusInjector: Injector = {
675
849
  };
676
850
 
677
851
  /**
678
- * Bundle every default injector into a single first-party plugin. Registered
679
- * at daemon startup via `external-plugins-bootstrap.ts`.
852
+ * Every default injector in ascending `order`. This is the canonical
853
+ * first-party injection sequence consumed by `applyRuntimeInjections` via the
854
+ * assembled injector chain.
680
855
  *
681
- * Using one plugin per injector would inflate the registry and create
682
- * spurious registration-order dependencies; a single plugin keeps the
683
- * ordering contract entirely in the `order` field.
856
+ * `order` is the source of truth for sequencing (see {@link DEFAULT_INJECTOR_ORDER});
857
+ * the chain assembler sorts by it, so this array's literal order is only a
858
+ * readability convenience.
684
859
  */
685
- export const defaultInjectorsPlugin: Plugin = {
686
- manifest: {
687
- name: pkg.name,
688
- version: pkg.version,
689
- },
690
- injectors: [
691
- diskPressureWarningInjector,
692
- workspaceContextInjector,
693
- backgroundTurnInjector,
694
- unifiedTurnContextInjector,
695
- pkbContextInjector,
696
- pkbReminderInjector,
697
- memoryV2StaticInjector,
698
- nowMdInjector,
699
- activeDocumentsInjector,
700
- documentCommentsInjector,
701
- subagentStatusInjector,
702
- slackMessagesInjector,
703
- threadFocusInjector,
704
- ],
705
- };
860
+ export const defaultInjectors: Injector[] = [
861
+ diskPressureWarningInjector,
862
+ workspaceContextInjector,
863
+ backgroundTurnInjector,
864
+ unifiedTurnContextInjector,
865
+ pkbContextInjector,
866
+ pkbReminderInjector,
867
+ memoryV2StaticInjector,
868
+ nowMdInjector,
869
+ activeDocumentsInjector,
870
+ documentCommentsInjector,
871
+ subagentStatusInjector,
872
+ slackMessagesInjector,
873
+ threadFocusInjector,
874
+ ];