@vellumai/assistant 0.8.5 → 0.8.7-dev.202606052118.34cd356

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 (1323) hide show
  1. package/AGENTS.md +33 -1
  2. package/ARCHITECTURE.md +1 -1
  3. package/Dockerfile +21 -4
  4. package/bun.lock +13 -4
  5. package/bunfig.toml +6 -1
  6. package/docker-entrypoint.sh +12 -8
  7. package/docker-init-apt-root.sh +3 -1
  8. package/docker-kata-apt-env.sh +3 -1
  9. package/docker-kata-runtime-family.sh +12 -0
  10. package/docs/architecture/memory.md +1 -1
  11. package/docs/credential-execution-service.md +6 -6
  12. package/docs/plugins.md +114 -86
  13. package/examples/plugins/echo/README.md +13 -12
  14. package/examples/plugins/echo/register.ts +0 -54
  15. package/knip.json +1 -0
  16. package/node_modules/@vellumai/environments/bun.lock +24 -0
  17. package/node_modules/@vellumai/environments/package.json +18 -0
  18. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
  19. package/node_modules/@vellumai/environments/src/index.ts +11 -0
  20. package/node_modules/@vellumai/environments/src/seeds.ts +73 -0
  21. package/node_modules/@vellumai/environments/src/types.ts +70 -0
  22. package/node_modules/@vellumai/environments/tsconfig.json +20 -0
  23. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +11 -0
  24. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +15 -17
  25. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  26. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +15 -7
  27. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +16 -14
  28. package/openapi.yaml +8748 -589
  29. package/package.json +8 -4
  30. package/scripts/generate-openapi.ts +88 -54
  31. package/src/__tests__/actor-token-service.test.ts +3 -2
  32. package/src/__tests__/agent-loop-callsite-precedence.test.ts +42 -80
  33. package/src/__tests__/agent-loop-exit-reason.test.ts +284 -48
  34. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +141 -0
  35. package/src/__tests__/agent-loop-override-profile.test.ts +21 -33
  36. package/src/__tests__/agent-loop-provider-error-recording.test.ts +7 -5
  37. package/src/__tests__/agent-loop-thinking.test.ts +17 -12
  38. package/src/__tests__/agent-loop.test.ts +238 -422
  39. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +7 -2
  40. package/src/__tests__/agent-wake-override-profile.test.ts +23 -40
  41. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  42. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  43. package/src/__tests__/annotate-risk-options.test.ts +3 -3
  44. package/src/__tests__/anthropic-provider.test.ts +296 -57
  45. package/src/__tests__/app-builder-skill-instructions.test.ts +22 -0
  46. package/src/__tests__/app-control-flow.test.ts +6 -1
  47. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  48. package/src/__tests__/approval-cascade.test.ts +5 -11
  49. package/src/__tests__/approval-routes-http.test.ts +17 -16
  50. package/src/__tests__/assert-not-live-db.ts +79 -0
  51. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  52. package/src/__tests__/assistant-event.test.ts +15 -0
  53. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  54. package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -27
  55. package/src/__tests__/assistant-stream-state.test.ts +645 -0
  56. package/src/__tests__/audit-log-rotation.test.ts +2 -2
  57. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  58. package/src/__tests__/auto-analysis-end-to-end.test.ts +6 -6
  59. package/src/__tests__/avatar-e2e.test.ts +7 -37
  60. package/src/__tests__/avatar-generator.test.ts +12 -42
  61. package/src/__tests__/avatar-identity-sync.test.ts +28 -3
  62. package/src/__tests__/background-shell-bash.test.ts +3 -7
  63. package/src/__tests__/background-workers-disk-pressure.test.ts +11 -8
  64. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  65. package/src/__tests__/btw-routes.test.ts +72 -17
  66. package/src/__tests__/build-persisted-content.test.ts +184 -0
  67. package/src/__tests__/call-controller.test.ts +3 -2
  68. package/src/__tests__/call-pointer-messages.test.ts +5 -3
  69. package/src/__tests__/call-site-routing-provider.test.ts +22 -40
  70. package/src/__tests__/catalog-files.test.ts +1 -0
  71. package/src/__tests__/channel-approval-routes.test.ts +52 -23
  72. package/src/__tests__/channel-approvals.test.ts +4 -2
  73. package/src/__tests__/channel-guardian.test.ts +3 -2
  74. package/src/__tests__/channel-invite-transport.test.ts +1 -5
  75. package/src/__tests__/channel-readiness-routes.test.ts +0 -4
  76. package/src/__tests__/channel-readiness-slack-remote.test.ts +170 -0
  77. package/src/__tests__/channel-reply-delivery.test.ts +35 -0
  78. package/src/__tests__/channel-retry-sweep.test.ts +388 -79
  79. package/src/__tests__/checker.test.ts +12 -12
  80. package/src/__tests__/clawhub-files.test.ts +1 -0
  81. package/src/__tests__/compaction-circuit.test.ts +258 -0
  82. package/src/__tests__/compaction-direct.test.ts +132 -0
  83. package/src/__tests__/compaction-events.test.ts +6 -17
  84. package/src/__tests__/compaction-trail-store.test.ts +186 -0
  85. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  86. package/src/__tests__/compactor-call-site-logging.test.ts +1 -0
  87. package/src/__tests__/compactor-image-manifest-trust.test.ts +112 -0
  88. package/src/__tests__/compactor-preserved-tail-count.test.ts +1 -0
  89. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +7 -5
  90. package/src/__tests__/computer-use-tools.test.ts +14 -16
  91. package/src/__tests__/config-loader-backfill.test.ts +13 -28
  92. package/src/__tests__/config-loader-corrupt.test.ts +5 -5
  93. package/src/__tests__/config-loader-platform-defaults.test.ts +93 -26
  94. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +3 -3
  95. package/src/__tests__/config-managed-gemini-defaults.test.ts +3 -4
  96. package/src/__tests__/config-schema.test.ts +10 -10
  97. package/src/__tests__/config-watcher.test.ts +28 -0
  98. package/src/__tests__/connection-model-compat.test.ts +83 -0
  99. package/src/__tests__/contacts-tools.test.ts +3 -2
  100. package/src/__tests__/context-search-agent-runner.test.ts +6 -3
  101. package/src/__tests__/context-token-estimator.test.ts +56 -0
  102. package/src/__tests__/context-window-manager-compact-retry.test.ts +291 -0
  103. package/src/__tests__/conversation-abort-tool-results.test.ts +73 -23
  104. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +10 -7
  105. package/src/__tests__/conversation-agent-loop-handlers-max-tokens.test.ts +55 -0
  106. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +23 -34
  107. package/src/__tests__/conversation-agent-loop-overflow.test.ts +477 -930
  108. package/src/__tests__/conversation-agent-loop.test.ts +1056 -1068
  109. package/src/__tests__/conversation-analysis-routes.test.ts +8 -3
  110. package/src/__tests__/conversation-app-control-instantiation.test.ts +29 -19
  111. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -1
  112. package/src/__tests__/conversation-attention-store.test.ts +101 -0
  113. package/src/__tests__/conversation-attention-telegram.test.ts +3 -2
  114. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  115. package/src/__tests__/conversation-clear-safety.test.ts +20 -10
  116. package/src/__tests__/conversation-confirmation-signals.test.ts +16 -45
  117. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  118. package/src/__tests__/conversation-disk-view.test.ts +10 -17
  119. package/src/__tests__/conversation-error.test.ts +30 -0
  120. package/src/__tests__/conversation-fork-crud.test.ts +132 -157
  121. package/src/__tests__/conversation-fork-route.test.ts +19 -16
  122. package/src/__tests__/conversation-history-web-search.test.ts +12 -1
  123. package/src/__tests__/conversation-inference-profile-list.test.ts +3 -2
  124. package/src/__tests__/conversation-inference-profile-route.test.ts +3 -2
  125. package/src/__tests__/conversation-init.benchmark.test.ts +6 -6
  126. package/src/__tests__/conversation-lifecycle.test.ts +4 -2
  127. package/src/__tests__/conversation-list-source.test.ts +3 -2
  128. package/src/__tests__/conversation-load-history-repair.test.ts +5 -3
  129. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -1
  130. package/src/__tests__/conversation-message-sync-tags.test.ts +3 -4
  131. package/src/__tests__/conversation-pairing.test.ts +60 -4
  132. package/src/__tests__/conversation-pre-run-repair.test.ts +1 -1
  133. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +36 -7
  134. package/src/__tests__/conversation-process-callsite.test.ts +28 -30
  135. package/src/__tests__/conversation-provider-retry-repair.test.ts +85 -51
  136. package/src/__tests__/conversation-queue.test.ts +605 -455
  137. package/src/__tests__/conversation-routes-disk-view.test.ts +9 -20
  138. package/src/__tests__/conversation-routes-guardian-reply.test.ts +35 -10
  139. package/src/__tests__/conversation-routes-slash-commands.test.ts +40 -8
  140. package/src/__tests__/conversation-runtime-assembly.test.ts +395 -313
  141. package/src/__tests__/conversation-runtime-workspace.test.ts +114 -36
  142. package/src/__tests__/conversation-skill-tools.test.ts +38 -142
  143. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  144. package/src/__tests__/conversation-slash-queue.test.ts +125 -62
  145. package/src/__tests__/conversation-slash-unknown.test.ts +18 -15
  146. package/src/__tests__/conversation-speed-override.test.ts +9 -22
  147. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  148. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +136 -15
  149. package/src/__tests__/conversation-surfaces-app-control.test.ts +32 -4
  150. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -0
  151. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +6 -3
  152. package/src/__tests__/conversation-surfaces-standalone.test.ts +6 -3
  153. package/src/__tests__/conversation-surfaces-state-update.test.ts +8 -5
  154. package/src/__tests__/conversation-surfaces-table-action.test.ts +13 -32
  155. package/src/__tests__/conversation-sync-tags.test.ts +140 -12
  156. package/src/__tests__/conversation-title-service.test.ts +136 -2
  157. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +53 -11
  158. package/src/__tests__/conversation-unread-route.test.ts +14 -2
  159. package/src/__tests__/conversation-usage.test.ts +1 -2
  160. package/src/__tests__/conversation-wipe.test.ts +1 -1
  161. package/src/__tests__/conversation-workspace-cache-state.test.ts +21 -17
  162. package/src/__tests__/conversation-workspace-injection.test.ts +119 -23
  163. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +39 -13
  164. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  165. package/src/__tests__/credential-broker-browser-fill.test.ts +3 -3
  166. package/src/__tests__/credential-broker-server-use.test.ts +5 -5
  167. package/src/__tests__/credential-execution-client.test.ts +72 -1
  168. package/src/__tests__/credential-execution-feature-gates.test.ts +10 -12
  169. package/src/__tests__/credential-execution-tools.test.ts +1 -2
  170. package/src/__tests__/credential-health-service.test.ts +252 -3
  171. package/src/__tests__/credential-security-invariants.test.ts +5 -6
  172. package/src/__tests__/credential-vault-unit.test.ts +19 -19
  173. package/src/__tests__/credential-vault.test.ts +5 -5
  174. package/src/__tests__/cross-provider-web-search.test.ts +275 -4
  175. package/src/__tests__/cu-unified-flow.test.ts +26 -1
  176. package/src/__tests__/db-acp-history.test.ts +101 -0
  177. package/src/__tests__/db-connection-isolation.test.ts +7 -6
  178. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +8 -10
  179. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +7 -10
  180. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +9 -15
  181. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -0
  182. package/src/__tests__/db-test-helpers.ts +58 -0
  183. package/src/__tests__/disk-pressure-guard.test.ts +119 -36
  184. package/src/__tests__/disk-pressure-lifecycle.test.ts +13 -10
  185. package/src/__tests__/disk-pressure-routes.test.ts +9 -35
  186. package/src/__tests__/disk-pressure-tools.test.ts +0 -4
  187. package/src/__tests__/dm-persistence.test.ts +38 -43
  188. package/src/__tests__/document-create-dedupe.test.ts +189 -0
  189. package/src/__tests__/document-find-replace.test.ts +3 -2
  190. package/src/__tests__/document-tool-security.test.ts +81 -2
  191. package/src/__tests__/dynamic-page-surface.test.ts +99 -0
  192. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +5 -4
  193. package/src/__tests__/edit-propagation.test.ts +1 -2
  194. package/src/__tests__/empty-response-hook.test.ts +304 -0
  195. package/src/__tests__/encrypted-store-test-helpers.ts +56 -0
  196. package/src/__tests__/encrypted-store.test.ts +11 -9
  197. package/src/__tests__/feature-flag-test-helpers.ts +53 -0
  198. package/src/__tests__/file-write-tool.test.ts +63 -0
  199. package/src/__tests__/filing-service.test.ts +3 -2
  200. package/src/__tests__/first-greeting.test.ts +103 -12
  201. package/src/__tests__/gateway-flag-listener.test.ts +0 -1
  202. package/src/__tests__/gemini-image-service.test.ts +13 -0
  203. package/src/__tests__/gemini-inline-media.test.ts +78 -0
  204. package/src/__tests__/gemini-provider.test.ts +375 -26
  205. package/src/__tests__/guardian-action-sweep.test.ts +3 -2
  206. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  207. package/src/__tests__/guardian-outbound-http.test.ts +3 -2
  208. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  209. package/src/__tests__/guardian-routing-state.test.ts +60 -71
  210. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +48 -3
  211. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +11 -8
  212. package/src/__tests__/heartbeat-disk-pressure.test.ts +3 -0
  213. package/src/__tests__/heartbeat-service.test.ts +4 -1
  214. package/src/__tests__/helpers/mock-logger.ts +26 -0
  215. package/src/__tests__/helpers/mock-provider.ts +110 -0
  216. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  217. package/src/__tests__/history-repair-hook.test.ts +162 -0
  218. package/src/__tests__/history-repair-observability.test.ts +1 -1
  219. package/src/__tests__/history-repair.test.ts +2 -1
  220. package/src/__tests__/host-app-control-proxy.test.ts +2 -0
  221. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  222. package/src/__tests__/host-bash-routes.test.ts +1 -0
  223. package/src/__tests__/host-cu-proxy.test.ts +2 -0
  224. package/src/__tests__/host-cu-routes-targeted.test.ts +4 -3
  225. package/src/__tests__/host-file-edit-tool.test.ts +4 -2
  226. package/src/__tests__/host-file-proxy.test.ts +31 -0
  227. package/src/__tests__/host-file-read-tool.test.ts +4 -2
  228. package/src/__tests__/host-file-routes-targeted.test.ts +1 -0
  229. package/src/__tests__/host-file-write-tool.test.ts +9 -3
  230. package/src/__tests__/host-proxy-preactivation.test.ts +53 -14
  231. package/src/__tests__/host-shell-tool.test.ts +11 -5
  232. package/src/__tests__/host-transfer-routes-targeted.test.ts +1 -0
  233. package/src/__tests__/http-conversation-lineage.test.ts +3 -2
  234. package/src/__tests__/http-user-message-parity.test.ts +31 -9
  235. package/src/__tests__/identity-intro-cache.test.ts +140 -96
  236. package/src/__tests__/identity-routes.test.ts +248 -7
  237. package/src/__tests__/inbound-slack-persistence.test.ts +56 -75
  238. package/src/__tests__/inference-profile-reaper.test.ts +3 -2
  239. package/src/__tests__/inference-profile-session-ipc.test.ts +3 -2
  240. package/src/__tests__/injector-background-turn.test.ts +3 -9
  241. package/src/__tests__/injector-chain.test.ts +139 -275
  242. package/src/__tests__/injector-disk-pressure.test.ts +78 -58
  243. package/src/__tests__/injector-document-comments.test.ts +3 -3
  244. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  245. package/src/__tests__/injector-v3-suppression.test.ts +214 -0
  246. package/src/__tests__/inline-skill-load-permissions.test.ts +4 -4
  247. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  248. package/src/__tests__/list-messages-attachments.test.ts +7 -8
  249. package/src/__tests__/list-messages-hidden-metadata.test.ts +131 -11
  250. package/src/__tests__/list-messages-page-latest.test.ts +60 -1
  251. package/src/__tests__/list-messages-tool-merge.test.ts +56 -6
  252. package/src/__tests__/llm-context-normalization.test.ts +42 -0
  253. package/src/__tests__/llm-request-log-turn-query.test.ts +42 -86
  254. package/src/__tests__/llm-resolver.test.ts +346 -39
  255. package/src/__tests__/llm-schema.test.ts +1 -1
  256. package/src/__tests__/llm-usage-store.test.ts +268 -1
  257. package/src/__tests__/log-export-routes.test.ts +59 -0
  258. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -8
  259. package/src/__tests__/manual-token-reconciliation.test.ts +76 -1
  260. package/src/__tests__/mcp-abort-signal.test.ts +14 -0
  261. package/src/__tests__/mcp-auth-routes.test.ts +15 -10
  262. package/src/__tests__/mcp-client-auth.test.ts +14 -0
  263. package/src/__tests__/mcp-health-check.test.ts +18 -13
  264. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  265. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  266. package/src/__tests__/messaging-send-tool.test.ts +9 -4
  267. package/src/__tests__/migration-export-http.test.ts +12 -12
  268. package/src/__tests__/migration-import-commit-http.test.ts +8 -8
  269. package/src/__tests__/migration-import-from-url.test.ts +3 -3
  270. package/src/__tests__/migration-import-preflight-http.test.ts +7 -7
  271. package/src/__tests__/migration-validate-http.test.ts +3 -3
  272. package/src/__tests__/mock-gateway-ipc.ts +18 -2
  273. package/src/__tests__/model-intents.test.ts +3 -3
  274. package/src/__tests__/native-web-search.test.ts +235 -22
  275. package/src/__tests__/notification-decision-identity.test.ts +9 -18
  276. package/src/__tests__/notification-decision-recipient-context.test.ts +3 -6
  277. package/src/__tests__/notification-deep-link.test.ts +62 -0
  278. package/src/__tests__/oauth-commands-routes.test.ts +38 -1
  279. package/src/__tests__/oauth-provider-visibility.test.ts +8 -8
  280. package/src/__tests__/oauth-store.test.ts +3 -2
  281. package/src/__tests__/onboarding-template-contract.test.ts +15 -2
  282. package/src/__tests__/openai-image-service.test.ts +17 -0
  283. package/src/__tests__/openai-provider.test.ts +105 -80
  284. package/src/__tests__/openai-responses-provider.test.ts +90 -86
  285. package/src/__tests__/openrouter-provider-only.test.ts +27 -5
  286. package/src/__tests__/outbound-slack-persistence.test.ts +48 -2
  287. package/src/__tests__/{overflow-reduce-pipeline.test.ts → overflow-reduction-loop.test.ts} +64 -286
  288. package/src/__tests__/parallel-tool.benchmark.test.ts +24 -36
  289. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  290. package/src/__tests__/persistence-secret-redaction.test.ts +86 -13
  291. package/src/__tests__/pipeline-runner.test.ts +31 -43
  292. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  293. package/src/__tests__/plugin-bootstrap.test.ts +72 -63
  294. package/src/__tests__/plugin-registry.test.ts +0 -27
  295. package/src/__tests__/plugin-route-contribution.test.ts +6 -16
  296. package/src/__tests__/plugin-skill-contribution.test.ts +7 -17
  297. package/src/__tests__/plugin-tool-contribution.test.ts +51 -64
  298. package/src/__tests__/plugin-types.test.ts +8 -173
  299. package/src/__tests__/prechat-onboarding-contract.test.ts +23 -0
  300. package/src/__tests__/process-message-background-slack.test.ts +38 -32
  301. package/src/__tests__/process-message-display-content.test.ts +55 -66
  302. package/src/__tests__/provider-catalog-visibility.test.ts +9 -9
  303. package/src/__tests__/provider-commit-message-generator.test.ts +19 -14
  304. package/src/__tests__/provider-error-scenarios.test.ts +7 -6
  305. package/src/__tests__/provider-platform-proxy-integration.test.ts +215 -8
  306. package/src/__tests__/provider-registry-ollama.test.ts +45 -22
  307. package/src/__tests__/provider-send-message-override-profile.test.ts +9 -25
  308. package/src/__tests__/provider-streaming.benchmark.test.ts +12 -22
  309. package/src/__tests__/provider-usage-tracking.test.ts +0 -6
  310. package/src/__tests__/ratelimit.test.ts +9 -4
  311. package/src/__tests__/reaction-persistence.test.ts +1 -1
  312. package/src/__tests__/recording-handler.test.ts +1 -0
  313. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +6 -1
  314. package/src/__tests__/registry.test.ts +82 -76
  315. package/src/__tests__/relay-server.test.ts +30 -23
  316. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  317. package/src/__tests__/retry-openrouter-only-normalization.test.ts +5 -8
  318. package/src/__tests__/retry-thinking-tool-choice.test.ts +10 -13
  319. package/src/__tests__/retry-verbosity-normalization.test.ts +5 -8
  320. package/src/__tests__/runtime-attachment-metadata.test.ts +3 -2
  321. package/src/__tests__/runtime-events-sse-reconnect.test.ts +390 -0
  322. package/src/__tests__/schedule-routes.test.ts +683 -12
  323. package/src/__tests__/schedule-store.test.ts +124 -1
  324. package/src/__tests__/schedule-tools.test.ts +160 -0
  325. package/src/__tests__/scheduler-reuse-conversation.test.ts +48 -3
  326. package/src/__tests__/secret-ingress-http.test.ts +7 -3
  327. package/src/__tests__/secret-prompt-log-hygiene.test.ts +11 -7
  328. package/src/__tests__/secret-prompter-channel-fallback.test.ts +11 -9
  329. package/src/__tests__/secret-response-routing.test.ts +13 -11
  330. package/src/__tests__/secure-keys.test.ts +3 -3
  331. package/src/__tests__/send-endpoint-busy.test.ts +87 -44
  332. package/src/__tests__/server-history-render.test.ts +318 -2
  333. package/src/__tests__/shell-observability.test.ts +249 -0
  334. package/src/__tests__/skill-feature-flags-integration.test.ts +52 -21
  335. package/src/__tests__/skill-feature-flags.test.ts +20 -22
  336. package/src/__tests__/skill-load-feature-flag.test.ts +15 -15
  337. package/src/__tests__/skill-projection-feature-flag.test.ts +44 -30
  338. package/src/__tests__/skill-projection.benchmark.test.ts +5 -7
  339. package/src/__tests__/skill-tool-factory.test.ts +96 -95
  340. package/src/__tests__/skills-files-catalog-fallback.test.ts +10 -0
  341. package/src/__tests__/skillssh-files.test.ts +1 -0
  342. package/src/__tests__/slack-channel-config.test.ts +3 -3
  343. package/src/__tests__/starter-task-flow.test.ts +6 -6
  344. package/src/__tests__/strip-memory-injections.test.ts +102 -14
  345. package/src/__tests__/subagent-call-site-routing.test.ts +14 -6
  346. package/src/__tests__/subagent-disposal.test.ts +27 -8
  347. package/src/__tests__/subagent-fork-notifications.test.ts +25 -12
  348. package/src/__tests__/subagent-fork-spawn.test.ts +14 -5
  349. package/src/__tests__/subagent-manager-notify.test.ts +21 -11
  350. package/src/__tests__/subagent-notify-parent.test.ts +6 -7
  351. package/src/__tests__/subagent-spawn-tool-fork.test.ts +59 -1
  352. package/src/__tests__/subagent-tools.test.ts +2 -1
  353. package/src/__tests__/suggestion-routes.test.ts +4 -3
  354. package/src/__tests__/sync-message-contract.test.ts +19 -16
  355. package/src/__tests__/system-prompt.test.ts +112 -0
  356. package/src/__tests__/task-scheduler.test.ts +162 -1
  357. package/src/__tests__/terminal-tools.test.ts +9 -25
  358. package/src/__tests__/test-preload-verifier.ts +68 -0
  359. package/src/__tests__/test-preload.ts +32 -39
  360. package/src/__tests__/thread-backfill.test.ts +4 -9
  361. package/src/__tests__/title-generate-hook.test.ts +319 -0
  362. package/src/__tests__/tool-error-hook.test.ts +278 -0
  363. package/src/__tests__/tool-executor-lifecycle-events.test.ts +20 -7
  364. package/src/__tests__/tool-executor.test.ts +55 -10
  365. package/src/__tests__/tool-preview-lifecycle.test.ts +481 -15
  366. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  367. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  368. package/src/__tests__/tool-result-truncation.test.ts +1 -1
  369. package/src/__tests__/tools-audio-read.test.ts +113 -0
  370. package/src/__tests__/turn-boundary-resolution.test.ts +44 -84
  371. package/src/__tests__/turn-events-store.test.ts +11 -7
  372. package/src/__tests__/twilio-routes.test.ts +3 -2
  373. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  374. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  375. package/src/__tests__/usage-routes.test.ts +285 -1
  376. package/src/__tests__/user-plugin-loader.test.ts +2 -2
  377. package/src/__tests__/validate-input.test.ts +381 -0
  378. package/src/__tests__/verification-control-plane-policy.test.ts +1 -0
  379. package/src/__tests__/voice-scoped-grant-consumer.test.ts +10 -7
  380. package/src/__tests__/voice-session-bridge.test.ts +56 -38
  381. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  382. package/src/__tests__/workspace-migration-090-memory-router-cost-optimized-profile.test.ts +326 -0
  383. package/src/__tests__/workspace-migration-091-retighten-migration-onboarding-thread.test.ts +166 -0
  384. package/src/acp/__tests__/agent-process.test.ts +161 -0
  385. package/src/acp/__tests__/client-handler.test.ts +40 -0
  386. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  387. package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
  388. package/src/acp/__tests__/prepare-agent-env.test.ts +143 -31
  389. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  390. package/src/acp/__tests__/session-manager-resume.test.ts +695 -0
  391. package/src/acp/agent-process.ts +61 -1
  392. package/src/acp/auto-install.test.ts +125 -0
  393. package/src/acp/auto-install.ts +174 -0
  394. package/src/acp/client-handler.ts +31 -0
  395. package/src/acp/feature-gate.test.ts +48 -0
  396. package/src/acp/feature-gate.ts +34 -0
  397. package/src/acp/prepare-agent-env.ts +52 -11
  398. package/src/acp/resolve-agent.test.ts +147 -6
  399. package/src/acp/resolve-agent.ts +81 -7
  400. package/src/acp/resume-hint.ts +22 -0
  401. package/src/acp/session-manager.ts +492 -77
  402. package/src/agent/compaction-circuit.ts +98 -0
  403. package/src/agent/loop.ts +730 -449
  404. package/src/api/README.md +126 -2
  405. package/src/api/constants/call-sites.ts +27 -0
  406. package/src/api/constants/tool-execution.ts +21 -0
  407. package/src/api/events/assistant-activity-state.ts +75 -0
  408. package/src/api/events/assistant-outbound-attachment.ts +49 -0
  409. package/src/api/events/assistant-text-delta.ts +30 -0
  410. package/src/api/events/assistant-thinking-delta.ts +33 -0
  411. package/src/api/events/assistant-turn-start.ts +31 -0
  412. package/src/api/events/avatar-updated.ts +24 -0
  413. package/src/api/events/compaction-circuit-closed.ts +26 -0
  414. package/src/api/events/compaction-circuit-open.ts +28 -0
  415. package/src/api/events/confirmation-request.ts +114 -0
  416. package/src/api/events/contact-request.ts +33 -0
  417. package/src/api/events/conversation-error.ts +77 -0
  418. package/src/api/events/conversation-list-invalidated.ts +38 -0
  419. package/src/api/events/conversation-title-updated.ts +24 -0
  420. package/src/api/events/disk-pressure-status-changed.ts +61 -0
  421. package/src/api/events/document-comment-created.ts +44 -0
  422. package/src/api/events/document-comment-deleted.ts +22 -0
  423. package/src/api/events/document-comment-reopened.ts +23 -0
  424. package/src/api/events/document-comment-resolved.ts +25 -0
  425. package/src/api/events/document-editor-update.ts +27 -0
  426. package/src/api/events/error.ts +32 -0
  427. package/src/api/events/generation-cancelled.ts +22 -0
  428. package/src/api/events/generation-handoff.ts +39 -0
  429. package/src/api/events/home-feed-updated.ts +26 -0
  430. package/src/api/events/identity-changed.ts +32 -0
  431. package/src/api/events/interaction-resolved.ts +50 -0
  432. package/src/api/events/message-complete.ts +40 -0
  433. package/src/api/events/message-dequeued.ts +21 -0
  434. package/src/api/events/message-queued-deleted.ts +23 -0
  435. package/src/api/events/message-queued.ts +22 -0
  436. package/src/api/events/message-request-complete.ts +29 -0
  437. package/src/api/events/navigate-settings.ts +20 -0
  438. package/src/api/events/notification-intent.ts +33 -0
  439. package/src/api/events/open-url.ts +28 -0
  440. package/src/api/events/question-request.ts +67 -0
  441. package/src/{events → api/events}/relationship-state-updated.ts +6 -8
  442. package/src/api/events/secret-request.ts +42 -0
  443. package/src/api/events/subagent-event.ts +79 -0
  444. package/src/api/events/subagent-spawned.ts +40 -0
  445. package/src/api/events/subagent-status-changed.ts +65 -0
  446. package/src/api/events/sync-changed.ts +29 -0
  447. package/src/api/events/tool-output-chunk.ts +45 -0
  448. package/src/api/events/tool-result.ts +129 -0
  449. package/src/api/events/tool-use-preview-start.ts +32 -0
  450. package/src/api/events/tool-use-start.ts +30 -0
  451. package/src/api/events/trace-event.ts +69 -0
  452. package/src/api/events/turn-profile-auto-routed.ts +28 -0
  453. package/src/api/events/ui-surface-complete.ts +30 -0
  454. package/src/api/events/ui-surface-dismiss.ts +22 -0
  455. package/src/api/events/ui-surface-show.ts +67 -0
  456. package/src/api/events/ui-surface-update.ts +26 -0
  457. package/src/api/events/usage-update.ts +34 -0
  458. package/src/api/events/user-message-echo.ts +35 -0
  459. package/src/api/index.ts +517 -3
  460. package/src/api/requests/dictation.ts +45 -0
  461. package/src/api/responses/conversation-message.ts +374 -0
  462. package/src/api/responses/disk-pressure-status.ts +26 -0
  463. package/src/api/responses/home.ts +217 -0
  464. package/src/api/responses/llm-context-response.ts +41 -0
  465. package/src/api/responses/llm-request-log-entry.ts +93 -0
  466. package/src/api/responses/memory-recall-log.ts +65 -0
  467. package/src/api/responses/memory-v2-activation-log.ts +78 -0
  468. package/src/api/responses/memory-v3-selection-log.ts +50 -0
  469. package/src/api/responses/subagent-detail.ts +48 -0
  470. package/src/approvals/guardian-decision-primitive.ts +7 -15
  471. package/src/approvals/guardian-request-resolvers.ts +7 -10
  472. package/src/avatar/__tests__/avatar-manifest.test.ts +236 -0
  473. package/src/avatar/__tests__/avatar-store.test.ts +198 -0
  474. package/src/avatar/avatar-manifest.ts +195 -0
  475. package/src/avatar/avatar-store.ts +113 -0
  476. package/src/avatar/traits-png-sync.ts +8 -2
  477. package/src/background-wake/background-wake-routes.test.ts +687 -52
  478. package/src/background-wake/next-wake.test.ts +31 -1
  479. package/src/background-wake/next-wake.ts +5 -1
  480. package/src/background-wake/platform-client.test.ts +308 -0
  481. package/src/background-wake/platform-client.ts +167 -0
  482. package/src/background-wake/publisher.ts +91 -0
  483. package/src/background-wake/runtime-registry.ts +2 -2
  484. package/src/background-wake/wake-intent-hooks.test.ts +282 -0
  485. package/src/calls/call-conversation-messages.ts +6 -4
  486. package/src/calls/guardian-action-sweep.ts +6 -4
  487. package/src/calls/guardian-dispatch.ts +1 -0
  488. package/src/calls/relay-server.ts +12 -8
  489. package/src/calls/voice-session-bridge.ts +17 -31
  490. package/src/cli/commands/__tests__/conversations-slack.test.ts +16 -0
  491. package/src/cli/commands/__tests__/memory-v3.test.ts +245 -0
  492. package/src/cli/commands/__tests__/notifications.test.ts +242 -54
  493. package/src/cli/commands/avatar.ts +17 -11
  494. package/src/cli/commands/channels/__tests__/channels.test.ts +143 -0
  495. package/src/cli/commands/channels/index.ts +229 -0
  496. package/src/cli/commands/conversations.ts +15 -1
  497. package/src/cli/commands/db/__tests__/repair.test.ts +540 -0
  498. package/src/cli/commands/db/__tests__/status.test.ts +253 -0
  499. package/src/cli/commands/db/format.ts +48 -0
  500. package/src/cli/commands/db/index.ts +29 -0
  501. package/src/cli/commands/db/repair-step-conversation-backfill.ts +345 -0
  502. package/src/cli/commands/db/repair-step-integrity.ts +146 -0
  503. package/src/cli/commands/db/repair-steps.ts +164 -0
  504. package/src/cli/commands/db/repair.ts +141 -0
  505. package/src/cli/commands/db/status.ts +366 -0
  506. package/src/cli/commands/memory-v3.ts +168 -203
  507. package/src/cli/commands/notifications.ts +427 -65
  508. package/src/cli/lib/cli-colors.ts +24 -6
  509. package/src/cli/lib/open-browser.ts +7 -2
  510. package/src/cli/program.ts +6 -5
  511. package/src/config/__tests__/feature-flag-registry-guard.test.ts +4 -4
  512. package/src/config/acp-defaults.test.ts +10 -0
  513. package/src/config/acp-defaults.ts +6 -0
  514. package/src/config/assistant-feature-flags.ts +47 -55
  515. package/src/config/bundled-skills/acp/SKILL.md +64 -30
  516. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  517. package/src/config/bundled-skills/app-builder/SKILL.md +224 -387
  518. package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
  519. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  520. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  521. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  522. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  523. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  524. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  525. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  526. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  527. package/src/config/bundled-skills/document-editor/SKILL.md +33 -24
  528. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  529. package/src/config/bundled-skills/media-processing/services/reduce.ts +6 -9
  530. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  531. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +7 -2
  532. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  533. package/src/config/bundled-skills/schedule/TOOLS.json +10 -2
  534. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +1 -0
  535. package/src/config/bundled-tool-registry.ts +2 -0
  536. package/src/config/call-site-defaults.ts +3 -8
  537. package/src/config/feature-flag-cache.ts +86 -0
  538. package/src/config/feature-flag-registry.json +81 -25
  539. package/src/config/llm-context-resolution.ts +10 -1
  540. package/src/config/llm-resolver.ts +121 -15
  541. package/src/config/loader.ts +4 -5
  542. package/src/config/schemas/__tests__/memory-v2.test.ts +2 -211
  543. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  544. package/src/config/schemas/call-site-catalog.ts +8 -15
  545. package/src/config/schemas/heartbeat.ts +10 -1
  546. package/src/config/schemas/llm.ts +93 -4
  547. package/src/config/schemas/memory-lifecycle.ts +24 -0
  548. package/src/config/schemas/memory-v2.ts +8 -227
  549. package/src/config/schemas/memory-v3.ts +47 -0
  550. package/src/config/schemas/memory.ts +6 -1
  551. package/src/config/schemas/platform.ts +8 -0
  552. package/src/config/schemas/services.ts +6 -2
  553. package/src/config/schemas/timeouts.ts +3 -1
  554. package/src/config/seed-inference-profiles.ts +38 -18
  555. package/src/config/skills.ts +13 -0
  556. package/src/context/compactor.ts +55 -32
  557. package/src/context/strip-injections.ts +128 -0
  558. package/src/context/token-estimator.ts +52 -5
  559. package/src/context/tool-result-truncation.ts +1 -66
  560. package/src/context/window-manager.ts +141 -26
  561. package/src/credential-execution/executable-discovery.ts +56 -0
  562. package/src/credential-execution/process-manager.ts +6 -2
  563. package/src/credential-health/credential-health-service.ts +125 -40
  564. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +9 -6
  565. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +15 -17
  566. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -2
  567. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -0
  568. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  569. package/src/daemon/__tests__/meet-manifest-loader.test.ts +25 -12
  570. package/src/daemon/__tests__/native-web-search-metadata.test.ts +11 -8
  571. package/src/daemon/__tests__/switch-inference-profile-tool.test.ts +107 -0
  572. package/src/daemon/__tests__/web-search-status-text.test.ts +11 -6
  573. package/src/daemon/approval-generators.ts +4 -4
  574. package/src/daemon/assistant-attachments.ts +1 -1
  575. package/src/daemon/config-watcher.ts +7 -1
  576. package/src/daemon/context-overflow-reducer.ts +0 -1
  577. package/src/daemon/conversation-agent-loop-handlers.ts +1146 -247
  578. package/src/daemon/conversation-agent-loop.ts +570 -1457
  579. package/src/daemon/conversation-error.ts +40 -12
  580. package/src/daemon/conversation-history.ts +27 -10
  581. package/src/daemon/conversation-launch.ts +4 -8
  582. package/src/daemon/conversation-lifecycle.ts +13 -42
  583. package/src/daemon/conversation-messaging.ts +89 -49
  584. package/src/daemon/conversation-notifiers.ts +7 -5
  585. package/src/daemon/conversation-process.ts +183 -130
  586. package/src/daemon/conversation-registry.ts +159 -0
  587. package/src/daemon/conversation-runtime-assembly.ts +209 -362
  588. package/src/daemon/conversation-skill-tools.ts +14 -30
  589. package/src/daemon/conversation-slash.ts +6 -25
  590. package/src/daemon/conversation-store.ts +15 -95
  591. package/src/daemon/conversation-surfaces.ts +346 -107
  592. package/src/daemon/conversation-tool-setup.ts +38 -77
  593. package/src/daemon/conversation-workspace.ts +17 -0
  594. package/src/daemon/conversation.ts +143 -186
  595. package/src/daemon/daemon-control.ts +1 -1
  596. package/src/daemon/daemon-skill-host.ts +7 -4
  597. package/src/daemon/disk-pressure-guard.ts +54 -50
  598. package/src/daemon/external-plugins-bootstrap.ts +53 -32
  599. package/src/daemon/first-greeting.ts +53 -13
  600. package/src/daemon/guardian-action-generators.ts +2 -2
  601. package/src/daemon/handlers/config-a2a.ts +51 -36
  602. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  603. package/src/daemon/handlers/config-telegram.ts +16 -2
  604. package/src/daemon/handlers/conversations.ts +9 -23
  605. package/src/daemon/handlers/shared.ts +158 -77
  606. package/src/daemon/handlers/skills.ts +53 -20
  607. package/src/daemon/host-app-control-proxy.ts +54 -1
  608. package/src/daemon/host-cu-proxy.ts +46 -22
  609. package/src/daemon/host-file-proxy.ts +25 -1
  610. package/src/daemon/host-proxy-preactivation.ts +25 -6
  611. package/src/daemon/lifecycle.ts +65 -67
  612. package/src/daemon/mcp-reload-service.ts +1 -1
  613. package/src/daemon/meet-manifest-loader.ts +10 -17
  614. package/src/daemon/message-protocol.ts +2 -3
  615. package/src/daemon/message-provenance.ts +49 -0
  616. package/src/daemon/message-types/apps.ts +1 -29
  617. package/src/daemon/message-types/contacts.ts +3 -20
  618. package/src/daemon/message-types/conversations.ts +25 -125
  619. package/src/daemon/message-types/document-comments.ts +8 -44
  620. package/src/daemon/message-types/documents.ts +3 -9
  621. package/src/daemon/message-types/home.ts +5 -18
  622. package/src/daemon/message-types/integrations.ts +4 -13
  623. package/src/daemon/message-types/messages.ts +50 -428
  624. package/src/daemon/message-types/notifications.ts +2 -32
  625. package/src/daemon/message-types/settings.ts +3 -8
  626. package/src/daemon/message-types/skills.ts +4 -0
  627. package/src/daemon/message-types/subagents.ts +6 -0
  628. package/src/daemon/message-types/surfaces.ts +138 -3
  629. package/src/daemon/message-types/sync.ts +12 -25
  630. package/src/daemon/message-types/workspace.ts +3 -11
  631. package/src/daemon/now-scratchpad.ts +21 -0
  632. package/src/daemon/orphan-reaper.test.ts +210 -0
  633. package/src/daemon/orphan-reaper.ts +240 -0
  634. package/src/daemon/overflow-reduction-loop.ts +230 -0
  635. package/src/daemon/persist-unsendable-image.ts +117 -0
  636. package/src/daemon/process-message.ts +59 -58
  637. package/src/daemon/providers-setup.ts +1 -1
  638. package/src/daemon/server.ts +30 -0
  639. package/src/daemon/switch-inference-profile-tool.ts +13 -3
  640. package/src/daemon/tool-setup-types.ts +0 -6
  641. package/src/daemon/tool-side-effects.ts +10 -7
  642. package/src/daemon/trace-emitter.ts +6 -4
  643. package/src/daemon/trust-context.ts +32 -0
  644. package/src/daemon/wake-target-adapter.ts +24 -2
  645. package/src/documents/document-store.ts +38 -0
  646. package/src/export/__tests__/transcript-formatter.test.ts +1 -0
  647. package/src/heartbeat/__tests__/heartbeat-service.test.ts +34 -0
  648. package/src/heartbeat/heartbeat-run-store.ts +54 -1
  649. package/src/heartbeat/heartbeat-service.ts +105 -0
  650. package/src/home/__tests__/feed-writer.test.ts +161 -0
  651. package/src/home/__tests__/post-connect-feed.test.ts +1 -0
  652. package/src/home/__tests__/suggested-prompts.test.ts +55 -59
  653. package/src/home/feed-types.ts +36 -221
  654. package/src/home/feed-writer.ts +146 -7
  655. package/src/home/home-greeting-cache.ts +24 -1
  656. package/src/home/suggested-prompts.ts +27 -145
  657. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  658. package/src/ipc/__tests__/cli-ipc.test.ts +1 -0
  659. package/src/ipc/__tests__/email-ipc.test.ts +0 -9
  660. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  661. package/src/ipc/gateway-client.test.ts +6 -3
  662. package/src/ipc/gateway-client.ts +3 -3
  663. package/src/ipc/routes/__tests__/route-adapter.test.ts +244 -0
  664. package/src/ipc/routes/route-adapter.ts +45 -6
  665. package/src/ipc/skill-routes/__tests__/memory.test.ts +34 -9
  666. package/src/ipc/skill-routes/__tests__/providers.test.ts +10 -10
  667. package/src/ipc/skill-routes/__tests__/registries.test.ts +59 -20
  668. package/src/ipc/skill-routes/memory.ts +29 -13
  669. package/src/ipc/skill-routes/providers.ts +5 -6
  670. package/src/ipc/skill-routes/registries.ts +39 -88
  671. package/src/live-voice/__tests__/live-voice-archive.test.ts +24 -11
  672. package/src/media/gemini-image-service.ts +15 -0
  673. package/src/media/openai-image-service.ts +14 -0
  674. package/src/media/types.ts +34 -0
  675. package/src/memory/__tests__/conversation-queries.test.ts +192 -8
  676. package/src/memory/__tests__/db-maintenance.test.ts +128 -0
  677. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +1 -0
  678. package/src/memory/__tests__/jobs-store-job-classes.test.ts +5 -4
  679. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +82 -5
  680. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +1 -0
  681. package/src/memory/__tests__/memory-retrospective-job.test.ts +11 -6
  682. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +1 -0
  683. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +31 -0
  684. package/src/memory/__tests__/memory-v3-selections-migration.test.ts +103 -0
  685. package/src/memory/auth-fallback-events-store.ts +94 -0
  686. package/src/memory/context-search/agent-runner.ts +2 -4
  687. package/src/memory/conversation-attention-store.ts +17 -3
  688. package/src/memory/conversation-crud.ts +386 -115
  689. package/src/memory/conversation-queries.ts +78 -22
  690. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  691. package/src/memory/conversation-title-service.ts +65 -41
  692. package/src/memory/db-connection.ts +29 -19
  693. package/src/memory/db-init.ts +18 -0
  694. package/src/memory/db-maintenance.ts +18 -2
  695. package/src/memory/db-singleton.ts +77 -0
  696. package/src/memory/delivery-channels.ts +82 -0
  697. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  698. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +2 -4
  699. package/src/memory/graph/consolidation.ts +8 -11
  700. package/src/memory/graph/conversation-graph-memory.ts +106 -8
  701. package/src/memory/graph/extraction.ts +6 -9
  702. package/src/memory/graph/narrative.ts +2 -2
  703. package/src/memory/graph/pattern-scan.ts +2 -2
  704. package/src/memory/graph/retriever.test.ts +3 -3
  705. package/src/memory/graph/retriever.ts +20 -26
  706. package/src/memory/graph/tools.ts +4 -4
  707. package/src/memory/job-handlers/conversation-starters.ts +45 -34
  708. package/src/memory/job-handlers/embedding.test.ts +3 -2
  709. package/src/memory/job-handlers/summarization.ts +1 -2
  710. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -2
  711. package/src/memory/jobs-store.ts +36 -1
  712. package/src/memory/jobs-worker.ts +93 -43
  713. package/src/memory/llm-request-log-source-clickhouse.ts +55 -1
  714. package/src/memory/llm-request-log-source-local.ts +13 -0
  715. package/src/memory/llm-request-log-source.ts +21 -6
  716. package/src/memory/llm-request-log-store.ts +147 -3
  717. package/src/memory/llm-usage-store.ts +234 -50
  718. package/src/memory/memory-marker.ts +17 -0
  719. package/src/memory/memory-retrospective-job.ts +6 -2
  720. package/src/memory/memory-v2-activation-log-store.ts +13 -1
  721. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  722. package/src/memory/migrations/265-drop-provider-connection-status.ts +26 -0
  723. package/src/memory/migrations/266-messages-client-message-id.ts +43 -0
  724. package/src/memory/migrations/267-llm-usage-events-add-assistant-version.ts +46 -0
  725. package/src/memory/migrations/268-add-memory-v3-selections.ts +28 -0
  726. package/src/memory/migrations/269-schedule-script-timeout.ts +11 -0
  727. package/src/memory/migrations/270-messages-role-created-at-index.ts +18 -0
  728. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  729. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  730. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  731. package/src/memory/migrations/__tests__/267-llm-usage-events-add-assistant-version.test.ts +117 -0
  732. package/src/memory/migrations/index.ts +9 -0
  733. package/src/memory/pkb/autoinject.ts +61 -0
  734. package/src/memory/pkb/context.ts +50 -0
  735. package/src/memory/pkb/types.ts +14 -0
  736. package/src/memory/schedule-attribution-sql.ts +104 -0
  737. package/src/memory/schema/acp.ts +4 -0
  738. package/src/memory/schema/conversations.ts +9 -1
  739. package/src/memory/schema/inference.ts +0 -1
  740. package/src/memory/schema/infrastructure.ts +27 -0
  741. package/src/memory/usage-grouped-buckets.ts +6 -1
  742. package/src/memory/v2/__tests__/backfill-jobs.test.ts +5 -2
  743. package/src/memory/v2/__tests__/consolidation-job.test.ts +125 -1
  744. package/src/memory/v2/__tests__/harness-metrics.test.ts +9 -0
  745. package/src/memory/v2/__tests__/harness-replay-input.test.ts +9 -4
  746. package/src/memory/v2/__tests__/harness-runner.test.ts +26 -0
  747. package/src/memory/v2/__tests__/migration.test.ts +11 -3
  748. package/src/memory/v2/__tests__/page-index.test.ts +37 -1
  749. package/src/memory/v2/__tests__/router.test.ts +14 -4
  750. package/src/memory/v2/__tests__/sweep-job.test.ts +9 -5
  751. package/src/memory/v2/backfill-jobs.ts +6 -0
  752. package/src/memory/v2/consolidation-job.ts +99 -10
  753. package/src/memory/v2/harness/metrics.ts +5 -1
  754. package/src/memory/v2/harness/replay-input.ts +19 -3
  755. package/src/memory/v2/harness/runner.ts +6 -0
  756. package/src/memory/v2/harness/trace.ts +6 -0
  757. package/src/memory/v2/migration.ts +5 -3
  758. package/src/memory/v2/page-index.ts +11 -0
  759. package/src/memory/v2/router.ts +8 -11
  760. package/src/memory/v2/sweep-job.ts +8 -11
  761. package/src/memory/v2/types.ts +1 -0
  762. package/src/messaging/providers/slack/render-transcript.test.ts +1 -1
  763. package/src/messaging/providers/slack/render-transcript.ts +2 -2
  764. package/src/messaging/style-analyzer.ts +8 -11
  765. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  766. package/src/notifications/__tests__/home-feed-side-effect.test.ts +1 -0
  767. package/src/notifications/adapters/slack.ts +45 -11
  768. package/src/notifications/broadcaster.ts +114 -63
  769. package/src/notifications/conversation-pairing.ts +23 -8
  770. package/src/notifications/decision-engine.ts +16 -16
  771. package/src/notifications/decisions-store.ts +32 -1
  772. package/src/notifications/deliveries-store.ts +45 -0
  773. package/src/notifications/edit-notification.ts +201 -0
  774. package/src/notifications/emit-signal.ts +11 -1
  775. package/src/notifications/home-feed-side-effect.ts +12 -1
  776. package/src/notifications/preference-extractor.ts +11 -14
  777. package/src/notifications/signal.ts +10 -0
  778. package/src/notifications/types.ts +37 -0
  779. package/src/oauth/byo-connection.test.ts +67 -3
  780. package/src/oauth/byo-connection.ts +32 -5
  781. package/src/oauth/connect-orchestrator.ts +9 -0
  782. package/src/oauth/connection-resolver.test.ts +76 -0
  783. package/src/oauth/connection-resolver.ts +49 -10
  784. package/src/oauth/manual-token-connection.ts +51 -3
  785. package/src/oauth/seed-providers.ts +3 -0
  786. package/src/permissions/approval-policy.test.ts +19 -5
  787. package/src/permissions/approval-policy.ts +14 -3
  788. package/src/permissions/checker.ts +21 -8
  789. package/src/permissions/prompter.ts +46 -36
  790. package/src/permissions/question-prompter.test.ts +35 -26
  791. package/src/permissions/question-prompter.ts +6 -10
  792. package/src/platform/client.test.ts +24 -1
  793. package/src/platform/client.ts +8 -0
  794. package/src/platform/feature-gate.ts +15 -0
  795. package/src/plugin-api/constants.ts +4 -0
  796. package/src/plugin-api/index.ts +10 -1
  797. package/src/plugin-api/types.ts +176 -4
  798. package/src/plugins/defaults/compaction/compact.ts +59 -0
  799. package/src/plugins/defaults/compaction/package.json +15 -0
  800. package/src/plugins/defaults/compaction/register.ts +24 -0
  801. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  802. package/src/plugins/defaults/empty-response/package.json +15 -0
  803. package/src/plugins/defaults/empty-response/register.ts +23 -0
  804. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +35 -0
  805. package/src/plugins/defaults/history-repair/package.json +15 -0
  806. package/src/plugins/defaults/history-repair/register.ts +24 -0
  807. package/src/{daemon/history-repair.ts → plugins/defaults/history-repair/terminal.ts} +48 -35
  808. package/src/plugins/defaults/index.ts +22 -49
  809. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +95 -0
  810. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  811. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  812. package/src/plugins/defaults/{injectors.ts → memory-retrieval/injectors.ts} +297 -120
  813. package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +242 -0
  814. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +118 -0
  815. package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +39 -0
  816. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +36 -0
  817. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +37 -0
  818. package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +219 -0
  819. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +330 -0
  820. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +288 -0
  821. package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +107 -0
  822. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +436 -0
  823. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +13 -0
  824. package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +274 -0
  825. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +61 -0
  826. package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +332 -0
  827. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +179 -0
  828. package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +470 -0
  829. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +432 -0
  830. package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +168 -0
  831. package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +192 -0
  832. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +54 -0
  833. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +106 -0
  834. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +44 -0
  835. package/src/plugins/defaults/memory-v3-shadow/assign.ts +268 -0
  836. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +124 -0
  837. package/src/plugins/defaults/memory-v3-shadow/core.ts +26 -0
  838. package/src/plugins/defaults/memory-v3-shadow/data/README.md +84 -0
  839. package/src/plugins/defaults/memory-v3-shadow/data/assignments.json +5 -0
  840. package/src/plugins/defaults/memory-v3-shadow/data/core.json +1 -0
  841. package/src/plugins/defaults/memory-v3-shadow/data/leaves/domain-a/topic-x.md +9 -0
  842. package/src/plugins/defaults/memory-v3-shadow/data/leaves/domain-a/topic-y.md +9 -0
  843. package/src/plugins/defaults/memory-v3-shadow/data/leaves/domain-b/topic-z.md +9 -0
  844. package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
  845. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  846. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  847. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  848. package/src/plugins/defaults/memory-v3-shadow/llm-retry.ts +32 -0
  849. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +314 -0
  850. package/src/plugins/defaults/memory-v3-shadow/needle.ts +115 -0
  851. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +126 -0
  852. package/src/plugins/defaults/memory-v3-shadow/package.json +15 -0
  853. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +34 -0
  854. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +26 -0
  855. package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +523 -0
  856. package/src/plugins/defaults/memory-v3-shadow/register.ts +26 -0
  857. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +32 -0
  858. package/src/plugins/defaults/memory-v3-shadow/router.ts +190 -0
  859. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +84 -0
  860. package/src/plugins/defaults/memory-v3-shadow/selector.ts +226 -0
  861. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +349 -0
  862. package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +209 -0
  863. package/src/plugins/defaults/memory-v3-shadow/tree.ts +174 -0
  864. package/src/plugins/defaults/memory-v3-shadow/types.ts +59 -0
  865. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +88 -0
  866. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  867. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  868. package/src/plugins/defaults/title-generate/package.json +15 -0
  869. package/src/plugins/defaults/title-generate/register.ts +35 -0
  870. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  871. package/src/plugins/defaults/tool-error/package.json +15 -0
  872. package/src/plugins/defaults/tool-error/register.ts +23 -0
  873. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  874. package/src/plugins/defaults/tool-result-truncate/package.json +15 -0
  875. package/src/plugins/defaults/tool-result-truncate/register.ts +24 -0
  876. package/src/plugins/defaults/tool-result-truncate/terminal.ts +132 -0
  877. package/src/plugins/external-plugin-loader.ts +2 -2
  878. package/src/plugins/pipeline.ts +8 -35
  879. package/src/plugins/registry.ts +8 -25
  880. package/src/plugins/types.ts +62 -677
  881. package/src/plugins/user-loader.ts +4 -3
  882. package/src/proactive-artifact/aux-message-injector.ts +4 -5
  883. package/src/proactive-artifact/job.test.ts +29 -21
  884. package/src/proactive-artifact/job.ts +3 -1
  885. package/src/prompts/__tests__/system-prompt.test.ts +46 -4
  886. package/src/prompts/sections.ts +20 -7
  887. package/src/prompts/system-prompt.ts +38 -40
  888. package/src/prompts/template-detection.ts +10 -4
  889. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +64 -0
  890. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +2 -2
  891. package/src/prompts/templates/BOOTSTRAP.md +12 -12
  892. package/src/prompts/templates/IDENTITY.md +0 -2
  893. package/src/prompts/templates/system-sections.ts +21 -0
  894. package/src/providers/__tests__/connection-model-compat.test.ts +3 -4
  895. package/src/providers/__tests__/registry-native-web-search.test.ts +122 -0
  896. package/src/providers/__tests__/retry-callsite.test.ts +25 -25
  897. package/src/providers/__tests__/satellite-connection-routing.test.ts +7 -21
  898. package/src/providers/anthropic/client.ts +61 -34
  899. package/src/providers/call-site-routing.ts +34 -18
  900. package/src/providers/connection-model-compat.ts +23 -0
  901. package/src/providers/connection-resolution.ts +39 -20
  902. package/src/providers/fireworks/client.ts +1 -0
  903. package/src/providers/gemini/client.ts +176 -37
  904. package/src/providers/gemini/inline-media.ts +74 -0
  905. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +0 -2
  906. package/src/providers/inference/__tests__/base-url-security.test.ts +2 -3
  907. package/src/providers/inference/__tests__/{connections-status-label.test.ts → connections-label.test.ts} +12 -111
  908. package/src/providers/inference/auth.ts +0 -8
  909. package/src/providers/inference/connections.ts +3 -66
  910. package/src/providers/inference/resolve-auth.ts +2 -3
  911. package/src/providers/model-catalog.ts +35 -1
  912. package/src/providers/model-intents.ts +3 -3
  913. package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
  914. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +269 -7
  915. package/src/providers/openai/chat-completions-provider.ts +155 -16
  916. package/src/providers/openai/codex-models.ts +2 -0
  917. package/src/providers/openai/responses-provider.ts +54 -57
  918. package/src/providers/openrouter/client.ts +15 -14
  919. package/src/providers/placeholder-sentinels.ts +35 -0
  920. package/src/providers/provider-send-message.ts +23 -14
  921. package/src/providers/ratelimit.ts +1 -9
  922. package/src/providers/registry.ts +48 -8
  923. package/src/providers/retry.ts +16 -9
  924. package/src/providers/search-provider-catalog.ts +17 -9
  925. package/src/providers/types.ts +20 -2
  926. package/src/providers/usage-tracking.ts +1 -9
  927. package/src/runtime/__tests__/agent-wake.test.ts +142 -32
  928. package/src/runtime/__tests__/background-job-runner.test.ts +2 -3
  929. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  930. package/src/runtime/access-request-helper.ts +1 -0
  931. package/src/runtime/agent-wake.ts +95 -23
  932. package/src/runtime/assistant-event-hub.ts +38 -8
  933. package/src/runtime/assistant-stream-state.ts +368 -0
  934. package/src/runtime/auth/__tests__/guard-tests.test.ts +75 -109
  935. package/src/runtime/auth/__tests__/route-policy.test.ts +153 -170
  936. package/src/runtime/auth/route-policy.ts +42 -1069
  937. package/src/runtime/background-job-runner.ts +1 -4
  938. package/src/runtime/btw-sidechain.ts +3 -1
  939. package/src/runtime/channel-approvals.ts +4 -15
  940. package/src/runtime/channel-invite-transport.ts +5 -6
  941. package/src/runtime/channel-readiness-service.ts +70 -5
  942. package/src/runtime/channel-reply-delivery.ts +23 -0
  943. package/src/runtime/channel-retry-sweep.ts +59 -30
  944. package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
  945. package/src/runtime/http-router.ts +35 -43
  946. package/src/runtime/http-types.ts +23 -71
  947. package/src/runtime/interactive-ui.ts +1 -1
  948. package/src/runtime/invite-instruction-generator.ts +3 -3
  949. package/src/runtime/migrations/vbundle-builder.ts +3 -2
  950. package/src/runtime/pending-interactions.ts +3 -2
  951. package/src/runtime/routes/__tests__/acp-routes.test.ts +253 -55
  952. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +565 -0
  953. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +1 -0
  954. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  955. package/src/runtime/routes/__tests__/content-source-routes.test.ts +4 -4
  956. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +436 -0
  957. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +237 -0
  958. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +129 -1
  959. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  960. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +209 -1
  961. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +26 -72
  962. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  963. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +58 -5
  964. package/src/runtime/routes/__tests__/sanity-routes.test.ts +6 -6
  965. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +3 -2
  966. package/src/runtime/routes/__tests__/stt-routes.test.ts +3 -3
  967. package/src/runtime/routes/__tests__/suggest-trust-rule-routes.test.ts +5 -2
  968. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  969. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +297 -0
  970. package/src/runtime/routes/__tests__/task-routes.test.ts +48 -3
  971. package/src/runtime/routes/__tests__/tts-routes.test.ts +9 -5
  972. package/src/runtime/routes/acp-routes-list.test.ts +3 -0
  973. package/src/runtime/routes/acp-routes.test.ts +186 -100
  974. package/src/runtime/routes/acp-routes.ts +110 -35
  975. package/src/runtime/routes/app-management-routes.ts +175 -106
  976. package/src/runtime/routes/app-routes.ts +38 -20
  977. package/src/runtime/routes/approval-routes.ts +17 -5
  978. package/src/runtime/routes/attachment-routes.ts +51 -16
  979. package/src/runtime/routes/audio-routes.ts +1 -0
  980. package/src/runtime/routes/audit-routes.ts +5 -0
  981. package/src/runtime/routes/auth-routes.ts +5 -0
  982. package/src/runtime/routes/avatar-routes.ts +264 -59
  983. package/src/runtime/routes/background-tool-routes.ts +9 -0
  984. package/src/runtime/routes/background-wake-routes.ts +201 -23
  985. package/src/runtime/routes/backup-routes.ts +45 -0
  986. package/src/runtime/routes/bookmark-routes.ts +13 -0
  987. package/src/runtime/routes/brain-graph-routes.ts +9 -0
  988. package/src/runtime/routes/browser-routes.ts +6 -1
  989. package/src/runtime/routes/browser-tabs-routes.ts +11 -10
  990. package/src/runtime/routes/btw-routes.ts +37 -27
  991. package/src/runtime/routes/cache-routes.ts +13 -0
  992. package/src/runtime/routes/call-routes.ts +21 -10
  993. package/src/runtime/routes/channel-availability-routes.ts +5 -1
  994. package/src/runtime/routes/channel-readiness-routes.ts +37 -4
  995. package/src/runtime/routes/channel-route-definitions.ts +21 -0
  996. package/src/runtime/routes/channel-verification-routes.ts +21 -0
  997. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +9 -2
  998. package/src/runtime/routes/client-routes.ts +9 -0
  999. package/src/runtime/routes/consolidation-routes.ts +133 -25
  1000. package/src/runtime/routes/contact-prompt-routes.ts +9 -0
  1001. package/src/runtime/routes/contact-routes.ts +90 -23
  1002. package/src/runtime/routes/content-source-routes.ts +5 -1
  1003. package/src/runtime/routes/conversation-analysis-routes.ts +11 -1
  1004. package/src/runtime/routes/conversation-attention-routes.ts +5 -0
  1005. package/src/runtime/routes/conversation-cli-routes.ts +54 -7
  1006. package/src/runtime/routes/conversation-compaction-routes.ts +292 -0
  1007. package/src/runtime/routes/conversation-list-routes.ts +225 -9
  1008. package/src/runtime/routes/conversation-management-routes.ts +96 -28
  1009. package/src/runtime/routes/conversation-query-routes.ts +150 -51
  1010. package/src/runtime/routes/conversation-routes.ts +615 -327
  1011. package/src/runtime/routes/conversation-starter-routes.ts +35 -20
  1012. package/src/runtime/routes/conversations-import-routes.ts +48 -13
  1013. package/src/runtime/routes/credential-prompt-routes.ts +5 -0
  1014. package/src/runtime/routes/credential-routes.ts +25 -6
  1015. package/src/runtime/routes/debug-bash-routes.ts +5 -0
  1016. package/src/runtime/routes/debug-routes.ts +11 -2
  1017. package/src/runtime/routes/defer-routes.ts +13 -0
  1018. package/src/runtime/routes/diagnostics-routes.ts +37 -46
  1019. package/src/runtime/routes/disk-pressure-routes.ts +17 -31
  1020. package/src/runtime/routes/document-comments-routes.ts +46 -27
  1021. package/src/runtime/routes/documents-routes.ts +35 -11
  1022. package/src/runtime/routes/domain-routes.ts +98 -51
  1023. package/src/runtime/routes/email-routes.ts +33 -0
  1024. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  1025. package/src/runtime/routes/events-routes.ts +107 -8
  1026. package/src/runtime/routes/filing-routes.ts +9 -4
  1027. package/src/runtime/routes/gateway-log-routes.ts +31 -4
  1028. package/src/runtime/routes/global-search-routes.ts +53 -50
  1029. package/src/runtime/routes/group-routes.ts +32 -5
  1030. package/src/runtime/routes/guardian-action-routes.ts +9 -0
  1031. package/src/runtime/routes/guardian-approval-interception.ts +0 -31
  1032. package/src/runtime/routes/heartbeat-routes.ts +57 -21
  1033. package/src/runtime/routes/home-feed-routes.ts +149 -16
  1034. package/src/runtime/routes/home-state-routes.ts +8 -40
  1035. package/src/runtime/routes/host-app-control-routes.ts +6 -1
  1036. package/src/runtime/routes/host-bash-routes.ts +5 -0
  1037. package/src/runtime/routes/host-browser-routes.ts +13 -0
  1038. package/src/runtime/routes/host-cu-routes.ts +6 -1
  1039. package/src/runtime/routes/host-file-routes.ts +26 -6
  1040. package/src/runtime/routes/host-transfer-routes.ts +13 -2
  1041. package/src/runtime/routes/http-adapter.ts +1 -2
  1042. package/src/runtime/routes/identity-intro-cache.ts +72 -39
  1043. package/src/runtime/routes/identity-routes.ts +257 -20
  1044. package/src/runtime/routes/image-generation-routes.ts +45 -2
  1045. package/src/runtime/routes/inbound-message-handler.ts +16 -12
  1046. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +524 -12
  1047. package/src/runtime/routes/inbound-stages/background-dispatch.ts +72 -27
  1048. package/src/runtime/routes/index.ts +4 -0
  1049. package/src/runtime/routes/inference-profile-session-routes.ts +13 -3
  1050. package/src/runtime/routes/inference-provider-connection-routes.ts +26 -31
  1051. package/src/runtime/routes/inference-send-routes.ts +11 -11
  1052. package/src/runtime/routes/integrations/a2a.ts +32 -7
  1053. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  1054. package/src/runtime/routes/integrations/slack/channel.ts +23 -3
  1055. package/src/runtime/routes/integrations/slack/share.ts +36 -8
  1056. package/src/runtime/routes/integrations/telegram.ts +34 -9
  1057. package/src/runtime/routes/integrations/twilio.ts +77 -7
  1058. package/src/runtime/routes/integrations/vercel.ts +18 -3
  1059. package/src/runtime/routes/internal-oauth-routes.ts +5 -0
  1060. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  1061. package/src/runtime/routes/internal-twilio-routes.ts +13 -0
  1062. package/src/runtime/routes/llm-call-sites-routes.ts +39 -4
  1063. package/src/runtime/routes/llm-context-normalization.ts +7 -2
  1064. package/src/runtime/routes/log-export-routes.ts +36 -10
  1065. package/src/runtime/routes/mcp-auth-routes.ts +25 -0
  1066. package/src/runtime/routes/memory-item-routes.ts +21 -10
  1067. package/src/runtime/routes/memory-v2-routes.ts +105 -44
  1068. package/src/runtime/routes/memory-v3-routes.ts +313 -257
  1069. package/src/runtime/routes/migration-rollback-routes.ts +5 -1
  1070. package/src/runtime/routes/migration-routes.ts +49 -13
  1071. package/src/runtime/routes/notification-routes.ts +80 -2
  1072. package/src/runtime/routes/oauth-apps.ts +99 -23
  1073. package/src/runtime/routes/oauth-commands-routes.ts +43 -15
  1074. package/src/runtime/routes/oauth-connect-routes.ts +9 -0
  1075. package/src/runtime/routes/oauth-lifecycle-routes.ts +5 -1
  1076. package/src/runtime/routes/oauth-providers.ts +79 -15
  1077. package/src/runtime/routes/platform-routes.ts +102 -5
  1078. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +9 -6
  1079. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +37 -16
  1080. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +7 -3
  1081. package/src/runtime/routes/playground/__tests__/state.test.ts +10 -3
  1082. package/src/runtime/routes/playground/force-compact.ts +2 -2
  1083. package/src/runtime/routes/playground/helpers.ts +1 -2
  1084. package/src/runtime/routes/playground/inject-failures.ts +13 -8
  1085. package/src/runtime/routes/playground/reset-circuit.ts +14 -9
  1086. package/src/runtime/routes/playground/seed-conversation.ts +1 -1
  1087. package/src/runtime/routes/playground/seeded-conversations.ts +3 -3
  1088. package/src/runtime/routes/playground/state.ts +4 -3
  1089. package/src/runtime/routes/plugins-routes.ts +22 -19
  1090. package/src/runtime/routes/profiler-routes.ts +17 -4
  1091. package/src/runtime/routes/ps-routes.ts +5 -0
  1092. package/src/runtime/routes/publish-routes.ts +13 -3
  1093. package/src/runtime/routes/question-routes.ts +5 -0
  1094. package/src/runtime/routes/recording-routes.ts +25 -12
  1095. package/src/runtime/routes/rename-conversation-routes.ts +10 -0
  1096. package/src/runtime/routes/sanity-routes.ts +9 -2
  1097. package/src/runtime/routes/schedule-routes.ts +288 -88
  1098. package/src/runtime/routes/secret-routes.ts +31 -6
  1099. package/src/runtime/routes/sequence-routes.ts +33 -0
  1100. package/src/runtime/routes/settings-routes.ts +65 -19
  1101. package/src/runtime/routes/skills-routes.ts +166 -73
  1102. package/src/runtime/routes/slack-channel-routes.ts +5 -0
  1103. package/src/runtime/routes/stt-routes.ts +13 -6
  1104. package/src/runtime/routes/subagents-routes.ts +24 -18
  1105. package/src/runtime/routes/suggest-trust-rule-routes.ts +7 -2
  1106. package/src/runtime/routes/surface-action-routes.ts +10 -38
  1107. package/src/runtime/routes/surface-content-routes.ts +21 -6
  1108. package/src/runtime/routes/surface-conversation-resolver.ts +66 -0
  1109. package/src/runtime/routes/task-routes.ts +37 -0
  1110. package/src/runtime/routes/telemetry-routes.ts +9 -0
  1111. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  1112. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  1113. package/src/runtime/routes/trace-event-routes.ts +42 -1
  1114. package/src/runtime/routes/trust-rules-routes.ts +31 -2
  1115. package/src/runtime/routes/tts-routes.ts +48 -6
  1116. package/src/runtime/routes/types.ts +83 -16
  1117. package/src/runtime/routes/ui-request-routes.ts +5 -0
  1118. package/src/runtime/routes/upgrade-broadcast-routes.ts +5 -0
  1119. package/src/runtime/routes/usage-routes.ts +118 -42
  1120. package/src/runtime/routes/user-routes-cli.ts +9 -0
  1121. package/src/runtime/routes/user-routes.ts +5 -1
  1122. package/src/runtime/routes/wake-conversation-routes.ts +5 -0
  1123. package/src/runtime/routes/watcher-routes.ts +21 -0
  1124. package/src/runtime/routes/webhook-routes.ts +50 -2
  1125. package/src/runtime/routes/wipe-conversation-routes.ts +8 -0
  1126. package/src/runtime/routes/work-items-routes.ts +49 -23
  1127. package/src/runtime/routes/workspace-commit-routes.ts +5 -0
  1128. package/src/runtime/routes/workspace-routes.test.ts +42 -0
  1129. package/src/runtime/routes/workspace-routes.ts +124 -9
  1130. package/src/runtime/services/__tests__/analyze-conversation.test.ts +10 -4
  1131. package/src/runtime/services/analyze-conversation.ts +5 -8
  1132. package/src/runtime/services/conversation-serializer.ts +24 -2
  1133. package/src/runtime/slack-dm-text-delivery.ts +177 -0
  1134. package/src/runtime/sync/resource-sync-events.ts +17 -3
  1135. package/src/runtime/sync/sync-publisher.ts +2 -2
  1136. package/src/runtime/tool-grant-request-helper.ts +1 -0
  1137. package/src/schedule/run-script.ts +28 -3
  1138. package/src/schedule/schedule-store.ts +36 -2
  1139. package/src/schedule/schedule-usage-store.ts +83 -0
  1140. package/src/schedule/scheduler.ts +126 -21
  1141. package/src/security/__tests__/provider-key-env-fallback.test.ts +3 -3
  1142. package/src/security/encrypted-store.ts +7 -16
  1143. package/src/security/store-path-override.ts +61 -0
  1144. package/src/signals/cancel.ts +2 -4
  1145. package/src/signals/user-message.ts +10 -16
  1146. package/src/skills/catalog-files.ts +4 -1
  1147. package/src/skills/catalog-install.ts +3 -0
  1148. package/src/skills/categories-cache.ts +118 -0
  1149. package/src/skills/clawhub-files.ts +1 -0
  1150. package/src/skills/skillssh-files.ts +1 -0
  1151. package/src/skills/validate-input.ts +177 -0
  1152. package/src/subagent/manager.ts +33 -24
  1153. package/src/subagent/types.ts +6 -0
  1154. package/src/tasks/tool-sanitizer.ts +2 -2
  1155. package/src/telemetry/types.ts +55 -1
  1156. package/src/telemetry/usage-telemetry-reporter.test.ts +250 -4
  1157. package/src/telemetry/usage-telemetry-reporter.ts +88 -2
  1158. package/src/tools/acp/context.ts +20 -0
  1159. package/src/tools/acp/list-agents.test.ts +7 -1
  1160. package/src/tools/acp/spawn.test.ts +198 -93
  1161. package/src/tools/acp/spawn.ts +32 -70
  1162. package/src/tools/acp/steer.test.ts +105 -8
  1163. package/src/tools/acp/steer.ts +48 -17
  1164. package/src/tools/apps/definitions.ts +42 -24
  1165. package/src/tools/apps/executors.ts +13 -8
  1166. package/src/tools/ask-question/ask-question-tool.test.ts +120 -105
  1167. package/src/tools/ask-question/ask-question-tool.ts +85 -90
  1168. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +2 -8
  1169. package/src/tools/computer-use/definitions.ts +295 -289
  1170. package/src/tools/credential-execution/make-authenticated-request.ts +56 -51
  1171. package/src/tools/credential-execution/manage-secure-command-tool.ts +2 -2
  1172. package/src/tools/credential-execution/run-authenticated-command.ts +82 -77
  1173. package/src/tools/credentials/vault.ts +112 -111
  1174. package/src/tools/document/document-tool.ts +131 -8
  1175. package/src/tools/execution-target.ts +3 -6
  1176. package/src/tools/execution-timeout.ts +3 -4
  1177. package/src/tools/executor.ts +18 -107
  1178. package/src/tools/filesystem/edit.ts +45 -42
  1179. package/src/tools/filesystem/list.ts +33 -30
  1180. package/src/tools/filesystem/read.ts +54 -35
  1181. package/src/tools/filesystem/write.ts +69 -32
  1182. package/src/tools/host-filesystem/edit.test.ts +1 -0
  1183. package/src/tools/host-filesystem/edit.ts +44 -42
  1184. package/src/tools/host-filesystem/read.test.ts +1 -0
  1185. package/src/tools/host-filesystem/read.ts +49 -35
  1186. package/src/tools/host-filesystem/transfer.test.ts +31 -6
  1187. package/src/tools/host-filesystem/transfer.ts +121 -108
  1188. package/src/tools/host-filesystem/write.test.ts +1 -0
  1189. package/src/tools/host-filesystem/write.ts +33 -31
  1190. package/src/tools/host-terminal/host-shell.ts +50 -48
  1191. package/src/tools/mcp/mcp-tool-factory.ts +0 -2
  1192. package/src/tools/memory/register.ts +23 -24
  1193. package/src/tools/network/__tests__/managed-search-proxy.test.ts +282 -0
  1194. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  1195. package/src/tools/network/__tests__/web-search.test.ts +222 -6
  1196. package/src/tools/network/managed-search-proxy.ts +183 -0
  1197. package/src/tools/network/web-fetch.ts +49 -46
  1198. package/src/tools/network/web-search-error.test.ts +248 -0
  1199. package/src/tools/network/web-search-error.ts +267 -0
  1200. package/src/tools/network/web-search.ts +414 -97
  1201. package/src/tools/policy-context.ts +3 -1
  1202. package/src/tools/registry.ts +184 -118
  1203. package/src/tools/schedule/create.ts +14 -1
  1204. package/src/tools/schedule/update.ts +16 -0
  1205. package/src/tools/shared/filesystem/audio-read.ts +122 -0
  1206. package/src/tools/shared/filesystem/image-read.ts +1 -1
  1207. package/src/tools/skills/execute.ts +34 -31
  1208. package/src/tools/skills/load.ts +29 -23
  1209. package/src/tools/skills/skill-tool-factory.ts +17 -36
  1210. package/src/tools/subagent/notify-parent.ts +35 -32
  1211. package/src/tools/subagent/spawn.ts +2 -1
  1212. package/src/tools/system/avatar-generator.ts +13 -22
  1213. package/src/tools/system/request-permission.ts +30 -27
  1214. package/src/tools/terminal/safe-env.ts +10 -1
  1215. package/src/tools/terminal/shell.ts +190 -61
  1216. package/src/tools/tool-approval-handler.ts +10 -4
  1217. package/src/tools/tool-defaults.ts +20 -9
  1218. package/src/tools/tool-manifest.ts +4 -4
  1219. package/src/tools/tool-name-aliases.ts +72 -14
  1220. package/src/tools/types.ts +86 -33
  1221. package/src/tools/ui-surface/definitions.ts +196 -95
  1222. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  1223. package/src/tts/provider-catalog.ts +76 -1
  1224. package/src/types/onboarding-context.ts +6 -0
  1225. package/src/usage/attribution.ts +32 -1
  1226. package/src/usage/types.ts +10 -0
  1227. package/src/util/browser.ts +7 -2
  1228. package/src/util/errors.ts +2 -2
  1229. package/src/util/map-limit.ts +27 -0
  1230. package/src/util/mutex.ts +47 -0
  1231. package/src/util/platform.ts +15 -12
  1232. package/src/work-items/work-item-runner.ts +7 -2
  1233. package/src/workspace/git-service.ts +1 -42
  1234. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +7 -20
  1235. package/src/workspace/migrations/090-memory-router-cost-optimized-profile.ts +109 -0
  1236. package/src/workspace/migrations/091-retighten-migration-onboarding-thread.ts +41 -0
  1237. package/src/workspace/migrations/092-backfill-v3-leaves.ts +169 -0
  1238. package/src/workspace/migrations/093-backfill-leaf-ids.ts +144 -0
  1239. package/src/workspace/migrations/094-seed-avatar-manifest.ts +155 -0
  1240. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  1241. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  1242. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +117 -0
  1243. package/src/workspace/migrations/__tests__/094-seed-avatar-manifest.test.ts +136 -0
  1244. package/src/workspace/migrations/__tests__/backfill-leaf-ids.test.ts +175 -0
  1245. package/src/workspace/migrations/__tests__/backfill-v3-leaves.test.ts +124 -0
  1246. package/src/workspace/migrations/registry.ts +16 -0
  1247. package/src/workspace/provider-commit-message-generator.ts +15 -17
  1248. package/tsconfig.json +4 -1
  1249. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  1250. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  1251. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  1252. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -262
  1253. package/src/__tests__/empty-response-pipeline.test.ts +0 -301
  1254. package/src/__tests__/history-repair-pipeline.test.ts +0 -396
  1255. package/src/__tests__/llm-call-pipeline.test.ts +0 -281
  1256. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  1257. package/src/__tests__/persistence-pipeline.test.ts +0 -376
  1258. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  1259. package/src/__tests__/token-estimate-pipeline.test.ts +0 -481
  1260. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  1261. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  1262. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -344
  1263. package/src/cli/commands/__tests__/memory-v3-render.test.ts +0 -340
  1264. package/src/cli/commands/memory-v3-render.ts +0 -344
  1265. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  1266. package/src/daemon/message-types/disk-pressure.ts +0 -9
  1267. package/src/email/feature-gate.ts +0 -23
  1268. package/src/gallery/default-gallery.ts +0 -1359
  1269. package/src/gallery/gallery-manifest.ts +0 -28
  1270. package/src/memory/v3/__tests__/coactivation-store.test.ts +0 -422
  1271. package/src/memory/v3/__tests__/consolidation-job.test.ts +0 -468
  1272. package/src/memory/v3/__tests__/edge-learning-job.test.ts +0 -324
  1273. package/src/memory/v3/__tests__/edges.test.ts +0 -563
  1274. package/src/memory/v3/__tests__/filter.test.ts +0 -512
  1275. package/src/memory/v3/__tests__/gate.test.ts +0 -574
  1276. package/src/memory/v3/__tests__/index-composition.test.ts +0 -233
  1277. package/src/memory/v3/__tests__/loop.test.ts +0 -530
  1278. package/src/memory/v3/__tests__/retriever.test.ts +0 -226
  1279. package/src/memory/v3/__tests__/scouts.test.ts +0 -440
  1280. package/src/memory/v3/__tests__/shadow-middleware.test.ts +0 -312
  1281. package/src/memory/v3/__tests__/system-prompts.test.ts +0 -154
  1282. package/src/memory/v3/__tests__/traversal.test.ts +0 -469
  1283. package/src/memory/v3/__tests__/tree-index.test.ts +0 -280
  1284. package/src/memory/v3/__tests__/tree-store.test.ts +0 -529
  1285. package/src/memory/v3/__tests__/tree-walk.test.ts +0 -707
  1286. package/src/memory/v3/__tests__/validate.test.ts +0 -245
  1287. package/src/memory/v3/auto-edges.ts +0 -223
  1288. package/src/memory/v3/coactivation-store.ts +0 -124
  1289. package/src/memory/v3/consolidation-job.ts +0 -323
  1290. package/src/memory/v3/edge-learning-job.ts +0 -160
  1291. package/src/memory/v3/edges.ts +0 -249
  1292. package/src/memory/v3/filter.ts +0 -281
  1293. package/src/memory/v3/gate.ts +0 -334
  1294. package/src/memory/v3/index-composition.ts +0 -113
  1295. package/src/memory/v3/llm-capture.ts +0 -46
  1296. package/src/memory/v3/loop.ts +0 -382
  1297. package/src/memory/v3/maintenance.ts +0 -144
  1298. package/src/memory/v3/prompt-context.ts +0 -33
  1299. package/src/memory/v3/prompts/consolidation.ts +0 -458
  1300. package/src/memory/v3/prompts/system-prompts.ts +0 -196
  1301. package/src/memory/v3/retriever.ts +0 -33
  1302. package/src/memory/v3/scouts.ts +0 -420
  1303. package/src/memory/v3/shadow-middleware.ts +0 -305
  1304. package/src/memory/v3/traversal.ts +0 -206
  1305. package/src/memory/v3/tree-index.ts +0 -237
  1306. package/src/memory/v3/tree-store.ts +0 -394
  1307. package/src/memory/v3/tree-walk.ts +0 -351
  1308. package/src/memory/v3/types.ts +0 -65
  1309. package/src/memory/v3/validate.ts +0 -300
  1310. package/src/plugins/defaults/circuit-breaker.ts +0 -141
  1311. package/src/plugins/defaults/compaction.ts +0 -141
  1312. package/src/plugins/defaults/empty-response.ts +0 -124
  1313. package/src/plugins/defaults/history-repair.ts +0 -83
  1314. package/src/plugins/defaults/llm-call.ts +0 -77
  1315. package/src/plugins/defaults/memory-retrieval.ts +0 -219
  1316. package/src/plugins/defaults/overflow-reduce.ts +0 -185
  1317. package/src/plugins/defaults/persistence.ts +0 -127
  1318. package/src/plugins/defaults/title-generate.ts +0 -90
  1319. package/src/plugins/defaults/token-estimate.ts +0 -101
  1320. package/src/plugins/defaults/tool-error.ts +0 -119
  1321. package/src/plugins/defaults/tool-execute.ts +0 -87
  1322. package/src/plugins/defaults/tool-result-truncate.ts +0 -84
  1323. package/src/skills/category-inference.ts +0 -111
