@vellumai/assistant 0.8.7 → 0.8.8-dev.202606052332.17fc8ea

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 (570) hide show
  1. package/Dockerfile +20 -4
  2. package/bun.lock +2 -2
  3. package/docker-entrypoint.sh +4 -2
  4. package/docker-init-apt-root.sh +3 -1
  5. package/docker-kata-apt-env.sh +3 -1
  6. package/docker-kata-runtime-family.sh +12 -0
  7. package/docs/architecture/memory.md +1 -1
  8. package/examples/plugins/echo/README.md +61 -66
  9. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  10. package/examples/plugins/echo/hooks/stop.ts +16 -0
  11. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  12. package/examples/plugins/echo/package.json +1 -2
  13. package/examples/plugins/echo/src/emit.ts +19 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  15. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  16. package/openapi.yaml +3378 -335
  17. package/package.json +2 -2
  18. package/scripts/generate-openapi.ts +68 -41
  19. package/src/__tests__/agent-loop-exit-reason.test.ts +35 -93
  20. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  21. package/src/__tests__/agent-loop.test.ts +37 -87
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  23. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  24. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  25. package/src/__tests__/anthropic-provider.test.ts +95 -2
  26. package/src/__tests__/app-control-flow.test.ts +1 -1
  27. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  30. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  31. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  32. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  33. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  34. package/src/__tests__/btw-routes.test.ts +62 -3
  35. package/src/__tests__/build-persisted-content.test.ts +184 -0
  36. package/src/__tests__/catalog-files.test.ts +1 -1
  37. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  38. package/src/__tests__/channel-approvals.test.ts +1 -1
  39. package/src/__tests__/clawhub-files.test.ts +1 -1
  40. package/src/__tests__/compaction-circuit.test.ts +258 -0
  41. package/src/__tests__/compaction-direct.test.ts +132 -0
  42. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  43. package/src/__tests__/config-watcher.test.ts +1 -1
  44. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  45. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -5
  46. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -7
  47. package/src/__tests__/conversation-agent-loop-overflow.test.ts +316 -1143
  48. package/src/__tests__/conversation-agent-loop.test.ts +638 -1655
  49. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  50. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  51. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  52. package/src/__tests__/conversation-pairing.test.ts +4 -31
  53. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  54. package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -10
  55. package/src/__tests__/conversation-queue.test.ts +2 -0
  56. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  57. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  58. package/src/__tests__/conversation-runtime-assembly.test.ts +310 -300
  59. package/src/__tests__/conversation-runtime-workspace.test.ts +105 -45
  60. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  61. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  62. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  63. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  64. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  65. package/src/__tests__/conversation-title-service.test.ts +135 -2
  66. package/src/__tests__/conversation-workspace-cache-state.test.ts +17 -16
  67. package/src/__tests__/conversation-workspace-injection.test.ts +67 -2
  68. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +7 -6
  69. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  70. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  71. package/src/__tests__/db-acp-history.test.ts +101 -0
  72. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  73. package/src/__tests__/dm-persistence.test.ts +5 -1
  74. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  75. package/src/__tests__/empty-response-hook.test.ts +304 -0
  76. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  77. package/src/__tests__/file-write-tool.test.ts +63 -0
  78. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  79. package/src/__tests__/gemini-image-service.test.ts +13 -0
  80. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  81. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  82. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  83. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  84. package/src/__tests__/heartbeat-service.test.ts +1 -0
  85. package/src/__tests__/helpers/mock-provider.ts +110 -0
  86. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  87. package/src/__tests__/history-repair-hook.test.ts +1 -0
  88. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  89. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  90. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  91. package/src/__tests__/identity-routes.test.ts +248 -7
  92. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  93. package/src/__tests__/injector-background-turn.test.ts +3 -9
  94. package/src/__tests__/injector-chain.test.ts +139 -275
  95. package/src/__tests__/injector-disk-pressure.test.ts +75 -41
  96. package/src/__tests__/injector-document-comments.test.ts +3 -3
  97. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  98. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  99. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  100. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  101. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  102. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  103. package/src/__tests__/llm-usage-store.test.ts +223 -1
  104. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  105. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  106. package/src/__tests__/native-web-search.test.ts +191 -0
  107. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  108. package/src/__tests__/openai-image-service.test.ts +17 -0
  109. package/src/__tests__/openai-provider.test.ts +31 -1
  110. package/src/__tests__/{overflow-reduce-pipeline.test.ts → overflow-reduction-loop.test.ts} +64 -284
  111. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  112. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  113. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  114. package/src/__tests__/plugin-api-shim.test.ts +3 -6
  115. package/src/__tests__/plugin-bootstrap.test.ts +14 -40
  116. package/src/__tests__/plugin-registry.test.ts +3 -76
  117. package/src/__tests__/plugin-types.test.ts +0 -193
  118. package/src/__tests__/process-message-display-content.test.ts +6 -2
  119. package/src/__tests__/reaction-persistence.test.ts +1 -1
  120. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  121. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  122. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  123. package/src/__tests__/schedule-routes.test.ts +603 -2
  124. package/src/__tests__/schedule-store.test.ts +41 -0
  125. package/src/__tests__/schedule-tools.test.ts +35 -0
  126. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  127. package/src/__tests__/server-history-render.test.ts +314 -1
  128. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  129. package/src/__tests__/skillssh-files.test.ts +1 -1
  130. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  131. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  132. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  133. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  134. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  135. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  136. package/src/__tests__/system-prompt.test.ts +20 -0
  137. package/src/__tests__/task-scheduler.test.ts +162 -1
  138. package/src/__tests__/terminal-tools.test.ts +6 -1
  139. package/src/__tests__/title-generate-hook.test.ts +319 -0
  140. package/src/__tests__/tool-error-hook.test.ts +278 -0
  141. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  142. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  143. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  144. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  145. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  146. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  147. package/src/__tests__/usage-routes.test.ts +285 -1
  148. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  149. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  150. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  151. package/src/acp/__tests__/agent-process.test.ts +161 -0
  152. package/src/acp/__tests__/client-handler.test.ts +40 -0
  153. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  154. package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
  155. package/src/acp/__tests__/prepare-agent-env.test.ts +137 -0
  156. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  157. package/src/acp/__tests__/session-manager-resume.test.ts +736 -0
  158. package/src/acp/agent-process.ts +61 -1
  159. package/src/acp/auto-install.test.ts +196 -0
  160. package/src/acp/auto-install.ts +177 -0
  161. package/src/acp/client-handler.ts +31 -0
  162. package/src/acp/feature-gate.test.ts +48 -0
  163. package/src/acp/feature-gate.ts +34 -0
  164. package/src/acp/prepare-agent-env.ts +83 -29
  165. package/src/acp/resolve-agent.test.ts +320 -7
  166. package/src/acp/resolve-agent.ts +182 -18
  167. package/src/acp/resume-hint.ts +25 -0
  168. package/src/acp/session-manager.ts +495 -73
  169. package/src/acp/types.ts +8 -0
  170. package/src/agent/compaction-circuit.ts +60 -102
  171. package/src/agent/loop.ts +362 -485
  172. package/src/api/events/assistant-thinking-delta.ts +33 -0
  173. package/src/api/events/tool-output-chunk.ts +45 -0
  174. package/src/api/events/tool-use-preview-start.ts +32 -0
  175. package/src/api/events/trace-event.ts +69 -0
  176. package/src/api/index.ts +48 -13
  177. package/src/api/responses/conversation-message.ts +374 -0
  178. package/src/approvals/guardian-request-resolvers.ts +1 -1
  179. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  180. package/src/background-wake/next-wake.ts +1 -0
  181. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  182. package/src/cli/commands/notifications.ts +112 -60
  183. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  184. package/src/config/acp-defaults.test.ts +10 -0
  185. package/src/config/acp-defaults.ts +6 -0
  186. package/src/config/assistant-feature-flags.ts +22 -11
  187. package/src/config/bundled-skills/acp/SKILL.md +83 -31
  188. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  189. package/src/config/bundled-skills/app-builder/SKILL.md +224 -398
  190. package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
  191. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  192. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  193. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  194. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  195. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  196. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  197. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  198. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  199. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  200. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  201. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  202. package/src/config/bundled-tool-registry.ts +2 -0
  203. package/src/config/feature-flag-cache.ts +3 -3
  204. package/src/config/feature-flag-registry.json +48 -7
  205. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  207. package/src/config/schemas/heartbeat.ts +9 -0
  208. package/src/config/schemas/llm.ts +1 -0
  209. package/src/config/schemas/memory-v2.ts +8 -0
  210. package/src/config/schemas/memory-v3.ts +8 -0
  211. package/src/config/schemas/platform.ts +8 -0
  212. package/src/config/seed-inference-profiles.ts +2 -2
  213. package/src/config/skills.ts +13 -0
  214. package/src/context/compactor.ts +1 -1
  215. package/src/context/strip-injections.ts +128 -0
  216. package/src/context/token-estimator.ts +23 -0
  217. package/src/context/tool-result-truncation.ts +0 -23
  218. package/src/context/window-manager.ts +5 -7
  219. package/src/credential-execution/executable-discovery.ts +16 -0
  220. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  221. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  222. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  223. package/src/daemon/assistant-attachments.ts +1 -1
  224. package/src/daemon/config-watcher.ts +2 -2
  225. package/src/daemon/context-overflow-reducer.ts +0 -1
  226. package/src/daemon/conversation-agent-loop-handlers.ts +594 -153
  227. package/src/daemon/conversation-agent-loop.ts +301 -997
  228. package/src/daemon/conversation-history.ts +5 -4
  229. package/src/daemon/conversation-lifecycle.ts +3 -4
  230. package/src/daemon/conversation-messaging.ts +7 -6
  231. package/src/daemon/conversation-process.ts +11 -16
  232. package/src/daemon/conversation-registry.ts +159 -0
  233. package/src/daemon/conversation-runtime-assembly.ts +218 -398
  234. package/src/daemon/conversation-slash.ts +6 -25
  235. package/src/daemon/conversation-store.ts +9 -90
  236. package/src/daemon/conversation-surfaces.ts +222 -4
  237. package/src/daemon/conversation-tool-setup.ts +2 -29
  238. package/src/daemon/conversation-workspace.ts +17 -0
  239. package/src/daemon/conversation.ts +32 -20
  240. package/src/daemon/external-plugins-bootstrap.ts +17 -18
  241. package/src/daemon/handlers/config-a2a.ts +51 -36
  242. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  243. package/src/daemon/handlers/config-telegram.ts +16 -2
  244. package/src/daemon/handlers/conversations.ts +3 -1
  245. package/src/daemon/handlers/shared.ts +156 -84
  246. package/src/daemon/handlers/skills.ts +42 -10
  247. package/src/daemon/lifecycle.ts +25 -0
  248. package/src/daemon/message-types/apps.ts +1 -29
  249. package/src/daemon/message-types/messages.ts +9 -57
  250. package/src/daemon/message-types/skills.ts +2 -0
  251. package/src/daemon/message-types/surfaces.ts +136 -3
  252. package/src/daemon/now-scratchpad.ts +21 -0
  253. package/src/daemon/orphan-reaper.test.ts +210 -0
  254. package/src/daemon/orphan-reaper.ts +240 -0
  255. package/src/daemon/overflow-reduction-loop.ts +230 -0
  256. package/src/daemon/persist-unsendable-image.ts +117 -0
  257. package/src/daemon/process-message.ts +1 -3
  258. package/src/daemon/server.ts +2 -0
  259. package/src/daemon/trace-emitter.ts +6 -4
  260. package/src/daemon/trust-context.ts +19 -0
  261. package/src/daemon/wake-target-adapter.ts +3 -1
  262. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  263. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  264. package/src/heartbeat/heartbeat-service.ts +26 -0
  265. package/src/home/home-greeting-cache.ts +24 -1
  266. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  267. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  268. package/src/ipc/gateway-client.test.ts +2 -2
  269. package/src/ipc/gateway-client.ts +3 -3
  270. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  271. package/src/ipc/skill-routes/memory.ts +4 -2
  272. package/src/media/gemini-image-service.ts +15 -0
  273. package/src/media/openai-image-service.ts +14 -0
  274. package/src/media/types.ts +34 -0
  275. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  276. package/src/memory/auth-fallback-events-store.ts +94 -0
  277. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  278. package/src/memory/conversation-title-service.ts +65 -41
  279. package/src/memory/db-init.ts +6 -0
  280. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  281. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  282. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  283. package/src/memory/jobs-store.ts +33 -0
  284. package/src/memory/jobs-worker.ts +32 -5
  285. package/src/memory/llm-usage-store.ts +224 -50
  286. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  287. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  288. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  289. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  290. package/src/memory/migrations/index.ts +3 -0
  291. package/src/memory/pkb/autoinject.ts +61 -0
  292. package/src/memory/pkb/context.ts +50 -0
  293. package/src/memory/pkb/types.ts +14 -0
  294. package/src/memory/schedule-attribution-sql.ts +104 -0
  295. package/src/memory/schema/acp.ts +4 -0
  296. package/src/memory/schema/infrastructure.ts +16 -0
  297. package/src/memory/usage-grouped-buckets.ts +6 -1
  298. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -4
  299. package/src/memory/v2/consolidation-job.ts +14 -5
  300. package/src/notifications/conversation-pairing.ts +8 -15
  301. package/src/notifications/decision-engine.ts +6 -3
  302. package/src/notifications/home-feed-side-effect.ts +12 -1
  303. package/src/permissions/prompter.ts +4 -0
  304. package/src/plugin-api/constants.ts +4 -0
  305. package/src/plugin-api/index.ts +7 -5
  306. package/src/plugin-api/types.ts +151 -1
  307. package/src/plugins/defaults/compaction/compact.ts +59 -0
  308. package/src/plugins/defaults/compaction/package.json +1 -1
  309. package/src/plugins/defaults/compaction/register.ts +8 -19
  310. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  311. package/src/plugins/defaults/empty-response/register.ts +8 -13
  312. package/src/plugins/defaults/index.ts +2 -18
  313. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +95 -0
  314. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  315. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  316. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +288 -81
  317. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  318. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +16 -0
  319. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +4 -4
  320. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  321. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +48 -12
  322. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +13 -0
  323. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  324. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  325. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +104 -32
  326. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  327. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +96 -30
  328. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +34 -16
  329. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +5 -5
  330. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +2 -2
  331. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  332. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  333. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  334. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  335. package/src/plugins/defaults/memory-v3-shadow/llm-retry.ts +32 -0
  336. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +8 -8
  337. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +26 -14
  338. package/src/plugins/defaults/{llm-call → memory-v3-shadow}/package.json +2 -2
  339. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  340. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +26 -0
  341. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +3 -3
  342. package/src/plugins/defaults/memory-v3-shadow/register.ts +26 -0
  343. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  344. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +51 -45
  345. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  346. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +61 -46
  347. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +69 -99
  348. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  349. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +8 -0
  350. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  351. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  352. package/src/plugins/defaults/title-generate/package.json +1 -1
  353. package/src/plugins/defaults/title-generate/register.ts +18 -18
  354. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  355. package/src/plugins/defaults/tool-error/package.json +1 -1
  356. package/src/plugins/defaults/tool-error/register.ts +9 -21
  357. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  358. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  359. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  360. package/src/plugins/external-api.ts +2 -2
  361. package/src/plugins/pipeline.ts +6 -305
  362. package/src/plugins/registry.ts +10 -55
  363. package/src/plugins/types.ts +62 -797
  364. package/src/plugins/user-loader.ts +30 -127
  365. package/src/proactive-artifact/aux-message-injector.ts +4 -4
  366. package/src/proactive-artifact/job.test.ts +8 -13
  367. package/src/prompts/__tests__/system-prompt.test.ts +42 -0
  368. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +64 -0
  369. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  370. package/src/prompts/templates/system-sections.ts +15 -0
  371. package/src/providers/anthropic/client.ts +37 -29
  372. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  373. package/src/providers/openai/chat-completions-provider.ts +44 -0
  374. package/src/providers/openrouter/client.ts +1 -0
  375. package/src/providers/placeholder-sentinels.ts +35 -0
  376. package/src/runtime/__tests__/agent-wake.test.ts +10 -6
  377. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  378. package/src/runtime/agent-wake.ts +2 -5
  379. package/src/runtime/assistant-event-hub.ts +37 -7
  380. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  381. package/src/runtime/channel-approvals.ts +1 -1
  382. package/src/runtime/http-router.ts +16 -21
  383. package/src/runtime/http-types.ts +16 -70
  384. package/src/runtime/interactive-ui.ts +1 -1
  385. package/src/runtime/pending-interactions.ts +1 -0
  386. package/src/runtime/routes/__tests__/acp-routes.test.ts +283 -55
  387. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  388. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  389. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  390. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  391. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  392. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  393. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  394. package/src/runtime/routes/acp-routes.test.ts +89 -25
  395. package/src/runtime/routes/acp-routes.ts +81 -29
  396. package/src/runtime/routes/app-management-routes.ts +6 -117
  397. package/src/runtime/routes/app-routes.ts +13 -15
  398. package/src/runtime/routes/approval-routes.ts +1 -1
  399. package/src/runtime/routes/attachment-routes.ts +26 -15
  400. package/src/runtime/routes/avatar-routes.ts +26 -0
  401. package/src/runtime/routes/browser-routes.ts +1 -1
  402. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  403. package/src/runtime/routes/btw-routes.ts +29 -23
  404. package/src/runtime/routes/consolidation-routes.ts +120 -20
  405. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  406. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  407. package/src/runtime/routes/conversation-query-routes.ts +3 -1
  408. package/src/runtime/routes/conversation-routes.ts +372 -185
  409. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  410. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  411. package/src/runtime/routes/documents-routes.ts +4 -0
  412. package/src/runtime/routes/domain-routes.ts +51 -37
  413. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  414. package/src/runtime/routes/events-routes.ts +28 -34
  415. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  416. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  417. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  418. package/src/runtime/routes/host-cu-routes.ts +1 -1
  419. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  420. package/src/runtime/routes/identity-routes.ts +224 -18
  421. package/src/runtime/routes/image-generation-routes.ts +40 -2
  422. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  423. package/src/runtime/routes/index.ts +2 -0
  424. package/src/runtime/routes/integrations/a2a.ts +12 -10
  425. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  426. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  427. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  428. package/src/runtime/routes/integrations/telegram.ts +6 -0
  429. package/src/runtime/routes/integrations/twilio.ts +42 -0
  430. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  431. package/src/runtime/routes/log-export-routes.ts +8 -0
  432. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  433. package/src/runtime/routes/memory-v3-routes.ts +66 -34
  434. package/src/runtime/routes/oauth-apps.ts +66 -12
  435. package/src/runtime/routes/oauth-providers.ts +44 -5
  436. package/src/runtime/routes/platform-routes.ts +81 -5
  437. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  438. package/src/runtime/routes/playground/force-compact.ts +1 -1
  439. package/src/runtime/routes/playground/helpers.ts +1 -1
  440. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  441. package/src/runtime/routes/schedule-routes.ts +152 -42
  442. package/src/runtime/routes/secret-routes.ts +14 -2
  443. package/src/runtime/routes/skills-routes.ts +43 -14
  444. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  445. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  446. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  447. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  448. package/src/runtime/routes/tts-routes.ts +35 -0
  449. package/src/runtime/routes/types.ts +66 -8
  450. package/src/runtime/routes/usage-routes.ts +47 -39
  451. package/src/runtime/routes/webhook-routes.ts +41 -2
  452. package/src/runtime/routes/work-items-routes.ts +2 -4
  453. package/src/runtime/routes/workspace-routes.ts +4 -0
  454. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  455. package/src/runtime/services/analyze-conversation.ts +2 -2
  456. package/src/runtime/services/conversation-serializer.ts +1 -1
  457. package/src/schedule/schedule-store.ts +20 -1
  458. package/src/schedule/schedule-usage-store.ts +83 -0
  459. package/src/schedule/scheduler.ts +12 -5
  460. package/src/signals/cancel.ts +2 -4
  461. package/src/skills/catalog-files.ts +2 -2
  462. package/src/skills/catalog-install.ts +3 -0
  463. package/src/skills/categories-cache.ts +118 -0
  464. package/src/skills/clawhub-files.ts +1 -2
  465. package/src/skills/skillssh-files.ts +1 -2
  466. package/src/subagent/manager.ts +17 -5
  467. package/src/telemetry/types.ts +29 -1
  468. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  469. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  470. package/src/tools/acp/context.ts +20 -0
  471. package/src/tools/acp/list-agents.test.ts +7 -1
  472. package/src/tools/acp/spawn.test.ts +158 -55
  473. package/src/tools/acp/spawn.ts +47 -72
  474. package/src/tools/acp/steer.test.ts +105 -8
  475. package/src/tools/acp/steer.ts +48 -17
  476. package/src/tools/apps/executors.ts +13 -8
  477. package/src/tools/executor.ts +1 -53
  478. package/src/tools/filesystem/write.ts +34 -0
  479. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  480. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  481. package/src/tools/network/web-search-error.test.ts +248 -0
  482. package/src/tools/network/web-search-error.ts +267 -0
  483. package/src/tools/network/web-search.ts +207 -48
  484. package/src/tools/schedule/create.ts +2 -0
  485. package/src/tools/subagent/spawn.ts +2 -4
  486. package/src/tools/terminal/safe-env.ts +10 -1
  487. package/src/tools/ui-surface/definitions.ts +34 -5
  488. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  489. package/src/tts/provider-catalog.ts +76 -1
  490. package/src/util/mutex.ts +47 -0
  491. package/src/workspace/git-service.ts +1 -42
  492. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  493. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  494. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  495. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +117 -0
  496. package/src/workspace/migrations/registry.ts +6 -0
  497. package/docs/plugins.md +0 -836
  498. package/examples/plugins/echo/register.ts +0 -184
  499. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  500. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  501. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  502. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  503. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  504. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  505. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  506. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  507. package/src/__tests__/pipeline-runner.test.ts +0 -564
  508. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  509. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  510. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  511. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  512. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  513. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  514. package/src/gallery/default-gallery.ts +0 -1359
  515. package/src/gallery/gallery-manifest.ts +0 -28
  516. package/src/home/feature-gate.ts +0 -22
  517. package/src/memory/v3/provider-blocks.ts +0 -16
  518. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  519. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  520. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  521. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  522. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  523. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  524. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  525. package/src/plugins/defaults/injectors/package.json +0 -15
  526. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  527. package/src/plugins/defaults/llm-call/register.ts +0 -45
  528. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  529. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  530. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  531. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  532. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  533. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  534. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  535. package/src/plugins/defaults/persistence/package.json +0 -15
  536. package/src/plugins/defaults/persistence/register.ts +0 -38
  537. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  538. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  539. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  540. package/src/plugins/defaults/token-estimate/package.json +0 -15
  541. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  542. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  543. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  544. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  545. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  546. package/src/plugins/defaults/tool-execute/package.json +0 -15
  547. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  548. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  549. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  550. package/src/skills/category-inference.ts +0 -111
  551. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  552. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  553. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  554. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  555. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  556. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  557. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  558. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +0 -0
  559. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  560. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  561. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  562. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  563. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  564. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  565. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  566. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  567. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  568. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  569. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  570. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -17,6 +17,7 @@ import type {
17
17
  import { getConfig } from "../config/loader.js";
18
18
  import { recordEstimate } from "../context/estimator-calibration.js";
19
19
  import { getCalibrationProviderKey } from "../context/token-estimator.js";
20
+ import type { ContextWindowResult } from "../context/window-manager.js";
20
21
  import { projectAssistantMessage } from "../memory/conversation-attention-store.js";
21
22
  import {
22
23
  deleteMessageById,
@@ -24,8 +25,12 @@ import {
24
25
  getMessageById,
25
26
  messageMetadataSchema,
26
27
  provenanceFromTrustContext,
28
+ reserveMessage,
29
+ setConversationHistoryStrippedAt,
30
+ setLastNotifiedInferenceProfile,
27
31
  updateMessageContent,
28
32
  } from "../memory/conversation-crud.js";
33
+ import { syncMessageToDisk } from "../memory/conversation-disk-view.js";
29
34
  import { indexMessageNow } from "../memory/indexer.js";
30
35
  import {
31
36
  backfillMessageIdOnLogs,
@@ -41,20 +46,24 @@ import {
41
46
  type SlackMessageMetadata,
42
47
  writeSlackMetadata,
43
48
  } from "../messaging/providers/slack/message-metadata.js";
44
- import { defaultPersistenceTerminal } from "../plugins/defaults/persistence/terminal.js";
45
- import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
46
- import { getMiddlewaresFor } from "../plugins/registry.js";
47
49
  import type {
48
- PersistArgs,
49
- PersistReserveResult,
50
- PersistResult,
51
- TurnContext,
52
- } from "../plugins/types.js";
53
- import type { ContentBlock, ImageContent } from "../providers/types.js";
50
+ ContentBlock,
51
+ ImageContent,
52
+ Message,
53
+ } from "../providers/types.js";
54
54
  import { isContextOverflowError } from "../providers/types.js";
55
+ import {
56
+ getCurrentSeq,
57
+ recordPersistedSeq,
58
+ } from "../runtime/assistant-stream-state.js";
55
59
  import { publishSyncInvalidation } from "../runtime/sync/sync-publisher.js";
56
60
  import { redactSecrets } from "../security/secret-scanner.js";
57
61
  import { extractDomain } from "../tools/network/domain-normalize.js";
62
+ import {
63
+ classifyWebSearchFailure,
64
+ logWebSearchBackendFailure,
65
+ WEB_SEARCH_BACKEND_FAILURE_MESSAGE,
66
+ } from "../tools/network/web-search-error.js";
58
67
  import {
59
68
  buildPricingUsage,
60
69
  resolveStructuredPricing,
@@ -87,45 +96,36 @@ import type {
87
96
  } from "./message-protocol.js";
88
97
  import { conversationMetadataSyncTag } from "./message-types/sync.js";
89
98
  import type {
99
+ ToolActivityMetadata,
90
100
  WebSearchMetadata,
91
101
  WebSearchResultItem,
92
102
  } from "./message-types/web-activity.js";
93
- import { FALLBACK_TURN_TRUST } from "./trust-context.js";
94
103
 
95
104
  const log = getLogger("agent-loop-handlers");
96
105
 
106
+ /**
107
+ * Persist the history-stripped marker after the loop strips runtime injections
108
+ * for compaction / overflow recovery. The marker is a durability hint, not
109
+ * turn-critical state — a transient SQLite write failure (SQLITE_BUSY,
110
+ * disk-full, read-only FS) must not abort the turn, so failures log a warning
111
+ * and continue.
112
+ */
113
+ export function markHistoryStrippedBestEffort(conversationId: string): void {
114
+ try {
115
+ setConversationHistoryStrippedAt(conversationId, Date.now());
116
+ } catch (err) {
117
+ log.warn(
118
+ { err, conversationId },
119
+ "Failed to persist history-stripped marker after compaction strip (non-fatal)",
120
+ );
121
+ }
122
+ }
123
+
97
124
  // ── Partial-persistence tunables ─────────────────────────────────────
98
125
  // Debounce for mid-turn `updateContent` writes from text deltas.
99
126
  // Indexer + projector still fire ONLY at `handleMessageComplete`.
100
127
  const PARTIAL_PERSIST_DEBOUNCE_MS = 1000;
101
128
 
102
- /**
103
- * Build a {@link TurnContext} from the handler's deps for pipeline logging
104
- * and plugin attribution.
105
- *
106
- * Reads `turnIndex` from `deps.ctx.turnCount` — the orchestrator-owned
107
- * per-turn counter that is stable for the entire duration of a single
108
- * `runAgentLoopImpl` invocation. The handlers fire after the orchestrator
109
- * has completed its in-turn pipeline work but before `ctx.turnCount++` runs
110
- * in the outer `finally` block, so this value always reflects the turn the
111
- * handler's event belongs to. Trust pulls from the per-turn snapshot first,
112
- * then the conversation-level context, then the canonical `unknown`
113
- * fallback so the required field stays populated for edge cases (fresh
114
- * conversations before the trust resolver runs, heartbeat turns that never
115
- * bind an actor).
116
- */
117
- function buildHandlerTurnContext(deps: EventHandlerDeps): TurnContext {
118
- return {
119
- requestId: deps.reqId,
120
- conversationId: deps.ctx.conversationId,
121
- turnIndex: deps.ctx.turnCount,
122
- trust:
123
- deps.ctx.currentTurnTrustContext ??
124
- deps.ctx.trustContext ??
125
- FALLBACK_TURN_TRUST,
126
- };
127
- }
128
-
129
129
  // ── Types ────────────────────────────────────────────────────────────
130
130
 
131
131
  export interface PendingToolResult {
@@ -137,6 +137,16 @@ export interface PendingToolResult {
137
137
  /** Mutable state shared across event handlers within a single agent loop run. */
138
138
  export interface EventHandlerState {
139
139
  llmCallStartedEmitted: boolean;
140
+ /**
141
+ * Profile key whose `model_profile` notice has been assembled into the turn
142
+ * context but not yet marked notified. Set when the turn injects the notice,
143
+ * and consumed the first time the model actually receives that context — i.e.
144
+ * on the first `message_complete`. Persisting on delivery (rather than inline
145
+ * before the provider call) means a cancelled or failed turn re-sends the
146
+ * notice next turn instead of silently marking the profile notified without
147
+ * the model ever having seen it.
148
+ */
149
+ pendingNotifiedInferenceProfile: string | null;
140
150
  pendingDirectiveDisplayBuffer: string;
141
151
  firstAssistantText: string;
142
152
  /** Most recent resolved provider for the current exchange's usage accounting. */
@@ -186,6 +196,15 @@ export interface EventHandlerState {
186
196
  */
187
197
  assistantRowAwaitingFinalization: boolean;
188
198
  readonly pendingToolResults: Map<string, PendingToolResult>;
199
+ /**
200
+ * Reservation of the grouped `user` tool-result row for the current batch,
201
+ * resolving to the row id. Shared across the concurrent `handleToolResult`
202
+ * calls of one parallel-tool batch so they reserve exactly one row and write
203
+ * into it as sibling results land. `undefined` until the first result of a
204
+ * batch triggers a reservation (reset on a failed reservation so the next
205
+ * arrival can retry) and again after the batch is finalized.
206
+ */
207
+ pendingToolResultRowReservation: Promise<string> | undefined;
189
208
  readonly persistedToolUseIds: Set<string>;
190
209
  readonly accumulatedDirectives: DirectiveRequest[];
191
210
  readonly accumulatedToolContentBlocks: ContentBlock[];
@@ -238,6 +257,13 @@ export interface EventHandlerState {
238
257
  riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
239
258
  }
240
259
  >;
260
+ /**
261
+ * Structured tool activity (web_search / web_fetch) keyed by tool_use_id,
262
+ * captured when a result lands so it can be persisted on the tool's content
263
+ * block and survive a history reopen. Populated for both external provider
264
+ * tools (in handleToolResult) and native server tools (server_tool_complete).
265
+ */
266
+ readonly toolActivityMetadata: Map<string, ToolActivityMetadata>;
241
267
  /** tool_use_ids emitted in the current turn (populated in handleToolUse, cleared after annotation). */
242
268
  currentTurnToolUseIds: string[];
243
269
  /** Wall-clock time (ms since epoch) when the agent loop turn started, used as the display timestamp for assistant messages. */
@@ -246,12 +272,38 @@ export interface EventHandlerState {
246
272
  readonly serverToolStartedAt: Map<string, number>;
247
273
  /** Original input from server_tool_start, keyed by tool_use_id, so the complete handler can read the query. */
248
274
  readonly serverToolInputs: Map<string, Record<string, unknown>>;
275
+ /** Request ids for which a user-facing web_search backend-failure notice was already surfaced this turn (dedup noisy repeats). Keyed by request id; each turn has a fresh request id, so this grows at most one entry per turn. */
276
+ readonly webSearchBackendFailureNotified: Set<string>;
249
277
  /** Active debounce timer for partial persistence; `undefined` when idle. */
250
278
  pendingPartialFlushTimer: ReturnType<typeof setTimeout> | undefined;
251
279
  /** In-flight partial flush write awaited at finalize to avoid overwrite races. */
252
280
  pendingPartialFlushPromise: Promise<void> | undefined;
253
- /** Running mirror of the in-flight assistant message's content. */
281
+ /**
282
+ * Running mirror of the in-flight assistant message's streamed content
283
+ * (text and thinking), flushed to the assistant row on the partial-persist
284
+ * debounce so a mid-turn snapshot reflects what the user is watching live.
285
+ */
254
286
  currentMessageContent: ContentBlock[];
287
+ /**
288
+ * `seq` of the most recent streamed content delta mirrored into
289
+ * `currentMessageContent`. Recorded as the conversation's persisted `seq`
290
+ * after each flush commits (the debounced partial flushes and the
291
+ * `message_complete` finalize), so the snapshot's advertised `seq` tracks
292
+ * exactly the streamed content the durable row holds. `undefined` until the
293
+ * first content delta of the in-flight message. Because every streamed
294
+ * content type rides the same mirror-and-flush path, this single field
295
+ * never claims content a flush has not yet written.
296
+ */
297
+ lastPersistedContentSeq: number | undefined;
298
+ /**
299
+ * Whether the reducer has compacted `ctx.messages`, gating the Slack
300
+ * chronological-transcript override on re-injection. The captured
301
+ * transcript is the full persisted history; blindly replaying it after
302
+ * compaction would overwrite the reduced messages and undo compaction, so
303
+ * once this is `true` the override falls back to the reduced
304
+ * `ctx.messages`.
305
+ */
306
+ reducerCompacted: boolean;
255
307
  }
256
308
 
257
309
  /** Immutable context shared across event handlers within a single agent loop run. */
@@ -265,6 +317,18 @@ export interface EventHandlerDeps {
265
317
  readonly rlog: pino.Logger;
266
318
  readonly turnChannelContext: TurnChannelContext;
267
319
  readonly turnInterfaceContext: TurnInterfaceContext;
320
+ /**
321
+ * Commit a successful inline compaction to durable state. Invoked from the
322
+ * `compaction_completed` dispatch case (when `result.compacted`) with the
323
+ * loop's compaction result and the stripped pre-compaction `basis`. Supplied
324
+ * by the orchestrator because the body writes Conversation DB-record fields,
325
+ * projects Slack provenance, and emits transport the loop is intentionally
326
+ * blind to.
327
+ */
328
+ readonly applyCompaction: (
329
+ result: ContextWindowResult,
330
+ basis: Message[],
331
+ ) => Promise<void>;
268
332
  }
269
333
 
270
334
  // ── Factory ──────────────────────────────────────────────────────────
@@ -272,6 +336,7 @@ export interface EventHandlerDeps {
272
336
  export function createEventHandlerState(): EventHandlerState {
273
337
  return {
274
338
  llmCallStartedEmitted: false,
339
+ pendingNotifiedInferenceProfile: null,
275
340
  pendingDirectiveDisplayBuffer: "",
276
341
  firstAssistantText: "",
277
342
  exchangeProviderName: undefined,
@@ -292,6 +357,7 @@ export function createEventHandlerState(): EventHandlerState {
292
357
  lastAssistantMessageId: undefined,
293
358
  assistantRowAwaitingFinalization: false,
294
359
  pendingToolResults: new Map(),
360
+ pendingToolResultRowReservation: undefined,
295
361
  persistedToolUseIds: new Set(),
296
362
  accumulatedDirectives: [],
297
363
  accumulatedToolContentBlocks: [],
@@ -307,22 +373,27 @@ export function createEventHandlerState(): EventHandlerState {
307
373
  requestIdToToolUseId: new Map(),
308
374
  toolConfirmationOutcomes: new Map(),
309
375
  toolRiskOutcomes: new Map(),
376
+ toolActivityMetadata: new Map(),
310
377
  currentTurnToolUseIds: [],
311
378
  turnStartedAt: Date.now(),
312
379
  serverToolStartedAt: new Map(),
313
380
  serverToolInputs: new Map(),
381
+ webSearchBackendFailureNotified: new Set(),
314
382
  pendingPartialFlushTimer: undefined,
315
383
  pendingPartialFlushPromise: undefined,
316
384
  currentMessageContent: [],
385
+ lastPersistedContentSeq: undefined,
386
+ reducerCompacted: false,
317
387
  };
318
388
  }
319
389
 
320
390
  // ── Partial-persistence helpers ──────────────────────────────────────
321
391
 
322
392
  /** Canonical persisted-content build: clean → append surfaces → redact. */
323
- function buildPersistedAssistantContent(
393
+ export function buildPersistedAssistantContent(
324
394
  rawBlocks: readonly ContentBlock[],
325
395
  surfaces: readonly AssistantSurface[],
396
+ activityByToolUseId?: ReadonlyMap<string, ToolActivityMetadata>,
326
397
  ): ContentBlock[] {
327
398
  const { cleanedContent } = cleanAssistantContent(rawBlocks);
328
399
  const cleaned = cleanedContent as ContentBlock[];
@@ -345,6 +416,18 @@ function buildPersistedAssistantContent(
345
416
  const tb = block as Extract<ContentBlock, { type: "text" }>;
346
417
  return { ...tb, text: redactSecrets(tb.text) };
347
418
  }
419
+ // Native server tools (Anthropic web_search) resolve mid-stream — their
420
+ // `server_tool_complete` fires before `message_complete` — so the captured
421
+ // activity is available at persist time. Stamp it on the server_tool_use
422
+ // block here so the web-search card survives a history reopen. External
423
+ // tool_use activity arrives only with the later tool_result, so it is
424
+ // stamped in `annotatePersistedAssistantMessage` instead.
425
+ if (block.type === "server_tool_use" && activityByToolUseId) {
426
+ const activity = activityByToolUseId.get(block.id);
427
+ if (activity) {
428
+ return { ...block, _activityMetadata: activity } as ContentBlock;
429
+ }
430
+ }
348
431
  return block;
349
432
  });
350
433
  }
@@ -363,6 +446,30 @@ function appendTextToCurrentMessage(
363
446
  }
364
447
  }
365
448
 
449
+ /**
450
+ * Append a streamed thinking chunk to `state.currentMessageContent`, fusing
451
+ * into the tail thinking block. The streamed delta carries no provider
452
+ * `signature` (that arrives only when the block closes), so the mirrored block
453
+ * holds an empty one; `message_complete` overwrites the row with the
454
+ * authoritative signed content before it is ever sent back to a provider.
455
+ */
456
+ function appendThinkingToCurrentMessage(
457
+ state: EventHandlerState,
458
+ thinking: string,
459
+ ): void {
460
+ if (thinking.length === 0) return;
461
+ const tail = state.currentMessageContent.at(-1);
462
+ if (tail && tail.type === "thinking") {
463
+ tail.thinking = tail.thinking + thinking;
464
+ } else {
465
+ state.currentMessageContent.push({
466
+ type: "thinking",
467
+ thinking,
468
+ signature: "",
469
+ });
470
+ }
471
+ }
472
+
366
473
  /** Reset partial-persist accumulator and any pending flush state. Idempotent. */
367
474
  function resetPartialPersistAccumulator(state: EventHandlerState): void {
368
475
  if (state.pendingPartialFlushTimer !== undefined) {
@@ -370,10 +477,11 @@ function resetPartialPersistAccumulator(state: EventHandlerState): void {
370
477
  state.pendingPartialFlushTimer = undefined;
371
478
  }
372
479
  state.currentMessageContent = [];
480
+ state.lastPersistedContentSeq = undefined;
373
481
  state.pendingPartialFlushPromise = undefined;
374
482
  }
375
483
 
376
- /** Flush `state.currentMessageContent` to the row via the persistence pipeline. */
484
+ /** Flush `state.currentMessageContent` to the persisted assistant row. */
377
485
  async function flushAccumulatedContent(
378
486
  state: EventHandlerState,
379
487
  deps: EventHandlerDeps,
@@ -382,22 +490,24 @@ async function flushAccumulatedContent(
382
490
  if (messageId === undefined) return;
383
491
  if (state.currentMessageContent.length === 0) return;
384
492
 
385
- const built = buildPersistedAssistantContent(state.currentMessageContent, []);
493
+ const built = buildPersistedAssistantContent(
494
+ state.currentMessageContent,
495
+ [],
496
+ state.toolActivityMetadata,
497
+ );
386
498
  const contentJson = JSON.stringify(built);
499
+ // Pair the seq with the exact content snapshot taken above: deltas that
500
+ // arrive while the write is in flight bump `lastPersistedContentSeq`
501
+ // again, but they are not part of this write.
502
+ const flushedSeq = state.lastPersistedContentSeq;
387
503
 
388
504
  try {
389
- await runPipeline<PersistArgs, PersistResult>(
390
- "persistence",
391
- getMiddlewaresFor("persistence"),
392
- defaultPersistenceTerminal,
393
- {
394
- op: "updateContent",
395
- messageId,
396
- content: contentJson,
397
- },
398
- buildHandlerTurnContext(deps),
399
- DEFAULT_TIMEOUTS.persistence,
400
- );
505
+ updateMessageContent(messageId, contentJson);
506
+ // Record only after the write commits, so the snapshot seq never
507
+ // claims content that is not yet durable.
508
+ if (flushedSeq != null) {
509
+ recordPersistedSeq(deps.ctx.conversationId, flushedSeq);
510
+ }
401
511
  } catch (err) {
402
512
  deps.rlog.warn(
403
513
  { err, messageId },
@@ -633,12 +743,6 @@ export async function handleLlmCallStarted(
633
743
  // the `assistantRowAwaitingFinalization` flag — `handleMessageComplete`
634
744
  // clears it after the successful `updateContent`, so the previous call's
635
745
  // committed row is never touched here.
636
- //
637
- // Direct `deleteMessageById` (not via the `persistence` pipeline) is
638
- // intentional: a never-finalized reservation has no segments, no
639
- // attachments, and no observable history — undoing it isn't a real
640
- // persistence event for plugins to react to, so routing through the
641
- // pipeline would only widen the mock surface for no observability win.
642
746
  if (state.assistantRowAwaitingFinalization && state.lastAssistantMessageId) {
643
747
  try {
644
748
  deleteMessageById(state.lastAssistantMessageId);
@@ -652,20 +756,12 @@ export async function handleLlmCallStarted(
652
756
  }
653
757
 
654
758
  const metadata = buildAssistantChannelMetadata(state, deps);
655
- const reserveResult = (await runPipeline<PersistArgs, PersistResult>(
656
- "persistence",
657
- getMiddlewaresFor("persistence"),
658
- defaultPersistenceTerminal,
659
- {
660
- op: "reserve",
661
- conversationId: deps.ctx.conversationId,
662
- role: "assistant",
663
- metadata,
664
- },
665
- buildHandlerTurnContext(deps),
666
- DEFAULT_TIMEOUTS.persistence,
667
- )) as PersistReserveResult;
668
- state.lastAssistantMessageId = reserveResult.message.id;
759
+ const reservedRow = await reserveMessage(
760
+ deps.ctx.conversationId,
761
+ "assistant",
762
+ metadata,
763
+ );
764
+ state.lastAssistantMessageId = reservedRow.id;
669
765
  state.assistantRowAwaitingFinalization = true;
670
766
  // Fresh row → fresh accumulator. If an earlier (failed) LLM call
671
767
  // within the same run left partial state behind, the
@@ -675,7 +771,7 @@ export async function handleLlmCallStarted(
675
771
  resetPartialPersistAccumulator(state);
676
772
  deps.onEvent({
677
773
  type: "assistant_turn_start",
678
- messageId: reserveResult.message.id,
774
+ messageId: reservedRow.id,
679
775
  conversationId: deps.ctx.conversationId,
680
776
  });
681
777
  }
@@ -711,6 +807,11 @@ function handleTextDelta(
711
807
  // Mirror the drained delta into state.currentMessageContent so partial
712
808
  // flushes mid-turn see the same content the user is watching live.
713
809
  appendTextToCurrentMessage(state, drained.emitText);
810
+ // The hub stamps `seq` synchronously on the delta emitted above, so
811
+ // `getCurrentSeq()` here is that delta's seq -- the position the
812
+ // mirrored content now reflects. A partial flush snapshots this to
813
+ // record how far the durable rows track the live stream.
814
+ state.lastPersistedContentSeq = getCurrentSeq();
714
815
  schedulePartialFlush(state, deps);
715
816
  }
716
817
  }
@@ -746,6 +847,14 @@ function handleThinkingDelta(
746
847
  conversationId: deps.ctx.conversationId,
747
848
  messageId: state.lastAssistantMessageId,
748
849
  });
850
+ // Mirror thinking into the same running view as text so the debounced
851
+ // partial flush persists it mid-turn -- long reasoning streams survive a
852
+ // refresh that outlives the SSE replay window, exactly as long answers do.
853
+ appendThinkingToCurrentMessage(state, event.thinking);
854
+ // The hub stamps `seq` synchronously on the delta emitted above, so
855
+ // `getCurrentSeq()` is that delta's position in the mirrored content.
856
+ state.lastPersistedContentSeq = getCurrentSeq();
857
+ schedulePartialFlush(state, deps);
749
858
  }
750
859
 
751
860
  export function handleToolUse(
@@ -773,6 +882,14 @@ export function handleToolUse(
773
882
  toolUseId: event.id,
774
883
  messageId: state.lastAssistantMessageId,
775
884
  });
885
+ // `message_complete` always precedes tool events (see handleMessageComplete),
886
+ // so this tool_use block is already durable in the assistant row. The
887
+ // `tool_use_start` emitted just above is therefore the newest stamped event
888
+ // whose content the `/messages` snapshot already reflects -- advance the
889
+ // persisted seq to it. Without this the snapshot would advertise a seq below
890
+ // an event it already incorporates, and a client applying `seq > snapshot.seq`
891
+ // would replay this tool start.
892
+ recordPersistedSeq(deps.ctx.conversationId, getCurrentSeq());
776
893
  }
777
894
 
778
895
  export function handleToolUsePreviewStart(
@@ -890,11 +1007,244 @@ export function handleInputJsonDelta(
890
1007
  });
891
1008
  }
892
1009
 
893
- export function handleToolResult(
1010
+ /**
1011
+ * Build the persisted `tool_result` content blocks for the buffered results,
1012
+ * redacting secrets from both the flat content and any structured blocks. All
1013
+ * results of one assistant turn share a single `user` row (the shape providers
1014
+ * expect for tool_result-in-user-turn).
1015
+ */
1016
+ function buildToolResultBlocks(
1017
+ pending: ReadonlyMap<string, PendingToolResult>,
1018
+ ) {
1019
+ return Array.from(pending.entries()).map(([toolUseId, result]) => ({
1020
+ type: "tool_result",
1021
+ tool_use_id: toolUseId,
1022
+ content: redactSecrets(result.content),
1023
+ is_error: result.isError,
1024
+ ...(result.contentBlocks
1025
+ ? {
1026
+ contentBlocks: result.contentBlocks.map((block) =>
1027
+ block.type === "text"
1028
+ ? { ...block, text: redactSecrets(block.text) }
1029
+ : block,
1030
+ ),
1031
+ }
1032
+ : {}),
1033
+ }));
1034
+ }
1035
+
1036
+ /**
1037
+ * Channel/interface provenance metadata for the grouped tool-result row,
1038
+ * stamped from the turn context so the row carries the same provenance the
1039
+ * snapshot reflects from the moment it lands in SQLite.
1040
+ */
1041
+ function buildToolResultMetadata(
1042
+ deps: EventHandlerDeps,
1043
+ ): Record<string, unknown> {
1044
+ return {
1045
+ ...provenanceFromTrustContext(deps.ctx.trustContext),
1046
+ userMessageChannel: deps.turnChannelContext.userMessageChannel,
1047
+ assistantMessageChannel: deps.turnChannelContext.assistantMessageChannel,
1048
+ userMessageInterface: deps.turnInterfaceContext.userMessageInterface,
1049
+ assistantMessageInterface:
1050
+ deps.turnInterfaceContext.assistantMessageInterface,
1051
+ };
1052
+ }
1053
+
1054
+ /**
1055
+ * Reserve the grouped `user` tool-result row for the current batch exactly
1056
+ * once. Parallel tool results are dispatched without awaiting (`agent/loop.ts`
1057
+ * emits each `tool_result` synchronously), so concurrent `handleToolResult`
1058
+ * calls can reach this before the first reservation resolves; sharing one
1059
+ * in-flight reservation promise keeps the whole batch in a single row. A
1060
+ * failed reservation resets the promise so the next caller can retry rather
1061
+ * than inheriting a settled rejection.
1062
+ */
1063
+ function ensureToolResultRowReserved(
1064
+ state: EventHandlerState,
1065
+ conversationId: string,
1066
+ metadata: Record<string, unknown>,
1067
+ ): Promise<string> {
1068
+ if (state.pendingToolResultRowReservation === undefined) {
1069
+ state.pendingToolResultRowReservation = reserveMessage(
1070
+ conversationId,
1071
+ "user",
1072
+ metadata,
1073
+ )
1074
+ .then((reserved) => reserved.id)
1075
+ .catch((err) => {
1076
+ state.pendingToolResultRowReservation = undefined;
1077
+ throw err;
1078
+ });
1079
+ }
1080
+ return state.pendingToolResultRowReservation;
1081
+ }
1082
+
1083
+ /**
1084
+ * Persist the buffered tool results into their grouped `user` row as each
1085
+ * result arrives, so a long-running tool's output survives a refresh that
1086
+ * outlives the SSE replay window. The row is reserved once per batch and
1087
+ * rewritten in place as sibling parallel results land, keeping all
1088
+ * `tool_result` blocks of one turn in a single message. `seq` is the position
1089
+ * stamped on the triggering `tool_result` event, captured by the caller before
1090
+ * any await so it reflects exactly the content now durable in the row.
1091
+ * Indexing and the buffer drain are deferred to `finalizePendingToolResultRow`.
1092
+ */
1093
+ async function persistPendingToolResultRow(
1094
+ state: EventHandlerState,
1095
+ deps: EventHandlerDeps,
1096
+ seq: number,
1097
+ ): Promise<void> {
1098
+ if (state.pendingToolResults.size === 0) return;
1099
+ const rowId = await ensureToolResultRowReserved(
1100
+ state,
1101
+ deps.ctx.conversationId,
1102
+ buildToolResultMetadata(deps),
1103
+ );
1104
+ // Serialize the content after the reservation resolves so the last of the
1105
+ // concurrent writers reflects the fullest batch.
1106
+ updateMessageContent(
1107
+ rowId,
1108
+ JSON.stringify(buildToolResultBlocks(state.pendingToolResults)),
1109
+ );
1110
+ recordPersistedSeq(deps.ctx.conversationId, seq);
1111
+ const conv = getConversation(deps.ctx.conversationId);
1112
+ if (conv != null) {
1113
+ syncMessageToDisk(deps.ctx.conversationId, rowId, conv.createdAt);
1114
+ }
1115
+ }
1116
+
1117
+ /**
1118
+ * Finalize the grouped tool-result row at a turn/loop boundary: ensure the row
1119
+ * is reserved (a fallback for the case where every on-arrival write failed),
1120
+ * rewrite it to the full batch, sync it to disk, index it for memory recall,
1121
+ * and clear the batch state. Shared by `message_complete` and the orchestrator
1122
+ * loop-exit flush so an aborted or yielded turn finalizes the same reserved row
1123
+ * instead of writing a duplicate.
1124
+ */
1125
+ export async function finalizePendingToolResultRow(
1126
+ state: EventHandlerState,
1127
+ conversationId: string,
1128
+ metadata: Record<string, unknown>,
1129
+ rlog: pino.Logger,
1130
+ ): Promise<void> {
1131
+ if (state.pendingToolResults.size === 0) return;
1132
+ const rowId = await ensureToolResultRowReserved(
1133
+ state,
1134
+ conversationId,
1135
+ metadata,
1136
+ );
1137
+ const contentJson = JSON.stringify(
1138
+ buildToolResultBlocks(state.pendingToolResults),
1139
+ );
1140
+ updateMessageContent(rowId, contentJson);
1141
+ // Sync the row to the JSONL disk view so it stays in lockstep with the DB.
1142
+ // `getConversation` returns `ConversationRow | null`, so `!= null` gates on a
1143
+ // real row (skipping the sync when the conversation was not found rather than
1144
+ // asking the disk-view to resolve a missing id).
1145
+ const conv = getConversation(conversationId);
1146
+ if (conv != null) {
1147
+ syncMessageToDisk(conversationId, rowId, conv.createdAt);
1148
+ }
1149
+ // `reserveMessage` + `updateMessageContent` are CRUD-only, so index the
1150
+ // finalized tool-result content explicitly here (mirroring the assistant-row
1151
+ // finalize) once it is durable. Non-fatal: a memory hiccup must not escalate
1152
+ // a successful turn into a throw.
1153
+ const row = getMessageById(rowId, conversationId);
1154
+ if (row) {
1155
+ let provenanceTrustClass:
1156
+ | "guardian"
1157
+ | "trusted_contact"
1158
+ | "unknown"
1159
+ | undefined;
1160
+ let automated: boolean | undefined;
1161
+ if (row.metadata) {
1162
+ try {
1163
+ const parsedMeta = messageMetadataSchema.safeParse(
1164
+ JSON.parse(row.metadata),
1165
+ );
1166
+ if (parsedMeta.success) {
1167
+ provenanceTrustClass = parsedMeta.data.provenanceTrustClass;
1168
+ automated = parsedMeta.data.automated;
1169
+ }
1170
+ } catch {
1171
+ // Malformed metadata JSON — index with undefined provenance fields.
1172
+ }
1173
+ }
1174
+ try {
1175
+ await indexMessageNow(
1176
+ {
1177
+ messageId: rowId,
1178
+ conversationId,
1179
+ role: "user",
1180
+ content: contentJson,
1181
+ createdAt: row.createdAt,
1182
+ scopeId: "default",
1183
+ provenanceTrustClass,
1184
+ automated,
1185
+ },
1186
+ getConfig().memory,
1187
+ );
1188
+ } catch (err) {
1189
+ rlog.warn(
1190
+ { err, conversationId, messageId: rowId },
1191
+ "Failed to index tool-result message for memory (non-fatal)",
1192
+ );
1193
+ }
1194
+ }
1195
+ for (const id of state.pendingToolResults.keys()) {
1196
+ state.persistedToolUseIds.add(id);
1197
+ }
1198
+ state.pendingToolResults.clear();
1199
+ state.pendingToolResultRowReservation = undefined;
1200
+ }
1201
+
1202
+ export async function handleToolResult(
894
1203
  state: EventHandlerState,
895
1204
  deps: EventHandlerDeps,
896
1205
  event: Extract<AgentEvent, { type: "tool_result" }>,
897
- ): void {
1206
+ ): Promise<void> {
1207
+ // A synthesized cancellation (the tool never executed) is captured for
1208
+ // persistence and forwarded to the client like any result, but skips every
1209
+ // side effect that assumes the tool ran. A real result already captured or
1210
+ // persisted for the same tool wins, so only fill genuine gaps.
1211
+ if (event.cancelled) {
1212
+ if (
1213
+ state.pendingToolResults.has(event.toolUseId) ||
1214
+ state.persistedToolUseIds.has(event.toolUseId)
1215
+ ) {
1216
+ return;
1217
+ }
1218
+ state.pendingToolResults.set(event.toolUseId, {
1219
+ content: event.content,
1220
+ isError: event.isError,
1221
+ });
1222
+ state.currentToolUseId = undefined;
1223
+ deps.onEvent({
1224
+ type: "tool_result",
1225
+ toolName: "",
1226
+ result: event.content,
1227
+ isError: event.isError,
1228
+ conversationId: deps.ctx.conversationId,
1229
+ messageId: state.lastAssistantMessageId,
1230
+ toolUseId: event.toolUseId,
1231
+ });
1232
+ // Capture the seq synchronously (before the persist await) so it reflects
1233
+ // the just-stamped tool_result event, then persist on arrival. A failure
1234
+ // here is non-fatal: the buffered result is still drained at
1235
+ // `message_complete`.
1236
+ const cancelledSeq = getCurrentSeq();
1237
+ try {
1238
+ await persistPendingToolResultRow(state, deps, cancelledSeq);
1239
+ } catch (err) {
1240
+ log.warn(
1241
+ { err, conversationId: deps.ctx.conversationId },
1242
+ "Failed to persist cancelled tool result on arrival (non-fatal; retried at message_complete)",
1243
+ );
1244
+ }
1245
+ return;
1246
+ }
1247
+
898
1248
  const imageBlocks = event.contentBlocks?.filter(
899
1249
  (b): b is ImageContent => b.type === "image",
900
1250
  );
@@ -945,6 +1295,13 @@ export function handleToolResult(
945
1295
  });
946
1296
  }
947
1297
 
1298
+ // Capture tool activity (web_search / web_fetch) so it can be persisted on
1299
+ // the tool_use block and the activity card survives a history reopen,
1300
+ // matching the live tool_result event's activityMetadata.
1301
+ if (event.activityMetadata) {
1302
+ state.toolActivityMetadata.set(event.toolUseId, event.activityMetadata);
1303
+ }
1304
+
948
1305
  const toolName = state.toolUseIdToName.get(event.toolUseId);
949
1306
  if (toolName === "file_write" || toolName === "bash") {
950
1307
  deps.ctx.markWorkspaceTopLevelDirty();
@@ -1026,6 +1383,20 @@ export function handleToolResult(
1026
1383
  riskThreshold: event.riskThreshold,
1027
1384
  activityMetadata: event.activityMetadata,
1028
1385
  });
1386
+
1387
+ // Capture the seq synchronously (before the persist await) so it reflects the
1388
+ // just-stamped tool_result event, then persist the grouped row on arrival. A
1389
+ // failure here is non-fatal: the buffered result is still drained at
1390
+ // `message_complete`.
1391
+ const resultSeq = getCurrentSeq();
1392
+ try {
1393
+ await persistPendingToolResultRow(state, deps, resultSeq);
1394
+ } catch (err) {
1395
+ log.warn(
1396
+ { err, conversationId: deps.ctx.conversationId },
1397
+ "Failed to persist tool result on arrival (non-fatal; retried at message_complete)",
1398
+ );
1399
+ }
1029
1400
  }
1030
1401
 
1031
1402
  /**
@@ -1097,6 +1468,16 @@ function annotatePersistedAssistantMessage(
1097
1468
  rec._riskDirectoryScopeOptions = risk.riskDirectoryScopeOptions;
1098
1469
  modified = true;
1099
1470
  }
1471
+ // External provider tools (brave/perplexity/tavily) + web_fetch produce
1472
+ // their activity only when the tool_result lands, after message_complete
1473
+ // has already persisted this block — so it is stamped here. Native
1474
+ // server_tool_use activity is stamped earlier, at persist time, in
1475
+ // `buildPersistedAssistantContent`.
1476
+ const activity = state.toolActivityMetadata.get(id);
1477
+ if (activity) {
1478
+ rec._activityMetadata = activity;
1479
+ modified = true;
1480
+ }
1100
1481
  }
1101
1482
  }
1102
1483
 
@@ -1259,6 +1640,18 @@ export async function handleMessageComplete(
1259
1640
  deps: EventHandlerDeps,
1260
1641
  event: Extract<AgentEvent, { type: "message_complete" }>,
1261
1642
  ): Promise<void> {
1643
+ // The model has now received the turn context, so persist any pending
1644
+ // inference-profile-change notification. Guarded by the pending slot so it
1645
+ // fires once per turn; a turn that fails before reaching delivery leaves the
1646
+ // slot unconsumed and re-sends the notice next turn.
1647
+ if (state.pendingNotifiedInferenceProfile != null) {
1648
+ setLastNotifiedInferenceProfile(
1649
+ deps.ctx.conversationId,
1650
+ state.pendingNotifiedInferenceProfile,
1651
+ );
1652
+ state.pendingNotifiedInferenceProfile = null;
1653
+ }
1654
+
1262
1655
  // Reset per-turn tool tracking for the new turn.
1263
1656
  state.currentTurnToolUseIds = [];
1264
1657
 
@@ -1298,62 +1691,16 @@ export async function handleMessageComplete(
1298
1691
  state.pendingDirectiveDisplayBuffer = "";
1299
1692
  }
1300
1693
 
1301
- // Persist pending tool results
1302
- if (state.pendingToolResults.size > 0) {
1303
- const toolResultBlocks = Array.from(state.pendingToolResults.entries()).map(
1304
- ([toolUseId, result]) => ({
1305
- type: "tool_result",
1306
- tool_use_id: toolUseId,
1307
- content: redactSecrets(result.content),
1308
- is_error: result.isError,
1309
- ...(result.contentBlocks
1310
- ? {
1311
- contentBlocks: result.contentBlocks.map((block) =>
1312
- block.type === "text"
1313
- ? { ...block, text: redactSecrets(block.text) }
1314
- : block,
1315
- ),
1316
- }
1317
- : {}),
1318
- }),
1319
- );
1320
- const toolResultMetadata = {
1321
- ...provenanceFromTrustContext(deps.ctx.trustContext),
1322
- userMessageChannel: deps.turnChannelContext.userMessageChannel,
1323
- assistantMessageChannel: deps.turnChannelContext.assistantMessageChannel,
1324
- userMessageInterface: deps.turnInterfaceContext.userMessageInterface,
1325
- assistantMessageInterface:
1326
- deps.turnInterfaceContext.assistantMessageInterface,
1327
- };
1328
- // Route the add + disk-view sync through the `persistence` pipeline so
1329
- // plugins can observe or override both operations together. The default
1330
- // plugin's terminal performs the add and, when `syncToDisk` is true,
1331
- // immediately calls `syncMessageToDisk` against the just-persisted row.
1332
- // `getConversation` returns `ConversationRow | null`, so `!= null`
1333
- // gates on a real row (skipping the sync when the conversation was
1334
- // not found rather than asking the disk-view to resolve a missing id).
1335
- const convForToolResult = getConversation(deps.ctx.conversationId);
1336
- await runPipeline<PersistArgs, PersistResult>(
1337
- "persistence",
1338
- getMiddlewaresFor("persistence"),
1339
- defaultPersistenceTerminal,
1340
- {
1341
- op: "add",
1342
- conversationId: deps.ctx.conversationId,
1343
- role: "user",
1344
- content: JSON.stringify(toolResultBlocks),
1345
- metadata: toolResultMetadata,
1346
- syncToDisk: convForToolResult != null,
1347
- createdAtMs: convForToolResult?.createdAt,
1348
- },
1349
- buildHandlerTurnContext(deps),
1350
- DEFAULT_TIMEOUTS.persistence,
1351
- );
1352
- for (const id of state.pendingToolResults.keys()) {
1353
- state.persistedToolUseIds.add(id);
1354
- }
1355
- state.pendingToolResults.clear();
1356
- }
1694
+ // Finalize the grouped tool-result row. Each result was persisted into this
1695
+ // row as it arrived (`persistPendingToolResultRow`); this rewrites it to the
1696
+ // full batch (covering the case where a mid-arrival write failed), indexes it
1697
+ // for memory recall, and clears the batch state.
1698
+ await finalizePendingToolResultRow(
1699
+ state,
1700
+ deps.ctx.conversationId,
1701
+ buildToolResultMetadata(deps),
1702
+ deps.rlog,
1703
+ );
1357
1704
 
1358
1705
  // Accumulate directives + warnings from the assistant content for
1359
1706
  // downstream attachment processing. `cleanAssistantContent` is also
@@ -1391,6 +1738,7 @@ export async function handleMessageComplete(
1391
1738
  const contentForPersistence = buildPersistedAssistantContent(
1392
1739
  event.message.content as ContentBlock[],
1393
1740
  deps.ctx.currentTurnSurfaces,
1741
+ state.toolActivityMetadata,
1394
1742
  );
1395
1743
 
1396
1744
  // The row was reserved at `llm_call_started` (with channel metadata
@@ -1407,28 +1755,30 @@ export async function handleMessageComplete(
1407
1755
  );
1408
1756
  }
1409
1757
  const contentJson = JSON.stringify(contentForPersistence);
1410
- await runPipeline<PersistArgs, PersistResult>(
1411
- "persistence",
1412
- getMiddlewaresFor("persistence"),
1413
- defaultPersistenceTerminal,
1414
- {
1415
- op: "updateContent",
1416
- messageId: assistantMessageId,
1417
- content: contentJson,
1418
- },
1419
- buildHandlerTurnContext(deps),
1420
- DEFAULT_TIMEOUTS.persistence,
1421
- );
1758
+ updateMessageContent(assistantMessageId, contentJson);
1422
1759
  state.assistantRowAwaitingFinalization = false;
1760
+ // The assistant row now holds the authoritative content (text + thinking +
1761
+ // tool_use blocks from `event.message`), and any drained tool-result rows
1762
+ // are durable. `lastPersistedContentSeq` is the last streamed text/thinking
1763
+ // delta's seq -- the highest stamped content event this row reflects -- so
1764
+ // recording it is honest. A drained tool result was stamped earlier in the
1765
+ // turn, so this seq already covers it; a call that streams no content (a
1766
+ // pure tool call) advances instead via `tool_use_start`. `recordPersistedSeq`
1767
+ // clamps monotonically, so a lower value here never regresses the seq.
1768
+ if (state.lastPersistedContentSeq != null) {
1769
+ recordPersistedSeq(deps.ctx.conversationId, state.lastPersistedContentSeq);
1770
+ }
1423
1771
  // Reset the partial-persist mirror so subsequent calls in this turn
1424
1772
  // start with an empty running view.
1425
1773
  state.currentMessageContent = [];
1774
+ state.lastPersistedContentSeq = undefined;
1426
1775
 
1427
- // ── Indexing + attention projection (restored from the pre-B3 `add` path) ──
1776
+ // ── Indexing + attention projection ──
1428
1777
  // `reserveMessage` + `updateMessageContent` are CRUD-only: they don't run
1429
- // the memory indexer or the attention-cursor projector. The pre-B3 path
1430
- // wrote the row via `addMessage`, which ran both as side-effects of the
1431
- // insert. Calling them here keeps the assistant row's external state
1778
+ // the memory indexer or the attention-cursor projector (unlike `addMessage`,
1779
+ // which runs both as side-effects of the insert). Because the assistant row
1780
+ // is reserved empty and finalized via `updateMessageContent`, both must be
1781
+ // invoked explicitly here to keep the assistant row's external state
1432
1782
  // (Qdrant segments, conversation attention cursor) in lockstep with the
1433
1783
  // finalized content. Both are non-fatal — a memory hiccup must not
1434
1784
  // escalate a successful generation into a turn-level throw. Indexing
@@ -1759,7 +2109,7 @@ export async function dispatchAgentEvent(
1759
2109
  handleInputJsonDelta(state, deps, event);
1760
2110
  break;
1761
2111
  case "tool_result":
1762
- handleToolResult(state, deps, event);
2112
+ await handleToolResult(state, deps, event);
1763
2113
  break;
1764
2114
  case "server_tool_start": {
1765
2115
  const query =
@@ -1828,9 +2178,65 @@ export async function dispatchAgentEvent(
1828
2178
  // for them would mis-label the provider and ship empty results.
1829
2179
  const isAnthropicNative = deps.ctx.provider.name === "anthropic";
1830
2180
 
1831
- const errorMessage = event.isError
1832
- ? (event.errorMessage ?? event.errorCode ?? "Search failed")
1833
- : undefined;
2181
+ // Classify provider failures through the shared normalizer so the same
2182
+ // friendly copy propagates to every client via WebSearchMetadata, while
2183
+ // the raw provider detail stays in telemetry only (ATL-727).
2184
+ const classification = classifyWebSearchFailure({
2185
+ errorCode: event.errorCode,
2186
+ error: event.errorMessage,
2187
+ isError: event.isError,
2188
+ hasResults: results.length > 0,
2189
+ });
2190
+
2191
+ let errorMessage: string | undefined;
2192
+ let fallbackShown = false;
2193
+ if (event.isError) {
2194
+ // A genuine backend failure OR an unclassifiable, message-less native
2195
+ // failure (e.g. `isError:true` with no `error_code`) both surface the
2196
+ // friendly backend copy: a terse "Search failed" placeholder is the
2197
+ // confusing copy this normalization exists to eliminate (ATL-727).
2198
+ // Recoverable categories that carry a real user message
2199
+ // (query_too_long, max_uses_exceeded) keep their own copy.
2200
+ const useBackendCopy =
2201
+ classification.isBackendFailure || !classification.userMessage;
2202
+ if (useBackendCopy) {
2203
+ // Dedup the user-facing friendly notice per turn (request id) so a
2204
+ // burst of failures surfaces at most one full notice. The raw
2205
+ // provider error is preserved on every failure via telemetry below.
2206
+ const alreadyNotified = state.webSearchBackendFailureNotified.has(
2207
+ deps.reqId,
2208
+ );
2209
+ if (alreadyNotified) {
2210
+ errorMessage = "Search is still having trouble.";
2211
+ } else {
2212
+ state.webSearchBackendFailureNotified.add(deps.reqId);
2213
+ errorMessage = WEB_SEARCH_BACKEND_FAILURE_MESSAGE;
2214
+ fallbackShown = true;
2215
+ }
2216
+
2217
+ // Backend-failure telemetry (provider outages / rate limits) must
2218
+ // fire only for genuine backend classifications so it does not
2219
+ // count recoverable input/quota errors — or a message-less unknown
2220
+ // failure that merely borrows the friendly copy — as provider
2221
+ // outages.
2222
+ if (classification.isBackendFailure) {
2223
+ logWebSearchBackendFailure(deps.rlog, {
2224
+ provider: isAnthropicNative
2225
+ ? "anthropic-native"
2226
+ : deps.ctx.provider.name,
2227
+ requestId: deps.reqId,
2228
+ errorCategory: classification.category,
2229
+ rawDetail: classification.rawDetail,
2230
+ fallbackShown,
2231
+ queryLength: query.length,
2232
+ });
2233
+ }
2234
+ } else {
2235
+ // Recoverable, non-backend categories with their own user-facing
2236
+ // copy (query_too_long, max_uses_exceeded) keep that message.
2237
+ errorMessage = classification.userMessage;
2238
+ }
2239
+ }
1834
2240
 
1835
2241
  const metadata: WebSearchMetadata | undefined = isAnthropicNative
1836
2242
  ? {
@@ -1847,6 +2253,14 @@ export async function dispatchAgentEvent(
1847
2253
  .map((r) => `${r.title}\n${r.url}`)
1848
2254
  .join("\n\n");
1849
2255
 
2256
+ // Capture activity so it persists on the server_tool_use block and the
2257
+ // web-search card survives a history reopen, matching the live event.
2258
+ if (metadata) {
2259
+ state.toolActivityMetadata.set(event.toolUseId, {
2260
+ webSearch: metadata,
2261
+ });
2262
+ }
2263
+
1850
2264
  deps.onEvent({
1851
2265
  type: "tool_result",
1852
2266
  toolName: "web_search",
@@ -1873,6 +2287,30 @@ export async function dispatchAgentEvent(
1873
2287
  // banner.
1874
2288
  deps.onEvent(event);
1875
2289
  break;
2290
+ case "compaction_completed":
2291
+ // Always commit the loop-stripped `basis` as the durable message base
2292
+ // so re-injection re-applies onto the stripped history even when the
2293
+ // pipeline ran but did not compact. When it did compact, commit the
2294
+ // durable result (DB-record fields, Slack provenance, SSE) — which
2295
+ // overwrites `ctx.messages` with the compacted history — and flip the
2296
+ // per-turn re-injection guards the orchestrator reads. This runs
2297
+ // before the loop's `reinject` hook (the loop awaits this dispatch),
2298
+ // so the guards are set in time. A failed durable commit re-throws
2299
+ // below to abort the turn rather than re-injecting against
2300
+ // half-applied state.
2301
+ deps.ctx.messages = event.basis;
2302
+ if (event.result.compacted) {
2303
+ await deps.applyCompaction(event.result, event.basis);
2304
+ state.reducerCompacted = true;
2305
+ }
2306
+ break;
2307
+ case "history_stripped":
2308
+ // Record the history-stripped DB marker right after the loop strips
2309
+ // injections (before the pipeline). Best-effort: a transient marker
2310
+ // write must not abort the turn, so unlike `compaction_completed` this
2311
+ // is not on the re-throw allowlist below.
2312
+ markHistoryStrippedBestEffort(deps.ctx.conversationId);
2313
+ break;
1876
2314
  case "error":
1877
2315
  handleError(state, deps, event);
1878
2316
  break;
@@ -1925,10 +2363,13 @@ export async function dispatchAgentEvent(
1925
2363
  // - message_complete: persists assistant message to DB, sets state flags
1926
2364
  // - error: sets recovery flags (contextTooLargeDetected, orderingErrorDetected)
1927
2365
  // - usage: records token accounting
2366
+ // - compaction_completed: durable compaction commit; aborting the turn is
2367
+ // safer than re-injecting against a half-applied compaction
1928
2368
  if (
1929
2369
  event.type === "message_complete" ||
1930
2370
  event.type === "error" ||
1931
- event.type === "usage"
2371
+ event.type === "usage" ||
2372
+ event.type === "compaction_completed"
1932
2373
  ) {
1933
2374
  throw err;
1934
2375
  }