@vellumai/assistant 0.8.8 → 0.8.9-staging.1

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 (340) hide show
  1. package/bun.lock +2 -2
  2. package/examples/plugins/echo/README.md +61 -60
  3. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  4. package/examples/plugins/echo/hooks/stop.ts +16 -0
  5. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  6. package/examples/plugins/echo/package.json +1 -2
  7. package/examples/plugins/echo/src/emit.ts +19 -0
  8. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  9. package/openapi.yaml +236 -6
  10. package/package.json +2 -2
  11. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  12. package/src/__tests__/agent-loop-exit-reason.test.ts +185 -140
  13. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  14. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  15. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  16. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  17. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  18. package/src/__tests__/agent-loop.test.ts +441 -96
  19. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  20. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  21. package/src/__tests__/anthropic-provider.test.ts +1 -1
  22. package/src/__tests__/app-control-flow.test.ts +1 -1
  23. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  24. package/src/__tests__/approval-cascade.test.ts +5 -4
  25. package/src/__tests__/approval-routes-http.test.ts +4 -1
  26. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  27. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  28. package/src/__tests__/channel-approvals.test.ts +1 -1
  29. package/src/__tests__/compaction-circuit.test.ts +258 -0
  30. package/src/__tests__/compaction-direct.test.ts +132 -0
  31. package/src/__tests__/compaction-events.test.ts +5 -5
  32. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
  33. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  34. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +59 -103
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +94 -189
  36. package/src/__tests__/conversation-agent-loop.test.ts +249 -490
  37. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  38. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
  39. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  40. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
  41. package/src/__tests__/conversation-process-callsite.test.ts +14 -14
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +18 -16
  43. package/src/__tests__/conversation-queue.test.ts +9 -9
  44. package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
  45. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  46. package/src/__tests__/conversation-slash-queue.test.ts +6 -4
  47. package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
  48. package/src/__tests__/conversation-speed-override.test.ts +10 -9
  49. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  50. package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
  51. package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
  53. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  54. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  55. package/src/__tests__/db-acp-history.test.ts +101 -0
  56. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  57. package/src/__tests__/empty-response-hook.test.ts +1 -1
  58. package/src/__tests__/file-write-tool.test.ts +63 -0
  59. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  60. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  61. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  63. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  64. package/src/__tests__/heartbeat-service.test.ts +1 -0
  65. package/src/__tests__/history-repair-hook.test.ts +1 -1
  66. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  67. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  68. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  69. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  70. package/src/__tests__/injector-background-turn.test.ts +13 -23
  71. package/src/__tests__/injector-chain.test.ts +268 -44
  72. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  73. package/src/__tests__/injector-document-comments.test.ts +97 -114
  74. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  75. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  76. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  77. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  78. package/src/__tests__/memory-retrieval-hook.test.ts +73 -6
  79. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  80. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  81. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  82. package/src/__tests__/plugin-bootstrap.test.ts +12 -23
  83. package/src/__tests__/plugin-registry.test.ts +3 -49
  84. package/src/__tests__/plugin-types.test.ts +0 -70
  85. package/src/__tests__/reaction-persistence.test.ts +1 -1
  86. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  87. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  88. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  89. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  90. package/src/__tests__/subagent-detail.test.ts +25 -7
  91. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  92. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  93. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  94. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  95. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  96. package/src/__tests__/title-generate-hook.test.ts +1 -1
  97. package/src/__tests__/tool-error-hook.test.ts +1 -1
  98. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  99. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  100. package/src/acp/__tests__/agent-process.test.ts +161 -0
  101. package/src/acp/__tests__/client-handler.test.ts +40 -0
  102. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  103. package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
  104. package/src/acp/__tests__/prepare-agent-env.test.ts +137 -0
  105. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  106. package/src/acp/__tests__/session-manager-resume.test.ts +736 -0
  107. package/src/acp/agent-process.ts +61 -1
  108. package/src/acp/auto-install.test.ts +196 -0
  109. package/src/acp/auto-install.ts +177 -0
  110. package/src/acp/client-handler.ts +31 -0
  111. package/src/acp/feature-gate.test.ts +48 -0
  112. package/src/acp/feature-gate.ts +34 -0
  113. package/src/acp/prepare-agent-env.ts +83 -29
  114. package/src/acp/resolve-agent.test.ts +320 -7
  115. package/src/acp/resolve-agent.ts +182 -18
  116. package/src/acp/resume-hint.ts +25 -0
  117. package/src/acp/session-manager.ts +495 -73
  118. package/src/acp/types.ts +8 -0
  119. package/src/agent/compaction-circuit.ts +60 -102
  120. package/src/agent/loop.ts +390 -240
  121. package/src/api/responses/conversation-message.ts +14 -1
  122. package/src/approvals/guardian-request-resolvers.ts +1 -1
  123. package/src/background-wake/next-wake.ts +1 -0
  124. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  125. package/src/cli/commands/plugins.ts +43 -37
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
  127. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  128. package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
  129. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
  130. package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
  131. package/src/cli/lib/install-from-github.ts +464 -55
  132. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  133. package/src/cli/lib/plugin-details.ts +409 -0
  134. package/src/cli/lib/plugin-marketplace.ts +197 -0
  135. package/src/cli/lib/search-plugins.ts +195 -29
  136. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  137. package/src/config/acp-defaults.test.ts +10 -0
  138. package/src/config/acp-defaults.ts +6 -0
  139. package/src/config/bundled-skills/acp/SKILL.md +83 -31
  140. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  141. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  142. package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
  143. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  144. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  145. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  146. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  147. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  148. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  149. package/src/config/bundled-tool-registry.ts +2 -0
  150. package/src/config/feature-flag-registry.json +14 -5
  151. package/src/config/schemas/heartbeat.ts +9 -0
  152. package/src/context/strip-injections.ts +8 -2
  153. package/src/context/window-manager.ts +27 -13
  154. package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
  155. package/src/daemon/conversation-agent-loop.ts +167 -1005
  156. package/src/daemon/conversation-lifecycle.ts +11 -255
  157. package/src/daemon/conversation-process.ts +8 -136
  158. package/src/daemon/conversation-registry.ts +159 -0
  159. package/src/daemon/conversation-runtime-assembly.ts +293 -392
  160. package/src/daemon/conversation-store.ts +9 -90
  161. package/src/daemon/conversation-surfaces.ts +24 -8
  162. package/src/daemon/conversation-workspace.ts +17 -0
  163. package/src/daemon/conversation.ts +383 -56
  164. package/src/daemon/external-plugins-bootstrap.ts +14 -19
  165. package/src/daemon/handlers/conversations.ts +3 -1
  166. package/src/daemon/handlers/skills.ts +4 -1
  167. package/src/daemon/host-proxy-preactivation.ts +1 -3
  168. package/src/daemon/lifecycle.ts +21 -0
  169. package/src/daemon/server.ts +2 -0
  170. package/src/daemon/wake-conversation-ops.ts +269 -0
  171. package/src/embedded/plugin-api.ts +2 -2
  172. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  173. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  174. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  175. package/src/heartbeat/heartbeat-service.ts +26 -0
  176. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  177. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  178. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  179. package/src/ipc/skill-routes/memory.ts +4 -2
  180. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  181. package/src/memory/conversation-crud.ts +29 -19
  182. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  183. package/src/memory/db-init.ts +2 -0
  184. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  185. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  186. package/src/memory/jobs-worker.ts +25 -1
  187. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  188. package/src/memory/migrations/index.ts +1 -0
  189. package/src/memory/schema/acp.ts +4 -0
  190. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  191. package/src/memory/v2/consolidation-job.ts +13 -4
  192. package/src/plugin-api/constants.ts +4 -0
  193. package/src/plugin-api/index.ts +6 -5
  194. package/src/plugin-api/types.ts +75 -0
  195. package/src/plugins/defaults/compaction/compact.ts +59 -0
  196. package/src/plugins/defaults/compaction/manager-store.ts +44 -0
  197. package/src/plugins/defaults/compaction/package.json +1 -2
  198. package/src/plugins/defaults/empty-response/package.json +0 -1
  199. package/src/plugins/defaults/history-repair/package.json +0 -1
  200. package/src/plugins/defaults/index.ts +135 -26
  201. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +103 -38
  202. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +167 -56
  203. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  204. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  205. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  206. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  207. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
  208. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  209. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
  210. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  211. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  212. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
  213. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  214. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
  215. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
  216. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
  217. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
  218. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
  219. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  220. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  221. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  222. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
  223. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
  224. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  225. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  226. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
  227. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
  228. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  229. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
  230. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  231. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
  232. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
  233. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  234. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
  235. package/src/plugins/defaults/title-generate/package.json +0 -1
  236. package/src/plugins/defaults/tool-error/package.json +0 -1
  237. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  238. package/src/plugins/pipeline.ts +6 -293
  239. package/src/plugins/registry.ts +9 -37
  240. package/src/plugins/types.ts +76 -381
  241. package/src/plugins/user-loader.ts +30 -127
  242. package/src/proactive-artifact/aux-message-injector.ts +1 -1
  243. package/src/proactive-artifact/job.test.ts +1 -1
  244. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  245. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
  246. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  247. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  248. package/src/runtime/agent-wake.ts +108 -209
  249. package/src/runtime/assistant-event-hub.ts +1 -1
  250. package/src/runtime/channel-approvals.ts +1 -1
  251. package/src/runtime/interactive-ui.ts +1 -1
  252. package/src/runtime/routes/__tests__/acp-routes.test.ts +283 -55
  253. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  254. package/src/runtime/routes/__tests__/plugins-routes.test.ts +398 -73
  255. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  256. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  257. package/src/runtime/routes/acp-routes.test.ts +89 -25
  258. package/src/runtime/routes/acp-routes.ts +81 -29
  259. package/src/runtime/routes/approval-routes.ts +1 -1
  260. package/src/runtime/routes/browser-routes.ts +1 -1
  261. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  262. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  263. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  264. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  265. package/src/runtime/routes/conversation-routes.ts +28 -2
  266. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  267. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  268. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  269. package/src/runtime/routes/host-cu-routes.ts +1 -1
  270. package/src/runtime/routes/identity-routes.ts +18 -3
  271. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  272. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  273. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  274. package/src/runtime/routes/memory-v3-routes.ts +16 -6
  275. package/src/runtime/routes/playground/helpers.ts +1 -1
  276. package/src/runtime/routes/plugins-routes.ts +336 -35
  277. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  278. package/src/runtime/routes/work-items-routes.ts +2 -4
  279. package/src/runtime/services/conversation-serializer.ts +1 -1
  280. package/src/signals/cancel.ts +2 -4
  281. package/src/subagent/manager.ts +21 -5
  282. package/src/tools/acp/context.ts +20 -0
  283. package/src/tools/acp/list-agents.test.ts +7 -1
  284. package/src/tools/acp/spawn.test.ts +158 -55
  285. package/src/tools/acp/spawn.ts +47 -72
  286. package/src/tools/acp/steer.test.ts +105 -8
  287. package/src/tools/acp/steer.ts +48 -17
  288. package/src/tools/apps/executors.ts +13 -8
  289. package/src/tools/filesystem/write.ts +34 -0
  290. package/src/tools/subagent/spawn.ts +2 -4
  291. package/src/tools/ui-surface/definitions.ts +25 -5
  292. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  293. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  294. package/docs/plugins.md +0 -832
  295. package/examples/plugins/echo/register.ts +0 -143
  296. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  297. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  298. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  299. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  300. package/src/__tests__/pipeline-runner.test.ts +0 -554
  301. package/src/__tests__/plugin-external-api.test.ts +0 -68
  302. package/src/daemon/wake-target-adapter.ts +0 -253
  303. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  304. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  305. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  306. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  307. package/src/plugins/defaults/compaction/register.ts +0 -35
  308. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  309. package/src/plugins/defaults/empty-response/register.ts +0 -23
  310. package/src/plugins/defaults/history-repair/register.ts +0 -24
  311. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  312. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  313. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  314. package/src/plugins/defaults/title-generate/register.ts +0 -35
  315. package/src/plugins/defaults/tool-error/register.ts +0 -23
  316. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  317. package/src/plugins/external-api.ts +0 -104
  318. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  319. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  320. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  321. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  322. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
  323. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  324. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
  325. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  326. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  327. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  328. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  329. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  330. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  331. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  332. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  333. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  334. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  335. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  336. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  337. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
  338. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  339. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  340. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -312,16 +312,14 @@ let mockInjectionBlocks: {
312
312
  pkbSystemReminder?: string;
313
313
  unifiedTurnContext?: string;
314
314
  } = {};