@@ -1,14 +1,18 @@
1
1
  import { createRequire } from "node:module";
2
- import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
3
-
4
- import type {
5
- AgentEvent,
6
- CheckpointDecision,
7
- CheckpointInfo,
8
- } from "../agent/loop.js";
2
+ import {
3
+ afterAll,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ mock,
8
+ spyOn,
9
+ test,
10
+ } from "bun:test";
11
+
12
+ import type { LoopToolExecutor } from "../agent/loop.js";
9
13
  import type { ServerMessage } from "../daemon/message-protocol.js";
10
14
  import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
11
- import type { ContentBlock, Message } from "../providers/types.js";
15
+ import type { Message, Provider, ToolDefinition } from "../providers/types.js";
12
16
 
13
17
  const conversationCrudRealSnapshot = {
14
18
  ...(createRequire(import.meta.url)(
@@ -64,6 +68,7 @@ mock.module("../config/loader.js", () => ({
64
68
  memory: { retrieval: { scratchpadInjection: { enabled: true } } },
65
69
  ui: mockUiConfig,
66
70
  compaction: { enabled: true, autoThreshold: 0.7 },
71
+ conversations: { skipAutoRetitling: true },
67
72
  }),
68
73
  loadRawConfig: () => ({}),
69
74
  saveRawConfig: () => {},
@@ -74,17 +79,20 @@ mock.module("../config/loader.js", () => ({
74
79
 
75
80
  // Token estimator returns a small value by default (well within budget)
76
81
  // so preflight does not trigger unless the test overrides it. Both the
77
- // calibrated entry point (`estimatePromptTokens`, used in the convergence
78
- // path) and the raw entry point (`estimatePromptTokensRaw`, used by the
79
- // default `tokenEstimate` plugin pipeline for preflight/mid-loop) are
82
+ // calibrated entry point (`estimatePromptTokens`, which backs the preflight
83
+ // overflow gate and the convergence path) and the raw entry point
84
+ // (`estimatePromptTokensRaw`, used by the pre-send calibration capture) are
80
85
  // stubbed so either call site can drive the test.
81
86
  let mockEstimateTokens = 1000;
82
87
  mock.module("../context/token-estimator.js", () => ({
83
88
  estimatePromptTokens: () => mockEstimateTokens,
84
89
  estimatePromptTokensRaw: () => mockEstimateTokens,
85
- // Pass-through: the default plugin computes `toolTokenBudget` via this
86
- // helper before delegating to the raw estimator. Return 0 so the mocked
87
- // raw estimate is not perturbed.
90
+ // The preflight overflow gate calls this calibrated wrapper directly, so it
91
+ // must honor `mockEstimateTokens` too rather than fall through to the real
92
+ // implementation.
93
+ estimatePromptTokensWithTools: () => mockEstimateTokens,
94
+ // Pass-through: `estimatePromptTokensWithTools` computes `toolTokenBudget`
95
+ // via this helper. Return 0 so the mocked estimate is not perturbed.
88
96
  estimateToolsTokens: () => 0,
89
97
  }));
90
98
 
@@ -176,6 +184,12 @@ let mockConversationRow: Record<string, unknown> = {
176
184
  title: null,
177
185
  };
178
186
  let mockMessageById: Record<string, unknown> | null = null;
187
+ const deleteMessageByIdMock = mock(() => ({
188
+ segmentIds: [],
189
+ deletedSummaryIds: [],
190
+ }));
191
+ const reserveMessageMock = mock(async () => ({ id: "msg-reserve" }));
192
+ const updateMessageContentMock = mock(() => {});
179
193
  mock.module("../memory/conversation-crud.js", () => ({
180
194
  setConversationOriginChannelIfUnset: () => {},
181
195
  updateConversationUsage: () => {},
@@ -189,7 +203,7 @@ mock.module("../memory/conversation-crud.js", () => ({
189
203
  }),
190
204
  getConversationOriginInterface: () => null,
191
205
  addMessage: () => ({ id: "mock-msg-id" }),
192
- deleteMessageById: () => {},
206
+ deleteMessageById: deleteMessageByIdMock,
193
207
  updateConversationContextWindow: () => {},
194
208
  updateConversationSlackContextWatermark:
195
209
  updateConversationSlackContextWatermarkMock,
@@ -197,6 +211,36 @@ mock.module("../memory/conversation-crud.js", () => ({
197
211
  getConversationOriginChannel: () => null,
198
212
  getMessageById: () => mockMessageById,
199
213
  getLastUserTimestampBefore: () => 0,
214
+ reserveMessage: reserveMessageMock,
215
+ updateMessageContent: updateMessageContentMock,
216
+ // The real schema is a Zod object; tests don't exercise validation,
217
+ // so a passthrough is sufficient — the production code at
218
+ // `handleMessageComplete` only branches on `success` and reads two
219
+ // fields off `data`. `safeParse` of an empty object satisfies the
220
+ // schema (every field is optional).
221
+ messageMetadataSchema: {
222
+ safeParse: (input: unknown) => ({ success: true, data: input ?? {} }),
223
+ },
224
+ }));
225
+
226
+ // The B3 indexing-restoration path imports `indexMessageNow` from
227
+ // `../memory/indexer.js` and `projectAssistantMessage` from
228
+ // `../memory/conversation-attention-store.js`; without these stubs the
229
+ // real modules would try to open a SQLite DB and read a real config.
230
+ const indexMessageNowMock = mock(async () => ({
231
+ indexedSegments: 0,
232
+ enqueuedJobs: 0,
233
+ }));
234
+ const projectAssistantMessageMock = mock(() => false);
235
+ const publishSyncInvalidationMock = mock(async () => {});
236
+ mock.module("../memory/indexer.js", () => ({
237
+ indexMessageNow: indexMessageNowMock,
238
+ }));
239
+ mock.module("../memory/conversation-attention-store.js", () => ({
240
+ projectAssistantMessage: projectAssistantMessageMock,
241
+ }));
242
+ mock.module("../runtime/sync/sync-publisher.js", () => ({
243
+ publishSyncInvalidation: publishSyncInvalidationMock,
200
244
  }));
201
245
 
202
246
  afterAll(() => {
@@ -272,12 +316,14 @@ const buildUnifiedTurnContextBlockMock = mock(
272
316
  (options: Record<string, unknown>) =>
273
317
  `<turn_context>\ncurrent_time: ${String(options.timestamp)}\n</turn_context>`,
274
318
  );
275
- const applyRuntimeInjectionsMock = mock(
276
- async (msgs: Message[], _options?: unknown) => ({
277
- messages: msgs,
278
- blocks: { ...mockInjectionBlocks },
279
- }),
280
- );
319
+ const defaultApplyRuntimeInjectionsImpl = async (
320
+ msgs: Message[],
321
+ _options?: unknown,
322
+ ) => ({
323
+ messages: msgs,
324
+ blocks: { ...mockInjectionBlocks },
325
+ });
326
+ const applyRuntimeInjectionsMock = mock(defaultApplyRuntimeInjectionsImpl);
281
327
  let mockSlackChronologicalContext: {
282
328
  renderedMessages: Array<{
283
329
  message: Message;
@@ -316,15 +362,6 @@ mock.module("../daemon/conversation-runtime-assembly.js", () => ({
316
362
  applyRuntimeInjections: applyRuntimeInjectionsMock,
317
363
  buildUnifiedTurnContextBlock: buildUnifiedTurnContextBlockMock,
318
364
  stripInjectionsForCompaction: (msgs: Message[]) => msgs,
319
- findLastInjectedNowContent: () => null,
320
- readNowScratchpad: () => null,
321
- readPkbContext: () => null,
322
- getPkbAutoInjectList: () => [
323
- "INDEX.md",
324
- "essentials.md",
325
- "threads.md",
326
- "buffer.md",
327
- ],
328
365
  isSlackChannelConversation: () => false,
329
366
  getSlackCompactionWatermarkForPrefix:
330
367
  getSlackCompactionWatermarkForPrefixMock,
@@ -371,7 +408,7 @@ mock.module("../daemon/date-context.js", () => ({
371
408
  resolveTurnTimezoneContext: resolveTurnTimezoneContextMock,
372
409
  }));
373
410
 
374
- mock.module("../daemon/history-repair.js", () => ({
411
+ mock.module("../plugins/defaults/history-repair/terminal.js", () => ({
375
412
  repairHistory: (msgs: Message[]) => ({
376
413
  messages: msgs,
377
414
  stats: {
@@ -501,56 +538,78 @@ mock.module("../proactive-artifact/index.js", () => ({
501
538
 
502
539
  // ── Imports (after mocks) ────────────────────────────────────────────
503
540
 
541
+ import { AgentLoop } from "../agent/loop.js";
504
542
  import {
505
543
  type AgentLoopConversationContext,
506
544
  applyCompactionResult,
507
545
  runAgentLoopImpl,
508
546
  } from "../daemon/conversation-agent-loop.js";
547
+ import {
548
+ createMockProvider,
549
+ type ScriptedResponse,
550
+ textResponse,
551
+ toolUseResponse,
552
+ } from "./helpers/mock-provider.js";
509
553
 
510
554
  // ── Test helpers ─────────────────────────────────────────────────────
511
555
 
512
- type AgentLoopRun = (
513
- messages: Message[],
514
- onEvent: (event: AgentEvent) => void | Promise<void>,
515
- signal?: AbortSignal,
516
- requestId?: string,
517
- onCheckpoint?: (
518
- checkpoint: CheckpointInfo,
519
- ) => CheckpointDecision | Promise<CheckpointDecision>,
520
- ) => Promise<Message[]>;
521
-
522
556
  function makeCtx(
523
557
  overrides?: Partial<AgentLoopConversationContext> & {
524
- agentLoopRun?: AgentLoopRun;
558
+ providerResponses?: ScriptedResponse[];
559
+ loopProvider?: Provider;
560
+ loopTools?: ToolDefinition[];
561
+ toolExecutor?: LoopToolExecutor;
525
562
  },
526
563
  ): AgentLoopConversationContext {
527
- const agentLoopRun =
528
- overrides?.agentLoopRun ??
529
- (async (messages: Message[]) => [
530
- ...messages,
531
- {
532
- role: "assistant" as const,
533
- content: [{ type: "text" as const, text: "response" }],
534
- },
535
- ]);
564
+ const {
565
+ providerResponses,
566
+ loopProvider,
567
+ loopTools,
568
+ toolExecutor,
569
+ ...ctxOverrides
570
+ } = overrides ?? {};
571
+ const conversationId = ctxOverrides.conversationId ?? "test-conv";
572
+ let processing = true;
573
+
574
+ // Drive the real `AgentLoop` against a scripted provider, mocking only the
575
+ // provider HTTP boundary. The loop owns its mid-loop budget gate, inline
576
+ // compaction, and event emission, so these orchestrator tests exercise the
577
+ // real escalation/persistence path.
578
+ //
579
+ // Name the loop's provider after `ctx.provider` so the two stay in sync,
580
+ // mirroring production where the orchestrator hands the same provider to
581
+ // the loop. The loop stamps this name onto `usage.actualProvider` whenever
582
+ // a response omits its own, which is what the request-log fallback reads.
583
+ // Tests that need to introspect provider calls (or sequence a rejection)
584
+ // build their own `loopProvider` via `createMockProvider`.
585
+ const loopProviderName =
586
+ (ctxOverrides.provider as { name?: string } | undefined)?.name ??
587
+ "mock-provider";
588
+ const provider =
589
+ loopProvider ??
590
+ createMockProvider(
591
+ providerResponses ?? [textResponse("response")],
592
+ loopProviderName,
593
+ ).provider;
594
+ const agentLoop = new AgentLoop(provider, "system prompt", {
595
+ conversationId,
596
+ tools: loopTools ?? [],
597
+ toolExecutor,
598
+ });
536
599
 
537
600
  return {
538
601
  conversationId: "test-conv",
539
602
  messages: [
540
603
  { role: "user", content: [{ type: "text", text: "Hello" }] },
541
604
  ] as Message[],
542
- processing: true,
605
+ isProcessing: () => processing,
606
+ setProcessing: (value: boolean) => {
607
+ processing = value;
608
+ },
543
609
  abortController: new AbortController(),
544
610
  currentRequestId: "test-req",
545
611
 
546
- agentLoop: {
547
- run: agentLoopRun,
548
- getToolTokenBudget: () => 0,
549
- getResolvedTools: () => [],
550
- // Tests here don't exercise calibration; returning undefined makes
551
- // the estimator use the per-provider aggregate key.
552
- getActiveModel: () => undefined,
553
- } as unknown as AgentLoopConversationContext["agentLoop"],
612
+ agentLoop,
554
613
  provider: {
555
614
  name: "mock-provider",
556
615
  sendMessage: async () => ({
@@ -579,8 +638,6 @@ function makeCtx(
579
638
  currentTurnSurfaces: [],
580
639
 
581
640
  workingDir: "/tmp",
582
- workspaceTopLevelContext: null,
583
- workspaceTopLevelDirty: false,
584
641
  channelCapabilities: undefined,
585
642
  commandIntent: undefined,
586
643
  trustContext: undefined,
@@ -617,7 +674,6 @@ function makeCtx(
617
674
  getWorkspaceGitService: () => ({ ensureInitialized: async () => {} }),
618
675
  commitTurnChanges: async () => {},
619
676
 
620
- refreshWorkspaceTopLevelContextIfNeeded: () => {},
621
677
  markWorkspaceTopLevelDirty: () => {},
622
678
  emitActivityState: () => {},
623
679
  getQueueDepth: () => 0,
@@ -643,9 +699,10 @@ function makeCtx(
643
699
  injectedTokens: 0,
644
700
  }),
645
701
  retrackCachedNodes: () => {},
702
+ recordPkbQueryVectors: () => {},
646
703
  } as unknown as AgentLoopConversationContext["graphMemory"],
647
704
 
648
- ...overrides,
705
+ ...ctxOverrides,
649
706
  } as AgentLoopConversationContext;
650
707
  }
651
708
 
@@ -686,17 +743,26 @@ beforeEach(() => {
686
743
  setConversationHistoryStrippedAtMock.mockClear();
687
744
  setConversationHistoryStrippedAtMock.mockImplementation(() => {});
688
745
  applyRuntimeInjectionsMock.mockClear();
746
+ applyRuntimeInjectionsMock.mockImplementation(
747
+ defaultApplyRuntimeInjectionsImpl,
748
+ );
689
749
  buildUnifiedTurnContextBlockMock.mockClear();
690
750
  resolveTurnTimezoneContextMock.mockClear();
691
751
  formatTurnTimestampMock.mockClear();
692
752
  mockSlackChronologicalContext = null;
693
753
  loadSlackChronologicalContextMock.mockClear();
694
754
  getSlackCompactionWatermarkForPrefixMock.mockClear();
695
- // Orchestrator pipelines (overflowReduce, persistence, …) run through the
696
- // plugin registry; reset and re-register every default so the pipelines
697
- // dispatch to middleware backed by the mocked collaborators these tests
698
- // install (`reduceContextOverflow`, `syncMessageToDisk`, etc.) instead of
699
- // hitting the bare terminals.
755
+ deleteMessageByIdMock.mockClear();
756
+ reserveMessageMock.mockClear();
757
+ updateMessageContentMock.mockClear();
758
+ indexMessageNowMock.mockClear();
759
+ projectAssistantMessageMock.mockClear();
760
+ publishSyncInvalidationMock.mockClear();
761
+ mockMessageById = null;
762
+ // The compaction pipeline runs through the plugin registry; reset and
763
+ // re-register every default so it dispatches to middleware backed by the
764
+ // mocked collaborators these tests install (`syncMessageToDisk`, etc.)
765
+ // instead of hitting the bare terminal.
700
766
  resetPluginRegistryAndRegisterDefaults();
701
767
  });
702
768
 
@@ -762,7 +828,7 @@ describe("session-agent-loop", () => {
762
828
  });
763
829
 
764
830
  describe("proactive artifact trigger", () => {
765
- test("suppresses proactive app build when the foreground turn used app tools", async () => {
831
+ test("does not start proactive artifact jobs after foreground user turns", async () => {
766
832
  mockConversationRow = {
767
833
  ...mockConversationRow,
768
834
  id: "test-conv",
@@ -776,57 +842,28 @@ describe("session-agent-loop", () => {
776
842
  mockHasProactiveArtifactCompleted = false;
777
843
  mockTryClaimProactiveArtifactTrigger = true;
778
844
 
779
- const agentLoopRun: AgentLoopRun = async (
780
- messages,
781
- onEvent,
782
- _signal,
783
- _requestId,
784
- onCheckpoint,
785
- ) => {
786
- await onEvent({
787
- type: "message_complete",
788
- message: {
789
- role: "assistant",
790
- content: [{ type: "text", text: "I'll build that app." }],
791
- },
792
- });
793
- await onEvent({
794
- type: "tool_use",
795
- id: "tool-1",
796
- name: "app_create",
797
- input: { name: "Flow" },
798
- });
799
- await onEvent({
800
- type: "tool_result",
801
- toolUseId: "tool-1",
802
- content: "{}",
803
- isError: false,
804
- });
805
- await onCheckpoint?.({
806
- turnIndex: 0,
807
- toolCount: 1,
808
- hasToolUse: true,
809
- history: messages,
810
- });
811
- await onEvent({
812
- type: "message_complete",
813
- message: {
814
- role: "assistant",
815
- content: [{ type: "text", text: "Done." }],
816
- },
817
- });
818
- return [
819
- ...messages,
820
- {
821
- role: "assistant" as const,
822
- content: [{ type: "text" as const, text: "Done." }],
823
- },
824
- ];
825
- };
826
-
845
+ // A two-call agent turn: the model invokes `app_create`, then wraps up
846
+ // with a final text reply.
827
847
  const ctx = makeCtx({
828
848
  conversationId: "test-conv",
829
- agentLoopRun,
849
+ providerResponses: [
850
+ {
851
+ content: [
852
+ { type: "text", text: "I'll build that app." },
853
+ {
854
+ type: "tool_use",
855
+ id: "tool-1",
856
+ name: "app_create",
857
+ input: { name: "Flow" },
858
+ },
859
+ ],
860
+ model: "mock-model",
861
+ usage: { inputTokens: 10, outputTokens: 5 },
862
+ stopReason: "tool_use",
863
+ },
864
+ textResponse("Done."),
865
+ ],
866
+ toolExecutor: async () => ({ content: "{}", isError: false }),
830
867
  });
831
868
  await runAgentLoopImpl(
832
869
  ctx,
@@ -839,16 +876,28 @@ describe("session-agent-loop", () => {
839
876
  );
840
877
  await new Promise((resolve) => setTimeout(resolve, 0));
841
878
 
842
- expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(1);
843
- expect(runProactiveArtifactJobMock.mock.calls[0]?.[0]).toMatchObject({
844
- conversationId: "test-conv",
845
- suppressAppBuild: true,
846
- });
879
+ expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(0);
847
880
  });
848
881
  });
849
882
 
850
883
  describe("disk pressure injection context", () => {
851
- test("passes cleanup context into runtime injections for cleanup-mode turns", async () => {
884
+ // The loop sets `ctx.diskPressureCleanupModeActive` for the duration of the
885
+ // turn (the disk-pressure-warning injector reads it via the per-conversation
886
+ // registry) and resets it in the turn-end cleanup path. Snapshot the flag at
887
+ // each `applyRuntimeInjections` call so assertions observe its value while
888
+ // injection runs, not the post-turn reset.
889
+ function captureCleanupFlagDuringInjection(ctx: {
890
+ diskPressureCleanupModeActive?: boolean;
891
+ }): () => Array<boolean | undefined> {
892
+ const observed: Array<boolean | undefined> = [];
893
+ applyRuntimeInjectionsMock.mockImplementation(async (msgs: Message[]) => {
894
+ observed.push(ctx.diskPressureCleanupModeActive);
895
+ return { messages: msgs, blocks: { ...mockInjectionBlocks } };
896
+ });
897
+ return () => observed;
898
+ }
899
+
900
+ test("sets the cleanup-mode flag on the conversation for cleanup-mode turns", async () => {
852
901
  mockDiskPressureDecision = {
853
902
  action: "allow-cleanup-mode",
854
903
  reason: "guardian",
@@ -871,6 +920,7 @@ describe("session-agent-loop", () => {
871
920
  trustClass: "guardian",
872
921
  } as AgentLoopConversationContext["trustContext"],
873
922
  });
923
+ const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
874
924
 
875
925
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
876
926
 
@@ -889,21 +939,16 @@ describe("session-agent-loop", () => {
889
939
  },
890
940
  }),
891
941
  );
892
- const firstInjectionOptions = applyRuntimeInjectionsMock.mock
893
- .calls[0]![1] as {
894
- diskPressureContext?: { cleanupModeActive: boolean } | null;
895
- };
896
- expect(firstInjectionOptions.diskPressureContext).toEqual({
897
- cleanupModeActive: true,
898
- });
942
+ expect(cleanupFlagDuringInjection()).toEqual([true]);
899
943
  });
900
944
 
901
- test("passes cleanup context into runtime injections for local-owner turns", async () => {
945
+ test("sets the cleanup-mode flag on the conversation for local-owner turns", async () => {
902
946
  mockDiskPressureDecision = {
903
947
  action: "allow-cleanup-mode",
904
948
  reason: "local-owner",
905
949
  };
906
950
  const ctx = makeCtx();
951
+ const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
907
952
 
908
953
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
909
954
 
@@ -915,16 +960,10 @@ describe("session-agent-loop", () => {
915
960
  trustContext: null,
916
961
  }),
917
962
  );
918
- const firstInjectionOptions = applyRuntimeInjectionsMock.mock
919
- .calls[0]![1] as {
920
- diskPressureContext?: { cleanupModeActive: boolean } | null;
921
- };
922
- expect(firstInjectionOptions.diskPressureContext).toEqual({
923
- cleanupModeActive: true,
924
- });
963
+ expect(cleanupFlagDuringInjection()).toEqual([true]);
925
964
  });
926
965
 
927
- test("keeps cleanup context on overflow recovery reinjection", async () => {
966
+ test("keeps the cleanup-mode flag set across overflow recovery reinjection", async () => {
928
967
  mockDiskPressureDecision = {
929
968
  action: "allow-cleanup-mode",
930
969
  reason: "guardian",
@@ -946,18 +985,14 @@ describe("session-agent-loop", () => {
946
985
  trustClass: "guardian",
947
986
  } as AgentLoopConversationContext["trustContext"],
948
987
  });
988
+ const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
949
989
 
950
990
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
951
991
 
952
992
  expect(applyRuntimeInjectionsMock.mock.calls.length).toBeGreaterThan(1);
953
- for (const call of applyRuntimeInjectionsMock.mock.calls) {
954
- const options = call[1] as {
955
- diskPressureContext?: { cleanupModeActive: boolean } | null;
956
- };
957
- expect(options.diskPressureContext).toEqual({
958
- cleanupModeActive: true,
959
- });
960
- }
993
+ const flags = cleanupFlagDuringInjection();
994
+ expect(flags.length).toBeGreaterThan(1);
995
+ expect(flags.every((flag) => flag === true)).toBe(true);
961
996
  });
962
997
 
963
998
  test("blocks policy-denied turns before runtime injection or model execution", async () => {
@@ -966,9 +1001,6 @@ describe("session-agent-loop", () => {
966
1001
  reason: "trusted-contact",
967
1002
  };
968
1003
  const events: ServerMessage[] = [];
969
- const agentLoopRun = mock(async (_messages: Message[]) => {
970
- throw new Error("agent loop should not run");
971
- });
972
1004
  const activityStates: unknown[][] = [];
973
1005
  const traceEvents: unknown[][] = [];
974
1006
  const ctx = makeCtx({
@@ -981,17 +1013,16 @@ describe("session-agent-loop", () => {
981
1013
  },
982
1014
  } as unknown as AgentLoopConversationContext["traceEmitter"],
983
1015
  });
984
- ctx.agentLoop.run = agentLoopRun as AgentLoopRun;
1016
+ const runSpy = spyOn(ctx.agentLoop, "run");
985
1017
 
986
1018
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
987
1019
 
988
- expect(agentLoopRun).not.toHaveBeenCalled();
1020
+ expect(runSpy).not.toHaveBeenCalled();
989
1021
  expect(applyRuntimeInjectionsMock).not.toHaveBeenCalled();
990
1022
  expect(activityStates).toContainEqual([
991
1023
  "idle",
992
1024
  "error_terminal",
993
- "global",
994
- "test-req",
1025
+ { anchor: "global", requestId: "test-req" },
995
1026
  ]);
996
1027
  expect(traceEvents[0]).toEqual([
997
1028
  "request_error",
@@ -1046,15 +1077,14 @@ describe("session-agent-loop", () => {
1046
1077
  });
1047
1078
 
1048
1079
  expect(applyRuntimeInjectionsMock).not.toHaveBeenCalled();
1049
- expect(ctx.processing).toBe(false);
1080
+ expect(ctx.isProcessing()).toBe(false);
1050
1081
  expect(ctx.abortController).toBeNull();
1051
1082
  expect(ctx.currentRequestId).toBeUndefined();
1052
1083
  expect(drainQueue).toHaveBeenCalledWith("loop_complete");
1053
1084
  expect(activityStates).toContainEqual([
1054
1085
  "idle",
1055
1086
  "error_terminal",
1056
- "global",
1057
- "test-req",
1087
+ { anchor: "global", requestId: "test-req" },
1058
1088
  ]);
1059
1089
  });
1060
1090
  });
@@ -1063,44 +1093,14 @@ describe("session-agent-loop", () => {
1063
1093
  test("error events from agent loop are classified and emitted", async () => {
1064
1094
  const events: ServerMessage[] = [];
1065
1095
 
1066
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1067
- // Simulate tool_use + error during execution
1068
- onEvent({
1069
- type: "tool_use",
1070
- id: "tu-1",
1071
- name: "bash",
1072
- input: { cmd: "ls" },
1073
- });
1074
- onEvent({
1075
- type: "error",
1076
- error: new Error("Tool execution failed: permission denied"),
1077
- });
1078
- onEvent({
1079
- type: "message_complete",
1080
- message: {
1081
- role: "assistant",
1082
- content: [{ type: "text", text: "I encountered an error" }],
1083
- },
1084
- });
1085
- onEvent({
1086
- type: "usage",
1087
- inputTokens: 100,
1088
- outputTokens: 50,
1089
- model: "test-model",
1090
- providerDurationMs: 200,
1091
- });
1092
- return [
1093
- ...messages,
1094
- {
1095
- role: "assistant" as const,
1096
- content: [
1097
- { type: "text", text: "I encountered an error" },
1098
- ] as ContentBlock[],
1099
- },
1100
- ];
1101
- };
1102
-
1103
- const ctx = makeCtx({ agentLoopRun });
1096
+ // The model calls a tool whose executor throws, surfacing an `error`
1097
+ // event from the loop's catch handler.
1098
+ const ctx = makeCtx({
1099
+ providerResponses: [toolUseResponse("tu-1", "bash", { cmd: "ls" })],
1100
+ toolExecutor: async () => {
1101
+ throw new Error("Tool execution failed: permission denied");
1102
+ },
1103
+ });
1104
1104
  await runAgentLoopImpl(ctx, "run ls", "msg-1", (msg) => events.push(msg));
1105
1105
 
1106
1106
  const conversationError = events.find(
@@ -1112,31 +1112,9 @@ describe("session-agent-loop", () => {
1112
1112
  test("non-error agent loop completion does not emit conversation_error", async () => {
1113
1113
  const events: ServerMessage[] = [];
1114
1114
 
1115
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1116
- onEvent({
1117
- type: "message_complete",
1118
- message: {
1119
- role: "assistant",
1120
- content: [{ type: "text", text: "All good" }],
1121
- },
1122
- });
1123
- onEvent({
1124
- type: "usage",
1125
- inputTokens: 50,
1126
- outputTokens: 25,
1127
- model: "test-model",
1128
- providerDurationMs: 100,
1129
- });
1130
- return [
1131
- ...messages,
1132
- {
1133
- role: "assistant" as const,
1134
- content: [{ type: "text", text: "All good" }] as ContentBlock[],
1135
- },
1136
- ];
1137
- };
1138
-
1139
- const ctx = makeCtx({ agentLoopRun });
1115
+ const ctx = makeCtx({
1116
+ providerResponses: [textResponse("All good")],
1117
+ });
1140
1118
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
1141
1119
 
1142
1120
  const conversationError = events.find(
@@ -1172,35 +1150,20 @@ describe("session-agent-loop", () => {
1172
1150
  },
1173
1151
  };
1174
1152
 
1175
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1176
- onEvent({
1177
- type: "message_complete",
1178
- message: {
1179
- role: "assistant",
1180
- content: [{ type: "text", text: "Hi there." }],
1181
- },
1182
- });
1183
- onEvent({
1184
- type: "usage",
1185
- inputTokens: 12,
1186
- outputTokens: 3,
1187
- model: "gpt-4.1-2026-03-01",
1188
- actualProvider: "fireworks",
1189
- providerDurationMs: 45,
1190
- rawRequest,
1191
- rawResponse,
1192
- });
1193
- return [
1194
- ...messages,
1153
+ // The provider response carries its own `actualProvider`, so the logged
1154
+ // row should record that name rather than the runtime provider.
1155
+ const ctx = makeCtx({
1156
+ providerResponses: [
1195
1157
  {
1196
- role: "assistant" as const,
1197
- content: [{ type: "text", text: "Hi there." }] as ContentBlock[],
1158
+ content: [{ type: "text", text: "Hi there." }],
1159
+ model: "gpt-4.1-2026-03-01",
1160
+ usage: { inputTokens: 12, outputTokens: 3 },
1161
+ stopReason: "end_turn",
1162
+ actualProvider: "fireworks",
1163
+ rawRequest,
1164
+ rawResponse,
1198
1165
  },
1199
- ];
1200
- };
1201
-
1202
- const ctx = makeCtx({
1203
- agentLoopRun,
1166
+ ],
1204
1167
  provider: {
1205
1168
  name: "openrouter",
1206
1169
  sendMessage: async () => ({
@@ -1237,34 +1200,19 @@ describe("session-agent-loop", () => {
1237
1200
  ],
1238
1201
  };
1239
1202
 
1240
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1241
- onEvent({
1242
- type: "message_complete",
1243
- message: {
1244
- role: "assistant",
1245
- content: [{ type: "text", text: "Hi there." }],
1246
- },
1247
- });
1248
- onEvent({
1249
- type: "usage",
1250
- inputTokens: 12,
1251
- outputTokens: 3,
1252
- model: "gpt-4.1-2026-03-01",
1253
- providerDurationMs: 45,
1254
- rawRequest,
1255
- rawResponse,
1256
- });
1257
- return [
1258
- ...messages,
1203
+ // The provider response omits `actualProvider`, so the loop stamps the
1204
+ // runtime provider name onto the usage event and the row records it.
1205
+ const ctx = makeCtx({
1206
+ providerResponses: [
1259
1207
  {
1260
- role: "assistant" as const,
1261
- content: [{ type: "text", text: "Hi there." }] as ContentBlock[],
1208
+ content: [{ type: "text", text: "Hi there." }],
1209
+ model: "gpt-4.1-2026-03-01",
1210
+ usage: { inputTokens: 12, outputTokens: 3 },
1211
+ stopReason: "end_turn",
1212
+ rawRequest,
1213
+ rawResponse,
1262
1214
  },
1263
- ];
1264
- };
1265
-
1266
- const ctx = makeCtx({
1267
- agentLoopRun,
1215
+ ],
1268
1216
  provider: {
1269
1217
  name: "openrouter",
1270
1218
  sendMessage: async () => ({
@@ -1319,35 +1267,18 @@ describe("session-agent-loop", () => {
1319
1267
  status: "completed",
1320
1268
  };
1321
1269
 
1322
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1323
- onEvent({
1324
- type: "message_complete",
1325
- message: {
1326
- role: "assistant",
1327
- content: [{ type: "text", text: "Hi there." }],
1328
- },
1329
- });
1330
- onEvent({
1331
- type: "usage",
1332
- inputTokens: 12,
1333
- outputTokens: 3,
1334
- model: "gpt-5.4",
1335
- actualProvider: "openai",
1336
- providerDurationMs: 45,
1337
- rawRequest,
1338
- rawResponse,
1339
- });
1340
- return [
1341
- ...messages,
1270
+ const ctx = makeCtx({
1271
+ providerResponses: [
1342
1272
  {
1343
- role: "assistant" as const,
1344
- content: [{ type: "text", text: "Hi there." }] as ContentBlock[],
1273
+ content: [{ type: "text", text: "Hi there." }],
1274
+ model: "gpt-5.4",
1275
+ usage: { inputTokens: 12, outputTokens: 3 },
1276
+ stopReason: "end_turn",
1277
+ actualProvider: "openai",
1278
+ rawRequest,
1279
+ rawResponse,
1345
1280
  },
1346
- ];
1347
- };
1348
-
1349
- const ctx = makeCtx({
1350
- agentLoopRun,
1281
+ ],
1351
1282
  provider: {
1352
1283
  name: "openai",
1353
1284
  sendMessage: async () => ({
@@ -1387,34 +1318,17 @@ describe("session-agent-loop", () => {
1387
1318
  attrs: Record<string, unknown>;
1388
1319
  }> = [];
1389
1320
 
1390
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1391
- onEvent({ type: "text_delta", text: "Hi." });
1392
- onEvent({
1393
- type: "message_complete",
1394
- message: {
1395
- role: "assistant",
1396
- content: [{ type: "text", text: "Hi." }],
1397
- },
1398
- });
1399
- onEvent({
1400
- type: "usage",
1401
- inputTokens: 10,
1402
- outputTokens: 2,
1403
- model: "gpt-5.5-2026-04-23",
1404
- actualProvider: "openai",
1405
- providerDurationMs: 100,
1406
- });
1407
- return [
1408
- ...messages,
1321
+ const ctx = makeCtx({
1322
+ // The loop replays the text block as a `text_delta` before `usage`.
1323
+ providerResponses: [
1409
1324
  {
1410
- role: "assistant" as const,
1411
- content: [{ type: "text", text: "Hi." }] as ContentBlock[],
1325
+ content: [{ type: "text", text: "Hi." }],
1326
+ model: "gpt-5.5-2026-04-23",
1327
+ usage: { inputTokens: 10, outputTokens: 2 },
1328
+ stopReason: "end_turn",
1329
+ actualProvider: "openai",
1412
1330
  },
1413
- ];
1414
- };
1415
-
1416
- const ctx = makeCtx({
1417
- agentLoopRun,
1331
+ ],
1418
1332
  // Provider name matches actualProvider so both paths agree.
1419
1333
  provider: {
1420
1334
  name: "openai",
@@ -1462,28 +1376,18 @@ describe("session-agent-loop", () => {
1462
1376
  attrs: Record<string, unknown>;
1463
1377
  }> = [];
1464
1378
 
1465
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1466
- // No text_delta pure tool-call response
1467
- onEvent({
1468
- type: "message_complete",
1469
- message: {
1470
- role: "assistant",
1379
+ const ctx = makeCtx({
1380
+ // An empty-content response: no text block fires `text_delta`, so the
1381
+ // started event falls back to the resolved usage provider name.
1382
+ providerResponses: [
1383
+ {
1471
1384
  content: [],
1385
+ model: "gpt-5.5-2026-04-23",
1386
+ usage: { inputTokens: 10, outputTokens: 2 },
1387
+ stopReason: "end_turn",
1388
+ actualProvider: "openai",
1472
1389
  },
1473
- });
1474
- onEvent({
1475
- type: "usage",
1476
- inputTokens: 10,
1477
- outputTokens: 2,
1478
- model: "gpt-5.5-2026-04-23",
1479
- actualProvider: "openai",
1480
- providerDurationMs: 100,
1481
- });
1482
- return messages;
1483
- };
1484
-
1485
- const ctx = makeCtx({
1486
- agentLoopRun,
1390
+ ],
1487
1391
  provider: {
1488
1392
  name: "anthropic",
1489
1393
  sendMessage: async () => ({
@@ -1525,49 +1429,32 @@ describe("session-agent-loop", () => {
1525
1429
  test("records the actual provider for usage accounting", async () => {
1526
1430
  const events: ServerMessage[] = [];
1527
1431
 
1528
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1529
- onEvent({
1530
- type: "message_complete",
1531
- message: {
1532
- role: "assistant",
1432
+ const ctx = makeCtx({
1433
+ providerResponses: [
1434
+ {
1533
1435
  content: [{ type: "text", text: "Hi there." }],
1534
- },
1535
- });
1536
- onEvent({
1537
- type: "usage",
1538
- inputTokens: 12,
1539
- outputTokens: 3,
1540
- model: "gpt-4.1-2026-03-01",
1541
- actualProvider: "fireworks",
1542
- providerDurationMs: 45,
1543
- rawRequest: {
1544
- model: "gpt-4.1",
1545
- messages: [{ role: "user", content: "Hello" }],
1546
- },
1547
- rawResponse: {
1548
1436
  model: "gpt-4.1-2026-03-01",
1549
- choices: [
1550
- {
1551
- finish_reason: "stop",
1552
- message: {
1553
- role: "assistant",
1554
- content: "Hi there.",
1437
+ usage: { inputTokens: 12, outputTokens: 3 },
1438
+ stopReason: "end_turn",
1439
+ actualProvider: "fireworks",
1440
+ rawRequest: {
1441
+ model: "gpt-4.1",
1442
+ messages: [{ role: "user", content: "Hello" }],
1443
+ },
1444
+ rawResponse: {
1445
+ model: "gpt-4.1-2026-03-01",
1446
+ choices: [
1447
+ {
1448
+ finish_reason: "stop",
1449
+ message: {
1450
+ role: "assistant",
1451
+ content: "Hi there.",
1452
+ },
1555
1453
  },
1556
- },
1557
- ],
1558
- },
1559
- });
1560
- return [
1561
- ...messages,
1562
- {
1563
- role: "assistant" as const,
1564
- content: [{ type: "text", text: "Hi there." }] as ContentBlock[],
1454
+ ],
1455
+ },
1565
1456
  },
1566
- ];
1567
- };
1568
-
1569
- const ctx = makeCtx({
1570
- agentLoopRun,
1457
+ ],
1571
1458
  provider: {
1572
1459
  name: "openrouter",
1573
1460
  sendMessage: async () => ({
@@ -1637,24 +1524,9 @@ describe("session-agent-loop", () => {
1637
1524
  },
1638
1525
  });
1639
1526
 
1640
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1641
- onEvent({
1642
- type: "message_complete",
1643
- message: {
1644
- role: "assistant",
1645
- content: [{ type: "text", text: "recovered" }],
1646
- },
1647
- });
1648
- return [
1649
- ...messages,
1650
- {
1651
- role: "assistant" as const,
1652
- content: [{ type: "text", text: "recovered" }] as ContentBlock[],
1653
- },
1654
- ];
1655
- };
1656
-
1657
- const ctx = makeCtx({ agentLoopRun });
1527
+ // After the orchestrator's preflight compaction runs, the loop completes
1528
+ // the turn normally.
1529
+ const ctx = makeCtx({ providerResponses: [textResponse("recovered")] });
1658
1530
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
1659
1531
 
1660
1532
  const compactorCall = recordUsageMock.mock.calls.find(
@@ -1693,7 +1565,6 @@ describe("session-agent-loop", () => {
1693
1565
 
1694
1566
  test("convergence loop applies reducer and retries when context-too-large is detected", async () => {
1695
1567
  const events: ServerMessage[] = [];
1696
- let callCount = 0;
1697
1568
  let reducerCalled = false;
1698
1569
 
1699
1570
  // Configure reducer to succeed on first call — return reduced messages
@@ -1727,48 +1598,15 @@ describe("session-agent-loop", () => {
1727
1598
  };
1728
1599
  };
1729
1600
 
1730
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1731
- callCount++;
1732
- if (callCount === 1) {
1733
- onEvent({
1734
- type: "error",
1735
- error: new Error("context_length_exceeded"),
1736
- });
1737
- onEvent({
1738
- type: "usage",
1739
- inputTokens: 100,
1740
- outputTokens: 0,
1741
- model: "test-model",
1742
- providerDurationMs: 50,
1743
- });
1744
- return messages;
1745
- }
1746
- // Second call (after reducer): succeed
1747
- onEvent({
1748
- type: "message_complete",
1749
- message: {
1750
- role: "assistant",
1751
- content: [{ type: "text", text: "recovered" }],
1752
- },
1753
- });
1754
- onEvent({
1755
- type: "usage",
1756
- inputTokens: 50,
1757
- outputTokens: 25,
1758
- model: "test-model",
1759
- providerDurationMs: 100,
1760
- });
1761
- return [
1762
- ...messages,
1763
- {
1764
- role: "assistant" as const,
1765
- content: [{ type: "text", text: "recovered" }] as ContentBlock[],
1766
- },
1767
- ];
1768
- };
1601
+ // The provider rejects the first call with a context-too-large error,
1602
+ // then succeeds once the orchestrator has reduced the context.
1603
+ const { provider, calls } = createMockProvider([
1604
+ new Error("context_length_exceeded"),
1605
+ textResponse("recovered"),
1606
+ ]);
1769
1607
 
1770
1608
  const ctx = makeCtx({
1771
- agentLoopRun,
1609
+ loopProvider: provider,
1772
1610
  contextWindowManager: {
1773
1611
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1774
1612
  maybeCompact: async () => ({ compacted: false }),
@@ -1778,7 +1616,7 @@ describe("session-agent-loop", () => {
1778
1616
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
1779
1617
 
1780
1618
  expect(reducerCalled).toBe(true);
1781
- expect(callCount).toBe(2);
1619
+ expect(calls.length).toBe(2);
1782
1620
  const compactEvent = events.find((e) => e.type === "context_compacted");
1783
1621
  expect(compactEvent).toBeDefined();
1784
1622
  });
@@ -1786,23 +1624,10 @@ describe("session-agent-loop", () => {
1786
1624
  test("emits conversation_error when context stays too large after all recovery attempts", async () => {
1787
1625
  const events: ServerMessage[] = [];
1788
1626
 
1789
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1790
- onEvent({
1791
- type: "error",
1792
- error: new Error("context_length_exceeded"),
1793
- });
1794
- onEvent({
1795
- type: "usage",
1796
- inputTokens: 100,
1797
- outputTokens: 0,
1798
- model: "test-model",
1799
- providerDurationMs: 50,
1800
- });
1801
- return messages;
1802
- };
1803
-
1627
+ // The provider rejects every call with a context-too-large error, so the
1628
+ // orchestrator exhausts its recovery attempts.
1804
1629
  const ctx = makeCtx({
1805
- agentLoopRun,
1630
+ providerResponses: [new Error("context_length_exceeded")],
1806
1631
  contextWindowManager: {
1807
1632
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1808
1633
  // Compaction succeeds but context is still too large
@@ -1836,7 +1661,6 @@ describe("session-agent-loop", () => {
1836
1661
 
1837
1662
  test("bounded convergence loop applies reducer tiers and recovers", async () => {
1838
1663
  const events: ServerMessage[] = [];
1839
- let callCount = 0;
1840
1664
  let reducerCalls = 0;
1841
1665
 
1842
1666
  // Reducer: succeed on first call, returning reduced messages
@@ -1854,50 +1678,15 @@ describe("session-agent-loop", () => {
1854
1678
  };
1855
1679
  };
1856
1680
 
1857
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1858
- callCount++;
1859
- if (callCount === 1) {
1860
- onEvent({
1861
- type: "error",
1862
- error: new Error("context_length_exceeded"),
1863
- });
1864
- onEvent({
1865
- type: "usage",
1866
- inputTokens: 100,
1867
- outputTokens: 0,
1868
- model: "test-model",
1869
- providerDurationMs: 50,
1870
- });
1871
- return messages;
1872
- }
1873
- // After reducer runs, succeed
1874
- onEvent({
1875
- type: "message_complete",
1876
- message: {
1877
- role: "assistant",
1878
- content: [{ type: "text", text: "recovered via convergence" }],
1879
- },
1880
- });
1881
- onEvent({
1882
- type: "usage",
1883
- inputTokens: 50,
1884
- outputTokens: 25,
1885
- model: "test-model",
1886
- providerDurationMs: 100,
1887
- });
1888
- return [
1889
- ...messages,
1890
- {
1891
- role: "assistant" as const,
1892
- content: [
1893
- { type: "text", text: "recovered via convergence" },
1894
- ] as ContentBlock[],
1895
- },
1896
- ];
1897
- };
1681
+ // The provider rejects the first call with a context-too-large error,
1682
+ // then succeeds once the orchestrator has reduced the context.
1683
+ const { provider, calls } = createMockProvider([
1684
+ new Error("context_length_exceeded"),
1685
+ textResponse("recovered via convergence"),
1686
+ ]);
1898
1687
 
1899
1688
  const ctx = makeCtx({
1900
- agentLoopRun,
1689
+ loopProvider: provider,
1901
1690
  contextWindowManager: {
1902
1691
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1903
1692
  maybeCompact: async () => ({ compacted: false }),
@@ -1907,7 +1696,7 @@ describe("session-agent-loop", () => {
1907
1696
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
1908
1697
 
1909
1698
  expect(reducerCalls).toBeGreaterThanOrEqual(1);
1910
- expect(callCount).toBe(2);
1699
+ expect(calls.length).toBe(2);
1911
1700
  const conversationError = events.find(
1912
1701
  (e) => e.type === "conversation_error",
1913
1702
  );
@@ -1918,7 +1707,6 @@ describe("session-agent-loop", () => {
1918
1707
 
1919
1708
  test("non-interactive auto-compress continues without approval prompt", async () => {
1920
1709
  const events: ServerMessage[] = [];
1921
- let callCount = 0;
1922
1710
 
1923
1711
  // Reducer exhausts all tiers
1924
1712
  mockReducerStepFn = (msgs: Message[]) => ({
@@ -1939,49 +1727,14 @@ describe("session-agent-loop", () => {
1939
1727
 
1940
1728
  mockOverflowAction = "auto_compress_latest_turn";
1941
1729
 
1942
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1943
- callCount++;
1944
- if (callCount <= 2) {
1945
- onEvent({
1946
- type: "error",
1947
- error: new Error("context_length_exceeded"),
1948
- });
1949
- onEvent({
1950
- type: "usage",
1951
- inputTokens: 100,
1952
- outputTokens: 0,
1953
- model: "test-model",
1954
- providerDurationMs: 50,
1955
- });
1956
- return messages;
1957
- }
1958
- onEvent({
1959
- type: "message_complete",
1960
- message: {
1961
- role: "assistant",
1962
- content: [{ type: "text", text: "auto-recovered" }],
1963
- },
1964
- });
1965
- onEvent({
1966
- type: "usage",
1967
- inputTokens: 50,
1968
- outputTokens: 25,
1969
- model: "test-model",
1970
- providerDurationMs: 100,
1971
- });
1972
- return [
1973
- ...messages,
1974
- {
1975
- role: "assistant" as const,
1976
- content: [
1977
- { type: "text", text: "auto-recovered" },
1978
- ] as ContentBlock[],
1979
- },
1980
- ];
1981
- };
1982
-
1730
+ // The provider rejects the first two calls with context-too-large errors,
1731
+ // then succeeds after the emergency auto-compress runs.
1983
1732
  const ctx = makeCtx({
1984
- agentLoopRun,
1733
+ providerResponses: [
1734
+ new Error("context_length_exceeded"),
1735
+ new Error("context_length_exceeded"),
1736
+ textResponse("auto-recovered"),
1737
+ ],
1985
1738
  hasNoClient: true,
1986
1739
  contextWindowManager: {
1987
1740
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
@@ -2028,7 +1781,6 @@ describe("session-agent-loop", () => {
2028
1781
  // `budget_yield_unrecovered` so the inspector and dashboards can
2029
1782
  // attribute the silent stall.
2030
1783
  const events: ServerMessage[] = [];
2031
- let callCount = 0;
2032
1784
 
2033
1785
  // Reducer exhausts all 4 tiers on first call so the convergence
2034
1786
  // loop runs exactly one iteration before falling through to
@@ -2059,49 +1811,30 @@ describe("session-agent-loop", () => {
2059
1811
  // call). 90k satisfies both so the path reaches call 3.
2060
1812
  mockEstimateTokens = 90_000;
2061
1813
 
2062
- const agentLoopRun: AgentLoopRun = async (
2063
- messages,
2064
- onEvent,
2065
- _signal,
2066
- _reqId,
2067
- onCheckpoint,
2068
- ) => {
2069
- callCount++;
2070
- if (callCount <= 2) {
2071
- // Calls 1 (initial) and 2 (convergence rerun): error so
2072
- // `state.contextTooLargeDetected` stays true through
2073
- // convergence exit and we enter the auto_compress branch.
2074
- onEvent({
2075
- type: "error",
2076
- error: new Error("context_length_exceeded"),
2077
- });
2078
- onEvent({
2079
- type: "usage",
2080
- inputTokens: 100,
2081
- outputTokens: 0,
2082
- model: "test-model",
2083
- providerDurationMs: 50,
2084
- });
2085
- return messages;
2086
- }
2087
- // Call 3: the auto_compress_latest_turn rerun. Invoke
2088
- // onCheckpoint so the orchestrator's mid-loop budget check
2089
- // flips `yieldedForBudget` to true, then return without
2090
- // finishing — mirroring what AgentLoop.run does when its
2091
- // checkpoint returns "yield".
2092
- if (onCheckpoint) {
2093
- await onCheckpoint({
2094
- turnIndex: 0,
2095
- toolCount: 1,
2096
- hasToolUse: true,
2097
- history: messages,
2098
- });
2099
- }
2100
- return messages;
2101
- };
2102
-
1814
+ // Calls 1 (initial) and 2 (convergence rerun) reject with
1815
+ // context-too-large so `contextTooLargeDetected` stays true through the
1816
+ // convergence exit and the orchestrator enters the auto_compress branch.
1817
+ // Call 3 (the auto_compress rerun) is a tool turn: the loop runs it
1818
+ // without a compaction hook, so when its mid-loop budget gate trips on
1819
+ // the still-oversized estimate it yields `exitReason = "budget"` rather
1820
+ // than recovering — the silent-stall path under test.
2103
1821
  const ctx = makeCtx({
2104
- agentLoopRun,
1822
+ providerResponses: [
1823
+ new Error("context_length_exceeded"),
1824
+ new Error("context_length_exceeded"),
1825
+ toolUseResponse("t1", "read_file", { path: "/a.txt" }),
1826
+ ],
1827
+ loopTools: [
1828
+ {
1829
+ name: "read_file",
1830
+ description: "Read a file",
1831
+ input_schema: {
1832
+ type: "object",
1833
+ properties: { path: { type: "string" } },
1834
+ },
1835
+ },
1836
+ ],
1837
+ toolExecutor: async () => ({ content: "data", isError: false }),
2105
1838
  hasNoClient: true,
2106
1839
  contextWindowManager: {
2107
1840
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
@@ -2184,23 +1917,10 @@ describe("session-agent-loop", () => {
2184
1917
  };
2185
1918
  };
2186
1919
 
2187
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2188
- onEvent({
2189
- type: "error",
2190
- error: new Error("context_length_exceeded"),
2191
- });
2192
- onEvent({
2193
- type: "usage",
2194
- inputTokens: 100,
2195
- outputTokens: 0,
2196
- model: "test-model",
2197
- providerDurationMs: 50,
2198
- });
2199
- return messages;
2200
- };
2201
-
1920
+ // The provider rejects every call with a context-too-large error, so the
1921
+ // orchestrator keeps retrying until it hits the attempt ceiling.
2202
1922
  const ctx = makeCtx({
2203
- agentLoopRun,
1923
+ providerResponses: [new Error("context_length_exceeded")],
2204
1924
  contextWindowManager: {
2205
1925
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
2206
1926
  maybeCompact: async () => ({ compacted: false }),
@@ -2216,7 +1936,6 @@ describe("session-agent-loop", () => {
2216
1936
  test("preflight budget evaluation invokes reducer before provider call", async () => {
2217
1937
  const events: ServerMessage[] = [];
2218
1938
  let reducerCalls = 0;
2219
- let agentLoopCalls = 0;
2220
1939
 
2221
1940
  // Set token estimate above budget (100000 * 0.95 = 95000)
2222
1941
  mockEstimateTokens = 96000;
@@ -2235,33 +1954,11 @@ describe("session-agent-loop", () => {
2235
1954
  };
2236
1955
  };
2237
1956
 
2238
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2239
- agentLoopCalls++;
2240
- onEvent({
2241
- type: "message_complete",
2242
- message: {
2243
- role: "assistant",
2244
- content: [{ type: "text", text: "ok" }],
2245
- },
2246
- });
2247
- onEvent({
2248
- type: "usage",
2249
- inputTokens: 50,
2250
- outputTokens: 25,
2251
- model: "test-model",
2252
- providerDurationMs: 100,
2253
- });
2254
- return [
2255
- ...messages,
2256
- {
2257
- role: "assistant" as const,
2258
- content: [{ type: "text", text: "ok" }] as ContentBlock[],
2259
- },
2260
- ];
2261
- };
2262
-
1957
+ // After the preflight reducer brings the estimate under budget, the loop
1958
+ // completes the turn in a single provider call.
1959
+ const { provider, calls } = createMockProvider([textResponse("ok")]);
2263
1960
  const ctx = makeCtx({
2264
- agentLoopRun,
1961
+ loopProvider: provider,
2265
1962
  contextWindowManager: {
2266
1963
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
2267
1964
  maybeCompact: async () => ({ compacted: false }),
@@ -2272,8 +1969,8 @@ describe("session-agent-loop", () => {
2272
1969
 
2273
1970
  // Reducer should have been called during preflight
2274
1971
  expect(reducerCalls).toBeGreaterThanOrEqual(1);
2275
- // Agent loop should still succeed
2276
- expect(agentLoopCalls).toBe(1);
1972
+ // Agent loop should still succeed in a single provider call
1973
+ expect(calls.length).toBe(1);
2277
1974
  const complete = events.find((e) => e.type === "message_complete");
2278
1975
  expect(complete).toBeDefined();
2279
1976
  });
@@ -2282,73 +1979,28 @@ describe("session-agent-loop", () => {
2282
1979
  describe("provider ordering error retry", () => {
2283
1980
  test("retries with deep repair when ordering error is detected", async () => {
2284
1981
  const events: ServerMessage[] = [];
2285
- let callCount = 0;
2286
-
2287
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2288
- callCount++;
2289
- if (callCount === 1) {
2290
- onEvent({
2291
- type: "error",
2292
- error: new Error("messages ordering error"),
2293
- });
2294
- onEvent({
2295
- type: "usage",
2296
- inputTokens: 100,
2297
- outputTokens: 0,
2298
- model: "test-model",
2299
- providerDurationMs: 50,
2300
- });
2301
- return messages;
2302
- }
2303
- // Retry succeeds
2304
- onEvent({
2305
- type: "message_complete",
2306
- message: {
2307
- role: "assistant",
2308
- content: [{ type: "text", text: "fixed" }],
2309
- },
2310
- });
2311
- onEvent({
2312
- type: "usage",
2313
- inputTokens: 50,
2314
- outputTokens: 25,
2315
- model: "test-model",
2316
- providerDurationMs: 100,
2317
- });
2318
- return [
2319
- ...messages,
2320
- {
2321
- role: "assistant" as const,
2322
- content: [{ type: "text", text: "fixed" }] as ContentBlock[],
2323
- },
2324
- ];
2325
- };
2326
1982
 
2327
- const ctx = makeCtx({ agentLoopRun });
1983
+ // The provider rejects the first call with an ordering error, then
1984
+ // succeeds once the orchestrator's deep repair re-sends the turn.
1985
+ const { provider, calls } = createMockProvider([
1986
+ new Error("messages ordering error"),
1987
+ textResponse("fixed"),
1988
+ ]);
1989
+
1990
+ const ctx = makeCtx({ loopProvider: provider });
2328
1991
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2329
1992
 
2330
- expect(callCount).toBe(2);
1993
+ expect(calls.length).toBe(2);
2331
1994
  });
2332
1995
 
2333
1996
  test("emits deferred ordering error when retry also fails", async () => {
2334
1997
  const events: ServerMessage[] = [];
2335
1998
 
2336
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2337
- onEvent({
2338
- type: "error",
2339
- error: new Error("messages ordering error"),
2340
- });
2341
- onEvent({
2342
- type: "usage",
2343
- inputTokens: 100,
2344
- outputTokens: 0,
2345
- model: "test-model",
2346
- providerDurationMs: 50,
2347
- });
2348
- return messages;
2349
- };
2350
-
2351
- const ctx = makeCtx({ agentLoopRun });
1999
+ // The provider rejects every call with an ordering error, so even the
2000
+ // deep-repair retry fails and the orchestrator surfaces the error.
2001
+ const ctx = makeCtx({
2002
+ providerResponses: [new Error("messages ordering error")],
2003
+ });
2352
2004
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2353
2005
 
2354
2006
  const conversationError = events.find(
@@ -2362,63 +2014,18 @@ describe("session-agent-loop", () => {
2362
2014
  test("yields at checkpoint when canHandoffAtCheckpoint returns true", async () => {
2363
2015
  const events: ServerMessage[] = [];
2364
2016
 
2365
- const agentLoopRun: AgentLoopRun = async (
2366
- messages,
2367
- onEvent,
2368
- _signal,
2369
- _reqId,
2370
- onCheckpoint,
2371
- ) => {
2372
- // Simulate tool use followed by checkpoint
2373
- onEvent({ type: "tool_use", id: "tu-1", name: "file_read", input: {} });
2374
- onEvent({
2375
- type: "tool_result",
2376
- toolUseId: "tu-1",
2377
- content: "file content",
2378
- isError: false,
2379
- });
2380
- onEvent({
2381
- type: "message_complete",
2382
- message: {
2383
- role: "assistant",
2384
- content: [{ type: "text", text: "partial" }],
2385
- },
2386
- });
2387
- onEvent({
2388
- type: "usage",
2389
- inputTokens: 100,
2390
- outputTokens: 50,
2391
- model: "test-model",
2392
- providerDurationMs: 100,
2393
- });
2394
- if (onCheckpoint) {
2395
- const decision = await onCheckpoint({
2396
- turnIndex: 0,
2397
- toolCount: 1,
2398
- hasToolUse: true,
2399
- history: messages,
2400
- });
2401
- if (decision === "yield") {
2402
- return [
2403
- ...messages,
2404
- {
2405
- role: "assistant" as const,
2406
- content: [{ type: "text", text: "partial" }] as ContentBlock[],
2407
- },
2408
- ];
2409
- }
2410
- }
2411
- return [
2412
- ...messages,
2017
+ // A tool turn drives the loop to its first mid-loop checkpoint, where the
2018
+ // orchestrator yields for a queued handoff.
2019
+ const ctx = makeCtx({
2020
+ providerResponses: [toolUseResponse("tu-1", "file_read", {})],
2021
+ loopTools: [
2413
2022
  {
2414
- role: "assistant" as const,
2415
- content: [{ type: "text", text: "partial" }] as ContentBlock[],
2023
+ name: "file_read",
2024
+ description: "Read a file",
2025
+ input_schema: { type: "object", properties: {} },
2416
2026
  },
2417
- ];
2418
- };
2419
-
2420
- const ctx = makeCtx({
2421
- agentLoopRun,
2027
+ ],
2028
+ toolExecutor: async () => ({ content: "file content", isError: false }),
2422
2029
  canHandoffAtCheckpoint: () => true,
2423
2030
  } as unknown as Partial<AgentLoopConversationContext>);
2424
2031
 
@@ -2435,53 +2042,21 @@ describe("session-agent-loop", () => {
2435
2042
  test("continues when canHandoffAtCheckpoint returns false", async () => {
2436
2043
  const events: ServerMessage[] = [];
2437
2044
 
2438
- const agentLoopRun: AgentLoopRun = async (
2439
- messages,
2440
- onEvent,
2441
- _signal,
2442
- _reqId,
2443
- onCheckpoint,
2444
- ) => {
2445
- onEvent({ type: "tool_use", id: "tu-1", name: "file_read", input: {} });
2446
- onEvent({
2447
- type: "tool_result",
2448
- toolUseId: "tu-1",
2449
- content: "content",
2450
- isError: false,
2451
- });
2452
- onEvent({
2453
- type: "message_complete",
2454
- message: {
2455
- role: "assistant",
2456
- content: [{ type: "text", text: "done" }],
2457
- },
2458
- });
2459
- onEvent({
2460
- type: "usage",
2461
- inputTokens: 100,
2462
- outputTokens: 50,
2463
- model: "test-model",
2464
- providerDurationMs: 100,
2465
- });
2466
- if (onCheckpoint) {
2467
- await onCheckpoint({
2468
- turnIndex: 0,
2469
- toolCount: 1,
2470
- hasToolUse: true,
2471
- history: messages,
2472
- });
2473
- }
2474
- return [
2475
- ...messages,
2045
+ // The tool turn reaches a checkpoint, but with handoff disabled the loop
2046
+ // continues to the next turn and completes normally.
2047
+ const ctx = makeCtx({
2048
+ providerResponses: [
2049
+ toolUseResponse("tu-1", "file_read", {}),
2050
+ textResponse("done"),
2051
+ ],
2052
+ loopTools: [
2476
2053
  {
2477
- role: "assistant" as const,
2478
- content: [{ type: "text", text: "done" }] as ContentBlock[],
2054
+ name: "file_read",
2055
+ description: "Read a file",
2056
+ input_schema: { type: "object", properties: {} },
2479
2057
  },
2480
- ];
2481
- };
2482
-
2483
- const ctx = makeCtx({
2484
- agentLoopRun,
2058
+ ],
2059
+ toolExecutor: async () => ({ content: "content", isError: false }),
2485
2060
  canHandoffAtCheckpoint: () => false,
2486
2061
  } as unknown as Partial<AgentLoopConversationContext>);
2487
2062
 
@@ -2503,33 +2078,18 @@ describe("session-agent-loop", () => {
2503
2078
  const events: ServerMessage[] = [];
2504
2079
  const abortController = new AbortController();
2505
2080
 
2506
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2507
- onEvent({
2508
- type: "message_complete",
2509
- message: {
2510
- role: "assistant",
2511
- content: [{ type: "text", text: "partial" }],
2512
- },
2513
- });
2514
- onEvent({
2515
- type: "usage",
2516
- inputTokens: 100,
2517
- outputTokens: 50,
2518
- model: "test-model",
2519
- providerDurationMs: 100,
2520
- });
2521
- // Simulate abort after processing
2522
- abortController.abort();
2523
- return [
2524
- ...messages,
2525
- {
2526
- role: "assistant" as const,
2527
- content: [{ type: "text", text: "partial" }] as ContentBlock[],
2528
- },
2529
- ];
2081
+ // The provider completes its response but the user cancels mid-turn, so
2082
+ // the orchestrator observes the aborted signal once the loop returns.
2083
+ const provider: Provider = {
2084
+ name: "mock",
2085
+ async sendMessage(_messages, options) {
2086
+ options?.onEvent?.({ type: "text_delta", text: "partial" });
2087
+ abortController.abort();
2088
+ return textResponse("partial");
2089
+ },
2530
2090
  };
2531
2091
 
2532
- const ctx = makeCtx({ agentLoopRun, abortController });
2092
+ const ctx = makeCtx({ loopProvider: provider, abortController });
2533
2093
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2534
2094
 
2535
2095
  const cancelled = events.find((e) => e.type === "generation_cancelled");
@@ -2540,13 +2100,16 @@ describe("session-agent-loop", () => {
2540
2100
  const events: ServerMessage[] = [];
2541
2101
  const abortController = new AbortController();
2542
2102
 
2543
- const agentLoopRun: AgentLoopRun = async () => {
2544
- abortController.abort();
2545
- const err = new DOMException("The operation was aborted", "AbortError");
2546
- throw err;
2103
+ // The provider rejects with an AbortError after the user cancels.
2104
+ const provider: Provider = {
2105
+ name: "mock",
2106
+ async sendMessage() {
2107
+ abortController.abort();
2108
+ throw new DOMException("The operation was aborted", "AbortError");
2109
+ },
2547
2110
  };
2548
2111
 
2549
- const ctx = makeCtx({ agentLoopRun, abortController });
2112
+ const ctx = makeCtx({ loopProvider: provider, abortController });
2550
2113
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2551
2114
 
2552
2115
  const cancelled = events.find((e) => e.type === "generation_cancelled");
@@ -2563,33 +2126,17 @@ describe("session-agent-loop", () => {
2563
2126
  const abortController = new AbortController();
2564
2127
  resolveAssistantAttachmentsMock.mockClear();
2565
2128
 
2566
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2567
- onEvent({
2568
- type: "message_complete",
2569
- message: {
2570
- role: "assistant",
2571
- content: [{ type: "text", text: "partial" }],
2572
- },
2573
- });
2574
- onEvent({
2575
- type: "usage",
2576
- inputTokens: 100,
2577
- outputTokens: 50,
2578
- model: "test-model",
2579
- providerDurationMs: 100,
2580
- });
2581
- // Simulate abort after processing
2582
- abortController.abort();
2583
- return [
2584
- ...messages,
2585
- {
2586
- role: "assistant" as const,
2587
- content: [{ type: "text", text: "partial" }] as ContentBlock[],
2588
- },
2589
- ];
2129
+ // The provider completes its response but the user cancels mid-turn.
2130
+ const provider: Provider = {
2131
+ name: "mock",
2132
+ async sendMessage(_messages, options) {
2133
+ options?.onEvent?.({ type: "text_delta", text: "partial" });
2134
+ abortController.abort();
2135
+ return textResponse("partial");
2136
+ },
2590
2137
  };
2591
2138
 
2592
- const ctx = makeCtx({ agentLoopRun, abortController });
2139
+ const ctx = makeCtx({ loopProvider: provider, abortController });
2593
2140
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2594
2141
 
2595
2142
  const cancelled = events.find((e) => e.type === "generation_cancelled");
@@ -2601,90 +2148,50 @@ describe("session-agent-loop", () => {
2601
2148
 
2602
2149
  describe("finally block cleanup", () => {
2603
2150
  test("increments turnCount after successful run", async () => {
2604
- const ctx = makeCtx({
2605
- agentLoopRun: async (messages, onEvent) => {
2606
- onEvent({
2607
- type: "message_complete",
2608
- message: {
2609
- role: "assistant",
2610
- content: [{ type: "text", text: "hi" }],
2611
- },
2612
- });
2613
- onEvent({
2614
- type: "usage",
2615
- inputTokens: 10,
2616
- outputTokens: 5,
2617
- model: "test",
2618
- providerDurationMs: 50,
2619
- });
2620
- return [
2621
- ...messages,
2622
- {
2623
- role: "assistant" as const,
2624
- content: [{ type: "text", text: "hi" }] as ContentBlock[],
2625
- },
2626
- ];
2627
- },
2628
- });
2151
+ // GIVEN a real loop that answers in a single text turn
2152
+ const ctx = makeCtx({ providerResponses: [textResponse("hi")] });
2629
2153
  expect(ctx.turnCount).toBe(0);
2630
2154
 
2155
+ // WHEN the orchestrator runs the turn to completion
2631
2156
  await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2632
2157
 
2158
+ // THEN the finally block increments the turn count
2633
2159
  expect(ctx.turnCount).toBe(1);
2634
2160
  });
2635
2161
 
2636
2162
  test("clears processing state and abort controller", async () => {
2637
- const ctx = makeCtx({
2638
- agentLoopRun: async (messages, onEvent) => {
2639
- onEvent({
2640
- type: "message_complete",
2641
- message: {
2642
- role: "assistant",
2643
- content: [{ type: "text", text: "hi" }],
2644
- },
2645
- });
2646
- onEvent({
2647
- type: "usage",
2648
- inputTokens: 10,
2649
- outputTokens: 5,
2650
- model: "test",
2651
- providerDurationMs: 50,
2652
- });
2653
- return [
2654
- ...messages,
2655
- {
2656
- role: "assistant" as const,
2657
- content: [{ type: "text", text: "hi" }] as ContentBlock[],
2658
- },
2659
- ];
2660
- },
2661
- });
2163
+ // GIVEN a real loop that answers in a single text turn
2164
+ const ctx = makeCtx({ providerResponses: [textResponse("hi")] });
2662
2165
 
2166
+ // WHEN the orchestrator runs the turn to completion
2663
2167
  await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2664
2168
 
2665
- expect(ctx.processing).toBe(false);
2169
+ // THEN the finally block clears all per-turn processing state
2170
+ expect(ctx.isProcessing()).toBe(false);
2666
2171
  expect(ctx.abortController).toBeNull();
2667
2172
  expect(ctx.currentRequestId).toBeUndefined();
2668
2173
  expect(ctx.commandIntent).toBeUndefined();
2669
2174
  });
2670
2175
 
2671
- test("clears state even when agent loop throws", async () => {
2176
+ test("clears state and surfaces a processing error when the provider call fails", async () => {
2177
+ // GIVEN a real loop whose provider rejects with an unexpected error
2672
2178
  const events: ServerMessage[] = [];
2673
2179
  const ctx = makeCtx({
2674
- agentLoopRun: async () => {
2675
- throw new Error("unexpected crash");
2676
- },
2180
+ loopProvider: {
2181
+ name: "mock-provider",
2182
+ async sendMessage() {
2183
+ throw new Error("unexpected crash");
2184
+ },
2185
+ } as unknown as Provider,
2677
2186
  });
2678
2187
 
2188
+ // WHEN the orchestrator runs the turn
2679
2189
  await runAgentLoopImpl(ctx, "hi", "msg-1", (msg) => events.push(msg));
2680
2190
 
2681
- expect(ctx.processing).toBe(false);
2191
+ // THEN the finally block clears per-turn state and the failure is
2192
+ // surfaced as a processing-failed conversation error
2193
+ expect(ctx.isProcessing()).toBe(false);
2682
2194
  expect(ctx.abortController).toBeNull();
2683
- expect(events.find((event) => event.type === "error")).toMatchObject({
2684
- type: "error",
2685
- code: "CONVERSATION_PROCESSING_FAILED",
2686
- errorCategory: "processing_failed",
2687
- });
2688
2195
  expect(
2689
2196
  events.find((event) => event.type === "conversation_error"),
2690
2197
  ).toMatchObject({
@@ -2695,41 +2202,19 @@ describe("session-agent-loop", () => {
2695
2202
  });
2696
2203
 
2697
2204
  test("drains queue after completion", async () => {
2205
+ // GIVEN a real loop that answers in a single text turn
2698
2206
  let drainReason: string | undefined;
2699
2207
  const ctx = makeCtx({
2700
- agentLoopRun: async (
2701
- messages: Message[],
2702
- onEvent: (event: AgentEvent) => void,
2703
- ) => {
2704
- onEvent({
2705
- type: "message_complete",
2706
- message: {
2707
- role: "assistant",
2708
- content: [{ type: "text", text: "ok" }],
2709
- },
2710
- });
2711
- onEvent({
2712
- type: "usage",
2713
- inputTokens: 10,
2714
- outputTokens: 5,
2715
- model: "test",
2716
- providerDurationMs: 50,
2717
- });
2718
- return [
2719
- ...messages,
2720
- {
2721
- role: "assistant" as const,
2722
- content: [{ type: "text", text: "ok" }] as ContentBlock[],
2723
- },
2724
- ];
2725
- },
2208
+ providerResponses: [textResponse("ok")],
2726
2209
  drainQueue: (reason: string) => {
2727
2210
  drainReason = reason;
2728
2211
  },
2729
2212
  } as unknown as Partial<AgentLoopConversationContext>);
2730
2213
 
2214
+ // WHEN the orchestrator runs the turn to completion
2731
2215
  await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2732
2216
 
2217
+ // THEN the queue is drained with the loop-complete reason
2733
2218
  expect(drainReason).toBe("loop_complete");
2734
2219
  });
2735
2220
  });
@@ -2848,7 +2333,7 @@ describe("session-agent-loop", () => {
2848
2333
  isUserMessage: true,
2849
2334
  });
2850
2335
 
2851
- expect(ctx.processing).toBe(false);
2336
+ expect(ctx.isProcessing()).toBe(false);
2852
2337
  expect(ctx.abortController).toBeNull();
2853
2338
  expect(ctx.currentRequestId).toBeUndefined();
2854
2339
  });
@@ -2958,24 +2443,17 @@ describe("session-agent-loop", () => {
2958
2443
  test("synthesizes error assistant message when provider returns no response", async () => {
2959
2444
  const events: ServerMessage[] = [];
2960
2445
 
2961
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2962
- // Emit a non-ordering, non-context-too-large error that sets providerErrorUserMessage
2963
- onEvent({
2964
- type: "error",
2965
- error: new Error("Internal processing failure"),
2966
- });
2967
- onEvent({
2968
- type: "usage",
2969
- inputTokens: 100,
2970
- outputTokens: 0,
2971
- model: "test-model",
2972
- providerDurationMs: 50,
2973
- });
2974
- // Return same messages (no assistant message appended)
2975
- return messages;
2976
- };
2977
-
2978
- const ctx = makeCtx({ agentLoopRun });
2446
+ // GIVEN a real loop whose provider rejects with a generic error
2447
+ // (non-ordering, non-context-too-large) so the loop emits `error` and
2448
+ // the orchestrator sets `providerErrorUserMessage`.
2449
+ const ctx = makeCtx({
2450
+ loopProvider: {
2451
+ name: "mock-provider",
2452
+ async sendMessage() {
2453
+ throw new Error("Internal processing failure");
2454
+ },
2455
+ } as unknown as Provider,
2456
+ });
2979
2457
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2980
2458
 
2981
2459
  // The error should be sent as a conversation_error (not as an
@@ -2999,26 +2477,19 @@ describe("session-agent-loop", () => {
2999
2477
  // sweep would wrong-attach this row to the wrong assistant message.
3000
2478
  const events: ServerMessage[] = [];
3001
2479
 
3002
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3003
- // 1) handleProviderError -> writes an `llm_request_logs` row with
3004
- // messageId=null (the orphan we are trying to link).
3005
- onEvent({
3006
- type: "provider_error",
3007
- error: new Error("upstream 500"),
3008
- rawRequest: { model: "gpt-4.1", messages: [] },
3009
- actualProvider: "openai",
3010
- });
3011
- // 2) handleError -> sets `state.providerErrorUserMessage`, which
3012
- // activates the synthetic-message branch below the loop.
3013
- onEvent({
3014
- type: "error",
3015
- error: new Error("upstream 500"),
3016
- });
3017
- // Provider returned no assistant content — same messages back.
3018
- return messages;
3019
- };
3020
-
3021
- const ctx = makeCtx({ agentLoopRun });
2480
+ // GIVEN a real loop whose provider rejects: the loop emits
2481
+ // `provider_error` (writing an `llm_request_logs` row with
2482
+ // messageId=null the orphan we link) then `error` (which sets
2483
+ // `state.providerErrorUserMessage`, activating the synthetic-message
2484
+ // branch below the loop).
2485
+ const ctx = makeCtx({
2486
+ loopProvider: {
2487
+ name: "mock-provider",
2488
+ async sendMessage() {
2489
+ throw new Error("upstream 500");
2490
+ },
2491
+ } as unknown as Provider,
2492
+ });
3022
2493
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
3023
2494
 
3024
2495
  // The orphan was written with messageId=undefined.
@@ -3044,6 +2515,520 @@ describe("session-agent-loop", () => {
3044
2515
  });
3045
2516
  });
3046
2517
 
2518
+ describe("B3 pre-allocation: indexing + cleanup", () => {
2519
+ test("handleMessageComplete indexes and projects the finalized assistant row", async () => {
2520
+ // The pre-B3 path inserted assistant rows via `addMessage`, which ran
2521
+ // the memory indexer and the conversation-attention projector as
2522
+ // side-effects of the insert. B3 splits the write into
2523
+ // `reserveMessage` + `updateMessageContent`, both of which are CRUD-only,
2524
+ // so the indexing + projection calls had to be re-driven explicitly
2525
+ // after `updateContent` succeeds. Codex P1 caught a regression where
2526
+ // this path was missing entirely; this test pins it down.
2527
+ mockMessageById = {
2528
+ id: "msg-reserve",
2529
+ conversationId: "test-conv",
2530
+ createdAt: 1234567,
2531
+ role: "assistant",
2532
+ content: "[]",
2533
+ metadata: null,
2534
+ };
2535
+ // Force attention projection to report a state change so we also
2536
+ // observe the sync-invalidation publish path on the same turn.
2537
+ projectAssistantMessageMock.mockImplementationOnce(() => true);
2538
+
2539
+ // GIVEN a real loop that answers with a single finalized assistant turn
2540
+ const ctx = makeCtx({
2541
+ providerResponses: [textResponse("indexed reply")],
2542
+ });
2543
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2544
+
2545
+ // Indexer fired with the reserved row's id + the finalized content.
2546
+ expect(indexMessageNowMock).toHaveBeenCalledTimes(1);
2547
+ const indexCallArgs = indexMessageNowMock.mock.calls[0] as unknown as [
2548
+ {
2549
+ messageId: string;
2550
+ conversationId: string;
2551
+ role: string;
2552
+ content: string;
2553
+ createdAt: number;
2554
+ scopeId: string;
2555
+ },
2556
+ unknown,
2557
+ ];
2558
+ const indexCall = indexCallArgs[0];
2559
+ expect(indexCall).toMatchObject({
2560
+ messageId: "msg-reserve",
2561
+ conversationId: "test-conv",
2562
+ role: "assistant",
2563
+ createdAt: 1234567,
2564
+ scopeId: "default",
2565
+ });
2566
+ expect(indexCall.content).toContain("indexed reply");
2567
+
2568
+ // Attention projector fired with the same row coordinates.
2569
+ expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
2570
+ const projectCall = projectAssistantMessageMock.mock
2571
+ .calls[0] as unknown as [
2572
+ { conversationId: string; messageId: string; messageAt: number },
2573
+ ];
2574
+ expect(projectCall[0]).toEqual({
2575
+ conversationId: "test-conv",
2576
+ messageId: "msg-reserve",
2577
+ messageAt: 1234567,
2578
+ });
2579
+
2580
+ // Projection reported a state change → sync invalidation fires with
2581
+ // the conversation `:metadata` tag. The mock also receives a
2582
+ // `:messages` invalidation from the orchestrator's
2583
+ // `publishLoopMessagesChanged` post-loop emit, so we filter by tag
2584
+ // rather than asserting a total call count.
2585
+ const metadataPublishes = (
2586
+ publishSyncInvalidationMock.mock.calls as unknown as Array<[string[]]>
2587
+ ).filter((args) => args[0]?.includes("conversation:test-conv:metadata"));
2588
+ expect(metadataPublishes).toHaveLength(1);
2589
+ });
2590
+
2591
+ test("handleMessageComplete skips sync invalidation when attention state unchanged", async () => {
2592
+ // Mirror of the previous test but with the default projector return
2593
+ // (`false`). The projection still runs every turn, but the sync
2594
+ // invalidation publish must be gated on attention-state movement to
2595
+ // avoid flooding clients with no-op metadata refreshes.
2596
+ mockMessageById = {
2597
+ id: "msg-reserve",
2598
+ conversationId: "test-conv",
2599
+ createdAt: 999,
2600
+ role: "assistant",
2601
+ content: "[]",
2602
+ metadata: null,
2603
+ };
2604
+
2605
+ // GIVEN a real loop that answers with a single finalized assistant turn
2606
+ const ctx = makeCtx({ providerResponses: [textResponse("quiet")] });
2607
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2608
+
2609
+ expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
2610
+ // The mock will still receive a `:messages` invalidation from the
2611
+ // orchestrator's `publishLoopMessagesChanged` — filter to the
2612
+ // `:metadata` tag and assert it never landed.
2613
+ const metadataPublishes = (
2614
+ publishSyncInvalidationMock.mock.calls as unknown as Array<[string[]]>
2615
+ ).filter((args) => args[0]?.includes("conversation:test-conv:metadata"));
2616
+ expect(metadataPublishes).toHaveLength(0);
2617
+ });
2618
+
2619
+ test("handleLlmCallStarted deletes a stranded reservation before reserving a new row", async () => {
2620
+ // Simulates a retry path: the first LLM call reserves an assistant row
2621
+ // but exits without `message_complete` (e.g. context-overflow rescue,
2622
+ // ordering-error rescue, image-overflow rescue). The next
2623
+ // `llm_call_started` must delete the stranded row so the transcript
2624
+ // does not accumulate empty assistant bubbles.
2625
+ reserveMessageMock
2626
+ .mockImplementationOnce(async () => ({ id: "msg-strand-A" }))
2627
+ .mockImplementationOnce(async () => ({ id: "msg-strand-B" }));
2628
+ // Indexer/projector mocks default to no-op; no finalized row in this
2629
+ // test, so `mockMessageById` stays null.
2630
+
2631
+ // A single reducer tier converges the oversized context so the
2632
+ // orchestrator re-enters the loop after the first call fails.
2633
+ mockReducerStepFn = (msgs: Message[]) => ({
2634
+ messages: msgs,
2635
+ tier: "forced_compaction",
2636
+ state: {
2637
+ appliedTiers: ["forced_compaction"],
2638
+ injectionMode: "full",
2639
+ exhausted: false,
2640
+ },
2641
+ estimatedTokens: 5000,
2642
+ });
2643
+
2644
+ // GIVEN a real loop whose first call rejects with context-too-large
2645
+ // (reserving msg-strand-A but never finalizing it), then recovers via
2646
+ // convergence on re-entry. The re-entry's `llm_call_started` must
2647
+ // delete the stranded msg-strand-A before reserving msg-strand-B.
2648
+ const ctx = makeCtx({
2649
+ providerResponses: [
2650
+ new Error("context_length_exceeded"),
2651
+ textResponse("retry succeeded"),
2652
+ ],
2653
+ contextWindowManager: {
2654
+ shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
2655
+ maybeCompact: async () => ({ compacted: false }),
2656
+ } as unknown as AgentLoopConversationContext["contextWindowManager"],
2657
+ });
2658
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2659
+
2660
+ // Exactly one delete fires — for msg-strand-A, before the second
2661
+ // reserve. The second reservation is committed via `updateContent`
2662
+ // (not deleted), and after the run completes
2663
+ // `assistantRowAwaitingFinalization` is false, so no further delete
2664
+ // is attempted on shutdown.
2665
+ expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
2666
+ const strandDeleteCall = deleteMessageByIdMock.mock
2667
+ .calls[0] as unknown as [string];
2668
+ expect(strandDeleteCall[0]).toBe("msg-strand-A");
2669
+ expect(reserveMessageMock).toHaveBeenCalledTimes(2);
2670
+ });
2671
+
2672
+ test("provider-error branch deletes the orphaned reservation and repoints lastAssistantMessageId", async () => {
2673
+ // Codex P2 regression: B3 reserves an empty assistant row at
2674
+ // `llm_call_started`. When the call exits via the provider-error
2675
+ // branch (no `message_complete`), the synthetic error message is
2676
+ // inserted separately. Without cleanup the transcript would carry
2677
+ // both the empty reserved row AND the error message, and
2678
+ // `syncLastAssistantMessageToDisk` (which reads
2679
+ // `state.lastAssistantMessageId`) would mis-target the deleted
2680
+ // reservation id.
2681
+ reserveMessageMock.mockImplementationOnce(async () => ({
2682
+ id: "msg-orphaned-reservation",
2683
+ }));
2684
+
2685
+ // GIVEN a real loop that reserves an assistant row at
2686
+ // `llm_call_started`, then whose provider rejects: the loop emits
2687
+ // `provider_error` (writing the llm_request_log row) and `error`
2688
+ // (arming `state.providerErrorUserMessage`), exiting with no
2689
+ // `message_complete` so the synthetic-error branch below the loop
2690
+ // fires.
2691
+ const ctx = makeCtx({
2692
+ loopProvider: {
2693
+ name: "mock-provider",
2694
+ async sendMessage() {
2695
+ throw new Error("upstream 500");
2696
+ },
2697
+ } as unknown as Provider,
2698
+ });
2699
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2700
+
2701
+ // The orphan was deleted exactly once, before the synthetic error
2702
+ // message landed.
2703
+ expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
2704
+ const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
2705
+ string,
2706
+ ];
2707
+ expect(deleteCall[0]).toBe("msg-orphaned-reservation");
2708
+
2709
+ // Post-loop `syncLastAssistantMessageToDisk` targets the synthetic
2710
+ // error row's id (`mock-msg-id` from the mocked `addMessage`), NOT
2711
+ // the deleted reservation id. This is the externally-observable
2712
+ // proof that `state.lastAssistantMessageId` was repointed.
2713
+ expect(syncMessageToDiskMock).toHaveBeenCalled();
2714
+ const syncCalls = syncMessageToDiskMock.mock.calls as unknown as Array<
2715
+ [string, string, number]
2716
+ >;
2717
+ const lastSync = syncCalls[syncCalls.length - 1];
2718
+ expect(lastSync?.[1]).toBe("mock-msg-id");
2719
+ expect(lastSync?.[1]).not.toBe("msg-orphaned-reservation");
2720
+ });
2721
+ });
2722
+
2723
+ describe("partial persistence", () => {
2724
+ // The legacy flow reserves an empty assistant row at `llm_call_started`
2725
+ // (`content: "[]"`) and never touches it again until
2726
+ // `handleMessageComplete` fires the single authoritative
2727
+ // `updateContent`. Between those events the row is empty for the full
2728
+ // duration of a turn — a browser refresh mid-turn sees nothing where
2729
+ // the in-progress assistant reply should be.
2730
+ //
2731
+ // Partial persistence closes that durability gap with a debounced
2732
+ // flush from `handleTextDelta` (250ms timer). `handleToolUse`
2733
+ // intentionally does NOT flush — `AgentLoop.run` emits `tool_use`
2734
+ // strictly AFTER `message_complete`, so any flush from that handler
2735
+ // would land after the authoritative finalize and overwrite the
2736
+ // finalized row. The indexer + projector still fire ONLY at
2737
+ // `message_complete` — partial rows are never indexed.
2738
+ //
2739
+ // These tests pin down the wire-level contract by counting
2740
+ // `updateMessageContent` calls and inspecting the JSON payload of the
2741
+ // partial-flush writes. The indexing / sync-invalidation paths are
2742
+ // covered by the pre-allocation block above.
2743
+
2744
+ test("debounced time gate flushes one partial write after PARTIAL_PERSIST_DEBOUNCE_MS", async () => {
2745
+ mockMessageById = {
2746
+ id: "msg-reserve",
2747
+ conversationId: "test-conv",
2748
+ createdAt: 1234567,
2749
+ role: "assistant",
2750
+ content: "[]",
2751
+ metadata: null,
2752
+ };
2753
+
2754
+ // GIVEN a real loop whose provider streams two small deltas (each under
2755
+ // the 1024-char size gate) then holds the turn open past the 250ms
2756
+ // debounce window before completing, so a single debounced partial
2757
+ // flush lands before `message_complete`.
2758
+ const ctx = makeCtx({
2759
+ loopProvider: {
2760
+ name: "mock-provider",
2761
+ async sendMessage(_messages, options) {
2762
+ options?.onEvent?.({ type: "text_delta", text: "Hello, " });
2763
+ options?.onEvent?.({ type: "text_delta", text: "world." });
2764
+ await new Promise((resolve) => setTimeout(resolve, 1100));
2765
+ return textResponse("Hello, world.");
2766
+ },
2767
+ },
2768
+ });
2769
+
2770
+ // WHEN the orchestrator runs the turn to completion
2771
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2772
+
2773
+ // Exactly two `updateContent` calls land:
2774
+ // 1. the debounced partial flush after both deltas accumulated, and
2775
+ // 2. the final authoritative flush in `handleMessageComplete`.
2776
+ // Without the debounce gate this would be one-per-delta + one final
2777
+ // (3). Without the partial flush at all it would be just 1.
2778
+ expect(updateMessageContentMock).toHaveBeenCalledTimes(2);
2779
+ const calls = updateMessageContentMock.mock.calls as unknown as Array<
2780
+ [string, string]
2781
+ >;
2782
+ const partialFlush = calls[0];
2783
+ expect(partialFlush?.[0]).toBe("msg-reserve");
2784
+ const partialBlocks = JSON.parse(partialFlush?.[1] ?? "[]") as Array<{
2785
+ type: string;
2786
+ text?: string;
2787
+ }>;
2788
+ expect(partialBlocks).toEqual([{ type: "text", text: "Hello, world." }]);
2789
+ });
2790
+
2791
+ test("handleToolUse does NOT trigger a partial flush of its own", async () => {
2792
+ // `AgentLoop.run` emits `tool_use` strictly AFTER `message_complete`,
2793
+ // so a flush from the tool_use handler would land after the
2794
+ // authoritative final `updateContent` and overwrite the finalized
2795
+ // row (Codex P1 / Vargas review feedback). The handler must be a
2796
+ // no-op for the partial-persist accumulator.
2797
+ mockMessageById = {
2798
+ id: "msg-reserve",
2799
+ conversationId: "test-conv",
2800
+ createdAt: 1234567,
2801
+ role: "assistant",
2802
+ content: "[]",
2803
+ metadata: null,
2804
+ };
2805
+
2806
+ // GIVEN a real loop that runs one tool turn — the loop emits `tool_use`
2807
+ // strictly AFTER `message_complete` — and then answers with a final
2808
+ // text turn. The tool executor returns immediately.
2809
+ const ctx = makeCtx({
2810
+ providerResponses: [
2811
+ toolUseResponse("tu-no-flush", "file_read", { path: "/foo" }),
2812
+ textResponse("done"),
2813
+ ],
2814
+ loopTools: [
2815
+ {
2816
+ name: "file_read",
2817
+ description: "Read a file",
2818
+ input_schema: {
2819
+ type: "object",
2820
+ properties: { path: { type: "string" } },
2821
+ },
2822
+ },
2823
+ ],
2824
+ toolExecutor: async () => ({ content: "ok", isError: false }),
2825
+ });
2826
+
2827
+ // WHEN the orchestrator runs the turn to completion
2828
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2829
+
2830
+ // Four authoritative writes land and no stray partial flush:
2831
+ // - one final flush per `message_complete` (the tool turn and the final
2832
+ // text turn), plus
2833
+ // - two grouped tool-result user-row writes (persist-on-arrival and the
2834
+ // turn-boundary finalize).
2835
+ // `handleToolUse` contributes no partial flush of its own; one would make
2836
+ // this 5. That stray flush is the regression this test guards against.
2837
+ expect(updateMessageContentMock).toHaveBeenCalledTimes(4);
2838
+ });
2839
+
2840
+ test("handleMessageComplete clears any pending debounce timer before the final flush", async () => {
2841
+ mockMessageById = {
2842
+ id: "msg-reserve",
2843
+ conversationId: "test-conv",
2844
+ createdAt: 1234567,
2845
+ role: "assistant",
2846
+ content: "[]",
2847
+ metadata: null,
2848
+ };
2849
+
2850
+ // GIVEN a real loop whose first turn streams a short delta (scheduling a
2851
+ // debounce timer) and completes as a tool turn — so `message_complete`
2852
+ // arrives before the 250ms timer and clears it. The tool executor then
2853
+ // holds the loop open well past the original debounce window, proving a
2854
+ // late timer does NOT fire a stray partial flush, before a final text
2855
+ // turn ends the run.
2856
+ const ctx = makeCtx({
2857
+ providerResponses: [
2858
+ {
2859
+ content: [
2860
+ { type: "text", text: "Quick reply." },
2861
+ {
2862
+ type: "tool_use",
2863
+ id: "tu-keep-alive",
2864
+ name: "file_read",
2865
+ input: {},
2866
+ },
2867
+ ],
2868
+ model: "mock-model",
2869
+ usage: { inputTokens: 10, outputTokens: 5 },
2870
+ stopReason: "tool_use",
2871
+ },
2872
+ textResponse("done"),
2873
+ ],
2874
+ loopTools: [
2875
+ {
2876
+ name: "file_read",
2877
+ description: "Read a file",
2878
+ input_schema: { type: "object", properties: {} },
2879
+ },
2880
+ ],
2881
+ toolExecutor: async () => {
2882
+ await new Promise((resolve) => setTimeout(resolve, 1100));
2883
+ return { content: "ok", isError: false };
2884
+ },
2885
+ });
2886
+
2887
+ // WHEN the orchestrator runs the turn to completion
2888
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2889
+
2890
+ // Four authoritative writes land: one final flush per `message_complete`
2891
+ // (the tool turn and the final text turn) plus two grouped tool-result
2892
+ // user-row writes (persist-on-arrival and the turn-boundary finalize).
2893
+ // The debounced partial would have fired around T+250ms — during the tool
2894
+ // executor's hold — but the timer-clear at the top of
2895
+ // `handleMessageComplete` cancels it, so no stray fifth flush appears.
2896
+ expect(updateMessageContentMock).toHaveBeenCalledTimes(4);
2897
+ });
2898
+
2899
+ test("partial flushes never trigger the indexer or attention projector", async () => {
2900
+ mockMessageById = {
2901
+ id: "msg-reserve",
2902
+ conversationId: "test-conv",
2903
+ createdAt: 1234567,
2904
+ role: "assistant",
2905
+ content: "[]",
2906
+ metadata: null,
2907
+ };
2908
+
2909
+ // GIVEN a real loop whose provider streams a delta then holds the turn
2910
+ // open past the 250ms debounce window so the partial flush lands BEFORE
2911
+ // `message_complete`. The indexer/projector counts are snapshotted at
2912
+ // that mid-turn point (after the partial flush, before completion).
2913
+ let snapshot: [number, number] | undefined;
2914
+ const ctx = makeCtx({
2915
+ loopProvider: {
2916
+ name: "mock-provider",
2917
+ async sendMessage(_messages, options) {
2918
+ options?.onEvent?.({ type: "text_delta", text: "hello world" });
2919
+ await new Promise((resolve) => setTimeout(resolve, 1100));
2920
+ snapshot = [
2921
+ indexMessageNowMock.mock.calls.length,
2922
+ projectAssistantMessageMock.mock.calls.length,
2923
+ ];
2924
+ return textResponse("hello world");
2925
+ },
2926
+ },
2927
+ });
2928
+
2929
+ // WHEN the orchestrator runs the turn to completion
2930
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2931
+
2932
+ expect(snapshot).toBeDefined();
2933
+ // Indexer + projector were both ZERO during the mid-turn partial
2934
+ // flush — they only fire from `handleMessageComplete` after the
2935
+ // authoritative `updateContent`.
2936
+ expect(snapshot![0]).toBe(0);
2937
+ expect(snapshot![1]).toBe(0);
2938
+ // After the loop completes the indexer + projector each ran exactly
2939
+ // once (the pre-allocation finalize path).
2940
+ expect(indexMessageNowMock).toHaveBeenCalledTimes(1);
2941
+ expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
2942
+ });
2943
+
2944
+ test("partial flushes redact secrets from text blocks before writing", async () => {
2945
+ mockMessageById = {
2946
+ id: "msg-reserve",
2947
+ conversationId: "test-conv",
2948
+ createdAt: 1234567,
2949
+ role: "assistant",
2950
+ content: "[]",
2951
+ metadata: null,
2952
+ };
2953
+ // A GitHub PAT-shaped token mid-stream — the redaction discipline
2954
+ // mirrors `handleMessageComplete`'s final flush so a refresh mid-turn
2955
+ // never sees plaintext credentials in the persisted row.
2956
+ const ghToken = "ghp_" + "a".repeat(36);
2957
+ const payload = "Here's the key: " + ghToken + " enjoy.";
2958
+
2959
+ // GIVEN a real loop whose provider streams the PAT-bearing payload as a
2960
+ // delta then holds the turn open past the 250ms debounce window so the
2961
+ // partial flush lands before `message_complete`.
2962
+ const ctx = makeCtx({
2963
+ loopProvider: {
2964
+ name: "mock-provider",
2965
+ async sendMessage(_messages, options) {
2966
+ options?.onEvent?.({ type: "text_delta", text: payload });
2967
+ await new Promise((resolve) => setTimeout(resolve, 1100));
2968
+ return textResponse(payload);
2969
+ },
2970
+ },
2971
+ });
2972
+
2973
+ // WHEN the orchestrator runs the turn to completion
2974
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
2975
+
2976
+ expect(updateMessageContentMock).toHaveBeenCalledTimes(2);
2977
+ const partialPayload = (
2978
+ updateMessageContentMock.mock.calls[0] as unknown as [string, string]
2979
+ )[1];
2980
+ // The raw PAT must never appear in the persisted snapshot. The
2981
+ // redaction substitute is implementation-defined; the contract here
2982
+ // is "the literal token string is gone".
2983
+ expect(partialPayload).not.toContain(ghToken);
2984
+ });
2985
+
2986
+ test("provider-error cleanup deletes a row that has accumulated partial content", async () => {
2987
+ // Regression check: the pre-allocation orphan-cleanup branch
2988
+ // already deletes the reserved row when the LLM call exits via
2989
+ // `provider_error`. Partial-persist writes content to that row
2990
+ // mid-turn; the cleanup must still fire and the row (along with
2991
+ // its partial content) must still be deleted before the synthetic
2992
+ // error message lands.
2993
+ reserveMessageMock.mockImplementationOnce(async () => ({
2994
+ id: "msg-orphan-with-partial",
2995
+ }));
2996
+
2997
+ // GIVEN a real loop whose provider streams a delta — landing a debounced
2998
+ // partial flush on the reserved row — then rejects, so the loop emits
2999
+ // `provider_error` and `error` and exits with no `message_complete`.
3000
+ const ctx = makeCtx({
3001
+ loopProvider: {
3002
+ name: "mock-provider",
3003
+ async sendMessage(_messages, options) {
3004
+ options?.onEvent?.({ type: "text_delta", text: "hello world" });
3005
+ await new Promise((resolve) => setTimeout(resolve, 1100));
3006
+ throw new Error("upstream 500");
3007
+ },
3008
+ },
3009
+ });
3010
+
3011
+ // WHEN the orchestrator runs the turn
3012
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3013
+
3014
+ // Partial flush fired exactly once (before the provider error).
3015
+ // The orphan row was then deleted; the synthetic error message is
3016
+ // inserted separately via `addMessage` (`mock-msg-id`) and never
3017
+ // touched by `updateContent`.
3018
+ const partialFlushes = (
3019
+ updateMessageContentMock.mock.calls as unknown as Array<
3020
+ [string, string]
3021
+ >
3022
+ ).filter(([id]) => id === "msg-orphan-with-partial");
3023
+ expect(partialFlushes).toHaveLength(1);
3024
+ expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
3025
+ const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
3026
+ string,
3027
+ ];
3028
+ expect(deleteCall[0]).toBe("msg-orphan-with-partial");
3029
+ });
3030
+ });
3031
+
3047
3032
  describe("pkbSystemReminderBlock metadata persistence", () => {
3048
3033
  test("persists pkbSystemReminderBlock in full mode with PKB active", async () => {
3049
3034
  const reminder = "<system_reminder>\npkb content\n</system_reminder>";
@@ -3524,50 +3509,32 @@ describe("session-agent-loop", () => {
3524
3509
  compactableStartIndex: 0,
3525
3510
  };
3526
3511
 
3527
- const rawMidLoopBasis: Message[] = [
3528
- {
3529
- role: "user",
3530
- content: [{ type: "text", text: "fresh DB basis user row" }],
3531
- },
3532
- {
3533
- role: "assistant",
3534
- content: [{ type: "text", text: "partial assistant response" }],
3535
- },
3536
- ];
3537
3512
  const maybeCompactInputs: Message[][] = [];
3538
- let runCount = 0;
3539
- const agentLoopRun: AgentLoopRun = async (
3540
- messages,
3541
- _onEvent,
3542
- _signal,
3543
- _reqId,
3544
- onCheckpoint,
3545
- ) => {
3546
- runCount++;
3547
- if (runCount === 1) {
3548
- mockEstimateTokens = 90_000;
3549
- const decision = await onCheckpoint?.({
3550
- turnIndex: 0,
3551
- toolCount: 1,
3552
- hasToolUse: true,
3553
- history: messages,
3554
- });
3555
- mockEstimateTokens = 1000;
3556
- if (decision === "yield") {
3557
- return rawMidLoopBasis;
3558
- }
3559
- }
3560
- return [
3561
- ...messages,
3562
- {
3563
- role: "assistant" as const,
3564
- content: [{ type: "text" as const, text: "final response" }],
3565
- },
3566
- ];
3567
- };
3568
3513
 
3514
+ // AND a real loop that runs one tool turn and then a final text turn.
3515
+ // The tool executor raises the token estimate above the mid-loop budget
3516
+ // threshold so the loop compacts in place at the post-tool checkpoint —
3517
+ // over its own in-loop history, which does not match the loaded Slack
3518
+ // rows.
3569
3519
  const ctx = makeCtx({
3570
- agentLoopRun,
3520
+ providerResponses: [
3521
+ toolUseResponse("tu-mid-loop", "file_read", { path: "/foo" }),
3522
+ textResponse("final response"),
3523
+ ],
3524
+ loopTools: [
3525
+ {
3526
+ name: "file_read",
3527
+ description: "Read a file",
3528
+ input_schema: {
3529
+ type: "object",
3530
+ properties: { path: { type: "string" } },
3531
+ },
3532
+ },
3533
+ ],
3534
+ toolExecutor: async () => {
3535
+ mockEstimateTokens = 90_000;
3536
+ return { content: "ok", isError: false };
3537
+ },
3571
3538
  channelCapabilities: {
3572
3539
  channel: "slack",
3573
3540
  dashboardCapable: false,
@@ -3604,6 +3571,9 @@ describe("session-agent-loop", () => {
3604
3571
  summaryText: "",
3605
3572
  };
3606
3573
  }
3574
+ // The mid-loop gate compacted its in-loop basis; drop the estimate
3575
+ // back under budget so the post-compaction provider call proceeds.
3576
+ mockEstimateTokens = 1000;
3607
3577
  return {
3608
3578
  compacted: true,
3609
3579
  messages: [
@@ -3632,7 +3602,9 @@ describe("session-agent-loop", () => {
3632
3602
  await runAgentLoopImpl(ctx, "next reply", "user-msg-mid-loop", () => {});
3633
3603
 
3634
3604
  expect(maybeCompactInputs[0]).toBe(renderedSlackMessages);
3635
- expect(maybeCompactInputs[1]).toBe(rawMidLoopBasis);
3605
+ // The mid-loop gate compacts the loop's own in-loop history, never the
3606
+ // loaded Slack rows — the mismatch this test guards against.
3607
+ expect(maybeCompactInputs[1]).not.toBe(renderedSlackMessages);
3636
3608
  expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
3637
3609
  null,
3638
3610
  2,
@@ -3905,62 +3877,32 @@ describe("session-agent-loop", () => {
3905
3877
  estimatedTokens: 5000,
3906
3878
  });
3907
3879
 
3908
- let callCount = 0;
3909
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3910
- callCount++;
3911
- if (callCount === 1) {
3912
- // Trigger convergence path: error + appended assistant message so
3913
- // updatedHistory.length > preRunHistoryLength at the strip site.
3914
- onEvent({
3915
- type: "error",
3916
- error: new Error("context_length_exceeded"),
3917
- });
3918
- onEvent({
3919
- type: "usage",
3920
- inputTokens: 100,
3921
- outputTokens: 0,
3922
- model: "test-model",
3923
- providerDurationMs: 50,
3924
- });
3925
- return [
3926
- ...messages,
3927
- {
3928
- role: "assistant" as const,
3929
- content: [{ type: "text", text: "partial" }] as ContentBlock[],
3930
- },
3931
- ];
3932
- }
3933
- onEvent({
3934
- type: "message_complete",
3935
- message: {
3936
- role: "assistant",
3937
- content: [{ type: "text", text: "recovered" }],
3938
- },
3939
- });
3940
- onEvent({
3941
- type: "usage",
3942
- inputTokens: 50,
3943
- outputTokens: 25,
3944
- model: "test-model",
3945
- providerDurationMs: 100,
3946
- });
3947
- return [
3948
- ...messages,
3880
+ // GIVEN a real loop that appends a tool turn (so the run reports
3881
+ // `appendedNewMessages`) and then rejects with a context-too-large
3882
+ // error on the following call — the orchestrator strips that appended
3883
+ // history during its bounded convergence path before a final call
3884
+ // recovers.
3885
+ const ctx = makeCtx({
3886
+ providerResponses: [
3887
+ toolUseResponse("t1", "file_read", {}),
3888
+ new Error("context_length_exceeded"),
3889
+ textResponse("recovered"),
3890
+ ],
3891
+ loopTools: [
3949
3892
  {
3950
- role: "assistant" as const,
3951
- content: [{ type: "text", text: "recovered" }] as ContentBlock[],
3893
+ name: "file_read",
3894
+ description: "Read a file",
3895
+ input_schema: { type: "object", properties: {} },
3952
3896
  },
3953
- ];
3954
- };
3955
-
3956
- const ctx = makeCtx({
3957
- agentLoopRun,
3897
+ ],
3898
+ toolExecutor: async () => ({ content: "ok", isError: false }),
3958
3899
  contextWindowManager: {
3959
3900
  shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3960
3901
  maybeCompact: async () => ({ compacted: false }),
3961
3902
  } as unknown as AgentLoopConversationContext["contextWindowManager"],
3962
3903
  });
3963
3904
 
3905
+ // WHEN the orchestrator runs the turn to completion
3964
3906
  await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
3965
3907
 
3966
3908
  const stripCalls = setConversationHistoryStrippedAtMock.mock.calls.filter(
@@ -3968,5 +3910,51 @@ describe("session-agent-loop", () => {
3968
3910
  );
3969
3911
  expect(stripCalls.length).toBeGreaterThanOrEqual(1);
3970
3912
  });
3913
+
3914
+ test("strip-site marker write is non-fatal when the helper throws", async () => {
3915
+ setConversationHistoryStrippedAtMock.mockImplementation(() => {
3916
+ throw new Error("db write failed");
3917
+ });
3918
+
3919
+ mockReducerStepFn = (msgs: Message[]) => ({
3920
+ messages: msgs,
3921
+ tier: "forced_compaction",
3922
+ state: {
3923
+ appliedTiers: ["forced_compaction"],
3924
+ injectionMode: "full",
3925
+ exhausted: false,
3926
+ },
3927
+ estimatedTokens: 5000,
3928
+ });
3929
+
3930
+ // GIVEN a real loop that appends a tool turn and then rejects with a
3931
+ // context-too-large error on the following call, driving the
3932
+ // convergence strip whose marker-write helper is stubbed to throw,
3933
+ // before a final call recovers.
3934
+ const ctx = makeCtx({
3935
+ providerResponses: [
3936
+ toolUseResponse("t1", "file_read", {}),
3937
+ new Error("context_length_exceeded"),
3938
+ textResponse("recovered"),
3939
+ ],
3940
+ loopTools: [
3941
+ {
3942
+ name: "file_read",
3943
+ description: "Read a file",
3944
+ input_schema: { type: "object", properties: {} },
3945
+ },
3946
+ ],
3947
+ toolExecutor: async () => ({ content: "ok", isError: false }),
3948
+ contextWindowManager: {
3949
+ shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
3950
+ maybeCompact: async () => ({ compacted: false }),
3951
+ } as unknown as AgentLoopConversationContext["contextWindowManager"],
3952
+ });
3953
+
3954
+ // Must not throw — the strip-site marker write is wrapped in try/catch.
3955
+ await expect(
3956
+ runAgentLoopImpl(ctx, "hello", "msg-1", () => {}),
3957
+ ).resolves.toBeUndefined();
3958
+ });
3971
3959
  });
3972
3960
  });