@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
@@ -14,11 +14,18 @@
14
14
  * turn after turn. The trailing recent-context / current-message block changes
15
15
  * every turn and carries no breakpoint. This mirrors `./router.ts`.
16
16
  *
17
- * Recall-safe fallbacks. Like the router, the selector exists to widen recall,
18
- * so every failure degrades toward selecting MORE pages, never fewer:
19
- * - omitted `ids` → select ALL members of the leaf,
20
- * - missing/failed tool_use, provider unavailable, or any throw ALL members.
21
- * Only an explicit empty `ids: []` returns nothing (deliberate abstention).
17
+ * Failure handling. A deliberate "select everything" and a model-call failure
18
+ * are different events with different outcomes:
19
+ * - explicit `ids` → select exactly those pages,
20
+ * - explicit empty `ids: []` select nothing (deliberate abstention),
21
+ * - omitted `ids` select ALL members of the leaf (the recall-safe "this
22
+ * whole leaf is relevant" signal, e.g. "give me all of X"); bounded to one
23
+ * leaf, so unlike the router this stays a select-all,
24
+ * - infrastructure failure (provider unavailable, a throw that survived the
25
+ * provider's own retries, no usable `tool_use`, or a schema mismatch) →
26
+ * select nothing after a short re-prompt retry, degrading to the
27
+ * deterministic recall lanes (core, needle, carry-forward working set) the
28
+ * orchestrator unions in regardless.
22
29
  */
23
30
 
24
31
  import { z } from "zod";
@@ -30,6 +37,7 @@ import {
30
37
  import type { Message, ToolDefinition } from "../../providers/types.js";
31
38
  import { getLogger } from "../../util/logger.js";
32
39
  import { mapLimit } from "../../util/map-limit.js";
40
+ import { retryForResult } from "./llm-retry.js";
33
41
  import { cachedTextBlock } from "./provider-blocks.js";
34
42
  import { membersOf } from "./tree.js";
35
43
  import type { LeafPath, LeafTree, Slug, TurnContext } from "./types.js";
@@ -55,11 +63,13 @@ const SelectPagesSchema = z.object({
55
63
  const SELECT_PAGES_TOOL: ToolDefinition = {
56
64
  name: SELECT_PAGES_TOOL_NAME,
57
65
  description:
58
- "Select the pages in this leaf whose content is relevant or useful for " +
59
- "the next reply. Lean toward inclusion — a missed relevant page is a " +
60
- "worse error than an unused one. Pass `pinned_ids` for pages the " +
61
- "conversation is centrally about. Omit `ids` entirely to select every " +
62
- "page; return `[]` only when none of the pages could possibly help.",
66
+ "Select the pages in this leaf whose content the reply would directly " +
67
+ "draw on. Be selectiveprefer a few precisely-relevant pages over many " +
68
+ "loosely-related ones; a leaf opened on a weak signal may yield none. " +
69
+ "Pass `pinned_ids` for pages the conversation is centrally about. Omit " +
70
+ "`ids` only as a recall-safe fallback when you cannot judge the leaf " +
71
+ "(selects every page); return `[]` when pages are present but none are " +
72
+ "directly relevant.",
63
73
  input_schema: {
64
74
  type: "object",
65
75
  properties: {
@@ -75,11 +85,13 @@ const SELECT_PAGES_TOOL: ToolDefinition = {
75
85
  },
76
86
  };
77
87
 
78
- const SYSTEM_PROMPT = `This leaf of the topic tree is potentially relevant to the conversation. Select the pages whose content is relevant or useful for responding.
88
+ const SYSTEM_PROMPT = `This leaf of the topic tree is potentially relevant to the conversation. Select ONLY the pages whose content the reply to THIS message would directly draw on.
79
89
 
80
- Be inclusive include frame and affect matches, not just literal-topic matches. A page that shares the conversation's mode or register can be as useful as one that names the same entity. Missing a relevant page is a worse error than selecting an unused one.
90
+ Be selective: exclude pages that are merely topically adjacent, part of the ever-present background, or only loosely related. Most opened leaves should contribute a few precisely-relevant pages, not most of their contents a leaf opened on a weak signal may yield none.
81
91
 
82
- If the conversation is centrally ABOUT a page (rather than only peripherally relevant to it), mark that page as pinned. Call \`select_pages\` with the chosen IDs. Omit \`ids\` to select every page; return \`[]\` only when none of the pages could possibly help.`;
92
+ A page can also be directly relevant because of the current situation the date or the live scratchpad not only the message: keep a page the situation makes pertinent (e.g. a person whose anniversary is today).
93
+
94
+ If the conversation is centrally ABOUT a page (rather than only peripherally relevant to it), mark that page as pinned. Call \`select_pages\` with the chosen IDs. Omit \`ids\` only as a recall-safe fallback when you cannot judge the leaf (selects every page); return \`[]\` when the pages are present but none are directly relevant.`;
83
95
 
84
96
  /**
85
97
  * Render the STATIC numbered `<pages>` block for a leaf from its member slugs.
@@ -103,8 +115,10 @@ async function renderPagesBlock(
103
115
  /**
104
116
  * Run the L2 selector for a single opened leaf. Returns the pages to inject.
105
117
  *
106
- * Recall-safe: any failure to obtain an explicit selection returns ALL members
107
- * of the leaf. Only an explicit empty `ids` array returns no pages.
118
+ * An omitted `ids` selects ALL members (the recall-safe "whole leaf is
119
+ * relevant" signal); an explicit `[]` selects none; an infrastructure failure
120
+ * (after a short re-prompt retry) selects none, degrading to the deterministic
121
+ * recall lanes the orchestrator unions in.
108
122
  */
109
123
  export async function selectFromLeaf(
110
124
  leaf: LeafPath,
@@ -120,8 +134,11 @@ export async function selectFromLeaf(
120
134
 
121
135
  const provider = await getConfiguredProvider("memoryV3SelectL2");
122
136
  if (!provider) {
123
- log.warn({ leaf }, "memoryV3SelectL2 provider unavailable; selecting all");
124
- return allMembers();
137
+ log.warn(
138
+ { leaf },
139
+ "L2 selector provider unavailable; degrading to deterministic lanes",
140
+ );
141
+ return [];
125
142
  }
126
143
 
127
144
  const userMsg: Message = {
@@ -134,15 +151,21 @@ export async function selectFromLeaf(
134
151
  {
135
152
  type: "text",
136
153
  text:
154
+ (turn.situationalContext
155
+ ? `<situation>${turn.situationalContext}</situation>\n`
156
+ : "") +
137
157
  `<recent_context>${turn.recentContext}</recent_context>\n` +
138
158
  `<current_message>${turn.currentMessage}</current_message>`,
139
159
  },
140
160
  ],
141
161
  };
142
162
 
143
- let response;
144
- try {
145
- response = await provider.sendMessage([userMsg], {
163
+ // One forced-tool call, retried a few times so a transient malformed response
164
+ // (no usable tool_use, or tool input that fails the schema) re-prompts before
165
+ // we give up. `null` from an attempt means "unusable, retry"; the provider
166
+ // layer already backs off transient throws, so this loop adds no delay.
167
+ const parsed = await retryForResult(async () => {
168
+ const response = await provider.sendMessage([userMsg], {
146
169
  tools: [SELECT_PAGES_TOOL],
147
170
  systemPrompt: SYSTEM_PROMPT,
148
171
  config: {
@@ -150,39 +173,31 @@ export async function selectFromLeaf(
150
173
  tool_choice: { type: "tool" as const, name: SELECT_PAGES_TOOL_NAME },
151
174
  },
152
175
  });
153
- } catch (err) {
154
- log.warn({ err, leaf }, "L2 selector call threw; selecting all");
155
- return allMembers();
156
- }
157
-
158
- const toolBlock = extractToolUse(response);
159
- if (!toolBlock || toolBlock.name !== SELECT_PAGES_TOOL_NAME) {
160
- log.warn(
161
- { stopReason: response.stopReason, leaf },
162
- "L2 selector returned no select_pages tool_use; selecting all",
163
- );
164
- return allMembers();
165
- }
176
+ const toolBlock = extractToolUse(response);
177
+ if (!toolBlock || toolBlock.name !== SELECT_PAGES_TOOL_NAME) return null;
178
+ const result = SelectPagesSchema.safeParse(toolBlock.input);
179
+ return result.success ? result.data : null;
180
+ });
166
181
 
167
- const parsed = SelectPagesSchema.safeParse(toolBlock.input);
168
- if (!parsed.success) {
182
+ if (parsed === null) {
169
183
  log.warn(
170
- { error: parsed.error.message, leaf },
171
- "L2 selector tool input did not match schema; selecting all",
184
+ { leaf },
185
+ "L2 selector could not obtain a selection after retries; degrading to deterministic lanes",
172
186
  );
173
- return allMembers();
187
+ return [];
174
188
  }
175
189
 
176
- // Omitted `ids` is the recall-safe "select everything" signal.
177
- if (parsed.data.ids === undefined) return allMembers();
190
+ // Omitted `ids` is the recall-safe "this whole leaf is relevant" signal.
191
+ // Bounded to one leaf, so it stays a select-all (unlike the L1 router).
192
+ if (parsed.ids === undefined) return allMembers();
178
193
 
179
- const pinned = new Set(parsed.data.pinned_ids ?? []);
194
+ const pinned = new Set(parsed.pinned_ids ?? []);
180
195
 
181
196
  // Map 1-based IDs back to member slugs, dropping out-of-range IDs without
182
197
  // throwing. De-duplicate while preserving model-returned order.
183
198
  const seen = new Set<number>();
184
199
  const selected: SelectedPage[] = [];
185
- for (const id of parsed.data.ids) {
200
+ for (const id of parsed.ids) {
186
201
  if (id < 1 || id > members.length || seen.has(id)) continue;
187
202
  seen.add(id);
188
203
  selected.push({ slug: members[id - 1]!, pinned: pinned.has(id) });
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Memory v3 — flag-gated shadow/live plugin.
2
+ * Memory v3 — flag-gated shadow/live injector.
3
3
  *
4
- * Registered as an {@link Injector} that runs the v3 orchestrator each turn and
4
+ * An {@link Injector} that runs the v3 orchestrator each turn and
5
5
  * records its selection set to `memory_v3_selections`. Two flags gate its
6
6
  * injection behavior:
7
7
  *
@@ -29,6 +29,8 @@
29
29
  * empty selection) falls back to v2 memory rather than dropping all memory.
30
30
  */
31
31
 
32
+ import { existsSync, readFileSync } from "node:fs";
33
+
32
34
  import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
33
35
  import { getConfig } from "../../config/loader.js";
34
36
  import type { AssistantConfig } from "../../config/schema.js";
@@ -38,11 +40,14 @@ import { stringifyMessageContent } from "../../memory/message-content.js";
38
40
  import {
39
41
  type InjectionBlock,
40
42
  type Injector,
41
- type Plugin,
42
43
  type TurnContext as PluginTurnContext,
43
44
  } from "../../plugins/types.js";
44
45
  import { getLogger } from "../../util/logger.js";
45
- import { getWorkspaceDir } from "../../util/platform.js";
46
+ import {
47
+ getWorkspaceDir,
48
+ getWorkspacePromptPath,
49
+ } from "../../util/platform.js";
50
+ import { stripCommentLines } from "../../util/strip-comment-lines.js";
46
51
  import { getPageIndex } from "../v2/page-index.js";
47
52
  import { injectCapabilitiesLeaf, isCapabilitySlug } from "./capabilities.js";
48
53
  import { loadCore } from "./core.js";
@@ -165,10 +170,42 @@ function getLanes(config: AssistantConfig): Promise<ShadowLanes> {
165
170
  return lanesPromise;
166
171
  }
167
172
 
173
+ /**
174
+ * Read the live NOW.md scratchpad (the user's short "what's salient right now"
175
+ * file), stripped of its comment lines. Mirrors `readNowScratchpad` but reads
176
+ * through the light platform / strip utilities directly, keeping the v3
177
+ * plugin's load (and its test) free of heavier module graphs. Returns `null`
178
+ * when absent, empty, or unreadable.
179
+ */
180
+ function readNowContext(): string | null {
181
+ const nowPath = getWorkspacePromptPath("NOW.md");
182
+ if (!existsSync(nowPath)) return null;
183
+ try {
184
+ const stripped = stripCommentLines(readFileSync(nowPath, "utf-8")).trim();
185
+ return stripped.length > 0 ? stripped : null;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Compose the situational signal threaded into L1 routing and L2 selection: the
193
+ * current date plus the live NOW.md scratchpad. The date alone is a weak signal,
194
+ * but together with the scratchpad it lets retrieval surface a leaf the message
195
+ * never names (e.g. an anniversary that falls today). Always returns at least
196
+ * the date line — this mirrors the `c_now`/NOW.md signal the v2 retriever uses.
197
+ */
198
+ function buildSituationalContext(): string {
199
+ const now = readNowContext();
200
+ const today = `Today is ${new Date().toDateString()}.`;
201
+ return now ? `${today}\n\n${now}` : today;
202
+ }
203
+
168
204
  /**
169
205
  * Build a v3 {@link TurnContext} from the conversation's persisted messages.
170
206
  * `currentMessage` is the latest user message; `recentContext` is the tail of
171
- * the recent transcript. Returns `null` when there is no user message to route
207
+ * the recent transcript; `situationalContext` carries the current date and the
208
+ * live NOW.md scratchpad. Returns `null` when there is no user message to route
172
209
  * on (nothing to shadow this turn).
173
210
  */
174
211
  function buildShadowTurn(
@@ -198,6 +235,7 @@ function buildShadowTurn(
198
235
  turnNumber: turnIndex,
199
236
  currentMessage,
200
237
  recentContext,
238
+ situationalContext: buildSituationalContext(),
201
239
  };
202
240
  }
203
241
 
@@ -280,13 +318,15 @@ async function observeTurn(
280
318
  const turn = buildShadowTurn(conversationId, turnIndex);
281
319
  if (!turn) return null;
282
320
 
283
- const lanes = await getLanes(getConfig());
321
+ const cfg = getConfig();
322
+ const lanes = await getLanes(cfg);
284
323
  const result = await orchestrate(turn, {
285
324
  tree: lanes.tree,
286
325
  core: lanes.core,
287
326
  needle: lanes.needle,
288
327
  workingSet: lanes.workingSet,
289
328
  pageSummary,
329
+ l2Concurrency: cfg.memory.v3.l2Concurrency,
290
330
  });
291
331
 
292
332
  const rows = attributeSelections(lanes.tree, lanes.core, result);
@@ -329,7 +369,7 @@ export async function runShadowObservation(
329
369
  * (failure or empty selection) falls back to v2 memory rather than dropping all
330
370
  * memory.
331
371
  */
332
- const memoryV3Injector: Injector = {
372
+ export const memoryV3Injector: Injector = {
333
373
  name: "memory-v3-shadow",
334
374
  // High order so it sorts last; the live `<memory>` block uses the
335
375
  // after-memory-prefix placement so it lands at the memory boundary regardless
@@ -369,11 +409,3 @@ const memoryV3Injector: Injector = {
369
409
  }
370
410
  },
371
411
  };
372
-
373
- export const memoryV3ShadowPlugin: Plugin = {
374
- manifest: {
375
- name: "memory-v3-shadow",
376
- version: "0.0.1",
377
- },
378
- injectors: [memoryV3Injector],
379
- };
@@ -46,6 +46,14 @@ export interface TurnContext {
46
46
  turnNumber: number;
47
47
  currentMessage: string;
48
48
  recentContext: string;
49
+ /**
50
+ * Optional situational signal — the current date plus the live NOW.md
51
+ * scratchpad — so a leaf or page can be routed/selected on a date or
52
+ * live-state cue the message itself never names (e.g. a person whose
53
+ * anniversary is today). Omitted when unavailable; the router and selector
54
+ * render nothing for an undefined value.
55
+ */
56
+ situationalContext?: string;
49
57
  }
50
58
 
51
59
  export type SelectionSource = "l1+l2" | "core+l2" | "needle" | "carry-forward";
@@ -21,7 +21,6 @@
21
21
  import type { ConversationStrategy } from "../channels/config.js";
22
22
  import { getConversationStrategy } from "../channels/config.js";
23
23
  import type { ChannelId } from "../channels/types.js";
24
- import { isHomePageEnabled } from "../home/feature-gate.js";
25
24
  import {
26
25
  addMessage,
27
26
  createConversation,
@@ -120,20 +119,14 @@ export async function pairDeliveryWithConversation(
120
119
  // Passive vellum notifications surface via the home feed alone and link
121
120
  // back to the originating conversation via `signal.sourceContextId`.
122
121
  // Materializing a fresh per-notification conversation just to host the
123
- // seed message leaves a graveyard entry in the sidebar; skip it unless
124
- // the producer opted in via `requiresConversation` or the decision engine
125
- // requested explicit reuse of a target conversation.
126
- //
127
- // Gated on `home-page`: the home feed is the surface that hosts passive
128
- // notifications, so when the flag is off there is nowhere for them to
129
- // land fall through and create a conversation (the pre-home-feed
130
- // behavior) to preserve the notification's only surface.
131
- if (
132
- strategy === "start_new_conversation" &&
133
- !signal.requiresConversation &&
134
- conversationAction?.action !== "reuse_existing" &&
135
- isHomePageEnabled()
136
- ) {
122
+ // seed message leaves a graveyard entry in the sidebar; skip it
123
+ // unconditionally when the producer did not opt in via
124
+ // `requiresConversation`. The decision engine's `reuse_existing` hint is
125
+ // also ignored here: even a successful reuse would append a seed message
126
+ // to a conversation that the user didn't ask for, and a failed reuse
127
+ // (stale target / source mismatch) falls through to `createConversation`
128
+ // — producing exactly the graveyard entry we want to avoid.
129
+ if (strategy === "start_new_conversation" && !signal.requiresConversation) {
137
130
  return {
138
131
  conversationId: null,
139
132
  messageId: null,
@@ -149,8 +149,8 @@ function buildSystemPrompt(
149
149
  ` - Avoid meta-send phrasing (e.g. "I'd like to send a notification", "May I go ahead with that?"). Write the recipient-facing message directly.`,
150
150
  ` - Avoid intermediary-instruction phrasing like "consider telling the guardian", "ask the recipient to", or "the assistant should remind them". Rewrite it as final copy the recipient can act on directly.`,
151
151
  ` - For telegram: 1-2 concise sentences.`,
152
- `- \`conversationSeedMessage\` is the opening message in the internal notification conversation it can be richer and more contextual.`,
153
- ` - For vellum (desktop): 2-4 short sentences with useful context and clear next step if action is required.`,
152
+ `- \`conversationSeedMessage\` is the opening message in the internal notification conversation and also the expanded detail shown in the home feed. It should be richer and more contextual than the popup body.`,
153
+ ` - For vellum (desktop): use structured markdown for readability. Break content into bullet points, numbered lists, or short sections with **bold** labels. Avoid long unbroken paragraphs scan-friendly formatting is preferred.`,
154
154
  ` - Never dump raw JSON. Include only human-readable context.`,
155
155
  ``,
156
156
  `Conversation reuse guidelines:`,
@@ -838,7 +838,10 @@ export async function evaluateSignal(
838
838
  selectedChannels,
839
839
  reasoningSummary: "assistant_tool pass-through",
840
840
  renderedCopy: Object.fromEntries(
841
- availableChannels.map((ch) => [ch, { title, body }]),
841
+ availableChannels.map((ch) => [
842
+ ch,
843
+ { title, body, conversationSeedMessage: body },
844
+ ]),
842
845
  ) as NotificationDecision["renderedCopy"],
843
846
  conversationActions: Object.fromEntries(
844
847
  availableChannels.map((ch) => [ch, { action: "start_new" as const }]),
@@ -21,6 +21,7 @@ import { appendFeedItem } from "../home/feed-writer.js";
21
21
  import { getConversation } from "../memory/conversation-crud.js";
22
22
  import { isBackgroundConversationType } from "../memory/conversation-types.js";
23
23
  import { getLogger } from "../util/logger.js";
24
+ import { isConversationSeedSane } from "./conversation-seed-composer.js";
24
25
  import type { NotificationSignal } from "./signal.js";
25
26
  import type { NotificationDecision, RenderedChannelCopy } from "./types.js";
26
27
 
@@ -77,8 +78,18 @@ export async function writeHomeFeedItemForSignal(
77
78
  // against `summary` in the row. Leave undefined when absent; renderers
78
79
  // fall back to `summary`.
79
80
  const resolvedTitle = payloadTitle?.trim() || undefined;
81
+ // Prefer conversationSeedMessage over body for the home feed: the seed
82
+ // message is richer and may contain structured markdown (lists, headers,
83
+ // bold) that the detail panel renders. The popup-oriented `body` is
84
+ // intentionally short (≤ 2 sentences) and loses formatting.
85
+ const seedCandidate = renderedCopy?.conversationSeedMessage;
80
86
  const resolvedSummary =
81
- renderedCopy?.body?.trim() || payloadBody?.trim() || "";
87
+ (isConversationSeedSane(seedCandidate)
88
+ ? seedCandidate.trim()
89
+ : undefined) ||
90
+ renderedCopy?.body?.trim() ||
91
+ payloadBody?.trim() ||
92
+ "";
82
93
  if (!resolvedSummary) {
83
94
  log.warn(
84
95
  { signalId: signal.signalId, sourceEventName: signal.sourceEventName },
@@ -118,6 +118,10 @@ export class PermissionPrompter {
118
118
  label: o.label,
119
119
  scope: o.scope,
120
120
  })),
121
+ directoryScopeOptions: directoryScopeOptions?.map((o) => ({
122
+ label: o.label,
123
+ scope: o.scope,
124
+ })),
121
125
  persistentDecisionsAllowed: persistentDecisionsAllowed ?? true,
122
126
  },
123
127
  rpcResolve: resolve as (value: unknown) => void,
@@ -20,6 +20,10 @@ export const HOOKS = {
20
20
  SHUTDOWN: "shutdown",
21
21
  /** Fires once per user turn, immediately before the agent loop receives `runMessages`. */
22
22
  USER_PROMPT_SUBMIT: "user-prompt-submit",
23
+ /** Fires once per tool result, after the tool returns and before the result is sent to the provider. */
24
+ POST_TOOL_USE: "post-tool-use",
25
+ /** Fires when the model yields a response with no tool calls — the run's stop boundary. Decides whether to stop or continue with a follow-up turn. */
26
+ STOP: "stop",
23
27
  } as const;
24
28
 
25
29
  /** Union of every hook name declared in {@link HOOKS}. */
@@ -24,6 +24,10 @@
24
24
  * - {@link PluginShutdownContext} — passed to `shutdown` hook at teardown
25
25
  * - {@link UserPromptSubmitContext} — passed to `user-prompt-submit` hook,
26
26
  * fired immediately before the agent loop receives a user's prompt
27
+ * - {@link PostToolUseContext} — passed to `post-tool-use` hook, fired once
28
+ * per tool result before it joins the provider-bound history
29
+ * - {@link StopContext} — passed to `stop` hook, fired when the model yields
30
+ * a response with no tool calls
27
31
  * - {@link PluginHookFn} — signature every lifecycle hook implements
28
32
  * - {@link PluginLogger} — pino-compatible logger shape on the contexts
29
33
  * - {@link ToolDefinition} — author-facing tool spec (default-export shape
@@ -31,7 +35,7 @@
31
35
  * - {@link ToolContext} — passed to a plugin tool's `execute` method
32
36
  * - {@link ToolExecutionResult} — return shape of a plugin tool's `execute`
33
37
  *
34
- * Pipeline-argument types (`LLMCallArgs`, `MemoryArgs`, etc.) currently
38
+ * Pipeline-argument types (`MemoryArgs`, `CompactionArgs`, etc.) currently
35
39
  * live in `assistant/src/plugins/types.ts` and have not yet migrated into
36
40
  * this package. A follow-up PR will move them into this surface as the
37
41
  * per-pipeline schemas stabilize.
@@ -44,6 +48,9 @@ export type {
44
48
  PluginInitContext,
45
49
  PluginLogger,
46
50
  PluginShutdownContext,
51
+ PostToolUseContext,
52
+ StopContext,
53
+ StopDecision,
47
54
  ToolContext,
48
55
  ToolDefinition,
49
56
  ToolExecutionResult,
@@ -4,7 +4,11 @@
4
4
  * removing is breaking and gated on a major bump.
5
5
  */
6
6
 
7
- import type { Message } from "../providers/types.js";
7
+ import type {
8
+ ContentBlock,
9
+ Message,
10
+ ToolResultContent,
11
+ } from "../providers/types.js";
8
12
 
9
13
  export type {
10
14
  ToolContext,
@@ -47,6 +51,8 @@ export interface PluginLogger {
47
51
  * - `init` — {@link PluginInitContext}
48
52
  * - `shutdown` — {@link PluginShutdownContext}
49
53
  * - `user-prompt-submit` — {@link UserPromptSubmitContext}
54
+ * - `post-tool-use` — {@link PostToolUseContext}
55
+ * - `stop` — {@link StopContext}
50
56
  */
51
57
  export type PluginHookFn<TCtx = unknown> = (ctx: TCtx) => Promise<TCtx | void>;
52
58
 
@@ -121,6 +127,16 @@ export interface PluginShutdownContext {
121
127
  export interface UserPromptSubmitContext {
122
128
  /** Conversation ID the user prompt was submitted on. */
123
129
  readonly conversationId: string;
130
+ /**
131
+ * The text of the user prompt that triggered this turn — the resolved
132
+ * user message (after slash-command expansion), independent of any
133
+ * internal rewriting applied to the message that flows into the model.
134
+ * Mirrors the `prompt` field Claude Code / Codex pass to their
135
+ * `UserPromptSubmit` hooks, so hooks that key off the submitted text
136
+ * (e.g. title generation) read it directly rather than reconstructing
137
+ * it from the message arrays.
138
+ */
139
+ readonly prompt: string;
124
140
  /**
125
141
  * The user's original message list, immutable for the hook. Plugins
126
142
  * may snapshot or compare against this but MUST NOT mutate it.
@@ -138,3 +154,137 @@ export interface UserPromptSubmitContext {
138
154
  */
139
155
  readonly logger: PluginLogger;
140
156
  }
157
+
158
+ // ─── Post-tool-use hook context ──────────────────────────────────────────────
159
+
160
+ /**
161
+ * Context passed to the `post-tool-use` hook. Fires once per tool result —
162
+ * after the tool returns and before the result is appended to the message
163
+ * history sent to the provider. With several tools dispatched in a single
164
+ * turn, the hook fires once per result, in tool-use order.
165
+ *
166
+ * The hook may transform the result either by mutating `toolResponse` in
167
+ * place (e.g. reassigning `toolResponse.content`) or by returning a new
168
+ * context with a fresh `toolResponse` — see {@link PluginHookFn}'s
169
+ * polymorphic return shape. The daemon threads the final `toolResponse`
170
+ * into the provider-bound history.
171
+ *
172
+ * Multiple plugins' hooks chain in registration order — each plugin's hook
173
+ * sees the previous plugin's mutations. The default tool-result-truncate
174
+ * plugin contributes a hook here that tail-drops oversized output to fit the
175
+ * model's context window; the default tool-error plugin sets
176
+ * {@link additionalContext} with retry coaching for failed results. User hooks
177
+ * can swap in a smarter strategy (e.g. a summarizer) or observe results for
178
+ * side effects.
179
+ */
180
+ export interface PostToolUseContext {
181
+ /** Conversation ID the tool ran on. */
182
+ readonly conversationId: string;
183
+ /**
184
+ * The tool result block. Plugins may mutate its `content` in place or
185
+ * replace the block by returning a new context.
186
+ */
187
+ toolResponse: ToolResultContent;
188
+ /**
189
+ * Conversation history up to and including the assistant turn that issued
190
+ * this tool call. The current result is not in it yet — it lives in
191
+ * {@link toolResponse}. A hook reasoning about prior tool outcomes (e.g.
192
+ * how many times a tool has failed in a row) derives that from the history
193
+ * content rather than a precomputed counter, so the signal survives mid-run
194
+ * compaction rewriting the array. Read-only: hooks transform the result via
195
+ * {@link toolResponse}, not by mutating history.
196
+ */
197
+ readonly messages: ReadonlyArray<Message>;
198
+ /**
199
+ * Extra guidance for the model that is not part of the tool's output. A hook
200
+ * sets this to surface provider-only context — e.g. retry coaching for a
201
+ * failed result — and the daemon appends it to the provider-bound history as
202
+ * a separate block *after* emitting the tool_result, so it reaches the model
203
+ * without polluting the client-facing or persisted tool output. Mirrors
204
+ * Claude Code's PostToolUse `hookSpecificOutput.additionalContext` and the
205
+ * singular of Codex's `additional_contexts`. Unset means no extra context.
206
+ */
207
+ additionalContext?: string;
208
+ /**
209
+ * The model's context-window size in tokens. Plugins derive their own
210
+ * character budget from this (e.g. a share of the window) rather than
211
+ * receiving a precomputed limit.
212
+ */
213
+ readonly maxInputTokens: number;
214
+ /**
215
+ * Logger scoped to the current turn. The same instance is shared by
216
+ * every hook in the chain, so plugins should tag their structured log
217
+ * fields (e.g. `{ plugin: "<name>" }`) for attribution.
218
+ */
219
+ readonly logger: PluginLogger;
220
+ }
221
+
222
+ // ─── Stop hook context ───────────────────────────────────────────────────────
223
+
224
+ /**
225
+ * Binary outcome of the `stop` hook. The agent loop seeds it to `"stop"`
226
+ * and acts on the value the chain settles on:
227
+ *
228
+ * - `"stop"` — let the turn end; the loop yields the assistant response
229
+ * to the user. This is the default.
230
+ * - `"continue"` — re-query the model. The hook is responsible for appending
231
+ * the follow-up turn it wants the model to see to
232
+ * {@link StopContext.messages} before returning.
233
+ *
234
+ * To abort with an error a hook should throw — the loop's error handler
235
+ * surfaces it. There is intentionally no error decision value.
236
+ */
237
+ export type StopDecision = "continue" | "stop";
238
+
239
+ /**
240
+ * Context passed to the `stop` hook. Fires when the model yields a response
241
+ * with no tool calls — the run's stop boundary, where the loop is about to
242
+ * hand the turn back to the user. The default empty-response plugin uses it
243
+ * to re-query the model when a turn came back empty or as a provider refusal.
244
+ *
245
+ * The hook decides the outcome by setting {@link decision}. When it sets
246
+ * `"continue"` it must also append the follow-up turn (e.g. a nudge `user`
247
+ * message) to {@link messages}; the loop threads those messages into the next
248
+ * iteration. {@link messages} is the full conversation history, carried back
249
+ * verbatim. A hook that needs to reason about just the current response cycle
250
+ * (e.g. whether an earlier turn already delivered visible text) derives that
251
+ * boundary from the history itself — the messages after the last genuine user
252
+ * prompt — rather than an index, since mid-run compaction can rewrite the
253
+ * array.
254
+ *
255
+ * Multiple plugins' hooks chain in registration order — each sees the
256
+ * previous hook's `decision` and `messages` mutations.
257
+ */
258
+ export interface StopContext {
259
+ /** Conversation ID the run belongs to. */
260
+ readonly conversationId: string;
261
+ /**
262
+ * Full conversation history: the inbound conversation followed by every
263
+ * message produced this run. A hook that sets `decision` to `"continue"`
264
+ * appends its follow-up turn here; the loop carries the result into the
265
+ * next iteration.
266
+ */
267
+ messages: Message[];
268
+ /**
269
+ * Content blocks of the assistant turn that triggered the stop. Guaranteed
270
+ * to contain no `tool_use` blocks — the hook only fires at the boundary
271
+ * where the model stopped requesting tools.
272
+ */
273
+ readonly responseContent: ReadonlyArray<ContentBlock>;
274
+ /**
275
+ * Provider-reported stop reason for the assistant turn (e.g. `"refusal"`,
276
+ * `"end_turn"`). `null`/`undefined` when the provider didn't report one.
277
+ */
278
+ readonly stopReason: string | null | undefined;
279
+ /**
280
+ * Seeded to `"stop"`. A hook sets it to `"continue"` to force another loop
281
+ * iteration; later hooks in the chain may override it.
282
+ */
283
+ decision: StopDecision;
284
+ /**
285
+ * Logger scoped to the current turn. The same instance is shared by
286
+ * every hook in the chain, so plugins should tag their structured log
287
+ * fields (e.g. `{ plugin: "<name>" }`) for attribution.
288
+ */
289
+ readonly logger: PluginLogger;
290
+ }