315
- const buildUnifiedTurnContextBlockMock = mock(
316
- (options: Record<string, unknown>) =>
317
- `<turn_context>\ncurrent_time: ${String(options.timestamp)}\n</turn_context>`,
318
- );
319
- const applyRuntimeInjectionsMock = mock(
320
- async (msgs: Message[], _options?: unknown) => ({
321
- messages: msgs,
322
- blocks: { ...mockInjectionBlocks },
323
- }),
324
- );
315
+ const defaultApplyRuntimeInjectionsImpl = async (
316
+ msgs: Message[],
317
+ _options?: unknown,
318
+ ) => ({
319
+ messages: msgs,
320
+ blocks: { ...mockInjectionBlocks },
321
+ });
322
+ const applyRuntimeInjectionsMock = mock(defaultApplyRuntimeInjectionsImpl);
325
323
  let mockSlackChronologicalContext: {
326
324
  renderedMessages: Array<{
327
325
  message: Message;
@@ -358,7 +356,6 @@ const getSlackCompactionWatermarkForPrefixMock = mock(
358
356
  );
359
357
  mock.module("../daemon/conversation-runtime-assembly.js", () => ({
360
358
  applyRuntimeInjections: applyRuntimeInjectionsMock,
361
- buildUnifiedTurnContextBlock: buildUnifiedTurnContextBlockMock,
362
359
  stripInjectionsForCompaction: (msgs: Message[]) => msgs,
363
360
  isSlackChannelConversation: () => false,
364
361
  getSlackCompactionWatermarkForPrefix:
@@ -537,8 +534,8 @@ mock.module("../proactive-artifact/index.js", () => ({
537
534
  // ── Imports (after mocks) ────────────────────────────────────────────
538
535
 
539
536
  import { AgentLoop } from "../agent/loop.js";
537
+ import type { Conversation } from "../daemon/conversation.js";
540
538
  import {
541
- type AgentLoopConversationContext,
542
539
  applyCompactionResult,
543
540
  runAgentLoopImpl,
544
541
  } from "../daemon/conversation-agent-loop.js";
@@ -552,13 +549,13 @@ import {
552
549
  // ── Test helpers ─────────────────────────────────────────────────────
553
550
 
554
551
  function makeCtx(
555
- overrides?: Partial<AgentLoopConversationContext> & {
552
+ overrides?: Partial<Conversation> & {
556
553
  providerResponses?: ScriptedResponse[];
557
554
  loopProvider?: Provider;
558
555
  loopTools?: ToolDefinition[];
559
556
  toolExecutor?: LoopToolExecutor;
560
557
  },
561
- ): AgentLoopConversationContext {
558
+ ): Conversation {
562
559
  const {
563
560
  providerResponses,
564
561
  loopProvider,
@@ -589,7 +586,9 @@ function makeCtx(
589
586
  providerResponses ?? [textResponse("response")],
590
587
  loopProviderName,
591
588
  ).provider;
592
- const agentLoop = new AgentLoop(provider, "system prompt", {
589
+ const agentLoop = new AgentLoop({
590
+ provider: provider,
591
+ systemPrompt: "system prompt",
593
592
  conversationId,
594
593
  tools: loopTools ?? [],
595
594
  toolExecutor,
@@ -616,15 +615,24 @@ function makeCtx(
616
615
  usage: { inputTokens: 0, outputTokens: 0 },
617
616
  stopReason: "end_turn",
618
617
  }),
619
- } as unknown as AgentLoopConversationContext["provider"],
618
+ } as unknown as Conversation["provider"],
620
619
  systemPrompt: "system prompt",
621
620
 
622
621
  contextWindowManager: {
622
+ updateConfig: () => {},
623
623
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
624
624
  maybeCompact: async () => ({ compacted: false }),
625
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
626
- contextCompactedMessageCount: 0,
627
- contextCompactedAt: null,
625
+ } as unknown as Conversation["contextWindowManager"],
626
+ conversationType: mockConversationRow?.conversationType ?? undefined,
627
+ source: mockConversationRow?.source ?? undefined,
628
+ contextSummary: mockConversationRow?.contextSummary ?? null,
629
+ contextCompactedMessageCount:
630
+ mockConversationRow?.contextCompactedMessageCount ?? 0,
631
+ contextCompactedAt: mockConversationRow?.contextCompactedAt ?? null,
632
+ slackContextCompactionWatermarkTs:
633
+ mockConversationRow?.slackContextCompactionWatermarkTs ?? null,
634
+ lastNotifiedInferenceProfile:
635
+ mockConversationRow?.lastNotifiedInferenceProfile ?? null,
628
636
 
629
637
  memoryPolicy: { scopeId: "default", includeDefaultFallback: true },
630
638
 
@@ -636,8 +644,6 @@ function makeCtx(
636
644
  currentTurnSurfaces: [],
637
645
 
638
646
  workingDir: "/tmp",
639
- workspaceTopLevelContext: null,
640
- workspaceTopLevelDirty: false,
641
647
  channelCapabilities: undefined,
642
648
  commandIntent: undefined,
643
649
  trustContext: undefined,
@@ -647,15 +653,15 @@ function makeCtx(
647
653
  preactivatedSkillIds: undefined,
648
654
  skillProjectionState: new Map(),
649
655
  skillProjectionCache:
650
- new Map() as unknown as AgentLoopConversationContext["skillProjectionCache"],
656
+ new Map() as unknown as Conversation["skillProjectionCache"],
651
657
 
652
658
  traceEmitter: {
653
659
  emit: () => {},
654
- } as unknown as AgentLoopConversationContext["traceEmitter"],
660
+ } as unknown as Conversation["traceEmitter"],
655
661
  profiler: {
656
662
  startRequest: () => {},
657
663
  emitSummary: () => {},
658
- } as unknown as AgentLoopConversationContext["profiler"],
664
+ } as unknown as Conversation["profiler"],
659
665
  usageStats: {
660
666
  totalInputTokens: 0,
661
667
  totalOutputTokens: 0,
@@ -668,13 +674,12 @@ function makeCtx(
668
674
  lastAttachmentWarnings: [],
669
675
 
670
676
  hasNoClient: false,
671
- prompter: {} as unknown as AgentLoopConversationContext["prompter"],
672
- queue: {} as unknown as AgentLoopConversationContext["queue"],
677
+ prompter: {} as unknown as Conversation["prompter"],
678
+ queue: {} as unknown as Conversation["queue"],
673
679
 
674
680
  getWorkspaceGitService: () => ({ ensureInitialized: async () => {} }),
675
681
  commitTurnChanges: async () => {},
676
682
 
677
- refreshWorkspaceTopLevelContextIfNeeded: () => {},
678
683
  markWorkspaceTopLevelDirty: () => {},
679
684
  emitActivityState: () => {},
680
685
  getQueueDepth: () => 0,
@@ -701,10 +706,32 @@ function makeCtx(
701
706
  }),
702
707
  retrackCachedNodes: () => {},
703
708
  recordPkbQueryVectors: () => {},
704
- } as unknown as AgentLoopConversationContext["graphMemory"],
709
+ } as unknown as Conversation["graphMemory"],
705
710
 
706
711
  ...ctxOverrides,
707
- } as AgentLoopConversationContext;
712
+ } as unknown as Conversation;
713
+ }
714
+
715
+ type CompactionResult = Parameters<typeof applyCompactionResult>[1];
716
+
717
+ function makeCompactionResult(
718
+ overrides?: Partial<CompactionResult>,
719
+ ): CompactionResult {
720
+ return {
721
+ messages: [{ role: "user", content: [{ type: "text", text: "summary" }] }],
722
+ compactedPersistedMessages: 4,
723
+ previousEstimatedInputTokens: 12000,
724
+ estimatedInputTokens: 3000,
725
+ maxInputTokens: 100000,
726
+ thresholdTokens: 80000,
727
+ compactedMessages: 4,
728
+ summaryCalls: 1,
729
+ summaryInputTokens: 100,
730
+ summaryOutputTokens: 20,
731
+ summaryModel: "mock-model",
732
+ summaryText: "summary",
733
+ ...overrides,
734
+ };
708
735
  }
709
736
 
710
737
  // ── Tests ────────────────────────────────────────────────────────────
@@ -744,7 +771,9 @@ beforeEach(() => {
744
771
  setConversationHistoryStrippedAtMock.mockClear();
745
772
  setConversationHistoryStrippedAtMock.mockImplementation(() => {});
746
773
  applyRuntimeInjectionsMock.mockClear();
747
- buildUnifiedTurnContextBlockMock.mockClear();
774
+ applyRuntimeInjectionsMock.mockImplementation(
775
+ defaultApplyRuntimeInjectionsImpl,
776
+ );
748
777
  resolveTurnTimezoneContextMock.mockClear();
749
778
  formatTurnTimestampMock.mockClear();
750
779
  mockSlackChronologicalContext = null;
@@ -757,11 +786,10 @@ beforeEach(() => {
757
786
  projectAssistantMessageMock.mockClear();
758
787
  publishSyncInvalidationMock.mockClear();
759
788
  mockMessageById = null;
760
- // Orchestrator pipelines (overflowReduce, persistence, …) run through the
761
- // plugin registry; reset and re-register every default so the pipelines
762
- // dispatch to middleware backed by the mocked collaborators these tests
763
- // install (`reduceContextOverflow`, `syncMessageToDisk`, etc.) instead of
764
- // hitting the bare terminals.
789
+ // The compaction pipeline runs through the plugin registry; reset and
790
+ // re-register every default so it dispatches to middleware backed by the
791
+ // mocked collaborators these tests install (`syncMessageToDisk`, etc.)
792
+ // instead of hitting the bare terminal.
765
793
  resetPluginRegistryAndRegisterDefaults();
766
794
  });
767
795
 
@@ -785,7 +813,7 @@ describe("session-agent-loop", () => {
785
813
  });
786
814
  });
787
815
 
788
- test("passes resolved canonical timezones into unified turn context", async () => {
816
+ test("freezes the client timezone snapshot on the conversation, not the options bag", async () => {
789
817
  mockUiConfig = {
790
818
  userTimezone: "US/Eastern",
791
819
  detectedTimezone: "US/Central",
@@ -802,17 +830,25 @@ describe("session-agent-loop", () => {
802
830
 
803
831
  await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
804
832
 
805
- expect(formatTurnTimestampMock).toHaveBeenCalledWith({
806
- timeZone: "America/New_York",
807
- });
808
- expect(buildUnifiedTurnContextBlockMock).toHaveBeenCalled();
809
- const turnContextOptions =
810
- buildUnifiedTurnContextBlockMock.mock.calls[0]?.[0];
811
- expect(turnContextOptions).toMatchObject({
812
- configuredUserTimezone: "America/New_York",
833
+ // The turn-start client timezone and the long-absence gap are frozen on
834
+ // the conversation as `currentTurnTemporalSnapshot`; the live
835
+ // `clientTimezone` would otherwise be clobbered mid-turn. `current_time`
836
+ // is not frozen — it is computed fresh at each injection inside
837
+ // `applyRuntimeInjections`.
838
+ expect(ctx.currentTurnTemporalSnapshot).toEqual({
813
839
  clientTimezone: "America/Los_Angeles",
814
- detectedTimezone: "America/Chicago",
815
- });
840
+ timeSinceLastMessage: null,
841
+ });
842
+ // Neither the timezones nor the long-absence gap are threaded through the
843
+ // options bag — `applyRuntimeInjections` sources the client timezone and
844
+ // gap from the snapshot and the config timezones from config
845
+ // (`ui.userTimezone`, `ui.detectedTimezone`).
846
+ const injectionOptions = applyRuntimeInjectionsMock.mock.calls[0]?.[1];
847
+ expect(injectionOptions).not.toHaveProperty("timestamp");
848
+ expect(injectionOptions).not.toHaveProperty("clientTimezone");
849
+ expect(injectionOptions).not.toHaveProperty("configuredUserTimezone");
850
+ expect(injectionOptions).not.toHaveProperty("detectedTimezone");
851
+ expect(injectionOptions).not.toHaveProperty("timeSinceLastMessage");
816
852
  });
817
853
  });
818
854
 
@@ -880,7 +916,23 @@ describe("session-agent-loop", () => {
880
916
  });
881
917
 
882
918
  describe("disk pressure injection context", () => {
883
- test("passes cleanup context into runtime injections for cleanup-mode turns", async () => {
919
+ // The loop sets `ctx.diskPressureCleanupModeActive` for the duration of the
920
+ // turn (the disk-pressure-warning injector reads it via the per-conversation
921
+ // registry) and resets it in the turn-end cleanup path. Snapshot the flag at
922
+ // each `applyRuntimeInjections` call so assertions observe its value while
923
+ // injection runs, not the post-turn reset.
924
+ function captureCleanupFlagDuringInjection(ctx: {
925
+ diskPressureCleanupModeActive?: boolean;
926
+ }): () => Array<boolean | undefined> {
927
+ const observed: Array<boolean | undefined> = [];
928
+ applyRuntimeInjectionsMock.mockImplementation(async (msgs: Message[]) => {
929
+ observed.push(ctx.diskPressureCleanupModeActive);
930
+ return { messages: msgs, blocks: { ...mockInjectionBlocks } };
931
+ });
932
+ return () => observed;
933
+ }
934
+
935
+ test("sets the cleanup-mode flag on the conversation for cleanup-mode turns", async () => {
884
936
  mockDiskPressureDecision = {
885
937
  action: "allow-cleanup-mode",
886
938
  reason: "guardian",
@@ -901,8 +953,9 @@ describe("session-agent-loop", () => {
901
953
  trustContext: {
902
954
  sourceChannel: "telegram",
903
955
  trustClass: "guardian",
904
- } as AgentLoopConversationContext["trustContext"],
956
+ } as Conversation["trustContext"],
905
957
  });
958
+ const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
906
959
 
907
960
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
908
961
 
@@ -921,21 +974,16 @@ describe("session-agent-loop", () => {
921
974
  },
922
975
  }),
923
976
  );
924
- const firstInjectionOptions = applyRuntimeInjectionsMock.mock
925
- .calls[0]![1] as {
926
- diskPressureContext?: { cleanupModeActive: boolean } | null;
927
- };
928
- expect(firstInjectionOptions.diskPressureContext).toEqual({
929
- cleanupModeActive: true,
930
- });
977
+ expect(cleanupFlagDuringInjection()).toEqual([true]);
931
978
  });
932
979
 
933
- test("passes cleanup context into runtime injections for local-owner turns", async () => {
980
+ test("sets the cleanup-mode flag on the conversation for local-owner turns", async () => {
934
981
  mockDiskPressureDecision = {
935
982
  action: "allow-cleanup-mode",
936
983
  reason: "local-owner",
937
984
  };
938
985
  const ctx = makeCtx();
986
+ const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
939
987
 
940
988
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
941
989
 
@@ -947,16 +995,10 @@ describe("session-agent-loop", () => {
947
995
  trustContext: null,
948
996
  }),
949
997
  );
950
- const firstInjectionOptions = applyRuntimeInjectionsMock.mock
951
- .calls[0]![1] as {
952
- diskPressureContext?: { cleanupModeActive: boolean } | null;
953
- };
954
- expect(firstInjectionOptions.diskPressureContext).toEqual({
955
- cleanupModeActive: true,
956
- });
998
+ expect(cleanupFlagDuringInjection()).toEqual([true]);
957
999
  });
958
1000
 
959
- test("keeps cleanup context on overflow recovery reinjection", async () => {
1001
+ test("keeps the cleanup-mode flag set across overflow recovery reinjection", async () => {
960
1002
  mockDiskPressureDecision = {
961
1003
  action: "allow-cleanup-mode",
962
1004
  reason: "guardian",
@@ -976,20 +1018,16 @@ describe("session-agent-loop", () => {
976
1018
  trustContext: {
977
1019
  sourceChannel: "telegram",
978
1020
  trustClass: "guardian",
979
- } as AgentLoopConversationContext["trustContext"],
1021
+ } as Conversation["trustContext"],
980
1022
  });
1023
+ const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
981
1024
 
982
1025
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
983
1026
 
984
1027
  expect(applyRuntimeInjectionsMock.mock.calls.length).toBeGreaterThan(1);
985
- for (const call of applyRuntimeInjectionsMock.mock.calls) {
986
- const options = call[1] as {
987
- diskPressureContext?: { cleanupModeActive: boolean } | null;
988
- };
989
- expect(options.diskPressureContext).toEqual({
990
- cleanupModeActive: true,
991
- });
992
- }
1028
+ const flags = cleanupFlagDuringInjection();
1029
+ expect(flags.length).toBeGreaterThan(1);
1030
+ expect(flags.every((flag) => flag === true)).toBe(true);
993
1031
  });
994
1032
 
995
1033
  test("blocks policy-denied turns before runtime injection or model execution", async () => {
@@ -1008,7 +1046,7 @@ describe("session-agent-loop", () => {
1008
1046
  emit: (...args: unknown[]) => {
1009
1047
  traceEvents.push(args);
1010
1048
  },
1011
- } as unknown as AgentLoopConversationContext["traceEmitter"],
1049
+ } as unknown as Conversation["traceEmitter"],
1012
1050
  });
1013
1051
  const runSpy = spyOn(ctx.agentLoop, "run");
1014
1052
 
@@ -1169,7 +1207,7 @@ describe("session-agent-loop", () => {
1169
1207
  usage: { inputTokens: 0, outputTokens: 0 },
1170
1208
  stopReason: "end_turn",
1171
1209
  }),
1172
- } as unknown as AgentLoopConversationContext["provider"],
1210
+ } as unknown as Conversation["provider"],
1173
1211
  });
1174
1212
 
1175
1213
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1218,7 +1256,7 @@ describe("session-agent-loop", () => {
1218
1256
  usage: { inputTokens: 0, outputTokens: 0 },
1219
1257
  stopReason: "end_turn",
1220
1258
  }),
1221
- } as unknown as AgentLoopConversationContext["provider"],
1259
+ } as unknown as Conversation["provider"],
1222
1260
  });
1223
1261
 
1224
1262
  await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
@@ -1284,7 +1322,7 @@ describe("session-agent-loop", () => {
1284
1322
  usage: { inputTokens: 0, outputTokens: 0 },
1285
1323
  stopReason: "end_turn",
1286
1324
  }),
1287
- } as unknown as AgentLoopConversationContext["provider"],
1325
+ } as unknown as Conversation["provider"],
1288
1326
  });
1289
1327
 
1290
1328
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1335,7 +1373,7 @@ describe("session-agent-loop", () => {
1335
1373
  usage: { inputTokens: 0, outputTokens: 0 },
1336
1374
  stopReason: "end_turn",
1337
1375
  }),
1338
- } as unknown as AgentLoopConversationContext["provider"],
1376
+ } as unknown as Conversation["provider"],
1339
1377
  traceEmitter: {
1340
1378
  emit: (
1341
1379
  event: string,
@@ -1346,7 +1384,7 @@ describe("session-agent-loop", () => {
1346
1384
  traceEvents.push({ label, attrs: payload.attributes ?? {} });
1347
1385
  }
1348
1386
  },
1349
- } as unknown as AgentLoopConversationContext["traceEmitter"],
1387
+ } as unknown as Conversation["traceEmitter"],
1350
1388
  });
1351
1389
 
1352
1390
  await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
@@ -1393,7 +1431,7 @@ describe("session-agent-loop", () => {
1393
1431
  usage: { inputTokens: 0, outputTokens: 0 },
1394
1432
  stopReason: "end_turn",
1395
1433
  }),
1396
- } as unknown as AgentLoopConversationContext["provider"],
1434
+ } as unknown as Conversation["provider"],
1397
1435
  traceEmitter: {
1398
1436
  emit: (
1399
1437
  event: string,
@@ -1404,7 +1442,7 @@ describe("session-agent-loop", () => {
1404
1442
  traceEvents.push({ label, attrs: payload.attributes ?? {} });
1405
1443
  }
1406
1444
  },
1407
- } as unknown as AgentLoopConversationContext["traceEmitter"],
1445
+ } as unknown as Conversation["traceEmitter"],
1408
1446
  });
