@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091853.fbaa2ae

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 (475) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/docs/activation-funnel-telemetry.md +310 -0
  4. package/examples/plugins/echo/README.md +61 -60
  5. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  6. package/examples/plugins/echo/hooks/stop.ts +16 -0
  7. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  8. package/examples/plugins/echo/package.json +1 -2
  9. package/examples/plugins/echo/src/emit.ts +19 -0
  10. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  11. package/openapi.yaml +263 -121
  12. package/package.json +2 -2
  13. package/src/__tests__/activation-early-marking.test.ts +120 -0
  14. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  15. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  16. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  17. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  18. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  19. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  20. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  21. package/src/__tests__/agent-loop.test.ts +441 -96
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  23. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  24. package/src/__tests__/anthropic-provider.test.ts +23 -8
  25. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  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__/app-executors.test.ts +132 -0
  29. package/src/__tests__/approval-cascade.test.ts +6 -5
  30. package/src/__tests__/approval-routes-http.test.ts +4 -1
  31. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  32. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  33. package/src/__tests__/channel-approvals.test.ts +1 -1
  34. package/src/__tests__/compaction-circuit.test.ts +258 -0
  35. package/src/__tests__/compaction-direct.test.ts +146 -0
  36. package/src/__tests__/compaction-events.test.ts +7 -7
  37. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  38. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  39. package/src/__tests__/context-overflow-reducer.test.ts +265 -125
  40. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  41. package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
  42. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  43. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  44. package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
  45. package/src/__tests__/conversation-agent-loop.test.ts +342 -623
  46. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  47. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
  48. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  49. package/src/__tests__/conversation-error.test.ts +15 -1
  50. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  51. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  52. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
  53. package/src/__tests__/conversation-process-callsite.test.ts +15 -15
  54. package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
  55. package/src/__tests__/conversation-queue.test.ts +10 -10
  56. package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
  57. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  58. package/src/__tests__/conversation-slash-queue.test.ts +7 -5
  59. package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
  60. package/src/__tests__/conversation-speed-override.test.ts +11 -10
  61. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  62. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  63. package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  65. package/src/__tests__/conversation-undo.test.ts +2 -2
  66. package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
  67. package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
  68. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
  69. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  70. package/src/__tests__/corrected-target.test.ts +93 -0
  71. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  72. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  73. package/src/__tests__/db-acp-history.test.ts +101 -0
  74. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  75. package/src/__tests__/empty-response-hook.test.ts +1 -1
  76. package/src/__tests__/file-write-tool.test.ts +63 -0
  77. package/src/__tests__/gateway-only-guard.test.ts +17 -2
  78. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  79. package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
  80. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  82. package/src/__tests__/heartbeat-service.test.ts +1 -0
  83. package/src/__tests__/history-repair-hook.test.ts +5 -1
  84. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  85. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  86. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  87. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  88. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  89. package/src/__tests__/injector-background-turn.test.ts +13 -23
  90. package/src/__tests__/injector-chain.test.ts +268 -44
  91. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  92. package/src/__tests__/injector-document-comments.test.ts +97 -114
  93. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  94. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  95. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  96. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  97. package/src/__tests__/llm-resolver.test.ts +73 -0
  98. package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
  99. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  100. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  101. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  102. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  103. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  104. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  105. package/src/__tests__/plugin-bootstrap.test.ts +91 -81
  106. package/src/__tests__/plugin-registry.test.ts +3 -49
  107. package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
  108. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  109. package/src/__tests__/plugin-types.test.ts +0 -70
  110. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  111. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  112. package/src/__tests__/published-app-updater.test.ts +138 -0
  113. package/src/__tests__/reaction-persistence.test.ts +1 -1
  114. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  115. package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
  116. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  117. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  118. package/src/__tests__/subagent-detail.test.ts +25 -7
  119. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  120. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  121. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  122. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  123. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  124. package/src/__tests__/title-generate-hook.test.ts +5 -1
  125. package/src/__tests__/tool-error-hook.test.ts +1 -1
  126. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  127. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  128. package/src/__tests__/web-fetch.test.ts +45 -0
  129. package/src/acp/__tests__/agent-process.test.ts +161 -0
  130. package/src/acp/__tests__/client-handler.test.ts +40 -0
  131. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  132. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  133. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  134. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  135. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  136. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  137. package/src/acp/agent-process.ts +61 -1
  138. package/src/acp/auto-install.test.ts +280 -0
  139. package/src/acp/auto-install.ts +232 -0
  140. package/src/acp/client-handler.ts +31 -0
  141. package/src/acp/prepare-agent-env.ts +80 -27
  142. package/src/acp/resolve-agent.test.ts +188 -28
  143. package/src/acp/resolve-agent.ts +119 -42
  144. package/src/acp/resume-hint.ts +23 -0
  145. package/src/acp/session-manager.ts +507 -73
  146. package/src/agent/compaction-circuit.ts +60 -102
  147. package/src/agent/loop.ts +403 -251
  148. package/src/api/responses/conversation-message.ts +22 -2
  149. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  150. package/src/approvals/guardian-request-resolvers.ts +27 -1
  151. package/src/background-wake/next-wake.ts +1 -0
  152. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  153. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  154. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  155. package/src/cli/commands/memory-v3.ts +57 -199
  156. package/src/cli/commands/plugins.ts +43 -37
  157. package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
  158. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  159. package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
  160. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
  161. package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
  162. package/src/cli/lib/install-from-github.ts +797 -60
  163. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  164. package/src/cli/lib/plugin-details.ts +416 -0
  165. package/src/cli/lib/plugin-marketplace.ts +215 -0
  166. package/src/cli/lib/search-plugins.ts +201 -29
  167. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  168. package/src/config/acp-defaults.test.ts +10 -0
  169. package/src/config/acp-defaults.ts +9 -3
  170. package/src/config/acp-schema.ts +1 -7
  171. package/src/config/bundled-skills/acp/SKILL.md +80 -41
  172. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  173. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  174. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  175. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  176. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  177. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  178. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  179. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  180. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  181. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  182. package/src/config/bundled-tool-registry.ts +4 -0
  183. package/src/config/call-site-defaults.ts +0 -3
  184. package/src/config/feature-flag-registry.json +10 -16
  185. package/src/config/llm-resolver.ts +39 -7
  186. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  187. package/src/config/schemas/call-site-catalog.ts +0 -21
  188. package/src/config/schemas/heartbeat.ts +9 -0
  189. package/src/config/schemas/llm.ts +17 -3
  190. package/src/config/schemas/memory-v3.ts +58 -8
  191. package/src/config/seed-inference-profiles.ts +18 -0
  192. package/src/context/compactor.ts +22 -4
  193. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  194. package/src/context/strip-injections.ts +8 -2
  195. package/src/daemon/conversation-agent-loop-handlers.ts +18 -36
  196. package/src/daemon/conversation-agent-loop.ts +308 -1297
  197. package/src/daemon/conversation-error.ts +31 -4
  198. package/src/daemon/conversation-history.ts +1 -1
  199. package/src/daemon/conversation-lifecycle.ts +11 -255
  200. package/src/daemon/conversation-media-retry.ts +19 -6
  201. package/src/daemon/conversation-messaging.ts +17 -0
  202. package/src/daemon/conversation-process.ts +22 -141
  203. package/src/daemon/conversation-queue-manager.ts +8 -0
  204. package/src/daemon/conversation-registry.ts +159 -0
  205. package/src/daemon/conversation-runtime-assembly.ts +361 -393
  206. package/src/daemon/conversation-store.ts +9 -90
  207. package/src/daemon/conversation-surfaces.ts +165 -11
  208. package/src/daemon/conversation-workspace.ts +17 -0
  209. package/src/daemon/conversation.ts +444 -62
  210. package/src/daemon/disk-pressure-policy.ts +0 -1
  211. package/src/daemon/external-plugins-bootstrap.ts +36 -46
  212. package/src/daemon/handlers/config-channels.ts +11 -2
  213. package/src/daemon/handlers/conversations.ts +3 -1
  214. package/src/daemon/handlers/skills.ts +4 -1
  215. package/src/daemon/host-app-control-proxy.ts +72 -57
  216. package/src/daemon/host-proxy-preactivation.ts +1 -3
  217. package/src/daemon/lifecycle.ts +21 -7
  218. package/src/daemon/persist-unsendable-image.ts +62 -25
  219. package/src/daemon/process-message.ts +1 -1
  220. package/src/daemon/server.ts +2 -0
  221. package/src/daemon/tool-side-effects.ts +15 -0
  222. package/src/daemon/wake-conversation-ops.ts +269 -0
  223. package/src/embedded/plugin-api.ts +2 -2
  224. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  225. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  226. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  227. package/src/heartbeat/heartbeat-service.ts +26 -0
  228. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  229. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  230. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  231. package/src/ipc/skill-routes/memory.ts +4 -2
  232. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  233. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  234. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  235. package/src/memory/activation-session-store.ts +43 -0
  236. package/src/memory/conversation-crud.ts +29 -19
  237. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  238. package/src/memory/db-init.ts +6 -0
  239. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  240. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  241. package/src/memory/jobs-worker.ts +25 -1
  242. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  243. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  244. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  245. package/src/memory/migrations/index.ts +3 -0
  246. package/src/memory/onboarding-events-store.ts +66 -18
  247. package/src/memory/schema/acp.ts +4 -0
  248. package/src/memory/schema/infrastructure.ts +13 -0
  249. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  250. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  251. package/src/memory/v2/consolidation-job.ts +2 -63
  252. package/src/memory/v2/types.ts +5 -0
  253. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  254. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  255. package/src/notifications/access-request-copy.ts +298 -0
  256. package/src/notifications/adapters/slack.ts +3 -3
  257. package/src/notifications/adapters/telegram.ts +9 -2
  258. package/src/notifications/copy-composer.ts +49 -267
  259. package/src/notifications/decision-engine.ts +16 -35
  260. package/src/notifications/home-feed-side-effect.ts +1 -6
  261. package/src/plugin-api/constants.ts +4 -0
  262. package/src/plugin-api/index.ts +40 -11
  263. package/src/plugin-api/types.ts +108 -0
  264. package/src/plugins/defaults/compaction/compact.ts +106 -0
  265. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
  266. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  267. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  268. package/src/plugins/defaults/compaction/package.json +1 -2
  269. package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
  270. package/src/plugins/defaults/empty-response/package.json +0 -1
  271. package/src/plugins/defaults/history-repair/package.json +0 -1
  272. package/src/plugins/defaults/index.ts +156 -26
  273. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  274. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
  275. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  276. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  277. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  278. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  282. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
  283. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
  284. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
  285. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  286. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
  287. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  288. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  289. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  290. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
  291. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  292. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
  293. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
  294. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
  295. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  296. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  297. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  298. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  299. package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
  300. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
  301. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
  302. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  303. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
  304. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  305. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
  306. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  307. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  308. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  309. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
  310. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
  311. package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
  312. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
  313. package/src/plugins/defaults/title-generate/package.json +0 -1
  314. package/src/plugins/defaults/tool-error/package.json +0 -1
  315. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  316. package/src/plugins/external-api.ts +12 -2
  317. package/src/plugins/pipeline.ts +6 -293
  318. package/src/plugins/registry.ts +9 -37
  319. package/src/plugins/types.ts +76 -381
  320. package/src/plugins/user-loader.ts +30 -127
  321. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  322. package/src/prompts/system-prompt.ts +61 -10
  323. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
  324. package/src/providers/anthropic/client.ts +9 -10
  325. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  326. package/src/providers/inference/logit-bias.ts +55 -0
  327. package/src/providers/model-catalog.ts +34 -0
  328. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  329. package/src/providers/openai/chat-completions-provider.ts +44 -1
  330. package/src/providers/retry.ts +22 -0
  331. package/src/providers/types.ts +6 -0
  332. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  333. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  334. package/src/runtime/agent-wake.ts +108 -209
  335. package/src/runtime/assistant-event-hub.ts +1 -1
  336. package/src/runtime/channel-approvals.ts +1 -1
  337. package/src/runtime/interactive-ui.ts +1 -1
  338. package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
  339. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  340. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  341. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  342. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  343. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  344. package/src/runtime/routes/acp-routes.test.ts +185 -32
  345. package/src/runtime/routes/acp-routes.ts +328 -30
  346. package/src/runtime/routes/app-management-routes.ts +3 -0
  347. package/src/runtime/routes/approval-routes.ts +1 -1
  348. package/src/runtime/routes/browser-routes.ts +1 -1
  349. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  350. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  351. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  352. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  353. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  354. package/src/runtime/routes/conversation-routes.ts +30 -2
  355. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  356. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  357. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  358. package/src/runtime/routes/host-cu-routes.ts +1 -1
  359. package/src/runtime/routes/identity-routes.ts +18 -3
  360. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  361. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  362. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  363. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  364. package/src/runtime/routes/memory-v3-routes.ts +65 -305
  365. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  366. package/src/runtime/routes/playground/helpers.ts +1 -1
  367. package/src/runtime/routes/plugins-routes.ts +337 -35
  368. package/src/runtime/routes/stt-routes.ts +45 -12
  369. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  370. package/src/runtime/routes/work-items-routes.ts +2 -4
  371. package/src/runtime/routes/workspace-routes.ts +50 -15
  372. package/src/runtime/services/conversation-serializer.ts +1 -1
  373. package/src/runtime/verification-outbound-actions.ts +147 -2
  374. package/src/runtime/verification-templates.ts +29 -3
  375. package/src/services/published-app-updater.ts +30 -8
  376. package/src/signals/cancel.ts +2 -4
  377. package/src/subagent/manager.ts +21 -5
  378. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  379. package/src/telemetry/activation-funnel.ts +167 -0
  380. package/src/telemetry/types.ts +13 -0
  381. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  382. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  383. package/src/tools/acp/context.ts +20 -0
  384. package/src/tools/acp/list-agents.test.ts +9 -19
  385. package/src/tools/acp/list-agents.ts +3 -15
  386. package/src/tools/acp/spawn.test.ts +173 -202
  387. package/src/tools/acp/spawn.ts +37 -172
  388. package/src/tools/acp/steer.test.ts +105 -8
  389. package/src/tools/acp/steer.ts +48 -17
  390. package/src/tools/apps/executors.ts +166 -50
  391. package/src/tools/browser/browser-execution.ts +12 -2
  392. package/src/tools/filesystem/write.ts +34 -0
  393. package/src/tools/network/web-fetch.ts +65 -24
  394. package/src/tools/skills/load.ts +1 -1
  395. package/src/tools/subagent/spawn.ts +2 -4
  396. package/src/tools/ui-surface/definitions.ts +32 -5
  397. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  398. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  399. package/docs/plugins.md +0 -832
  400. package/examples/plugins/echo/register.ts +0 -143
  401. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  402. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  403. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  404. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  405. package/src/__tests__/pipeline-runner.test.ts +0 -554
  406. package/src/daemon/wake-target-adapter.ts +0 -253
  407. package/src/memory/v3/__tests__/assign.test.ts +0 -242
  408. package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
  409. package/src/memory/v3/__tests__/core.test.ts +0 -39
  410. package/src/memory/v3/__tests__/health.test.ts +0 -219
  411. package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
  412. package/src/memory/v3/__tests__/needle.test.ts +0 -107
  413. package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
  414. package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
  415. package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
  416. package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
  417. package/src/memory/v3/__tests__/router.test.ts +0 -332
  418. package/src/memory/v3/__tests__/selector.test.ts +0 -470
  419. package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
  420. package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
  421. package/src/memory/v3/__tests__/tree.test.ts +0 -192
  422. package/src/memory/v3/assign.ts +0 -268
  423. package/src/memory/v3/capabilities.ts +0 -124
  424. package/src/memory/v3/core.ts +0 -26
  425. package/src/memory/v3/health.ts +0 -0
  426. package/src/memory/v3/maintain-job.ts +0 -314
  427. package/src/memory/v3/needle.ts +0 -115
  428. package/src/memory/v3/orchestrate.ts +0 -126
  429. package/src/memory/v3/page-content.ts +0 -34
  430. package/src/memory/v3/provider-blocks.ts +0 -26
  431. package/src/memory/v3/reconcile.ts +0 -523
  432. package/src/memory/v3/router.ts +0 -190
  433. package/src/memory/v3/selection-log-store.ts +0 -84
  434. package/src/memory/v3/selector.ts +0 -226
  435. package/src/memory/v3/shadow-plugin.ts +0 -411
  436. package/src/memory/v3/snapshot.ts +0 -209
  437. package/src/memory/v3/tree.ts +0 -174
  438. package/src/memory/v3/types.ts +0 -59
  439. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  440. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  441. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  442. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  443. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  444. package/src/plugins/defaults/compaction/register.ts +0 -35
  445. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  446. package/src/plugins/defaults/empty-response/register.ts +0 -23
  447. package/src/plugins/defaults/history-repair/register.ts +0 -24
  448. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
  449. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  450. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  451. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  452. package/src/plugins/defaults/title-generate/register.ts +0 -35
  453. package/src/plugins/defaults/tool-error/register.ts +0 -23
  454. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  455. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  456. package/src/proactive-artifact/decision.test.ts +0 -226
  457. package/src/proactive-artifact/decision.ts +0 -165
  458. package/src/proactive-artifact/index.ts +0 -7
  459. package/src/proactive-artifact/job.test.ts +0 -962
  460. package/src/proactive-artifact/job.ts +0 -372
  461. package/src/proactive-artifact/message-copy.ts +0 -58
  462. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  463. package/src/proactive-artifact/trigger-state.ts +0 -123
  464. package/src/util/map-limit.ts +0 -27
  465. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  466. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  467. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  468. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  469. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  470. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  471. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  472. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  473. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  474. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  475. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