1409
1447
 
1410
1448
  await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
@@ -1460,7 +1498,7 @@ describe("session-agent-loop", () => {
1460
1498
  usage: { inputTokens: 0, outputTokens: 0 },
1461
1499
  stopReason: "end_turn",
1462
1500
  }),
1463
- } as unknown as AgentLoopConversationContext["provider"],
1501
+ } as unknown as Conversation["provider"],
1464
1502
  });
1465
1503
 
1466
1504
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1521,9 +1559,21 @@ describe("session-agent-loop", () => {
1521
1559
  },
1522
1560
  });
1523
1561
 
1524
- // After the orchestrator's preflight compaction runs, the loop completes
1525
- // the turn normally.
1526
- const ctx = makeCtx({ providerResponses: [textResponse("recovered")] });
1562
+ // The provider rejects the first call as too large; the convergence
1563
+ // reducer compacts and the rerun completes the turn, forwarding the
1564
+ // compaction's cache-aware usage to recordUsage.
1565
+ const { provider } = createMockProvider([
1566
+ new Error("context_length_exceeded"),
1567
+ textResponse("recovered"),
1568
+ ]);
1569
+ const ctx = makeCtx({
1570
+ loopProvider: provider,
1571
+ contextWindowManager: {
1572
+ updateConfig: () => {},
1573
+ shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1574
+ maybeCompact: async () => ({ compacted: false }),
1575
+ } as unknown as Conversation["contextWindowManager"],
1576
+ });
1527
1577
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
1528
1578
 