@@ -28,6 +28,20 @@ let mockUiConfig: { userTimezone?: string; detectedTimezone?: string } = {};
28
28
 
29
29
  // ── Module mocks (must precede imports of the module under test) ─────
30
30
 
31
+ // The real AgentLoop resolves the per-conversation ContextWindowManager from
32
+ // the compaction store keyed by conversationId. These orchestrator tests build
33
+ // fake conversations whose manager is a canned stub, so register each stub in a
34
+ // map the mocked store reads from.
35
+ const fakeContextWindowManagers = new Map<string, unknown>();
36
+ mock.module("../plugins/defaults/compaction/manager-store.js", () => ({
37
+ createContextWindowManager: () => undefined,
38
+ getContextWindowManager: (conversationId: string) =>
39
+ fakeContextWindowManagers.get(conversationId),
40
+ disposeContextWindowManager: (conversationId: string) => {
41
+ fakeContextWindowManagers.delete(conversationId);
42
+ },
43
+ }));
44
+
31
45
  mock.module("../util/logger.js", () => ({
32
46
  getLogger: () =>
33
47
  new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
@@ -100,35 +114,38 @@ mock.module("../context/token-estimator.js", () => ({
100
114
  let mockReducerStepFn:
101
115
  | ((msgs: Message[], cfg: unknown, state: unknown) => unknown)
102
116
  | null = null;
103
- mock.module("../daemon/context-overflow-reducer.js", () => ({
104
- createInitialReducerState: () => ({
105
- appliedTiers: [],
106
- injectionMode: "full" as const,
107
- exhausted: false,
117
+ mock.module(
118
+ "../plugins/defaults/compaction/context-overflow-reducer.js",
119
+ () => ({
120
+ createInitialReducerState: () => ({
121
+ appliedTiers: [],
122
+ injectionMode: "full" as const,
123
+ exhausted: false,
124
+ }),
125
+ reduceContextOverflow: async (
126
+ msgs: Message[],
127
+ cfg: unknown,
128
+ state: unknown,
129
+ ) => {
130
+ if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
131
+ return {
132
+ messages: msgs,
133
+ tier: "forced_compaction",
134
+ state: {
135
+ appliedTiers: [
136
+ "forced_compaction",
137
+ "tool_result_truncation",
138
+ "media_stubbing",
139
+ "injection_downgrade",
140
+ ],
141
+ injectionMode: "full",
142
+ exhausted: true,
143
+ },
144
+ estimatedTokens: 1000,
145
+ };
146
+ },
108
147
  }),
109
- reduceContextOverflow: async (
110
- msgs: Message[],
111
- cfg: unknown,
112
- state: unknown,
113
- ) => {
114
- if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
115
- return {
116
- messages: msgs,
117
- tier: "forced_compaction",
118
- state: {
119
- appliedTiers: [
120
- "forced_compaction",
121
- "tool_result_truncation",
122
- "media_stubbing",
123
- "injection_downgrade",
124
- ],
125
- injectionMode: "full",
126
- exhausted: true,
127
- },
128
- estimatedTokens: 1000,
129
- };
130
- },
131
- }));
148
+ );
132
149
 
133
150
  // Policy: default to fail_gracefully
134
151
  let mockOverflowAction: string = "fail_gracefully";
@@ -312,16 +329,14 @@ let mockInjectionBlocks: {
312
329
  pkbSystemReminder?: string;
313
330
  unifiedTurnContext?: string;
314
331
  } = {};
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
- );
332
+ const defaultApplyRuntimeInjectionsImpl = async (
333
+ msgs: Message[],
334
+ _options?: unknown,
335
+ ) => ({
336
+ messages: msgs,
337
+ blocks: { ...mockInjectionBlocks },
338
+ });
339
+ const applyRuntimeInjectionsMock = mock(defaultApplyRuntimeInjectionsImpl);
325
340
  let mockSlackChronologicalContext: {
326
341
  renderedMessages: Array<{
327
342
  message: Message;
@@ -358,7 +373,6 @@ const getSlackCompactionWatermarkForPrefixMock = mock(
358
373
  );
359
374
  mock.module("../daemon/conversation-runtime-assembly.js", () => ({
360
375
  applyRuntimeInjections: applyRuntimeInjectionsMock,
361
- buildUnifiedTurnContextBlock: buildUnifiedTurnContextBlockMock,
362
376
  stripInjectionsForCompaction: (msgs: Message[]) => msgs,
363
377
  isSlackChannelConversation: () => false,
364
378
  getSlackCompactionWatermarkForPrefix:
@@ -523,22 +537,11 @@ mock.module("../memory/llm-request-log-store.js", () => ({
523
537
  setAgentLoopExitReasonOnLatestLog: setAgentLoopExitReasonOnLatestLogMock,
524
538
  }));
525
539
 
526
- let mockHasProactiveArtifactCompleted = true;
527
- let mockTryClaimProactiveArtifactTrigger = false;
528
- const runProactiveArtifactJobMock = mock(
529
- async (_params: Record<string, unknown>) => {},
530
- );
531
- mock.module("../proactive-artifact/index.js", () => ({
532
- hasProactiveArtifactCompleted: () => mockHasProactiveArtifactCompleted,
533
- tryClaimProactiveArtifactTrigger: () => mockTryClaimProactiveArtifactTrigger,
534
- runProactiveArtifactJob: runProactiveArtifactJobMock,
535
- }));
536
-
537
540
  // ── Imports (after mocks) ────────────────────────────────────────────
538
541
 
539
542
  import { AgentLoop } from "../agent/loop.js";
543
+ import type { Conversation } from "../daemon/conversation.js";
540
544
  import {
541
- type AgentLoopConversationContext,
542
545
  applyCompactionResult,
543
546
  runAgentLoopImpl,
544
547
  } from "../daemon/conversation-agent-loop.js";
@@ -552,13 +555,13 @@ import {
552
555
  // ── Test helpers ─────────────────────────────────────────────────────
553
556
 
554
557
  function makeCtx(
555
- overrides?: Partial<AgentLoopConversationContext> & {
558
+ overrides?: Partial<Conversation> & {
556
559
  providerResponses?: ScriptedResponse[];
557
560
  loopProvider?: Provider;
558
561
  loopTools?: ToolDefinition[];
559
562
  toolExecutor?: LoopToolExecutor;
560
563
  },
561
- ): AgentLoopConversationContext {
564
+ ): Conversation {
562
565
  const {
563
566
  providerResponses,
564
567
  loopProvider,
@@ -589,13 +592,15 @@ function makeCtx(
589
592
  providerResponses ?? [textResponse("response")],
590
593
  loopProviderName,
591
594
  ).provider;
592
- const agentLoop = new AgentLoop(provider, "system prompt", {
595
+ const agentLoop = new AgentLoop({
596
+ provider: provider,
597
+ systemPrompt: "system prompt",
593
598
  conversationId,
594
599
  tools: loopTools ?? [],
595
600
  toolExecutor,
596
601
  });
597
602
 
598
- return {
603
+ const ctx = {
599
604
  conversationId: "test-conv",
600
605
  messages: [
601
606
  { role: "user", content: [{ type: "text", text: "Hello" }] },
@@ -616,15 +621,24 @@ function makeCtx(
616
621
  usage: { inputTokens: 0, outputTokens: 0 },
617
622
  stopReason: "end_turn",
618
623
  }),
619
- } as unknown as AgentLoopConversationContext["provider"],
624
+ } as unknown as Conversation["provider"],
620
625
  systemPrompt: "system prompt",
621
626
 
622
627
  contextWindowManager: {
628
+ updateConfig: () => {},
623
629
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
624
630
  maybeCompact: async () => ({ compacted: false }),
625
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
626
- contextCompactedMessageCount: 0,
627
- contextCompactedAt: null,
631
+ } as unknown as Conversation["contextWindowManager"],
632
+ conversationType: mockConversationRow?.conversationType ?? undefined,
633
+ source: mockConversationRow?.source ?? undefined,
634
+ contextSummary: mockConversationRow?.contextSummary ?? null,
635
+ contextCompactedMessageCount:
636
+ mockConversationRow?.contextCompactedMessageCount ?? 0,
637
+ contextCompactedAt: mockConversationRow?.contextCompactedAt ?? null,
638
+ slackContextCompactionWatermarkTs:
639
+ mockConversationRow?.slackContextCompactionWatermarkTs ?? null,
640
+ lastNotifiedInferenceProfile:
641
+ mockConversationRow?.lastNotifiedInferenceProfile ?? null,
628
642
 
629
643
  memoryPolicy: { scopeId: "default", includeDefaultFallback: true },
630
644
 
@@ -636,8 +650,6 @@ function makeCtx(
636
650
  currentTurnSurfaces: [],
637
651
 
638
652
  workingDir: "/tmp",
639
- workspaceTopLevelContext: null,
640
- workspaceTopLevelDirty: false,
641
653
  channelCapabilities: undefined,
642
654
  commandIntent: undefined,
643
655
  trustContext: undefined,
@@ -647,15 +659,15 @@ function makeCtx(
647
659
  preactivatedSkillIds: undefined,
648
660
  skillProjectionState: new Map(),
649
661
  skillProjectionCache:
650
- new Map() as unknown as AgentLoopConversationContext["skillProjectionCache"],
662
+ new Map() as unknown as Conversation["skillProjectionCache"],
651
663
 
652
664
  traceEmitter: {
653
665
  emit: () => {},
654
- } as unknown as AgentLoopConversationContext["traceEmitter"],
666
+ } as unknown as Conversation["traceEmitter"],
655
667
  profiler: {
656
668
  startRequest: () => {},
657
669
  emitSummary: () => {},
658
- } as unknown as AgentLoopConversationContext["profiler"],
670
+ } as unknown as Conversation["profiler"],
659
671
  usageStats: {
660
672
  totalInputTokens: 0,
661
673
  totalOutputTokens: 0,
@@ -668,13 +680,12 @@ function makeCtx(
668
680
  lastAttachmentWarnings: [],
669
681
 
670
682
  hasNoClient: false,
671
- prompter: {} as unknown as AgentLoopConversationContext["prompter"],
672
- queue: {} as unknown as AgentLoopConversationContext["queue"],
683
+ prompter: {} as unknown as Conversation["prompter"],
684
+ queue: {} as unknown as Conversation["queue"],
673
685
 
674
686
  getWorkspaceGitService: () => ({ ensureInitialized: async () => {} }),
675
687
  commitTurnChanges: async () => {},
676
688
 
677
- refreshWorkspaceTopLevelContextIfNeeded: () => {},
678
689
  markWorkspaceTopLevelDirty: () => {},
679
690
  emitActivityState: () => {},
680
691
  getQueueDepth: () => 0,
@@ -701,10 +712,34 @@ function makeCtx(
701
712
  }),
702
713
  retrackCachedNodes: () => {},
703
714
  recordPkbQueryVectors: () => {},
704
- } as unknown as AgentLoopConversationContext["graphMemory"],
715
+ } as unknown as Conversation["graphMemory"],
705
716
 
706
717
  ...ctxOverrides,
707
- } as AgentLoopConversationContext;
718
+ } as unknown as Conversation;
719
+ fakeContextWindowManagers.set(conversationId, ctx.contextWindowManager);
720
+ return ctx;
721
+ }
722
+
723
+ type CompactionResult = Parameters<typeof applyCompactionResult>[1];
724
+
725
+ function makeCompactionResult(
726
+ overrides?: Partial<CompactionResult>,
727
+ ): CompactionResult {
728
+ return {
729
+ messages: [{ role: "user", content: [{ type: "text", text: "summary" }] }],
730
+ compactedPersistedMessages: 4,
731
+ previousEstimatedInputTokens: 12000,
732
+ estimatedInputTokens: 3000,
733
+ maxInputTokens: 100000,
734
+ thresholdTokens: 80000,
735
+ compactedMessages: 4,
736
+ summaryCalls: 1,
737
+ summaryInputTokens: 100,
738
+ summaryOutputTokens: 20,
739
+ summaryModel: "mock-model",
740
+ summaryText: "summary",
741
+ ...overrides,
742
+ };
708
743
  }
709
744
 
710
745
  // ── Tests ────────────────────────────────────────────────────────────
@@ -738,13 +773,12 @@ beforeEach(() => {
738
773
  title: null,
739
774
  };
740
775
  mockMessageById = null;
741
- mockHasProactiveArtifactCompleted = true;
742
- mockTryClaimProactiveArtifactTrigger = false;
743
- runProactiveArtifactJobMock.mockClear();
744
776
  setConversationHistoryStrippedAtMock.mockClear();
745
777
  setConversationHistoryStrippedAtMock.mockImplementation(() => {});
746
778
  applyRuntimeInjectionsMock.mockClear();
747
- buildUnifiedTurnContextBlockMock.mockClear();
779
+ applyRuntimeInjectionsMock.mockImplementation(
780
+ defaultApplyRuntimeInjectionsImpl,
781
+ );
748
782
  resolveTurnTimezoneContextMock.mockClear();
749
783
  formatTurnTimestampMock.mockClear();
750
784
  mockSlackChronologicalContext = null;
@@ -757,11 +791,10 @@ beforeEach(() => {
757
791
  projectAssistantMessageMock.mockClear();
758
792
  publishSyncInvalidationMock.mockClear();
759
793
  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.
794
+ // The compaction pipeline runs through the plugin registry; reset and
795
+ // re-register every default so it dispatches to middleware backed by the
796
+ // mocked collaborators these tests install (`syncMessageToDisk`, etc.)
797
+ // instead of hitting the bare terminal.
765
798
  resetPluginRegistryAndRegisterDefaults();
766
799
  });
767
800
 
@@ -785,7 +818,7 @@ describe("session-agent-loop", () => {
785
818
  });
786
819
  });
787
820
 
788
- test("passes resolved canonical timezones into unified turn context", async () => {
821
+ test("freezes the client timezone snapshot on the conversation, not the options bag", async () => {
789
822
  mockUiConfig = {
790
823
  userTimezone: "US/Eastern",
791
824
  detectedTimezone: "US/Central",
@@ -802,17 +835,25 @@ describe("session-agent-loop", () => {
802
835
 
803
836
  await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
804
837
 
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",
838
+ // The turn-start client timezone and the long-absence gap are frozen on
839
+ // the conversation as `currentTurnTemporalSnapshot`; the live
840
+ // `clientTimezone` would otherwise be clobbered mid-turn. `current_time`
841
+ // is not frozen — it is computed fresh at each injection inside
842
+ // `applyRuntimeInjections`.
843
+ expect(ctx.currentTurnTemporalSnapshot).toEqual({
813
844
  clientTimezone: "America/Los_Angeles",
814
- detectedTimezone: "America/Chicago",
815
- });
845
+ timeSinceLastMessage: null,
846
+ });
847
+ // Neither the timezones nor the long-absence gap are threaded through the
848
+ // options bag — `applyRuntimeInjections` sources the client timezone and
849
+ // gap from the snapshot and the config timezones from config
850
+ // (`ui.userTimezone`, `ui.detectedTimezone`).
851
+ const injectionOptions = applyRuntimeInjectionsMock.mock.calls[0]?.[1];
852
+ expect(injectionOptions).not.toHaveProperty("timestamp");
853
+ expect(injectionOptions).not.toHaveProperty("clientTimezone");
854
+ expect(injectionOptions).not.toHaveProperty("configuredUserTimezone");
855
+ expect(injectionOptions).not.toHaveProperty("detectedTimezone");
856
+ expect(injectionOptions).not.toHaveProperty("timeSinceLastMessage");
816
857
  });
817
858
  });
818
859
 
@@ -826,61 +867,24 @@ describe("session-agent-loop", () => {
826
867
  });
827
868
  });
828
869
 
829
- describe("proactive artifact trigger", () => {
830
- test("does not start proactive artifact jobs after foreground user turns", async () => {
831
- mockConversationRow = {
832
- ...mockConversationRow,
833
- id: "test-conv",
834
- conversationType: "standard",
835
- };
836
- mockMessageById = {
837
- id: "user-msg-1",
838
- conversationId: "test-conv",
839
- createdAt: 1000,
840
- };
841
- mockHasProactiveArtifactCompleted = false;
842
- mockTryClaimProactiveArtifactTrigger = true;
843
-
844
- // A two-call agent turn: the model invokes `app_create`, then wraps up
845
- // with a final text reply.
846
- const ctx = makeCtx({
847
- conversationId: "test-conv",
848
- providerResponses: [
849
- {
850
- content: [
851
- { type: "text", text: "I'll build that app." },
852
- {
853
- type: "tool_use",
854
- id: "tool-1",
855
- name: "app_create",
856
- input: { name: "Flow" },
857
- },
858
- ],
859
- model: "mock-model",
860
- usage: { inputTokens: 10, outputTokens: 5 },
861
- stopReason: "tool_use",
862
- },
863
- textResponse("Done."),
864
- ],
865
- toolExecutor: async () => ({ content: "{}", isError: false }),
866
- });
867
- await runAgentLoopImpl(
868
- ctx,
869
- "build a kanban app",
870
- "user-msg-1",
871
- () => {},
872
- {
873
- isUserMessage: true,
874
- },
875
- );
876
- await new Promise((resolve) => setTimeout(resolve, 0));
877
-
878
- expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(0);
879
- });
880
- });
881
-
882
870
  describe("disk pressure injection context", () => {
883
- test("passes cleanup context into runtime injections for cleanup-mode turns", async () => {
871
+ // The loop sets `ctx.diskPressureCleanupModeActive` for the duration of the
872
+ // turn (the disk-pressure-warning injector reads it via the per-conversation
873
+ // registry) and resets it in the turn-end cleanup path. Snapshot the flag at
874
+ // each `applyRuntimeInjections` call so assertions observe its value while
875
+ // injection runs, not the post-turn reset.
876
+ function captureCleanupFlagDuringInjection(ctx: {
877
+ diskPressureCleanupModeActive?: boolean;
878
+ }): () => Array<boolean | undefined> {
879
+ const observed: Array<boolean | undefined> = [];
880
+ applyRuntimeInjectionsMock.mockImplementation(async (msgs: Message[]) => {
881
+ observed.push(ctx.diskPressureCleanupModeActive);
882
+ return { messages: msgs, blocks: { ...mockInjectionBlocks } };
883
+ });
884
+ return () => observed;
885
+ }
886
+
887
+ test("sets the cleanup-mode flag on the conversation for cleanup-mode turns", async () => {
884
888
  mockDiskPressureDecision = {
885
889
  action: "allow-cleanup-mode",
886
890
  reason: "guardian",
@@ -901,8 +905,9 @@ describe("session-agent-loop", () => {
901
905
  trustContext: {
902
906
  sourceChannel: "telegram",
903
907
  trustClass: "guardian",
904
- } as AgentLoopConversationContext["trustContext"],
908
+ } as Conversation["trustContext"],
905
909
  });
910
+ const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
906
911
 
907
912
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
908
913
 
@@ -921,21 +926,16 @@ describe("session-agent-loop", () => {
921
926
  },
922
927
  }),
923
928
  );
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
- });
929
+ expect(cleanupFlagDuringInjection()).toEqual([true]);
931
930
  });
932
931
 
933
- test("passes cleanup context into runtime injections for local-owner turns", async () => {
932
+ test("sets the cleanup-mode flag on the conversation for local-owner turns", async () => {
934
933
  mockDiskPressureDecision = {
935
934
  action: "allow-cleanup-mode",
936
935
  reason: "local-owner",
937
936
  };
938
937
  const ctx = makeCtx();
938
+ const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
939
939
 
940
940
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
941
941
 
@@ -947,16 +947,10 @@ describe("session-agent-loop", () => {
947
947
  trustContext: null,
948
948
  }),
949
949
  );
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
- });
950
+ expect(cleanupFlagDuringInjection()).toEqual([true]);
957
951
  });
958
952
 
959
- test("keeps cleanup context on overflow recovery reinjection", async () => {
953
+ test("keeps the cleanup-mode flag set across overflow recovery reinjection", async () => {
960
954
  mockDiskPressureDecision = {
961
955
  action: "allow-cleanup-mode",
962
956
  reason: "guardian",
@@ -976,20 +970,16 @@ describe("session-agent-loop", () => {
976
970
  trustContext: {
977
971
  sourceChannel: "telegram",
978
972
  trustClass: "guardian",
979
- } as AgentLoopConversationContext["trustContext"],
973
+ } as Conversation["trustContext"],
980
974
  });
975
+ const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
981
976
 
982
977
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
983
978
 
984
979
  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
- }
980
+ const flags = cleanupFlagDuringInjection();
981
+ expect(flags.length).toBeGreaterThan(1);
982
+ expect(flags.every((flag) => flag === true)).toBe(true);
993
983
  });
994
984
 
995
985
  test("blocks policy-denied turns before runtime injection or model execution", async () => {
@@ -1008,7 +998,7 @@ describe("session-agent-loop", () => {
1008
998
  emit: (...args: unknown[]) => {
1009
999
  traceEvents.push(args);
1010
1000
  },
1011
- } as unknown as AgentLoopConversationContext["traceEmitter"],
1001
+ } as unknown as Conversation["traceEmitter"],
1012
1002
  });
1013
1003
  const runSpy = spyOn(ctx.agentLoop, "run");
1014
1004
 
@@ -1169,7 +1159,7 @@ describe("session-agent-loop", () => {
1169
1159
  usage: { inputTokens: 0, outputTokens: 0 },
1170
1160
  stopReason: "end_turn",
1171
1161
  }),
1172
- } as unknown as AgentLoopConversationContext["provider"],
1162
+ } as unknown as Conversation["provider"],
1173
1163
  });
1174
1164
 
1175
1165
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1218,7 +1208,7 @@ describe("session-agent-loop", () => {
1218
1208
  usage: { inputTokens: 0, outputTokens: 0 },
1219
1209
  stopReason: "end_turn",
1220
1210
  }),
1221
- } as unknown as AgentLoopConversationContext["provider"],
1211
+ } as unknown as Conversation["provider"],
1222
1212
  });
1223
1213
 
1224
1214
  await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
@@ -1284,7 +1274,7 @@ describe("session-agent-loop", () => {
1284
1274
  usage: { inputTokens: 0, outputTokens: 0 },
1285
1275
  stopReason: "end_turn",
1286
1276
  }),
1287
- } as unknown as AgentLoopConversationContext["provider"],
1277
+ } as unknown as Conversation["provider"],
1288
1278
  });
1289
1279
 
1290
1280
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1335,7 +1325,7 @@ describe("session-agent-loop", () => {
1335
1325
  usage: { inputTokens: 0, outputTokens: 0 },
1336
1326
  stopReason: "end_turn",
1337
1327
  }),
1338
- } as unknown as AgentLoopConversationContext["provider"],
1328
+ } as unknown as Conversation["provider"],
1339
1329
  traceEmitter: {
1340
1330
  emit: (
1341
1331
  event: string,
@@ -1346,7 +1336,7 @@ describe("session-agent-loop", () => {
1346
1336
  traceEvents.push({ label, attrs: payload.attributes ?? {} });
1347
1337
  }
1348
1338
  },
1349
- } as unknown as AgentLoopConversationContext["traceEmitter"],
1339
+ } as unknown as Conversation["traceEmitter"],
1350
1340
  });
1351
1341
 
1352
1342
  await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
@@ -1393,7 +1383,7 @@ describe("session-agent-loop", () => {
1393
1383
  usage: { inputTokens: 0, outputTokens: 0 },
1394
1384
  stopReason: "end_turn",
1395
1385
  }),
1396
- } as unknown as AgentLoopConversationContext["provider"],
1386
+ } as unknown as Conversation["provider"],
1397
1387
  traceEmitter: {
1398
1388
  emit: (
1399
1389
  event: string,
@@ -1404,7 +1394,7 @@ describe("session-agent-loop", () => {
1404
1394
  traceEvents.push({ label, attrs: payload.attributes ?? {} });
1405
1395
  }
1406
1396
  },
1407
- } as unknown as AgentLoopConversationContext["traceEmitter"],
1397
+ } as unknown as Conversation["traceEmitter"],
1408
1398
  });