1529
1579
  const compactorCall = recordUsageMock.mock.calls.find(
@@ -1605,9 +1655,10 @@ describe("session-agent-loop", () => {
1605
1655
  const ctx = makeCtx({
1606
1656
  loopProvider: provider,
1607
1657
  contextWindowManager: {
1658
+ updateConfig: () => {},
1608
1659
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1609
1660
  maybeCompact: async () => ({ compacted: false }),
1610
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1661
+ } as unknown as Conversation["contextWindowManager"],
1611
1662
  });
1612
1663
 
1613
1664
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1626,6 +1677,7 @@ describe("session-agent-loop", () => {
1626
1677
  const ctx = makeCtx({
1627
1678
  providerResponses: [new Error("context_length_exceeded")],
1628
1679
  contextWindowManager: {
1680
+ updateConfig: () => {},
1629
1681
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1630
1682
  // Compaction succeeds but context is still too large
1631
1683
  maybeCompact: async () => ({
@@ -1645,7 +1697,7 @@ describe("session-agent-loop", () => {
1645
1697
  summaryOutputTokens: 200,
1646
1698
  summaryModel: "mock-model",
1647
1699
  }),
1648
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1700
+ } as unknown as Conversation["contextWindowManager"],
1649
1701
  });
1650
1702
 
1651
1703
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1685,9 +1737,10 @@ describe("session-agent-loop", () => {
1685
1737
  const ctx = makeCtx({
1686
1738
  loopProvider: provider,
1687
1739
  contextWindowManager: {
1740
+ updateConfig: () => {},
1688
1741
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1689
1742
  maybeCompact: async () => ({ compacted: false }),
1690
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1743
+ } as unknown as Conversation["contextWindowManager"],
1691
1744
  });
1692
1745
 
1693
1746
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1734,6 +1787,7 @@ describe("session-agent-loop", () => {
1734
1787
  ],
1735
1788
  hasNoClient: true,
1736
1789
  contextWindowManager: {
1790
+ updateConfig: () => {},
1737
1791
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1738
1792
  maybeCompact: async () => ({
1739
1793
  compacted: true,
@@ -1752,7 +1806,7 @@ describe("session-agent-loop", () => {
1752
1806
  summaryOutputTokens: 100,
1753
1807
  summaryModel: "mock-model",
1754
1808
  }),
1755
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1809
+ } as unknown as Conversation["contextWindowManager"],
1756
1810
  });
1757
1811
 
1758
1812
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1834,6 +1888,7 @@ describe("session-agent-loop", () => {
1834
1888
  toolExecutor: async () => ({ content: "data", isError: false }),
1835
1889
  hasNoClient: true,
1836
1890
  contextWindowManager: {
1891
+ updateConfig: () => {},
1837
1892
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1838
1893
  maybeCompact: async () => ({
1839
1894
  compacted: true,
@@ -1852,7 +1907,7 @@ describe("session-agent-loop", () => {
1852
1907
  summaryOutputTokens: 100,
1853
1908
  summaryModel: "mock-model",
1854
1909
  }),
1855
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1910
+ } as unknown as Conversation["contextWindowManager"],
1856
1911
  });
1857
1912
 
1858
1913
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1919,9 +1974,10 @@ describe("session-agent-loop", () => {
1919
1974
  const ctx = makeCtx({
1920
1975
  providerResponses: [new Error("context_length_exceeded")],
1921
1976
  contextWindowManager: {
1977
+ updateConfig: () => {},
1922
1978
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1923
1979
  maybeCompact: async () => ({ compacted: false }),
1924
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1980
+ } as unknown as Conversation["contextWindowManager"],
1925
1981
  });
1926
1982
 
1927
1983
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1929,48 +1985,6 @@ describe("session-agent-loop", () => {
1929
1985
  // maxAttempts is 3 — reducer should be called at most 3 times
1930
1986
  expect(reducerCalls).toBeLessThanOrEqual(3);
1931
1987
  });
1932
-
1933
- test("preflight budget evaluation invokes reducer before provider call", async () => {
1934
- const events: ServerMessage[] = [];
1935
- let reducerCalls = 0;
1936
-
1937
- // Set token estimate above budget (100000 * 0.95 = 95000)
1938
- mockEstimateTokens = 96000;
1939
-
1940
- mockReducerStepFn = (msgs: Message[]) => {
1941
- reducerCalls++;
1942
- return {
1943
- messages: msgs,
1944
- tier: "forced_compaction",
1945
- state: {
1946
- appliedTiers: ["forced_compaction"],
1947
- injectionMode: "full",
1948
- exhausted: true,
1949
- },
1950
- estimatedTokens: 50000,
1951
- };
1952
- };
1953
-
1954
- // After the preflight reducer brings the estimate under budget, the loop
1955
- // completes the turn in a single provider call.
1956
- const { provider, calls } = createMockProvider([textResponse("ok")]);
1957
- const ctx = makeCtx({
1958
- loopProvider: provider,
1959
- contextWindowManager: {
1960
- shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1961
- maybeCompact: async () => ({ compacted: false }),
1962
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1963
- });
1964
-
1965
- await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
1966
-
1967
- // Reducer should have been called during preflight
1968
- expect(reducerCalls).toBeGreaterThanOrEqual(1);
1969
- // Agent loop should still succeed in a single provider call
1970
- expect(calls.length).toBe(1);
1971
- const complete = events.find((e) => e.type === "message_complete");
1972
- expect(complete).toBeDefined();
1973
- });
1974
1988
  });
1975
1989
 
1976
1990
  describe("provider ordering error retry", () => {
@@ -2024,7 +2038,7 @@ describe("session-agent-loop", () => {
2024
2038
  ],
2025
2039
  toolExecutor: async () => ({ content: "file content", isError: false }),
2026
2040
  canHandoffAtCheckpoint: () => true,
2027
- } as unknown as Partial<AgentLoopConversationContext>);
2041
+ } as unknown as Partial<Conversation>);
2028
2042
 
2029
2043
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2030
2044
 
@@ -2055,7 +2069,7 @@ describe("session-agent-loop", () => {
2055
2069
  ],
2056
2070
  toolExecutor: async () => ({ content: "content", isError: false }),
2057
2071
  canHandoffAtCheckpoint: () => false,
2058
- } as unknown as Partial<AgentLoopConversationContext>);
2072
+ } as unknown as Partial<Conversation>);
2059
2073
 
2060
2074
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2061
2075
 
@@ -2206,7 +2220,7 @@ describe("session-agent-loop", () => {
2206
2220
  drainQueue: (reason: string) => {
2207
2221
  drainReason = reason;
2208
2222
  },
2209
- } as unknown as Partial<AgentLoopConversationContext>);
2223
+ } as unknown as Partial<Conversation>);
2210
2224
 
2211
2225
  // WHEN the orchestrator runs the turn to completion
2212
2226
  await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
@@ -2648,9 +2662,10 @@ describe("session-agent-loop", () => {
2648
2662
  textResponse("retry succeeded"),
2649
2663
  ],
2650
2664
  contextWindowManager: {
2665
+ updateConfig: () => {},
2651
2666
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
2652
2667
  maybeCompact: async () => ({ compacted: false }),
2653
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
2668
+ } as unknown as Conversation["contextWindowManager"],
2654
2669
  });
2655
2670
  await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2656
2671
 
@@ -3112,7 +3127,7 @@ describe("session-agent-loop", () => {
3112
3127
  });
3113
3128
 
3114
3129
  describe("Slack compaction watermarks", () => {
3115
- test("start-of-turn Slack compaction derives and persists watermark from rendered context", async () => {
3130
+ test("first-call compaction runs in the loop and drops the Slack watermark", async () => {
3116
3131
  const renderedSlackMessages: Message[] = [
3117
3132
  {
3118
3133
  role: "user",
@@ -3140,9 +3155,12 @@ describe("session-agent-loop", () => {
3140
3155
  })),
3141
3156
  compactableStartIndex: 0,
3142
3157
  };
3143
- const shouldCompactInputs: Message[][] = [];
3144
3158
  const maybeCompactInputs: Message[][] = [];
3145
3159
 
3160
+ // Sits above the loop's first-call gate threshold (~80.75k), so the
3161
+ // loop's first-call gate owns the turn-start compaction.
3162
+ mockEstimateTokens = 90_000;
3163
+
3146
3164
  const ctx = makeCtx({
3147
3165
  channelCapabilities: {
3148
3166
  channel: "slack",
@@ -3154,18 +3172,19 @@ describe("session-agent-loop", () => {
3154
3172
  trustContext: {
3155
3173
  sourceChannel: "slack",
3156
3174
  trustClass: "guardian",
3157
- } as AgentLoopConversationContext["trustContext"],
3175
+ } as Conversation["trustContext"],
3158
3176
  getTurnChannelContext: () => ({
3159
3177
  userMessageChannel: "slack" as const,
3160
3178
  assistantMessageChannel: "slack" as const,
3161
3179
  }),
3162
3180
  contextWindowManager: {
3163
- shouldCompact: (messages: Message[]) => {
3164
- shouldCompactInputs.push(messages);
3165
- return { needed: true, estimatedTokens: 95_000 };
3166
- },
3181
+ updateConfig: () => {},
3182
+ shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3167
3183
  maybeCompact: async (messages: Message[]) => {
3168
3184
  maybeCompactInputs.push(messages);
3185
+ // Drop back under budget so the post-compaction provider call
3186
+ // proceeds.
3187
+ mockEstimateTokens = 1000;
3169
3188
  return {
3170
3189
  compacted: true,
3171
3190
  messages: [
@@ -3173,10 +3192,10 @@ describe("session-agent-loop", () => {
3173
3192
  role: "user",
3174
3193
  content: [{ type: "text", text: "summary" }],
3175
3194
  },
3176
- messages[2]!,
3195
+ messages[messages.length - 1]!,
3177
3196
  ],
3178
3197
  compactedPersistedMessages: 2,
3179
- previousEstimatedInputTokens: 95_000,
3198
+ previousEstimatedInputTokens: 90_000,
3180
3199
  estimatedInputTokens: 5_000,
3181
3200
  maxInputTokens: 100_000,
3182
3201
  thresholdTokens: 80_000,
@@ -3189,292 +3208,24 @@ describe("session-agent-loop", () => {
3189
3208
  summaryFailed: false,
3190
3209
  };
3191
3210
  },
3192
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
3193
- });
3194
-
3195
- await runAgentLoopImpl(ctx, "next reply", "user-msg-start", () => {});
3196
-
3197
- expect(shouldCompactInputs[0]).toBe(renderedSlackMessages);
3198
- expect(maybeCompactInputs[0]).toBe(renderedSlackMessages);
3199
- expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
3200
- mockSlackChronologicalContext,
3201
- 2,
3202
- );
3203
- expect(updateConversationSlackContextWatermarkMock).toHaveBeenCalledWith(
3204
- "test-conv",
3205
- "1700000020.000000",
3206
- expect.any(Number),
3207
- );
3208
- const firstInjectionOptions = applyRuntimeInjectionsMock.mock
3209
- .calls[0]![1] as {
3210
- slackChronologicalMessages?: Message[] | null;
3211
- };
3212
- expect(firstInjectionOptions.slackChronologicalMessages).toBeNull();
3213
- });
3214
-
3215
- test("overflow reducer Slack compaction persists watermark from rendered context", async () => {
3216
- const renderedSlackMessages: Message[] = [
3217
- {
3218
- role: "user",
3219
- content: [{ type: "text", text: "first rendered Slack row" }],
3220
- },
3221
- {
3222
- role: "user",
3223
- content: [{ type: "text", text: "second rendered Slack row" }],
3224
- },
3225
- {
3226
- role: "user",
3227
- content: [{ type: "text", text: "retained Slack row" }],
3228
- },
3229
- ];
3230
- mockSlackChronologicalContext = {
3231
- messages: renderedSlackMessages,
3232
- renderedMessages: renderedSlackMessages.map((message, index) => ({
3233
- message,
3234
- sourceChannelTs: [
3235
- "1700000010.000000",
3236
- "1700000020.000000",
3237
- "1700000030.000000",
3238
- ][index]!,
3239
- tagLineProvenance: "none",
3240
- })),
3241
- compactableStartIndex: 0,
3242
- };
3243
- mockEstimateTokens = 120_000;
3244
- mockReducerStepFn = (_msgs: Message[]) => ({
3245
- messages: [
3246
- {
3247
- role: "user",
3248
- content: [{ type: "text", text: "summary" }],
3249
- },
3250
- renderedSlackMessages[2]!,
3251
- ],
3252
- tier: "forced_compaction",
3253
- state: {
3254
- appliedTiers: ["forced_compaction"],
3255
- injectionMode: "full",
3256
- exhausted: false,
3257
- },
3258
- estimatedTokens: 5_000,
3259
- compactionResult: {
3260
- compacted: true,
3261
- messages: [
3262
- {
3263
- role: "user",
3264
- content: [{ type: "text", text: "summary" }],
3265
- },
3266
- renderedSlackMessages[2]!,
3267
- ],
3268
- compactedPersistedMessages: 2,
3269
- previousEstimatedInputTokens: 120_000,
3270
- estimatedInputTokens: 5_000,
3271
- maxInputTokens: 100_000,
3272
- thresholdTokens: 80_000,
3273
- compactedMessages: 2,
3274
- summaryCalls: 1,
3275
- summaryInputTokens: 100,
3276
- summaryOutputTokens: 20,
3277
- summaryModel: "mock-model",
3278
- summaryText: "summary",
3279
- summaryFailed: false,
3280
- },
3281
- });
3282
-
3283
- const ctx = makeCtx({
3284
- channelCapabilities: {
3285
- channel: "slack",
3286
- dashboardCapable: false,
3287
- supportsDynamicUi: false,
3288
- supportsVoiceInput: false,
3289
- chatType: "channel",
3290
- },
3291
- trustContext: {
3292
- sourceChannel: "slack",
3293
- trustClass: "guardian",
3294
- } as AgentLoopConversationContext["trustContext"],
3295
- getTurnChannelContext: () => ({
3296
- userMessageChannel: "slack" as const,
3297
- assistantMessageChannel: "slack" as const,
3298
- }),
3299
- contextWindowManager: {
3300
- shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3301
- maybeCompact: async () => ({ compacted: false }),
3302
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
3211
+ } as unknown as Conversation["contextWindowManager"],
3303
3212
  });
3304
3213
 
3305
- await runAgentLoopImpl(ctx, "next reply", "user-msg-overflow", () => {});
3306
-
3307
- expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
3308
- mockSlackChronologicalContext,
3309
- 2,
3310
- );
3311
- expect(updateConversationSlackContextWatermarkMock).toHaveBeenCalledWith(
3312
- "test-conv",
3313
- "1700000020.000000",
3314
- expect.any(Number),
3214
+ await runAgentLoopImpl(
3215
+ ctx,
3216
+ "next reply",
3217
+ "user-msg-first-call",
3218
+ () => {},
3315
3219
  );
3316
- const reinjectionOptions = applyRuntimeInjectionsMock.mock.calls.find(
3317
- (call) => {
3318
- const options = call[1] as {
3319
- slackChronologicalMessages?: Message[] | null;
3320
- };
3321
- return options.slackChronologicalMessages === null;
3322
- },
3323
- )?.[1] as { slackChronologicalMessages?: Message[] | null } | undefined;
3324
- expect(reinjectionOptions?.slackChronologicalMessages).toBeNull();
3325
- });
3326
-
3327
- test("same-turn Slack compaction updates watermark from projected provenance", async () => {
3328
- const renderedSlackMessages: Message[] = [
3329
- {
3330
- role: "user",
3331
- content: [{ type: "text", text: "first rendered Slack row" }],
3332
- },
3333
- {
3334
- role: "user",
3335
- content: [{ type: "text", text: "second rendered Slack row" }],
3336
- },
3337
- {
3338
- role: "user",
3339
- content: [{ type: "text", text: "third rendered Slack row" }],
3340
- },
3341
- {
3342
- role: "user",
3343
- content: [{ type: "text", text: "retained Slack row" }],
3344
- },
3345
- ];
3346
- mockSlackChronologicalContext = {
3347
- messages: renderedSlackMessages,
3348
- renderedMessages: renderedSlackMessages.map((message, index) => ({
3349
- message,
3350
- sourceChannelTs: [
3351
- "1700000010.000000",
3352
- "1700000020.000000",
3353
- "1700000030.000000",
3354
- "1700000040.000000",
3355
- ][index]!,
3356
- tagLineProvenance: "none",
3357
- })),
3358
- compactableStartIndex: 0,
3359
- };
3360
-
3361
- const firstSummaryMessage: Message = {
3362
- role: "user",
3363
- content: [{ type: "text", text: "first summary" }],
3364
- };
3365
- const firstCompactedMessages: Message[] = [
3366
- firstSummaryMessage,
3367
- renderedSlackMessages[2]!,
3368
- renderedSlackMessages[3]!,
3369
- ];
3370
- const secondSummaryMessage: Message = {
3371
- role: "user",
3372
- content: [{ type: "text", text: "second summary" }],
3373
- };
3374
- const secondCompactedMessages: Message[] = [
3375
- secondSummaryMessage,
3376
- renderedSlackMessages[3]!,
3377
- ];
3378
- const reducerInputs: Message[][] = [];
3379
- mockEstimateTokens = 120_000;
3380
- mockReducerStepFn = (msgs: Message[]) => {
3381
- reducerInputs.push(msgs);
3382
- mockEstimateTokens = 1000;
3383
- return {
3384
- messages: secondCompactedMessages,
3385
- tier: "forced_compaction",
3386
- state: {
3387
- appliedTiers: ["forced_compaction"],
3388
- injectionMode: "full",
3389
- exhausted: false,
3390
- },
3391
- estimatedTokens: 5_000,
3392
- compactionResult: {
3393
- compacted: true,
3394
- messages: secondCompactedMessages,
3395
- compactedPersistedMessages: 1,
3396
- previousEstimatedInputTokens: 120_000,
3397
- estimatedInputTokens: 5_000,
3398
- maxInputTokens: 100_000,
3399
- thresholdTokens: 80_000,
3400
- compactedMessages: 1,
3401
- summaryCalls: 1,
3402
- summaryInputTokens: 100,
3403
- summaryOutputTokens: 20,
3404
- summaryModel: "mock-model",
3405
- summaryText: "second summary",
3406
- summaryFailed: false,
3407
- },
3408
- };
3409
- };
3410
3220
 
3411
- const ctx = makeCtx({
3412
- channelCapabilities: {
3413
- channel: "slack",
3414
- dashboardCapable: false,
3415
- supportsDynamicUi: false,
3416
- supportsVoiceInput: false,
3417
- chatType: "channel",
3418
- },
3419
- trustContext: {
3420
- sourceChannel: "slack",
3421
- trustClass: "guardian",
3422
- } as AgentLoopConversationContext["trustContext"],
3423
- getTurnChannelContext: () => ({
3424
- userMessageChannel: "slack" as const,
3425
- assistantMessageChannel: "slack" as const,
3426
- }),
3427
- contextWindowManager: {
3428
- shouldCompact: () => ({ needed: true, estimatedTokens: 120_000 }),
3429
- maybeCompact: async (messages: Message[]) => {
3430
- expect(messages).toBe(renderedSlackMessages);
3431
- return {
3432
- compacted: true,
3433
- messages: firstCompactedMessages,
3434
- compactedPersistedMessages: 2,
3435
- previousEstimatedInputTokens: 120_000,
3436
- estimatedInputTokens: 60_000,
3437
- maxInputTokens: 100_000,
3438
- thresholdTokens: 80_000,
3439
- compactedMessages: 2,
3440
- summaryCalls: 1,
3441
- summaryInputTokens: 100,
3442
- summaryOutputTokens: 20,
3443
- summaryModel: "mock-model",
3444
- summaryText: "first summary",
3445
- summaryFailed: false,
3446
- };
3447
- },
3448
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
3449
- });
3450
-
3451
- await runAgentLoopImpl(ctx, "next reply", "user-msg-repeat", () => {});
3452
-
3453
- expect(reducerInputs[0]).toBe(firstCompactedMessages);
3454
- expect(getSlackCompactionWatermarkForPrefixMock.mock.calls).toEqual([
3455
- [mockSlackChronologicalContext, 2],
3456
- [
3457
- {
3458
- renderedMessages: [
3459
- {
3460
- message: firstSummaryMessage,
3461
- sourceChannelTs: null,
3462
- tagLineProvenance: "none",
3463
- },
3464
- mockSlackChronologicalContext!.renderedMessages[2],
3465
- mockSlackChronologicalContext!.renderedMessages[3],
3466
- ],
3467
- messages: firstCompactedMessages,
3468
- compactableStartIndex: 1,
3469
- },
3470
- 1,
3471
- ],
3472
- ]);
3473
- expect(updateConversationSlackContextWatermarkMock.mock.calls).toEqual([
3474
- ["test-conv", "1700000020.000000", expect.any(Number)],
3475
- ["test-conv", "1700000030.000000", expect.any(Number)],
3476
- ]);
3477
- expect(loadSlackChronologicalContextMock).toHaveBeenCalledTimes(1);
3221
+ // The loop compacts its own stripped working history, never the loaded
3222
+ // Slack transcript array, so provenance cannot be projected and the
3223
+ // watermark is left untouched — matching mid-loop compaction.
3224
+ expect(maybeCompactInputs.length).toBeGreaterThan(0);
3225
+ expect(maybeCompactInputs[0]).not.toBe(renderedSlackMessages);
3226
+ expect(
3227
+ updateConversationSlackContextWatermarkMock,
3228
+ ).not.toHaveBeenCalled();
3478
3229
  });
3479
3230
 
3480
3231
  test("mid-loop Slack compaction does not persist watermark from mismatched loaded context", async () => {
@@ -3542,32 +3293,16 @@ describe("session-agent-loop", () => {
3542
3293
  trustContext: {
3543
3294
  sourceChannel: "slack",
3544
3295
  trustClass: "guardian",
3545
- } as AgentLoopConversationContext["trustContext"],
3296
+ } as Conversation["trustContext"],
3546
3297
  getTurnChannelContext: () => ({
3547
3298
  userMessageChannel: "slack" as const,
3548
3299
  assistantMessageChannel: "slack" as const,
3549
3300
  }),
3550
3301
  contextWindowManager: {
3302
+ updateConfig: () => {},
3551
3303
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3552
3304
  maybeCompact: async (messages: Message[]) => {
3553
3305
  maybeCompactInputs.push(messages);
3554
- if (messages === renderedSlackMessages) {
3555
- return {
3556
- compacted: false,
3557
- messages,
3558
- compactedPersistedMessages: 0,
3559
- previousEstimatedInputTokens: 1000,
3560
- estimatedInputTokens: 1000,
3561
- maxInputTokens: 100_000,
3562
- thresholdTokens: 80_000,
3563
- compactedMessages: 0,
3564
- summaryCalls: 0,
3565
- summaryInputTokens: 0,
3566
- summaryOutputTokens: 0,
3567
- summaryModel: "",
3568
- summaryText: "",
3569
- };
3570
- }
3571
3306
  // The mid-loop gate compacted its in-loop basis; drop the estimate
3572
3307
  // back under budget so the post-compaction provider call proceeds.
3573
3308
  mockEstimateTokens = 1000;
@@ -3593,15 +3328,16 @@ describe("session-agent-loop", () => {
3593
3328
  summaryFailed: false,
3594
3329
  };
3595
3330
  },
3596
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
3331
+ } as unknown as Conversation["contextWindowManager"],
3597
3332
  });
3598
3333
 
3599
3334
  await runAgentLoopImpl(ctx, "next reply", "user-msg-mid-loop", () => {});
3600
3335
 
3601
- expect(maybeCompactInputs[0]).toBe(renderedSlackMessages);
3602
3336
  // The mid-loop gate compacts the loop's own in-loop history, never the
3603
- // loaded Slack rows — the mismatch this test guards against.
3604
- expect(maybeCompactInputs[1]).not.toBe(renderedSlackMessages);
3337
+ // loaded Slack rows — the mismatch this test guards against. No
3338
+ // start-of-turn compaction runs, so this is the turn's only compaction.
3339
+ expect(maybeCompactInputs).toHaveLength(1);
3340
+ expect(maybeCompactInputs[0]).not.toBe(renderedSlackMessages);
3605
3341
  expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
3606
3342
  null,
3607
3343
  2,
@@ -3611,7 +3347,7 @@ describe("session-agent-loop", () => {
3611
3347
  ).not.toHaveBeenCalled();
3612
3348
  });
3613
3349
 
3614
- test("next inbound Slack turn injects the watermark-filtered chronological context", async () => {
3350
+ test("next inbound Slack turn loads chronological context using the persisted watermark", async () => {
3615
3351
  mockConversationRow = {
3616
3352
  ...mockConversationRow,
3617
3353
  contextSummary: "## Summary\n- compacted Slack context",
@@ -3671,7 +3407,7 @@ describe("session-agent-loop", () => {
3671
3407
  trustContext: {
3672
3408
  sourceChannel: "slack",
3673
3409
  trustClass: "guardian",
3674
- } as AgentLoopConversationContext["trustContext"],
3410
+ } as Conversation["trustContext"],
3675
3411
  getTurnChannelContext: () => ({
3676
3412
  userMessageChannel: "slack" as const,
3677
3413
  assistantMessageChannel: "slack" as const,
@@ -3690,26 +3426,9 @@ describe("session-agent-loop", () => {
3690
3426
  trustClass: "guardian",
3691
3427
  }),
3692
3428
  );
3693
- const firstInjectionOptions = applyRuntimeInjectionsMock.mock
3694
- .calls[0]![1] as {
3695
- slackChronologicalMessages?: Message[] | null;
3696
- };
3697
- expect(firstInjectionOptions.slackChronologicalMessages).toBe(
3698
- mockSlackChronologicalContext.messages,
3699
- );
3700
- const rendered = firstInjectionOptions
3701
- .slackChronologicalMessages!.flatMap((message) => message.content)
3702
- .filter((block): block is { type: "text"; text: string } => {
3703
- return block.type === "text";
3704
- })
3705
- .map((block) => block.text)
3706
- .join("\n");
3707
- expect(rendered).toContain("compacted Slack context");
3708
- expect(rendered).toContain("after watermark reply");
3709
- expect(rendered).not.toContain("before watermark");
3710
- });
3711
-
3712
- test("subsequent Slack turn keeps long-thread compaction summary and filtered tail", async () => {
3429
+ });
3430
+
3431
+ test("subsequent Slack turn loads chronological context using the persisted long-thread watermark", async () => {
3713
3432
  mockConversationRow = {
3714
3433
  ...mockConversationRow,
3715
3434
  contextSummary: "## Summary\n- compacted long Slack thread",
@@ -3779,7 +3498,7 @@ describe("session-agent-loop", () => {
3779
3498
  trustContext: {
3780
3499
  sourceChannel: "slack",
3781
3500
  trustClass: "guardian",
3782
- } as AgentLoopConversationContext["trustContext"],
3501
+ } as Conversation["trustContext"],
3783
3502
  getTurnChannelContext: () => ({
3784
3503
  userMessageChannel: "slack" as const,
3785
3504
  assistantMessageChannel: "slack" as const,
@@ -3802,21 +3521,6 @@ describe("session-agent-loop", () => {
3802
3521
  slackContextCompactionWatermarkTs: "1700000080.000000",
3803
3522
  }),
3804
3523
  );
3805
- const firstInjectionOptions = applyRuntimeInjectionsMock.mock
3806
- .calls[0]![1] as {
3807
- slackChronologicalMessages?: Message[] | null;
3808
- };
3809
- const rendered = firstInjectionOptions
3810
- .slackChronologicalMessages!.flatMap((message) => message.content)
3811
- .filter((block): block is { type: "text"; text: string } => {
3812
- return block.type === "text";
3813
- })
3814
- .map((block) => block.text)
3815
- .join("\n");
3816
- expect(rendered).toContain("compacted long Slack thread");
3817
- expect(rendered).toContain("reply after compaction");
3818
- expect(rendered).not.toContain("pre-compaction");
3819
- expect(rendered).not.toContain("original root");
3820
3524
  });
3821
3525
 
3822
3526
  test("applyCompactionResult records Slack timestamp watermark when provided", async () => {
@@ -3858,6 +3562,59 @@ describe("session-agent-loop", () => {
3858
3562
  true,
3859
3563
  );
3860
3564
  });
3565
+
3566
+ test("applyCompactionResult advances the persisted count from the trusted in-context boundary", async () => {
3567
+ // Trusted views slice past the already-compacted prefix, so a further
3568
+ // compaction advances the persisted count from the mirrored DB boundary.
3569
+
3570
+ // GIVEN a trusted (guardian) conversation that has already compacted 5
3571
+ // persisted messages, so its in-context history starts past that prefix
3572
+ const ctx = makeCtx({
3573
+ contextCompactedMessageCount: 5,
3574
+ trustContext: {
3575
+ trustClass: "guardian",
3576
+ } as Conversation["trustContext"],
3577
+ });
3578
+
3579
+ // WHEN a turn compacts 4 more in-context messages
3580
+ await applyCompactionResult(
3581
+ ctx,
3582
+ makeCompactionResult({ compactedPersistedMessages: 4 }),
3583
+ () => {},
3584
+ "req-1",
3585
+ );
3586
+
3587
+ // THEN the persisted count advances from the prior boundary (5 + 4)
3588
+ expect(ctx.contextCompactedMessageCount).toBe(9);
3589
+ });
3590
+
3591
+ test("applyCompactionResult resets the persisted count to the unsliced boundary for untrusted views", async () => {
3592
+ // Untrusted views render history unsliced (boundary 0), so a compaction
3593
+ // must record only the new summary's prefix instead of adding to the raw
3594
+ // mirror — otherwise future loads slice past unsummarized rows.
3595
+
3596
+ // GIVEN an untrusted view of a conversation whose raw DB count mirrors a
3597
+ // 5-message compacted prefix — but untrusted views render that history
3598
+ // unsliced (boundary 0), so the compactor operates on the full list
3599
+ const ctx = makeCtx({
3600
+ contextCompactedMessageCount: 5,
3601
+ trustContext: {
3602
+ trustClass: "unknown",
3603
+ } as Conversation["trustContext"],
3604
+ });
3605
+
3606
+ // WHEN that turn compacts 4 in-context messages
3607
+ await applyCompactionResult(
3608
+ ctx,
3609
+ makeCompactionResult({ compactedPersistedMessages: 4 }),
3610
+ () => {},
3611
+ "req-1",
3612
+ );
3613
+
3614
+ // THEN the persisted count reflects only the new summary's prefix (0 + 4)
3615
+ // rather than double-counting the raw mirror (which would yield 9)
3616
+ expect(ctx.contextCompactedMessageCount).toBe(4);
3617
+ });
3861
3618
  });
3862
3619
 
3863
3620
  describe("compaction-strip marker persistence", () => {
@@ -3894,9 +3651,10 @@ describe("session-agent-loop", () => {
3894
3651
  ],
3895
3652
  toolExecutor: async () => ({ content: "ok", isError: false }),
3896
3653
  contextWindowManager: {
3654
+ updateConfig: () => {},
3897
3655
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3898
3656
  maybeCompact: async () => ({ compacted: false }),
3899
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
3657
+ } as unknown as Conversation["contextWindowManager"],
3900
3658
  });
3901
3659
 
3902
3660
  // WHEN the orchestrator runs the turn to completion
@@ -3943,9 +3701,10 @@ describe("session-agent-loop", () => {
3943
3701
  ],
3944
3702
  toolExecutor: async () => ({ content: "ok", isError: false }),
3945
3703
  contextWindowManager: {
3704
+ updateConfig: () => {},
3946
3705
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3947
3706
  maybeCompact: async () => ({ compacted: false }),
3948
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
3707
+ } as unknown as Conversation["contextWindowManager"],
3949
3708
  });
3950
3709
 
3951
3710
  // Must not throw — the strip-site marker write is wrapped in try/catch.