1409
1399
 
1410
1400
  await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
@@ -1460,7 +1450,7 @@ describe("session-agent-loop", () => {
1460
1450
  usage: { inputTokens: 0, outputTokens: 0 },
1461
1451
  stopReason: "end_turn",
1462
1452
  }),
1463
- } as unknown as AgentLoopConversationContext["provider"],
1453
+ } as unknown as Conversation["provider"],
1464
1454
  });
1465
1455
 
1466
1456
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1521,9 +1511,21 @@ describe("session-agent-loop", () => {
1521
1511
  },
1522
1512
  });
1523
1513
 
1524
- // After the orchestrator's preflight compaction runs, the loop completes
1525
- // the turn normally.
1526
- const ctx = makeCtx({ providerResponses: [textResponse("recovered")] });
1514
+ // The provider rejects the first call as too large; the convergence
1515
+ // reducer compacts and the rerun completes the turn, forwarding the
1516
+ // compaction's cache-aware usage to recordUsage.
1517
+ const { provider } = createMockProvider([
1518
+ new Error("context_length_exceeded"),
1519
+ textResponse("recovered"),
1520
+ ]);
1521
+ const ctx = makeCtx({
1522
+ loopProvider: provider,
1523
+ contextWindowManager: {
1524
+ updateConfig: () => {},
1525
+ shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1526
+ maybeCompact: async () => ({ compacted: false }),
1527
+ } as unknown as Conversation["contextWindowManager"],
1528
+ });
1527
1529
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
1528
1530
 
1529
1531
  const compactorCall = recordUsageMock.mock.calls.find(
@@ -1605,9 +1607,10 @@ describe("session-agent-loop", () => {
1605
1607
  const ctx = makeCtx({
1606
1608
  loopProvider: provider,
1607
1609
  contextWindowManager: {
1610
+ updateConfig: () => {},
1608
1611
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1609
1612
  maybeCompact: async () => ({ compacted: false }),
1610
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1613
+ } as unknown as Conversation["contextWindowManager"],
1611
1614
  });
1612
1615
 
1613
1616
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1626,6 +1629,7 @@ describe("session-agent-loop", () => {
1626
1629
  const ctx = makeCtx({
1627
1630
  providerResponses: [new Error("context_length_exceeded")],
1628
1631
  contextWindowManager: {
1632
+ updateConfig: () => {},
1629
1633
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1630
1634
  // Compaction succeeds but context is still too large
1631
1635
  maybeCompact: async () => ({
@@ -1645,7 +1649,7 @@ describe("session-agent-loop", () => {
1645
1649
  summaryOutputTokens: 200,
1646
1650
  summaryModel: "mock-model",
1647
1651
  }),
1648
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1652
+ } as unknown as Conversation["contextWindowManager"],
1649
1653
  });
1650
1654
 
1651
1655
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1685,9 +1689,10 @@ describe("session-agent-loop", () => {
1685
1689
  const ctx = makeCtx({
1686
1690
  loopProvider: provider,
1687
1691
  contextWindowManager: {
1692
+ updateConfig: () => {},
1688
1693
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1689
1694
  maybeCompact: async () => ({ compacted: false }),
1690
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1695
+ } as unknown as Conversation["contextWindowManager"],
1691
1696
  });
1692
1697
 
1693
1698
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1705,27 +1710,31 @@ describe("session-agent-loop", () => {
1705
1710
  test("non-interactive auto-compress continues without approval prompt", async () => {
1706
1711
  const events: ServerMessage[] = [];
1707
1712
 
1708
- // Reducer exhausts all tiers
1709
- mockReducerStepFn = (msgs: Message[]) => ({
1710
- messages: msgs,
1711
- tier: "injection_downgrade",
1712
- state: {
1713
- appliedTiers: [
1714
- "forced_compaction",
1715
- "tool_result_truncation",
1716
- "media_stubbing",
1717
- "injection_downgrade",
1718
- ],
1719
- injectionMode: "minimal",
1720
- exhausted: true,
1721
- },
1722
- estimatedTokens: 120000,
1723
- });
1713
+ // The ladder applies a middle tier first, then escalates to its terminal
1714
+ // auto-compress-latest-turn rung (permitted by the overflow policy) and
1715
+ // reports exhaustion — mirroring the reducer owning every rung.
1716
+ let reducerCalls = 0;
1717
+ mockReducerStepFn = (msgs: Message[]) => {
1718
+ reducerCalls++;
1719
+ const terminal = reducerCalls >= 2;
1720
+ return {
1721
+ messages: msgs,
1722
+ tier: terminal ? "auto_compress_latest_turn" : "forced_compaction",
1723
+ state: {
1724
+ appliedTiers: terminal
1725
+ ? ["forced_compaction", "auto_compress_latest_turn"]
1726
+ : ["forced_compaction"],
1727
+ injectionMode: "full",
1728
+ exhausted: terminal,
1729
+ },
1730
+ estimatedTokens: 120000,
1731
+ };
1732
+ };
1724
1733
 
1725
1734
  mockOverflowAction = "auto_compress_latest_turn";
1726
1735
 
1727
1736
  // The provider rejects the first two calls with context-too-large errors,
1728
- // then succeeds after the emergency auto-compress runs.
1737
+ // then succeeds after the terminal auto-compress rung runs.
1729
1738
  const ctx = makeCtx({
1730
1739
  providerResponses: [
1731
1740
  new Error("context_length_exceeded"),
@@ -1734,6 +1743,7 @@ describe("session-agent-loop", () => {
1734
1743
  ],
1735
1744
  hasNoClient: true,
1736
1745
  contextWindowManager: {
1746
+ updateConfig: () => {},
1737
1747
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1738
1748
  maybeCompact: async () => ({
1739
1749
  compacted: true,
@@ -1752,7 +1762,7 @@ describe("session-agent-loop", () => {
1752
1762
  summaryOutputTokens: 100,
1753
1763
  summaryModel: "mock-model",
1754
1764
  }),
1755
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1765
+ } as unknown as Conversation["contextWindowManager"],
1756
1766
  });
1757
1767
 
1758
1768
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1779,24 +1789,26 @@ describe("session-agent-loop", () => {
1779
1789
  // attribute the silent stall.
1780
1790
  const events: ServerMessage[] = [];
1781
1791
 
1782
- // Reducer exhausts all 4 tiers on first call so the convergence
1783
- // loop runs exactly one iteration before falling through to
1784
- // the auto_compress_latest_turn branch.
1785
- mockReducerStepFn = (msgs: Message[]) => ({
1786
- messages: msgs,
1787
- tier: "injection_downgrade",
1788
- state: {
1789
- appliedTiers: [
1790
- "forced_compaction",
1791
- "tool_result_truncation",
1792
- "media_stubbing",
1793
- "injection_downgrade",
1794
- ],
1795
- injectionMode: "minimal",
1796
- exhausted: true,
1797
- },
1798
- estimatedTokens: 120_000,
1799
- });
1792
+ // The ladder applies a middle tier, then runs its terminal
1793
+ // auto-compress-latest-turn rung and reports exhaustion. Its rerun is the
1794
+ // one that still yields at the mid-loop budget checkpoint.
1795
+ let reducerCalls = 0;
1796
+ mockReducerStepFn = (msgs: Message[]) => {
1797
+ reducerCalls++;
1798
+ const terminal = reducerCalls >= 2;
1799
+ return {
1800
+ messages: msgs,
1801
+ tier: terminal ? "auto_compress_latest_turn" : "forced_compaction",
1802
+ state: {
1803
+ appliedTiers: terminal
1804
+ ? ["forced_compaction", "auto_compress_latest_turn"]
1805
+ : ["forced_compaction"],
1806
+ injectionMode: "full",
1807
+ exhausted: terminal,
1808
+ },
1809
+ estimatedTokens: 120_000,
1810
+ };
1811
+ };
1800
1812
 
1801
1813
  mockOverflowAction = "auto_compress_latest_turn";
1802
1814
 
@@ -1834,6 +1846,7 @@ describe("session-agent-loop", () => {
1834
1846
  toolExecutor: async () => ({ content: "data", isError: false }),
1835
1847
  hasNoClient: true,
1836
1848
  contextWindowManager: {
1849
+ updateConfig: () => {},
1837
1850
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1838
1851
  maybeCompact: async () => ({
1839
1852
  compacted: true,
@@ -1852,7 +1865,7 @@ describe("session-agent-loop", () => {
1852
1865
  summaryOutputTokens: 100,
1853
1866
  summaryModel: "mock-model",
1854
1867
  }),
1855
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1868
+ } as unknown as Conversation["contextWindowManager"],
1856
1869
  });
1857
1870
 
1858
1871
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
@@ -1919,57 +1932,18 @@ describe("session-agent-loop", () => {
1919
1932
  const ctx = makeCtx({
1920
1933
  providerResponses: [new Error("context_length_exceeded")],
1921
1934
  contextWindowManager: {
1935
+ updateConfig: () => {},
1922
1936
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1923
1937
  maybeCompact: async () => ({ compacted: false }),
1924
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1925
- });
1926
-
1927
- await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
1928
-
1929
- // maxAttempts is 3 — reducer should be called at most 3 times
1930
- expect(reducerCalls).toBeLessThanOrEqual(3);
1931
- });
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"],
1938
+ } as unknown as Conversation["contextWindowManager"],
1963
1939
  });
1964
1940
 
1965
1941
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
1966
1942
 
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();
1943
+ // A misbehaving reducer that never reports exhaustion is bounded by the
1944
+ // driver's safety backstop: at most the middle-tier budget plus the
1945
+ // emergency and terminal rungs (maxAttempts + 2 = 5).
1946
+ expect(reducerCalls).toBeLessThanOrEqual(5);
1973
1947
  });
1974
1948
  });
1975
1949
 
@@ -2024,7 +1998,7 @@ describe("session-agent-loop", () => {
2024
1998
  ],
2025
1999
  toolExecutor: async () => ({ content: "file content", isError: false }),
2026
2000
  canHandoffAtCheckpoint: () => true,
2027
- } as unknown as Partial<AgentLoopConversationContext>);
2001
+ } as unknown as Partial<Conversation>);
2028
2002
 
2029
2003
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2030
2004
 
@@ -2055,7 +2029,7 @@ describe("session-agent-loop", () => {
2055
2029
  ],
2056
2030
  toolExecutor: async () => ({ content: "content", isError: false }),
2057
2031
  canHandoffAtCheckpoint: () => false,
2058
- } as unknown as Partial<AgentLoopConversationContext>);
2032
+ } as unknown as Partial<Conversation>);
2059
2033
 
2060
2034
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2061
2035
 
@@ -2206,7 +2180,7 @@ describe("session-agent-loop", () => {
2206
2180
  drainQueue: (reason: string) => {
2207
2181
  drainReason = reason;
2208
2182
  },
2209
- } as unknown as Partial<AgentLoopConversationContext>);
2183
+ } as unknown as Partial<Conversation>);
2210
2184
 
2211
2185
  // WHEN the orchestrator runs the turn to completion
2212
2186
  await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
@@ -2648,9 +2622,10 @@ describe("session-agent-loop", () => {
2648
2622
  textResponse("retry succeeded"),
2649
2623
  ],
2650
2624
  contextWindowManager: {
2625
+ updateConfig: () => {},
2651
2626
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
2652
2627
  maybeCompact: async () => ({ compacted: false }),
2653
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
2628
+ } as unknown as Conversation["contextWindowManager"],
2654
2629
  });
2655
2630
  await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2656
2631
 
@@ -3112,7 +3087,7 @@ describe("session-agent-loop", () => {
3112
3087
  });
3113
3088
 
3114
3089
  describe("Slack compaction watermarks", () => {
3115
- test("start-of-turn Slack compaction derives and persists watermark from rendered context", async () => {
3090
+ test("first-call compaction runs in the loop and drops the Slack watermark", async () => {
3116
3091
  const renderedSlackMessages: Message[] = [
3117
3092
  {
3118
3093
  role: "user",
@@ -3140,9 +3115,12 @@ describe("session-agent-loop", () => {
3140
3115
  })),
3141
3116
  compactableStartIndex: 0,
3142
3117
  };
3143
- const shouldCompactInputs: Message[][] = [];
3144
3118
  const maybeCompactInputs: Message[][] = [];
3145
3119
 
3120
+ // Sits above the loop's first-call gate threshold (~80.75k), so the
3121
+ // loop's first-call gate owns the turn-start compaction.
3122
+ mockEstimateTokens = 90_000;
3123
+
3146
3124
  const ctx = makeCtx({
3147
3125
  channelCapabilities: {
3148
3126
  channel: "slack",
@@ -3154,18 +3132,19 @@ describe("session-agent-loop", () => {
3154
3132
  trustContext: {
3155
3133
  sourceChannel: "slack",
3156
3134
  trustClass: "guardian",
3157
- } as AgentLoopConversationContext["trustContext"],
3135
+ } as Conversation["trustContext"],
3158
3136
  getTurnChannelContext: () => ({
3159
3137
  userMessageChannel: "slack" as const,
3160
3138
  assistantMessageChannel: "slack" as const,
3161
3139
  }),
3162
3140
  contextWindowManager: {
3163
- shouldCompact: (messages: Message[]) => {
3164
- shouldCompactInputs.push(messages);
3165
- return { needed: true, estimatedTokens: 95_000 };
3166
- },
3141
+ updateConfig: () => {},
3142
+ shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3167
3143
  maybeCompact: async (messages: Message[]) => {
3168
3144
  maybeCompactInputs.push(messages);
3145
+ // Drop back under budget so the post-compaction provider call
3146
+ // proceeds.
3147
+ mockEstimateTokens = 1000;
3169
3148
  return {
3170
3149
  compacted: true,
3171
3150
  messages: [
@@ -3173,10 +3152,10 @@ describe("session-agent-loop", () => {
3173
3152
  role: "user",
3174
3153
  content: [{ type: "text", text: "summary" }],
3175
3154
  },
3176
- messages[2]!,
3155
+ messages[messages.length - 1]!,
3177
3156
  ],
3178
3157
  compactedPersistedMessages: 2,
3179
- previousEstimatedInputTokens: 95_000,
3158
+ previousEstimatedInputTokens: 90_000,
3180
3159
  estimatedInputTokens: 5_000,
3181
3160
  maxInputTokens: 100_000,
3182
3161
  thresholdTokens: 80_000,
@@ -3189,292 +3168,24 @@ describe("session-agent-loop", () => {
3189
3168
  summaryFailed: false,
3190
3169
  };
3191
3170
  },
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"],
3171
+ } as unknown as Conversation["contextWindowManager"],
3303
3172
  });
3304
3173
 
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),
3174
+ await runAgentLoopImpl(
3175
+ ctx,
3176
+ "next reply",
3177
+ "user-msg-first-call",
3178
+ () => {},
3315
3179
  );
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
-
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
3180
 
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);
3181
+ // The loop compacts its own stripped working history, never the loaded
3182
+ // Slack transcript array, so provenance cannot be projected and the
3183
+ // watermark is left untouched — matching mid-loop compaction.
3184
+ expect(maybeCompactInputs.length).toBeGreaterThan(0);
3185
+ expect(maybeCompactInputs[0]).not.toBe(renderedSlackMessages);
3186
+ expect(
3187
+ updateConversationSlackContextWatermarkMock,
3188
+ ).not.toHaveBeenCalled();
3478
3189
  });
3479
3190
 
3480
3191
  test("mid-loop Slack compaction does not persist watermark from mismatched loaded context", async () => {
@@ -3542,32 +3253,16 @@ describe("session-agent-loop", () => {
3542
3253
  trustContext: {
3543
3254
  sourceChannel: "slack",
3544
3255
  trustClass: "guardian",
3545
- } as AgentLoopConversationContext["trustContext"],
3256
+ } as Conversation["trustContext"],
3546
3257
  getTurnChannelContext: () => ({
3547
3258
  userMessageChannel: "slack" as const,
3548
3259
  assistantMessageChannel: "slack" as const,
3549
3260
  }),
3550
3261
  contextWindowManager: {
3262
+ updateConfig: () => {},
3551
3263
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3552
3264
  maybeCompact: async (messages: Message[]) => {
3553
3265
  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
3266
  // The mid-loop gate compacted its in-loop basis; drop the estimate
3572
3267
  // back under budget so the post-compaction provider call proceeds.
3573
3268
  mockEstimateTokens = 1000;
@@ -3593,15 +3288,16 @@ describe("session-agent-loop", () => {
3593
3288
  summaryFailed: false,
3594
3289
  };
3595
3290
  },
3596
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
3291
+ } as unknown as Conversation["contextWindowManager"],
3597
3292
  });
3598
3293
 
3599
3294
  await runAgentLoopImpl(ctx, "next reply", "user-msg-mid-loop", () => {});
3600
3295
 
3601
- expect(maybeCompactInputs[0]).toBe(renderedSlackMessages);
3602
3296
  // 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);
3297
+ // loaded Slack rows — the mismatch this test guards against. No
3298
+ // start-of-turn compaction runs, so this is the turn's only compaction.
3299
+ expect(maybeCompactInputs).toHaveLength(1);
3300
+ expect(maybeCompactInputs[0]).not.toBe(renderedSlackMessages);
3605
3301
  expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
3606
3302
  null,
3607
3303
  2,
@@ -3611,7 +3307,7 @@ describe("session-agent-loop", () => {
3611
3307
  ).not.toHaveBeenCalled();
3612
3308
  });
3613
3309
 
3614
- test("next inbound Slack turn injects the watermark-filtered chronological context", async () => {
3310
+ test("next inbound Slack turn loads chronological context using the persisted watermark", async () => {
3615
3311
  mockConversationRow = {
3616
3312
  ...mockConversationRow,
3617
3313
  contextSummary: "## Summary\n- compacted Slack context",
@@ -3671,7 +3367,7 @@ describe("session-agent-loop", () => {
3671
3367
  trustContext: {
3672
3368
  sourceChannel: "slack",
3673
3369
  trustClass: "guardian",
3674
- } as AgentLoopConversationContext["trustContext"],
3370
+ } as Conversation["trustContext"],
3675
3371
  getTurnChannelContext: () => ({
3676
3372
  userMessageChannel: "slack" as const,
3677
3373
  assistantMessageChannel: "slack" as const,
@@ -3690,26 +3386,9 @@ describe("session-agent-loop", () => {
3690
3386
  trustClass: "guardian",
3691
3387
  }),
3692
3388
  );
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 () => {
3389
+ });
3390
+
3391
+ test("subsequent Slack turn loads chronological context using the persisted long-thread watermark", async () => {
3713
3392
  mockConversationRow = {
3714
3393
  ...mockConversationRow,
3715
3394
  contextSummary: "## Summary\n- compacted long Slack thread",
@@ -3779,7 +3458,7 @@ describe("session-agent-loop", () => {
3779
3458
  trustContext: {
3780
3459
  sourceChannel: "slack",
3781
3460
  trustClass: "guardian",
3782
- } as AgentLoopConversationContext["trustContext"],
3461
+ } as Conversation["trustContext"],
3783
3462
  getTurnChannelContext: () => ({
3784
3463
  userMessageChannel: "slack" as const,
3785
3464
  assistantMessageChannel: "slack" as const,
@@ -3802,21 +3481,6 @@ describe("session-agent-loop", () => {
3802
3481
  slackContextCompactionWatermarkTs: "1700000080.000000",
3803
3482
  }),
3804
3483
  );
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
3484
  });
3821
3485
 
3822
3486
  test("applyCompactionResult records Slack timestamp watermark when provided", async () => {
@@ -3858,6 +3522,59 @@ describe("session-agent-loop", () => {
3858
3522
  true,
3859
3523
  );
3860
3524
  });
3525
+
3526
+ test("applyCompactionResult advances the persisted count from the trusted in-context boundary", async () => {
3527
+ // Trusted views slice past the already-compacted prefix, so a further
3528
+ // compaction advances the persisted count from the mirrored DB boundary.
3529
+
3530
+ // GIVEN a trusted (guardian) conversation that has already compacted 5
3531
+ // persisted messages, so its in-context history starts past that prefix
3532
+ const ctx = makeCtx({
3533
+ contextCompactedMessageCount: 5,
3534
+ trustContext: {
3535
+ trustClass: "guardian",
3536
+ } as Conversation["trustContext"],
3537
+ });
3538
+
3539
+ // WHEN a turn compacts 4 more in-context messages
3540
+ await applyCompactionResult(
3541
+ ctx,
3542
+ makeCompactionResult({ compactedPersistedMessages: 4 }),
3543
+ () => {},
3544
+ "req-1",
3545
+ );
3546
+
3547
+ // THEN the persisted count advances from the prior boundary (5 + 4)
3548
+ expect(ctx.contextCompactedMessageCount).toBe(9);
3549
+ });
3550
+
3551
+ test("applyCompactionResult resets the persisted count to the unsliced boundary for untrusted views", async () => {
3552
+ // Untrusted views render history unsliced (boundary 0), so a compaction
3553
+ // must record only the new summary's prefix instead of adding to the raw
3554
+ // mirror — otherwise future loads slice past unsummarized rows.
3555
+
3556
+ // GIVEN an untrusted view of a conversation whose raw DB count mirrors a
3557
+ // 5-message compacted prefix — but untrusted views render that history
3558
+ // unsliced (boundary 0), so the compactor operates on the full list
3559
+ const ctx = makeCtx({
3560
+ contextCompactedMessageCount: 5,
3561
+ trustContext: {
3562
+ trustClass: "unknown",
3563
+ } as Conversation["trustContext"],
3564
+ });
3565
+
3566
+ // WHEN that turn compacts 4 in-context messages
3567
+ await applyCompactionResult(
3568
+ ctx,
3569
+ makeCompactionResult({ compactedPersistedMessages: 4 }),
3570
+ () => {},
3571
+ "req-1",
3572
+ );
3573
+
3574
+ // THEN the persisted count reflects only the new summary's prefix (0 + 4)
3575
+ // rather than double-counting the raw mirror (which would yield 9)
3576
+ expect(ctx.contextCompactedMessageCount).toBe(4);
3577
+ });
3861
3578
  });
3862
3579
 
3863
3580
  describe("compaction-strip marker persistence", () => {
@@ -3894,9 +3611,10 @@ describe("session-agent-loop", () => {
3894
3611
  ],
3895
3612
  toolExecutor: async () => ({ content: "ok", isError: false }),
3896
3613
  contextWindowManager: {
3614
+ updateConfig: () => {},
3897
3615
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3898
3616
  maybeCompact: async () => ({ compacted: false }),
3899
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
3617
+ } as unknown as Conversation["contextWindowManager"],
3900
3618
  });
3901
3619
 
3902
3620
  // WHEN the orchestrator runs the turn to completion
@@ -3943,9 +3661,10 @@ describe("session-agent-loop", () => {
3943
3661
  ],
3944
3662
  toolExecutor: async () => ({ content: "ok", isError: false }),
3945
3663
  contextWindowManager: {
3664
+ updateConfig: () => {},
3946
3665
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3947
3666
  maybeCompact: async () => ({ compacted: false }),
3948
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
3667
+ } as unknown as Conversation["contextWindowManager"],
3949
3668
  });
3950
3669
 
3951
3670
  // Must not throw — the strip-site marker write is wrapped in try/catch.