@vellumai/assistant 0.8.5 → 0.8.7

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 (1171) hide show
  1. package/AGENTS.md +33 -1
  2. package/ARCHITECTURE.md +1 -1
  3. package/Dockerfile +1 -0
  4. package/bun.lock +11 -2
  5. package/bunfig.toml +6 -1
  6. package/docker-entrypoint.sh +8 -6
  7. package/docs/credential-execution-service.md +6 -6
  8. package/docs/plugins.md +67 -31
  9. package/examples/plugins/echo/register.ts +4 -7
  10. package/knip.json +1 -0
  11. package/node_modules/@vellumai/environments/bun.lock +24 -0
  12. package/node_modules/@vellumai/environments/package.json +18 -0
  13. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
  14. package/node_modules/@vellumai/environments/src/index.ts +11 -0
  15. package/node_modules/@vellumai/environments/src/seeds.ts +73 -0
  16. package/node_modules/@vellumai/environments/src/types.ts +70 -0
  17. package/node_modules/@vellumai/environments/tsconfig.json +20 -0
  18. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +11 -0
  19. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +15 -17
  20. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +10 -3
  21. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +16 -14
  22. package/openapi.yaml +5585 -469
  23. package/package.json +7 -3
  24. package/scripts/generate-openapi.ts +20 -13
  25. package/src/__tests__/actor-token-service.test.ts +3 -2
  26. package/src/__tests__/agent-loop-callsite-precedence.test.ts +42 -80
  27. package/src/__tests__/agent-loop-exit-reason.test.ts +336 -42
  28. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +141 -0
  29. package/src/__tests__/agent-loop-override-profile.test.ts +21 -33
  30. package/src/__tests__/agent-loop-provider-error-recording.test.ts +6 -4
  31. package/src/__tests__/agent-loop-thinking.test.ts +17 -12
  32. package/src/__tests__/agent-loop.test.ts +207 -341
  33. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +5 -2
  34. package/src/__tests__/agent-wake-override-profile.test.ts +23 -40
  35. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  36. package/src/__tests__/annotate-risk-options.test.ts +1 -0
  37. package/src/__tests__/anthropic-provider.test.ts +201 -55
  38. package/src/__tests__/app-builder-skill-instructions.test.ts +22 -0
  39. package/src/__tests__/app-control-flow.test.ts +5 -0
  40. package/src/__tests__/approval-cascade.test.ts +5 -11
  41. package/src/__tests__/approval-routes-http.test.ts +13 -15
  42. package/src/__tests__/assert-not-live-db.ts +79 -0
  43. package/src/__tests__/assistant-event.test.ts +15 -0
  44. package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -27
  45. package/src/__tests__/audit-log-rotation.test.ts +2 -2
  46. package/src/__tests__/auto-analysis-end-to-end.test.ts +6 -6
  47. package/src/__tests__/avatar-e2e.test.ts +7 -37
  48. package/src/__tests__/avatar-generator.test.ts +12 -42
  49. package/src/__tests__/avatar-identity-sync.test.ts +28 -3
  50. package/src/__tests__/background-shell-bash.test.ts +3 -7
  51. package/src/__tests__/background-workers-disk-pressure.test.ts +5 -8
  52. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  53. package/src/__tests__/btw-routes.test.ts +10 -14
  54. package/src/__tests__/call-controller.test.ts +3 -2
  55. package/src/__tests__/call-pointer-messages.test.ts +5 -3
  56. package/src/__tests__/call-site-routing-provider.test.ts +22 -40
  57. package/src/__tests__/catalog-files.test.ts +1 -0
  58. package/src/__tests__/channel-approval-routes.test.ts +51 -22
  59. package/src/__tests__/channel-approvals.test.ts +3 -1
  60. package/src/__tests__/channel-guardian.test.ts +3 -2
  61. package/src/__tests__/channel-invite-transport.test.ts +1 -5
  62. package/src/__tests__/channel-readiness-routes.test.ts +0 -4
  63. package/src/__tests__/channel-readiness-slack-remote.test.ts +170 -0
  64. package/src/__tests__/channel-reply-delivery.test.ts +35 -0
  65. package/src/__tests__/channel-retry-sweep.test.ts +388 -79
  66. package/src/__tests__/checker.test.ts +12 -12
  67. package/src/__tests__/circuit-breaker-pipeline.test.ts +3 -3
  68. package/src/__tests__/clawhub-files.test.ts +1 -0
  69. package/src/__tests__/compaction-events.test.ts +6 -17
  70. package/src/__tests__/compaction-pipeline.test.ts +1 -1
  71. package/src/__tests__/compaction-timeout-recovery.test.ts +37 -48
  72. package/src/__tests__/compaction-trail-store.test.ts +186 -0
  73. package/src/__tests__/compactor-call-site-logging.test.ts +1 -0
  74. package/src/__tests__/compactor-image-manifest-trust.test.ts +112 -0
  75. package/src/__tests__/compactor-preserved-tail-count.test.ts +1 -0
  76. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +7 -5
  77. package/src/__tests__/computer-use-tools.test.ts +14 -16
  78. package/src/__tests__/config-loader-backfill.test.ts +13 -28
  79. package/src/__tests__/config-loader-corrupt.test.ts +5 -5
  80. package/src/__tests__/config-loader-platform-defaults.test.ts +93 -26
  81. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +3 -3
  82. package/src/__tests__/config-managed-gemini-defaults.test.ts +3 -4
  83. package/src/__tests__/config-schema.test.ts +10 -10
  84. package/src/__tests__/config-watcher.test.ts +28 -0
  85. package/src/__tests__/connection-model-compat.test.ts +83 -0
  86. package/src/__tests__/contacts-tools.test.ts +3 -2
  87. package/src/__tests__/context-search-agent-runner.test.ts +6 -3
  88. package/src/__tests__/context-token-estimator.test.ts +56 -0
  89. package/src/__tests__/context-window-manager-compact-retry.test.ts +291 -0
  90. package/src/__tests__/conversation-abort-tool-results.test.ts +19 -7
  91. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +4 -2
  92. package/src/__tests__/conversation-agent-loop-handlers-max-tokens.test.ts +55 -0
  93. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -27
  94. package/src/__tests__/conversation-agent-loop-overflow.test.ts +464 -90
  95. package/src/__tests__/conversation-agent-loop.test.ts +1069 -64
  96. package/src/__tests__/conversation-analysis-routes.test.ts +2 -3
  97. package/src/__tests__/conversation-app-control-instantiation.test.ts +29 -19
  98. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -1
  99. package/src/__tests__/conversation-attention-store.test.ts +101 -0
  100. package/src/__tests__/conversation-attention-telegram.test.ts +3 -2
  101. package/src/__tests__/conversation-clear-safety.test.ts +20 -10
  102. package/src/__tests__/conversation-confirmation-signals.test.ts +16 -45
  103. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  104. package/src/__tests__/conversation-disk-view.test.ts +10 -17
  105. package/src/__tests__/conversation-error.test.ts +30 -0
  106. package/src/__tests__/conversation-fork-crud.test.ts +132 -157
  107. package/src/__tests__/conversation-fork-route.test.ts +19 -16
  108. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  109. package/src/__tests__/conversation-inference-profile-list.test.ts +3 -2
  110. package/src/__tests__/conversation-inference-profile-route.test.ts +3 -2
  111. package/src/__tests__/conversation-init.benchmark.test.ts +6 -6
  112. package/src/__tests__/conversation-lifecycle.test.ts +4 -2
  113. package/src/__tests__/conversation-list-source.test.ts +3 -2
  114. package/src/__tests__/conversation-load-history-repair.test.ts +5 -3
  115. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -1
  116. package/src/__tests__/conversation-message-sync-tags.test.ts +3 -4
  117. package/src/__tests__/conversation-pairing.test.ts +87 -4
  118. package/src/__tests__/conversation-pre-run-repair.test.ts +1 -1
  119. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +30 -7
  120. package/src/__tests__/conversation-process-callsite.test.ts +28 -30
  121. package/src/__tests__/conversation-provider-retry-repair.test.ts +58 -44
  122. package/src/__tests__/conversation-queue.test.ts +603 -455
  123. package/src/__tests__/conversation-routes-disk-view.test.ts +6 -20
  124. package/src/__tests__/conversation-routes-guardian-reply.test.ts +35 -10
  125. package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -4
  126. package/src/__tests__/conversation-runtime-assembly.test.ts +98 -22
  127. package/src/__tests__/conversation-runtime-workspace.test.ts +19 -1
  128. package/src/__tests__/conversation-skill-tools.test.ts +38 -142
  129. package/src/__tests__/conversation-slash-queue.test.ts +120 -62
  130. package/src/__tests__/conversation-slash-unknown.test.ts +18 -15
  131. package/src/__tests__/conversation-speed-override.test.ts +9 -22
  132. package/src/__tests__/conversation-stream-state.test.ts +484 -0
  133. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +52 -15
  134. package/src/__tests__/conversation-surfaces-app-control.test.ts +32 -4
  135. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -0
  136. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +6 -3
  137. package/src/__tests__/conversation-surfaces-standalone.test.ts +6 -3
  138. package/src/__tests__/conversation-surfaces-state-update.test.ts +8 -5
  139. package/src/__tests__/conversation-surfaces-table-action.test.ts +13 -32
  140. package/src/__tests__/conversation-sync-tags.test.ts +128 -12
  141. package/src/__tests__/conversation-title-service.test.ts +1 -0
  142. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +53 -11
  143. package/src/__tests__/conversation-unread-route.test.ts +14 -2
  144. package/src/__tests__/conversation-usage.test.ts +1 -2
  145. package/src/__tests__/conversation-wipe.test.ts +1 -1
  146. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  147. package/src/__tests__/conversation-workspace-injection.test.ts +53 -22
  148. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +32 -7
  149. package/src/__tests__/credential-broker-browser-fill.test.ts +3 -3
  150. package/src/__tests__/credential-broker-server-use.test.ts +5 -5
  151. package/src/__tests__/credential-execution-client.test.ts +72 -1
  152. package/src/__tests__/credential-execution-feature-gates.test.ts +10 -12
  153. package/src/__tests__/credential-execution-tools.test.ts +1 -2
  154. package/src/__tests__/credential-health-service.test.ts +252 -3
  155. package/src/__tests__/credential-security-invariants.test.ts +5 -6
  156. package/src/__tests__/credential-vault-unit.test.ts +19 -19
  157. package/src/__tests__/credential-vault.test.ts +5 -5
  158. package/src/__tests__/cross-provider-web-search.test.ts +61 -3
  159. package/src/__tests__/cu-unified-flow.test.ts +26 -1
  160. package/src/__tests__/db-connection-isolation.test.ts +7 -6
  161. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +8 -10
  162. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +7 -10
  163. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +9 -15
  164. package/src/__tests__/db-schedule-syntax-migration.test.ts +11 -0
  165. package/src/__tests__/db-test-helpers.ts +58 -0
  166. package/src/__tests__/disk-pressure-guard.test.ts +119 -36
  167. package/src/__tests__/disk-pressure-lifecycle.test.ts +13 -10
  168. package/src/__tests__/disk-pressure-routes.test.ts +9 -35
  169. package/src/__tests__/disk-pressure-tools.test.ts +0 -4
  170. package/src/__tests__/dm-persistence.test.ts +33 -42
  171. package/src/__tests__/document-create-dedupe.test.ts +189 -0
  172. package/src/__tests__/document-find-replace.test.ts +3 -2
  173. package/src/__tests__/document-tool-security.test.ts +81 -2
  174. package/src/__tests__/dynamic-page-surface.test.ts +68 -0
  175. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +5 -4
  176. package/src/__tests__/edit-propagation.test.ts +1 -2
  177. package/src/__tests__/empty-response-pipeline.test.ts +127 -5
  178. package/src/__tests__/encrypted-store-test-helpers.ts +56 -0
  179. package/src/__tests__/encrypted-store.test.ts +11 -9
  180. package/src/__tests__/feature-flag-test-helpers.ts +53 -0
  181. package/src/__tests__/filing-service.test.ts +3 -2
  182. package/src/__tests__/first-greeting.test.ts +103 -12
  183. package/src/__tests__/gateway-flag-listener.test.ts +0 -1
  184. package/src/__tests__/gemini-inline-media.test.ts +78 -0
  185. package/src/__tests__/gemini-provider.test.ts +375 -26
  186. package/src/__tests__/guardian-action-sweep.test.ts +3 -2
  187. package/src/__tests__/guardian-outbound-http.test.ts +3 -2
  188. package/src/__tests__/guardian-routing-state.test.ts +60 -71
  189. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +48 -3
  190. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +10 -7
  191. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  192. package/src/__tests__/heartbeat-service.test.ts +3 -1
  193. package/src/__tests__/helpers/mock-logger.ts +26 -0
  194. package/src/__tests__/history-repair-hook.test.ts +161 -0
  195. package/src/__tests__/history-repair-observability.test.ts +1 -1
  196. package/src/__tests__/history-repair.test.ts +2 -1
  197. package/src/__tests__/host-app-control-proxy.test.ts +2 -0
  198. package/src/__tests__/host-bash-routes.test.ts +1 -0
  199. package/src/__tests__/host-cu-proxy.test.ts +2 -0
  200. package/src/__tests__/host-cu-routes-targeted.test.ts +1 -0
  201. package/src/__tests__/host-file-edit-tool.test.ts +4 -2
  202. package/src/__tests__/host-file-proxy.test.ts +31 -0
  203. package/src/__tests__/host-file-read-tool.test.ts +4 -2
  204. package/src/__tests__/host-file-routes-targeted.test.ts +1 -0
  205. package/src/__tests__/host-file-write-tool.test.ts +9 -3
  206. package/src/__tests__/host-proxy-preactivation.test.ts +53 -14
  207. package/src/__tests__/host-shell-tool.test.ts +11 -5
  208. package/src/__tests__/host-transfer-routes-targeted.test.ts +1 -0
  209. package/src/__tests__/http-conversation-lineage.test.ts +3 -2
  210. package/src/__tests__/http-user-message-parity.test.ts +31 -9
  211. package/src/__tests__/identity-intro-cache.test.ts +154 -22
  212. package/src/__tests__/inbound-slack-persistence.test.ts +51 -74
  213. package/src/__tests__/inference-profile-reaper.test.ts +3 -2
  214. package/src/__tests__/inference-profile-session-ipc.test.ts +3 -2
  215. package/src/__tests__/injector-background-turn.test.ts +1 -1
  216. package/src/__tests__/injector-chain.test.ts +1 -1
  217. package/src/__tests__/injector-disk-pressure.test.ts +4 -18
  218. package/src/__tests__/injector-document-comments.test.ts +1 -1
  219. package/src/__tests__/injector-pkb-v2-silenced.test.ts +1 -1
  220. package/src/__tests__/injector-v3-suppression.test.ts +220 -0
  221. package/src/__tests__/inline-skill-load-permissions.test.ts +4 -4
  222. package/src/__tests__/list-messages-attachments.test.ts +7 -8
  223. package/src/__tests__/list-messages-hidden-metadata.test.ts +93 -11
  224. package/src/__tests__/list-messages-page-latest.test.ts +0 -1
  225. package/src/__tests__/list-messages-tool-merge.test.ts +36 -6
  226. package/src/__tests__/llm-call-pipeline.test.ts +21 -15
  227. package/src/__tests__/llm-context-normalization.test.ts +42 -0
  228. package/src/__tests__/llm-request-log-turn-query.test.ts +42 -86
  229. package/src/__tests__/llm-resolver.test.ts +346 -39
  230. package/src/__tests__/llm-schema.test.ts +1 -1
  231. package/src/__tests__/llm-usage-store.test.ts +45 -0
  232. package/src/__tests__/log-export-routes.test.ts +59 -0
  233. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -8
  234. package/src/__tests__/manual-token-reconciliation.test.ts +76 -1
  235. package/src/__tests__/mcp-abort-signal.test.ts +14 -0
  236. package/src/__tests__/mcp-auth-routes.test.ts +15 -10
  237. package/src/__tests__/mcp-client-auth.test.ts +14 -0
  238. package/src/__tests__/mcp-health-check.test.ts +18 -13
  239. package/src/__tests__/memory-retrieval-pipeline.test.ts +1 -1
  240. package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
  241. package/src/__tests__/messaging-send-tool.test.ts +9 -4
  242. package/src/__tests__/migration-export-http.test.ts +12 -12
  243. package/src/__tests__/migration-import-commit-http.test.ts +8 -8
  244. package/src/__tests__/migration-import-from-url.test.ts +3 -3
  245. package/src/__tests__/migration-import-preflight-http.test.ts +7 -7
  246. package/src/__tests__/migration-validate-http.test.ts +3 -3
  247. package/src/__tests__/mock-gateway-ipc.ts +18 -2
  248. package/src/__tests__/model-intents.test.ts +3 -3
  249. package/src/__tests__/native-web-search.test.ts +44 -22
  250. package/src/__tests__/notification-decision-identity.test.ts +9 -18
  251. package/src/__tests__/notification-decision-recipient-context.test.ts +3 -6
  252. package/src/__tests__/notification-deep-link.test.ts +62 -0
  253. package/src/__tests__/oauth-commands-routes.test.ts +38 -1
  254. package/src/__tests__/oauth-provider-visibility.test.ts +8 -8
  255. package/src/__tests__/oauth-store.test.ts +3 -2
  256. package/src/__tests__/onboarding-template-contract.test.ts +13 -2
  257. package/src/__tests__/openai-provider.test.ts +74 -79
  258. package/src/__tests__/openai-responses-provider.test.ts +90 -86
  259. package/src/__tests__/openrouter-provider-only.test.ts +27 -5
  260. package/src/__tests__/outbound-slack-persistence.test.ts +48 -2
  261. package/src/__tests__/overflow-reduce-pipeline.test.ts +2 -4
  262. package/src/__tests__/parallel-tool.benchmark.test.ts +24 -36
  263. package/src/__tests__/persistence-pipeline.test.ts +154 -27
  264. package/src/__tests__/persistence-secret-redaction.test.ts +85 -13
  265. package/src/__tests__/pipeline-runner.test.ts +2 -3
  266. package/src/__tests__/plugin-bootstrap.test.ts +60 -36
  267. package/src/__tests__/plugin-route-contribution.test.ts +6 -16
  268. package/src/__tests__/plugin-skill-contribution.test.ts +7 -17
  269. package/src/__tests__/plugin-tool-contribution.test.ts +51 -64
  270. package/src/__tests__/plugin-types.test.ts +7 -14
  271. package/src/__tests__/prechat-onboarding-contract.test.ts +23 -0
  272. package/src/__tests__/process-message-background-slack.test.ts +38 -32
  273. package/src/__tests__/process-message-display-content.test.ts +49 -64
  274. package/src/__tests__/provider-catalog-visibility.test.ts +9 -9
  275. package/src/__tests__/provider-commit-message-generator.test.ts +19 -14
  276. package/src/__tests__/provider-error-scenarios.test.ts +7 -6
  277. package/src/__tests__/provider-platform-proxy-integration.test.ts +215 -8
  278. package/src/__tests__/provider-registry-ollama.test.ts +45 -22
  279. package/src/__tests__/provider-send-message-override-profile.test.ts +9 -25
  280. package/src/__tests__/provider-streaming.benchmark.test.ts +12 -22
  281. package/src/__tests__/provider-usage-tracking.test.ts +0 -6
  282. package/src/__tests__/ratelimit.test.ts +9 -4
  283. package/src/__tests__/recording-handler.test.ts +1 -0
  284. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  285. package/src/__tests__/registry.test.ts +82 -76
  286. package/src/__tests__/relay-server.test.ts +30 -23
  287. package/src/__tests__/retry-openrouter-only-normalization.test.ts +5 -8
  288. package/src/__tests__/retry-thinking-tool-choice.test.ts +10 -13
  289. package/src/__tests__/retry-verbosity-normalization.test.ts +5 -8
  290. package/src/__tests__/runtime-attachment-metadata.test.ts +3 -2
  291. package/src/__tests__/runtime-events-sse-reconnect.test.ts +353 -0
  292. package/src/__tests__/schedule-routes.test.ts +80 -10
  293. package/src/__tests__/schedule-store.test.ts +83 -1
  294. package/src/__tests__/schedule-tools.test.ts +125 -0
  295. package/src/__tests__/scheduler-reuse-conversation.test.ts +48 -3
  296. package/src/__tests__/secret-ingress-http.test.ts +7 -3
  297. package/src/__tests__/secret-prompt-log-hygiene.test.ts +11 -7
  298. package/src/__tests__/secret-prompter-channel-fallback.test.ts +11 -9
  299. package/src/__tests__/secret-response-routing.test.ts +13 -11
  300. package/src/__tests__/secure-keys.test.ts +3 -3
  301. package/src/__tests__/send-endpoint-busy.test.ts +83 -43
  302. package/src/__tests__/server-history-render.test.ts +4 -1
  303. package/src/__tests__/shell-observability.test.ts +249 -0
  304. package/src/__tests__/skill-feature-flags-integration.test.ts +19 -21
  305. package/src/__tests__/skill-feature-flags.test.ts +20 -22
  306. package/src/__tests__/skill-load-feature-flag.test.ts +15 -15
  307. package/src/__tests__/skill-projection-feature-flag.test.ts +44 -30
  308. package/src/__tests__/skill-projection.benchmark.test.ts +5 -7
  309. package/src/__tests__/skill-tool-factory.test.ts +96 -95
  310. package/src/__tests__/skills-files-catalog-fallback.test.ts +10 -0
  311. package/src/__tests__/skillssh-files.test.ts +1 -0
  312. package/src/__tests__/slack-channel-config.test.ts +3 -3
  313. package/src/__tests__/starter-task-flow.test.ts +6 -6
  314. package/src/__tests__/strip-memory-injections.test.ts +102 -14
  315. package/src/__tests__/subagent-call-site-routing.test.ts +13 -5
  316. package/src/__tests__/subagent-disposal.test.ts +27 -8
  317. package/src/__tests__/subagent-fork-notifications.test.ts +24 -9
  318. package/src/__tests__/subagent-fork-spawn.test.ts +13 -4
  319. package/src/__tests__/subagent-manager-notify.test.ts +20 -8
  320. package/src/__tests__/subagent-notify-parent.test.ts +5 -4
  321. package/src/__tests__/subagent-spawn-tool-fork.test.ts +58 -0
  322. package/src/__tests__/subagent-tools.test.ts +2 -1
  323. package/src/__tests__/suggestion-routes.test.ts +4 -3
  324. package/src/__tests__/sync-message-contract.test.ts +19 -16
  325. package/src/__tests__/system-prompt.test.ts +92 -0
  326. package/src/__tests__/terminal-tools.test.ts +3 -24
  327. package/src/__tests__/test-preload-verifier.ts +68 -0
  328. package/src/__tests__/test-preload.ts +32 -39
  329. package/src/__tests__/thread-backfill.test.ts +4 -9
  330. package/src/__tests__/title-generate-pipeline.test.ts +1 -1
  331. package/src/__tests__/token-estimate-pipeline.test.ts +2 -4
  332. package/src/__tests__/tool-error-pipeline.test.ts +2 -2
  333. package/src/__tests__/tool-execute-pipeline.test.ts +1 -1
  334. package/src/__tests__/tool-executor-lifecycle-events.test.ts +20 -7
  335. package/src/__tests__/tool-executor.test.ts +55 -10
  336. package/src/__tests__/tool-preview-lifecycle.test.ts +14 -11
  337. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  338. package/src/__tests__/tool-result-truncate-pipeline.test.ts +9 -12
  339. package/src/__tests__/tool-result-truncation.test.ts +3 -1
  340. package/src/__tests__/tools-audio-read.test.ts +113 -0
  341. package/src/__tests__/turn-boundary-resolution.test.ts +44 -84
  342. package/src/__tests__/turn-events-store.test.ts +11 -7
  343. package/src/__tests__/twilio-routes.test.ts +3 -2
  344. package/src/__tests__/validate-input.test.ts +381 -0
  345. package/src/__tests__/verification-control-plane-policy.test.ts +1 -0
  346. package/src/__tests__/voice-scoped-grant-consumer.test.ts +10 -7
  347. package/src/__tests__/voice-session-bridge.test.ts +50 -35
  348. package/src/__tests__/workspace-migration-090-memory-router-cost-optimized-profile.test.ts +326 -0
  349. package/src/__tests__/workspace-migration-091-retighten-migration-onboarding-thread.test.ts +166 -0
  350. package/src/acp/__tests__/prepare-agent-env.test.ts +143 -31
  351. package/src/acp/prepare-agent-env.ts +52 -11
  352. package/src/acp/session-manager.ts +5 -6
  353. package/src/agent/compaction-circuit.ts +140 -0
  354. package/src/agent/loop.ts +489 -85
  355. package/src/api/README.md +126 -2
  356. package/src/api/constants/call-sites.ts +27 -0
  357. package/src/api/constants/tool-execution.ts +21 -0
  358. package/src/api/events/assistant-activity-state.ts +75 -0
  359. package/src/api/events/assistant-outbound-attachment.ts +49 -0
  360. package/src/api/events/assistant-text-delta.ts +30 -0
  361. package/src/api/events/assistant-turn-start.ts +31 -0
  362. package/src/api/events/avatar-updated.ts +24 -0
  363. package/src/api/events/compaction-circuit-closed.ts +26 -0
  364. package/src/api/events/compaction-circuit-open.ts +28 -0
  365. package/src/api/events/confirmation-request.ts +114 -0
  366. package/src/api/events/contact-request.ts +33 -0
  367. package/src/api/events/conversation-error.ts +77 -0
  368. package/src/api/events/conversation-list-invalidated.ts +38 -0
  369. package/src/api/events/conversation-title-updated.ts +24 -0
  370. package/src/api/events/disk-pressure-status-changed.ts +61 -0
  371. package/src/api/events/document-comment-created.ts +44 -0
  372. package/src/api/events/document-comment-deleted.ts +22 -0
  373. package/src/api/events/document-comment-reopened.ts +23 -0
  374. package/src/api/events/document-comment-resolved.ts +25 -0
  375. package/src/api/events/document-editor-update.ts +27 -0
  376. package/src/api/events/error.ts +32 -0
  377. package/src/api/events/generation-cancelled.ts +22 -0
  378. package/src/api/events/generation-handoff.ts +39 -0
  379. package/src/api/events/home-feed-updated.ts +26 -0
  380. package/src/api/events/identity-changed.ts +32 -0
  381. package/src/api/events/interaction-resolved.ts +50 -0
  382. package/src/api/events/message-complete.ts +40 -0
  383. package/src/api/events/message-dequeued.ts +21 -0
  384. package/src/api/events/message-queued-deleted.ts +23 -0
  385. package/src/api/events/message-queued.ts +22 -0
  386. package/src/api/events/message-request-complete.ts +29 -0
  387. package/src/api/events/navigate-settings.ts +20 -0
  388. package/src/api/events/notification-intent.ts +33 -0
  389. package/src/api/events/open-url.ts +28 -0
  390. package/src/api/events/question-request.ts +67 -0
  391. package/src/{events → api/events}/relationship-state-updated.ts +6 -8
  392. package/src/api/events/secret-request.ts +42 -0
  393. package/src/api/events/subagent-event.ts +79 -0
  394. package/src/api/events/subagent-spawned.ts +40 -0
  395. package/src/api/events/subagent-status-changed.ts +65 -0
  396. package/src/api/events/sync-changed.ts +29 -0
  397. package/src/api/events/tool-result.ts +129 -0
  398. package/src/api/events/tool-use-start.ts +30 -0
  399. package/src/api/events/turn-profile-auto-routed.ts +28 -0
  400. package/src/api/events/ui-surface-complete.ts +30 -0
  401. package/src/api/events/ui-surface-dismiss.ts +22 -0
  402. package/src/api/events/ui-surface-show.ts +67 -0
  403. package/src/api/events/ui-surface-update.ts +26 -0
  404. package/src/api/events/usage-update.ts +34 -0
  405. package/src/api/events/user-message-echo.ts +35 -0
  406. package/src/api/index.ts +482 -3
  407. package/src/api/requests/dictation.ts +45 -0
  408. package/src/api/responses/disk-pressure-status.ts +26 -0
  409. package/src/api/responses/home.ts +217 -0
  410. package/src/api/responses/llm-context-response.ts +41 -0
  411. package/src/api/responses/llm-request-log-entry.ts +93 -0
  412. package/src/api/responses/memory-recall-log.ts +65 -0
  413. package/src/api/responses/memory-v2-activation-log.ts +78 -0
  414. package/src/api/responses/memory-v3-selection-log.ts +50 -0
  415. package/src/api/responses/subagent-detail.ts +48 -0
  416. package/src/approvals/guardian-decision-primitive.ts +7 -15
  417. package/src/approvals/guardian-request-resolvers.ts +6 -9
  418. package/src/avatar/__tests__/avatar-manifest.test.ts +236 -0
  419. package/src/avatar/__tests__/avatar-store.test.ts +193 -0
  420. package/src/avatar/avatar-manifest.ts +195 -0
  421. package/src/avatar/avatar-store.ts +113 -0
  422. package/src/avatar/traits-png-sync.ts +8 -2
  423. package/src/background-wake/background-wake-routes.test.ts +687 -52
  424. package/src/background-wake/next-wake.test.ts +31 -1
  425. package/src/background-wake/next-wake.ts +4 -1
  426. package/src/background-wake/platform-client.test.ts +308 -0
  427. package/src/background-wake/platform-client.ts +167 -0
  428. package/src/background-wake/publisher.ts +91 -0
  429. package/src/background-wake/runtime-registry.ts +2 -2
  430. package/src/background-wake/wake-intent-hooks.test.ts +282 -0
  431. package/src/calls/call-conversation-messages.ts +6 -4
  432. package/src/calls/guardian-action-sweep.ts +6 -4
  433. package/src/calls/guardian-dispatch.ts +1 -0
  434. package/src/calls/relay-server.ts +12 -8
  435. package/src/calls/voice-session-bridge.ts +17 -31
  436. package/src/cli/commands/__tests__/conversations-slack.test.ts +16 -0
  437. package/src/cli/commands/__tests__/memory-v3.test.ts +245 -0
  438. package/src/cli/commands/__tests__/notifications.test.ts +184 -40
  439. package/src/cli/commands/avatar.ts +17 -11
  440. package/src/cli/commands/channels/__tests__/channels.test.ts +143 -0
  441. package/src/cli/commands/channels/index.ts +229 -0
  442. package/src/cli/commands/conversations.ts +15 -1
  443. package/src/cli/commands/db/__tests__/repair.test.ts +540 -0
  444. package/src/cli/commands/db/__tests__/status.test.ts +253 -0
  445. package/src/cli/commands/db/format.ts +48 -0
  446. package/src/cli/commands/db/index.ts +29 -0
  447. package/src/cli/commands/db/repair-step-conversation-backfill.ts +345 -0
  448. package/src/cli/commands/db/repair-step-integrity.ts +146 -0
  449. package/src/cli/commands/db/repair-steps.ts +164 -0
  450. package/src/cli/commands/db/repair.ts +141 -0
  451. package/src/cli/commands/db/status.ts +366 -0
  452. package/src/cli/commands/memory-v3.ts +168 -203
  453. package/src/cli/commands/notifications.ts +365 -55
  454. package/src/cli/lib/cli-colors.ts +24 -6
  455. package/src/cli/lib/open-browser.ts +7 -2
  456. package/src/cli/program.ts +6 -5
  457. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  458. package/src/config/assistant-feature-flags.ts +25 -44
  459. package/src/config/bundled-skills/app-builder/SKILL.md +14 -3
  460. package/src/config/bundled-skills/document-editor/SKILL.md +5 -1
  461. package/src/config/bundled-skills/media-processing/services/reduce.ts +6 -9
  462. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +7 -2
  463. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  464. package/src/config/bundled-skills/schedule/TOOLS.json +10 -2
  465. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +1 -0
  466. package/src/config/call-site-defaults.ts +3 -8
  467. package/src/config/feature-flag-cache.ts +86 -0
  468. package/src/config/feature-flag-registry.json +42 -26
  469. package/src/config/llm-context-resolution.ts +10 -1
  470. package/src/config/llm-resolver.ts +121 -15
  471. package/src/config/loader.ts +4 -5
  472. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -211
  473. package/src/config/schemas/call-site-catalog.ts +8 -15
  474. package/src/config/schemas/heartbeat.ts +1 -1
  475. package/src/config/schemas/llm.ts +92 -4
  476. package/src/config/schemas/memory-lifecycle.ts +24 -0
  477. package/src/config/schemas/memory-v2.ts +0 -227
  478. package/src/config/schemas/memory-v3.ts +39 -0
  479. package/src/config/schemas/memory.ts +6 -1
  480. package/src/config/schemas/services.ts +6 -2
  481. package/src/config/schemas/timeouts.ts +3 -1
  482. package/src/config/seed-inference-profiles.ts +36 -16
  483. package/src/context/compactor.ts +54 -31
  484. package/src/context/token-estimator.ts +29 -5
  485. package/src/context/tool-result-truncation.ts +1 -43
  486. package/src/context/window-manager.ts +138 -20
  487. package/src/credential-execution/executable-discovery.ts +40 -0
  488. package/src/credential-execution/process-manager.ts +6 -2
  489. package/src/credential-health/credential-health-service.ts +125 -40
  490. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -6
  491. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +15 -17
  492. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -2
  493. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -0
  494. package/src/daemon/__tests__/meet-manifest-loader.test.ts +25 -12
  495. package/src/daemon/__tests__/native-web-search-metadata.test.ts +1 -0
  496. package/src/daemon/__tests__/switch-inference-profile-tool.test.ts +107 -0
  497. package/src/daemon/__tests__/web-search-status-text.test.ts +11 -6
  498. package/src/daemon/approval-generators.ts +4 -4
  499. package/src/daemon/config-watcher.ts +7 -1
  500. package/src/daemon/conversation-agent-loop-handlers.ts +613 -155
  501. package/src/daemon/conversation-agent-loop.ts +409 -605
  502. package/src/daemon/conversation-error.ts +40 -12
  503. package/src/daemon/conversation-history.ts +22 -6
  504. package/src/daemon/conversation-launch.ts +4 -8
  505. package/src/daemon/conversation-lifecycle.ts +10 -38
  506. package/src/daemon/conversation-messaging.ts +83 -44
  507. package/src/daemon/conversation-notifiers.ts +7 -5
  508. package/src/daemon/conversation-process.ts +174 -116
  509. package/src/daemon/conversation-runtime-assembly.ts +76 -30
  510. package/src/daemon/conversation-skill-tools.ts +14 -30
  511. package/src/daemon/conversation-store.ts +6 -5
  512. package/src/daemon/conversation-surfaces.ts +124 -103
  513. package/src/daemon/conversation-tool-setup.ts +36 -48
  514. package/src/daemon/conversation.ts +111 -166
  515. package/src/daemon/daemon-control.ts +1 -1
  516. package/src/daemon/daemon-skill-host.ts +7 -4
  517. package/src/daemon/disk-pressure-guard.ts +54 -50
  518. package/src/daemon/external-plugins-bootstrap.ts +46 -24
  519. package/src/daemon/first-greeting.ts +53 -13
  520. package/src/daemon/guardian-action-generators.ts +2 -2
  521. package/src/daemon/handlers/conversations.ts +6 -22
  522. package/src/daemon/handlers/shared.ts +10 -1
  523. package/src/daemon/handlers/skills.ts +15 -14
  524. package/src/daemon/host-app-control-proxy.ts +54 -1
  525. package/src/daemon/host-cu-proxy.ts +46 -22
  526. package/src/daemon/host-file-proxy.ts +25 -1
  527. package/src/daemon/host-proxy-preactivation.ts +25 -6
  528. package/src/daemon/lifecycle.ts +40 -67
  529. package/src/daemon/mcp-reload-service.ts +1 -1
  530. package/src/daemon/meet-manifest-loader.ts +10 -17
  531. package/src/daemon/message-protocol.ts +2 -3
  532. package/src/daemon/message-provenance.ts +49 -0
  533. package/src/daemon/message-types/contacts.ts +3 -20
  534. package/src/daemon/message-types/conversations.ts +25 -125
  535. package/src/daemon/message-types/document-comments.ts +8 -44
  536. package/src/daemon/message-types/documents.ts +3 -9
  537. package/src/daemon/message-types/home.ts +5 -18
  538. package/src/daemon/message-types/integrations.ts +4 -13
  539. package/src/daemon/message-types/messages.ts +47 -377
  540. package/src/daemon/message-types/notifications.ts +2 -32
  541. package/src/daemon/message-types/settings.ts +3 -8
  542. package/src/daemon/message-types/skills.ts +2 -0
  543. package/src/daemon/message-types/subagents.ts +6 -0
  544. package/src/daemon/message-types/surfaces.ts +2 -0
  545. package/src/daemon/message-types/sync.ts +12 -25
  546. package/src/daemon/message-types/workspace.ts +3 -11
  547. package/src/daemon/process-message.ts +58 -55
  548. package/src/daemon/providers-setup.ts +1 -1
  549. package/src/daemon/server.ts +28 -0
  550. package/src/daemon/switch-inference-profile-tool.ts +13 -3
  551. package/src/daemon/tool-setup-types.ts +0 -6
  552. package/src/daemon/tool-side-effects.ts +10 -7
  553. package/src/daemon/trust-context.ts +13 -0
  554. package/src/daemon/wake-target-adapter.ts +21 -1
  555. package/src/documents/document-store.ts +38 -0
  556. package/src/export/__tests__/transcript-formatter.test.ts +1 -0
  557. package/src/heartbeat/__tests__/heartbeat-service.test.ts +31 -0
  558. package/src/heartbeat/heartbeat-run-store.ts +31 -0
  559. package/src/heartbeat/heartbeat-service.ts +79 -0
  560. package/src/home/__tests__/feed-writer.test.ts +161 -0
  561. package/src/home/__tests__/post-connect-feed.test.ts +1 -0
  562. package/src/home/__tests__/suggested-prompts.test.ts +55 -59
  563. package/src/home/feature-gate.ts +22 -0
  564. package/src/home/feed-types.ts +36 -221
  565. package/src/home/feed-writer.ts +146 -7
  566. package/src/home/suggested-prompts.ts +27 -145
  567. package/src/ipc/__tests__/cli-ipc.test.ts +1 -0
  568. package/src/ipc/__tests__/email-ipc.test.ts +0 -9
  569. package/src/ipc/gateway-client.test.ts +4 -1
  570. package/src/ipc/routes/__tests__/route-adapter.test.ts +244 -0
  571. package/src/ipc/routes/route-adapter.ts +45 -6
  572. package/src/ipc/skill-routes/__tests__/memory.test.ts +19 -9
  573. package/src/ipc/skill-routes/__tests__/providers.test.ts +10 -10
  574. package/src/ipc/skill-routes/__tests__/registries.test.ts +59 -20
  575. package/src/ipc/skill-routes/memory.ts +27 -13
  576. package/src/ipc/skill-routes/providers.ts +5 -6
  577. package/src/ipc/skill-routes/registries.ts +39 -88
  578. package/src/live-voice/__tests__/live-voice-archive.test.ts +24 -11
  579. package/src/memory/__tests__/conversation-queries.test.ts +192 -8
  580. package/src/memory/__tests__/db-maintenance.test.ts +128 -0
  581. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +1 -0
  582. package/src/memory/__tests__/jobs-store-job-classes.test.ts +5 -4
  583. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +26 -5
  584. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +1 -0
  585. package/src/memory/__tests__/memory-retrospective-job.test.ts +11 -6
  586. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +1 -0
  587. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +31 -0
  588. package/src/memory/__tests__/memory-v3-selections-migration.test.ts +103 -0
  589. package/src/memory/context-search/agent-runner.ts +2 -4
  590. package/src/memory/conversation-attention-store.ts +17 -3
  591. package/src/memory/conversation-crud.ts +386 -115
  592. package/src/memory/conversation-queries.ts +78 -22
  593. package/src/memory/db-connection.ts +29 -19
  594. package/src/memory/db-init.ts +12 -0
  595. package/src/memory/db-maintenance.ts +18 -2
  596. package/src/memory/db-singleton.ts +77 -0
  597. package/src/memory/delivery-channels.ts +82 -0
  598. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +2 -4
  599. package/src/memory/graph/consolidation.ts +8 -11
  600. package/src/memory/graph/conversation-graph-memory.ts +41 -8
  601. package/src/memory/graph/extraction.ts +6 -9
  602. package/src/memory/graph/narrative.ts +2 -2
  603. package/src/memory/graph/pattern-scan.ts +2 -2
  604. package/src/memory/graph/retriever.test.ts +3 -3
  605. package/src/memory/graph/retriever.ts +20 -26
  606. package/src/memory/graph/tools.ts +4 -4
  607. package/src/memory/job-handlers/conversation-starters.ts +32 -32
  608. package/src/memory/job-handlers/embedding.test.ts +3 -2
  609. package/src/memory/job-handlers/summarization.ts +1 -2
  610. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -2
  611. package/src/memory/jobs-store.ts +3 -1
  612. package/src/memory/jobs-worker.ts +63 -40
  613. package/src/memory/llm-request-log-source-clickhouse.ts +55 -1
  614. package/src/memory/llm-request-log-source-local.ts +13 -0
  615. package/src/memory/llm-request-log-source.ts +21 -6
  616. package/src/memory/llm-request-log-store.ts +147 -3
  617. package/src/memory/llm-usage-store.ts +10 -0
  618. package/src/memory/memory-marker.ts +17 -0
  619. package/src/memory/memory-retrospective-job.ts +6 -2
  620. package/src/memory/memory-v2-activation-log-store.ts +13 -1
  621. package/src/memory/migrations/265-drop-provider-connection-status.ts +26 -0
  622. package/src/memory/migrations/266-messages-client-message-id.ts +43 -0
  623. package/src/memory/migrations/267-llm-usage-events-add-assistant-version.ts +46 -0
  624. package/src/memory/migrations/268-add-memory-v3-selections.ts +28 -0
  625. package/src/memory/migrations/269-schedule-script-timeout.ts +11 -0
  626. package/src/memory/migrations/270-messages-role-created-at-index.ts +18 -0
  627. package/src/memory/migrations/__tests__/267-llm-usage-events-add-assistant-version.test.ts +117 -0
  628. package/src/memory/migrations/index.ts +6 -0
  629. package/src/memory/schema/conversations.ts +9 -1
  630. package/src/memory/schema/inference.ts +0 -1
  631. package/src/memory/schema/infrastructure.ts +11 -0
  632. package/src/memory/v2/__tests__/backfill-jobs.test.ts +5 -2
  633. package/src/memory/v2/__tests__/consolidation-job.test.ts +124 -0
  634. package/src/memory/v2/__tests__/harness-metrics.test.ts +9 -0
  635. package/src/memory/v2/__tests__/harness-replay-input.test.ts +9 -4
  636. package/src/memory/v2/__tests__/harness-runner.test.ts +26 -0
  637. package/src/memory/v2/__tests__/migration.test.ts +11 -3
  638. package/src/memory/v2/__tests__/page-index.test.ts +37 -1
  639. package/src/memory/v2/__tests__/router.test.ts +14 -4
  640. package/src/memory/v2/__tests__/sweep-job.test.ts +9 -5
  641. package/src/memory/v2/backfill-jobs.ts +6 -0
  642. package/src/memory/v2/consolidation-job.ts +89 -9
  643. package/src/memory/v2/harness/metrics.ts +5 -1
  644. package/src/memory/v2/harness/replay-input.ts +19 -3
  645. package/src/memory/v2/harness/runner.ts +6 -0
  646. package/src/memory/v2/harness/trace.ts +6 -0
  647. package/src/memory/v2/migration.ts +5 -3
  648. package/src/memory/v2/page-index.ts +11 -0
  649. package/src/memory/v2/router.ts +8 -11
  650. package/src/memory/v2/sweep-job.ts +8 -11
  651. package/src/memory/v2/types.ts +1 -0
  652. package/src/memory/v3/__tests__/assign.test.ts +242 -0
  653. package/src/memory/v3/__tests__/capabilities.test.ts +118 -0
  654. package/src/memory/v3/__tests__/core.test.ts +39 -0
  655. package/src/memory/v3/__tests__/fixtures/eval-turns.json +36 -0
  656. package/src/memory/v3/__tests__/fixtures/live-turns.json +37 -0
  657. package/src/memory/v3/__tests__/health.test.ts +203 -0
  658. package/src/memory/v3/__tests__/live-integration.test.ts +330 -0
  659. package/src/memory/v3/__tests__/maintain-job.test.ts +288 -0
  660. package/src/memory/v3/__tests__/needle.test.ts +107 -0
  661. package/src/memory/v3/__tests__/orchestrate.test.ts +400 -0
  662. package/src/memory/v3/__tests__/reconcile.test.ts +274 -0
  663. package/src/memory/v3/__tests__/render-injection.test.ts +61 -0
  664. package/src/memory/v3/__tests__/router.test.ts +260 -0
  665. package/src/memory/v3/__tests__/selection-log-store.test.ts +179 -0
  666. package/src/memory/v3/__tests__/selector.test.ts +404 -0
  667. package/src/memory/v3/__tests__/shadow-plugin.test.ts +414 -0
  668. package/src/memory/v3/__tests__/snapshot.test.ts +168 -0
  669. package/src/memory/v3/__tests__/tree.test.ts +192 -0
  670. package/src/memory/v3/__tests__/types.test.ts +54 -0
  671. package/src/memory/v3/__tests__/working-set-eviction.test.ts +106 -0
  672. package/src/memory/v3/__tests__/working-set-skeleton.test.ts +44 -0
  673. package/src/memory/v3/assign.ts +268 -0
  674. package/src/memory/v3/capabilities.ts +124 -0
  675. package/src/memory/v3/core.ts +26 -0
  676. package/src/memory/v3/data/README.md +84 -0
  677. package/src/memory/v3/data/assignments.json +5 -0
  678. package/src/memory/v3/data/core.json +1 -0
  679. package/src/memory/v3/data/leaves/domain-a/topic-x.md +9 -0
  680. package/src/memory/v3/data/leaves/domain-a/topic-y.md +9 -0
  681. package/src/memory/v3/data/leaves/domain-b/topic-z.md +9 -0
  682. package/src/memory/v3/health.ts +0 -0
  683. package/src/memory/v3/maintain-job.ts +314 -0
  684. package/src/memory/v3/needle.ts +115 -0
  685. package/src/memory/v3/orchestrate.ts +114 -0
  686. package/src/memory/v3/page-content.ts +34 -0
  687. package/src/memory/v3/provider-blocks.ts +16 -0
  688. package/src/memory/v3/reconcile.ts +523 -0
  689. package/src/memory/v3/render-injection.ts +32 -0
  690. package/src/memory/v3/router.ts +184 -0
  691. package/src/memory/v3/selection-log-store.ts +84 -0
  692. package/src/memory/v3/selector.ts +211 -0
  693. package/src/memory/v3/shadow-plugin.ts +379 -0
  694. package/src/memory/v3/snapshot.ts +209 -0
  695. package/src/memory/v3/tree.ts +174 -0
  696. package/src/memory/v3/types.ts +46 -60
  697. package/src/memory/v3/working-set.ts +88 -0
  698. package/src/messaging/providers/slack/render-transcript.test.ts +1 -1
  699. package/src/messaging/providers/slack/render-transcript.ts +2 -2
  700. package/src/messaging/style-analyzer.ts +8 -11
  701. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  702. package/src/notifications/__tests__/home-feed-side-effect.test.ts +1 -0
  703. package/src/notifications/adapters/slack.ts +45 -11
  704. package/src/notifications/broadcaster.ts +114 -63
  705. package/src/notifications/conversation-pairing.ts +30 -8
  706. package/src/notifications/decision-engine.ts +10 -13
  707. package/src/notifications/decisions-store.ts +32 -1
  708. package/src/notifications/deliveries-store.ts +45 -0
  709. package/src/notifications/edit-notification.ts +201 -0
  710. package/src/notifications/emit-signal.ts +11 -1
  711. package/src/notifications/preference-extractor.ts +11 -14
  712. package/src/notifications/signal.ts +10 -0
  713. package/src/notifications/types.ts +37 -0
  714. package/src/oauth/byo-connection.test.ts +67 -3
  715. package/src/oauth/byo-connection.ts +32 -5
  716. package/src/oauth/connect-orchestrator.ts +9 -0
  717. package/src/oauth/connection-resolver.test.ts +76 -0
  718. package/src/oauth/connection-resolver.ts +49 -10
  719. package/src/oauth/manual-token-connection.ts +51 -3
  720. package/src/oauth/seed-providers.ts +3 -0
  721. package/src/permissions/approval-policy.test.ts +19 -5
  722. package/src/permissions/approval-policy.ts +14 -3
  723. package/src/permissions/checker.ts +21 -8
  724. package/src/permissions/prompter.ts +42 -36
  725. package/src/permissions/question-prompter.test.ts +35 -26
  726. package/src/permissions/question-prompter.ts +6 -10
  727. package/src/platform/client.test.ts +24 -1
  728. package/src/platform/client.ts +8 -0
  729. package/src/platform/feature-gate.ts +15 -0
  730. package/src/plugin-api/index.ts +2 -0
  731. package/src/plugin-api/types.ts +25 -3
  732. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
  733. package/src/plugins/defaults/circuit-breaker/package.json +15 -0
  734. package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
  735. package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
  736. package/src/plugins/defaults/compaction/package.json +15 -0
  737. package/src/plugins/defaults/compaction/register.ts +35 -0
  738. package/src/plugins/defaults/compaction/terminal.ts +73 -0
  739. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +22 -0
  740. package/src/plugins/defaults/empty-response/package.json +15 -0
  741. package/src/plugins/defaults/empty-response/register.ts +28 -0
  742. package/src/plugins/defaults/empty-response/terminal.ts +106 -0
  743. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +35 -0
  744. package/src/plugins/defaults/history-repair/package.json +15 -0
  745. package/src/plugins/defaults/history-repair/register.ts +24 -0
  746. package/src/{daemon/history-repair.ts → plugins/defaults/history-repair/terminal.ts} +48 -35
  747. package/src/plugins/defaults/index.ts +29 -40
  748. package/src/plugins/defaults/injectors/package.json +15 -0
  749. package/src/plugins/defaults/{injectors.ts → injectors/register.ts} +16 -46
  750. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +17 -0
  751. package/src/plugins/defaults/llm-call/package.json +15 -0
  752. package/src/plugins/defaults/{llm-call.ts → llm-call/register.ts} +6 -38
  753. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +17 -0
  754. package/src/plugins/defaults/memory-retrieval/package.json +15 -0
  755. package/src/plugins/defaults/{memory-retrieval.ts → memory-retrieval/register.ts} +10 -48
  756. package/src/plugins/defaults/{overflow-reduce.ts → overflow-reduce/middlewares/overflowReduce.ts} +18 -77
  757. package/src/plugins/defaults/overflow-reduce/package.json +15 -0
  758. package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
  759. package/src/plugins/defaults/persistence/middlewares/persistence.ts +19 -0
  760. package/src/plugins/defaults/persistence/package.json +15 -0
  761. package/src/plugins/defaults/persistence/register.ts +38 -0
  762. package/src/plugins/defaults/persistence/terminal.ts +83 -0
  763. package/src/plugins/defaults/title-generate/package.json +15 -0
  764. package/src/plugins/defaults/title-generate/register.ts +35 -0
  765. package/src/plugins/defaults/title-generate/terminal.ts +31 -0
  766. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +23 -0
  767. package/src/plugins/defaults/token-estimate/package.json +15 -0
  768. package/src/plugins/defaults/token-estimate/register.ts +34 -0
  769. package/src/plugins/defaults/token-estimate/terminal.ts +40 -0
  770. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +21 -0
  771. package/src/plugins/defaults/tool-error/package.json +15 -0
  772. package/src/plugins/defaults/tool-error/register.ts +35 -0
  773. package/src/plugins/defaults/tool-error/terminal.ts +47 -0
  774. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +23 -0
  775. package/src/plugins/defaults/tool-execute/package.json +15 -0
  776. package/src/plugins/defaults/{tool-execute.ts → tool-execute/register.ts} +8 -46
  777. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +23 -0
  778. package/src/plugins/defaults/tool-result-truncate/package.json +15 -0
  779. package/src/plugins/defaults/tool-result-truncate/register.ts +35 -0
  780. package/src/plugins/defaults/tool-result-truncate/terminal.ts +113 -0
  781. package/src/plugins/defaults/tool-result-truncate/types.ts +22 -0
  782. package/src/plugins/external-plugin-loader.ts +2 -2
  783. package/src/plugins/pipeline.ts +0 -12
  784. package/src/plugins/types.ts +107 -102
  785. package/src/plugins/user-loader.ts +4 -3
  786. package/src/proactive-artifact/aux-message-injector.ts +0 -1
  787. package/src/proactive-artifact/job.test.ts +21 -8
  788. package/src/proactive-artifact/job.ts +3 -1
  789. package/src/prompts/__tests__/system-prompt.test.ts +4 -4
  790. package/src/prompts/sections.ts +20 -7
  791. package/src/prompts/system-prompt.ts +38 -40
  792. package/src/prompts/template-detection.ts +10 -4
  793. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +2 -2
  794. package/src/prompts/templates/BOOTSTRAP.md +10 -10
  795. package/src/prompts/templates/IDENTITY.md +0 -2
  796. package/src/prompts/templates/system-sections.ts +6 -0
  797. package/src/providers/__tests__/connection-model-compat.test.ts +3 -4
  798. package/src/providers/__tests__/registry-native-web-search.test.ts +122 -0
  799. package/src/providers/__tests__/retry-callsite.test.ts +25 -25
  800. package/src/providers/__tests__/satellite-connection-routing.test.ts +7 -21
  801. package/src/providers/anthropic/client.ts +24 -5
  802. package/src/providers/call-site-routing.ts +34 -18
  803. package/src/providers/connection-model-compat.ts +23 -0
  804. package/src/providers/connection-resolution.ts +39 -20
  805. package/src/providers/fireworks/client.ts +1 -0
  806. package/src/providers/gemini/client.ts +176 -37
  807. package/src/providers/gemini/inline-media.ts +74 -0
  808. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +0 -2
  809. package/src/providers/inference/__tests__/base-url-security.test.ts +2 -3
  810. package/src/providers/inference/__tests__/{connections-status-label.test.ts → connections-label.test.ts} +12 -111
  811. package/src/providers/inference/auth.ts +0 -8
  812. package/src/providers/inference/connections.ts +3 -66
  813. package/src/providers/inference/resolve-auth.ts +2 -3
  814. package/src/providers/model-catalog.ts +35 -1
  815. package/src/providers/model-intents.ts +3 -3
  816. package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
  817. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +157 -7
  818. package/src/providers/openai/chat-completions-provider.ts +111 -16
  819. package/src/providers/openai/codex-models.ts +2 -0
  820. package/src/providers/openai/responses-provider.ts +54 -57
  821. package/src/providers/openrouter/client.ts +14 -14
  822. package/src/providers/provider-send-message.ts +23 -14
  823. package/src/providers/ratelimit.ts +1 -9
  824. package/src/providers/registry.ts +48 -8
  825. package/src/providers/retry.ts +16 -9
  826. package/src/providers/search-provider-catalog.ts +17 -9
  827. package/src/providers/types.ts +20 -2
  828. package/src/providers/usage-tracking.ts +1 -9
  829. package/src/runtime/__tests__/agent-wake.test.ts +132 -26
  830. package/src/runtime/__tests__/background-job-runner.test.ts +2 -3
  831. package/src/runtime/access-request-helper.ts +1 -0
  832. package/src/runtime/agent-wake.ts +93 -18
  833. package/src/runtime/assistant-event-hub.ts +2 -2
  834. package/src/runtime/auth/__tests__/guard-tests.test.ts +75 -109
  835. package/src/runtime/auth/__tests__/route-policy.test.ts +153 -170
  836. package/src/runtime/auth/route-policy.ts +42 -1069
  837. package/src/runtime/background-job-runner.ts +1 -4
  838. package/src/runtime/btw-sidechain.ts +3 -1
  839. package/src/runtime/channel-approvals.ts +3 -14
  840. package/src/runtime/channel-invite-transport.ts +5 -6
  841. package/src/runtime/channel-readiness-service.ts +70 -5
  842. package/src/runtime/channel-reply-delivery.ts +23 -0
  843. package/src/runtime/channel-retry-sweep.ts +59 -30
  844. package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
  845. package/src/runtime/conversation-stream-state.ts +294 -0
  846. package/src/runtime/http-router.ts +19 -22
  847. package/src/runtime/http-types.ts +12 -6
  848. package/src/runtime/invite-instruction-generator.ts +3 -3
  849. package/src/runtime/migrations/vbundle-builder.ts +3 -2
  850. package/src/runtime/pending-interactions.ts +2 -2
  851. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +565 -0
  852. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +1 -0
  853. package/src/runtime/routes/__tests__/content-source-routes.test.ts +4 -4
  854. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +436 -0
  855. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +237 -0
  856. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
  857. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  858. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +209 -1
  859. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +26 -72
  860. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +58 -5
  861. package/src/runtime/routes/__tests__/sanity-routes.test.ts +6 -6
  862. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +3 -2
  863. package/src/runtime/routes/__tests__/stt-routes.test.ts +3 -3
  864. package/src/runtime/routes/__tests__/suggest-trust-rule-routes.test.ts +5 -2
  865. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +294 -0
  866. package/src/runtime/routes/__tests__/task-routes.test.ts +48 -3
  867. package/src/runtime/routes/__tests__/tts-routes.test.ts +3 -3
  868. package/src/runtime/routes/acp-routes-list.test.ts +3 -0
  869. package/src/runtime/routes/acp-routes.test.ts +97 -75
  870. package/src/runtime/routes/acp-routes.ts +29 -6
  871. package/src/runtime/routes/app-management-routes.ts +208 -28
  872. package/src/runtime/routes/app-routes.ts +25 -5
  873. package/src/runtime/routes/approval-routes.ts +16 -4
  874. package/src/runtime/routes/attachment-routes.ts +25 -1
  875. package/src/runtime/routes/audio-routes.ts +1 -0
  876. package/src/runtime/routes/audit-routes.ts +5 -0
  877. package/src/runtime/routes/auth-routes.ts +5 -0
  878. package/src/runtime/routes/avatar-routes.ts +238 -59
  879. package/src/runtime/routes/background-tool-routes.ts +9 -0
  880. package/src/runtime/routes/background-wake-routes.ts +201 -23
  881. package/src/runtime/routes/backup-routes.ts +45 -0
  882. package/src/runtime/routes/bookmark-routes.ts +13 -0
  883. package/src/runtime/routes/brain-graph-routes.ts +9 -0
  884. package/src/runtime/routes/browser-routes.ts +5 -0
  885. package/src/runtime/routes/browser-tabs-routes.ts +5 -0
  886. package/src/runtime/routes/btw-routes.ts +9 -5
  887. package/src/runtime/routes/cache-routes.ts +13 -0
  888. package/src/runtime/routes/call-routes.ts +21 -10
  889. package/src/runtime/routes/channel-availability-routes.ts +5 -1
  890. package/src/runtime/routes/channel-readiness-routes.ts +37 -4
  891. package/src/runtime/routes/channel-route-definitions.ts +21 -0
  892. package/src/runtime/routes/channel-verification-routes.ts +21 -0
  893. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +9 -2
  894. package/src/runtime/routes/client-routes.ts +9 -0
  895. package/src/runtime/routes/consolidation-routes.ts +13 -5
  896. package/src/runtime/routes/contact-prompt-routes.ts +9 -0
  897. package/src/runtime/routes/contact-routes.ts +90 -23
  898. package/src/runtime/routes/content-source-routes.ts +5 -1
  899. package/src/runtime/routes/conversation-analysis-routes.ts +11 -1
  900. package/src/runtime/routes/conversation-attention-routes.ts +5 -0
  901. package/src/runtime/routes/conversation-cli-routes.ts +54 -7
  902. package/src/runtime/routes/conversation-compaction-routes.ts +292 -0
  903. package/src/runtime/routes/conversation-list-routes.ts +225 -9
  904. package/src/runtime/routes/conversation-management-routes.ts +96 -28
  905. package/src/runtime/routes/conversation-query-routes.ts +148 -51
  906. package/src/runtime/routes/conversation-routes.ts +259 -158
  907. package/src/runtime/routes/conversation-starter-routes.ts +22 -13
  908. package/src/runtime/routes/conversations-import-routes.ts +25 -7
  909. package/src/runtime/routes/credential-prompt-routes.ts +5 -0
  910. package/src/runtime/routes/credential-routes.ts +25 -6
  911. package/src/runtime/routes/debug-bash-routes.ts +5 -0
  912. package/src/runtime/routes/debug-routes.ts +11 -2
  913. package/src/runtime/routes/defer-routes.ts +13 -0
  914. package/src/runtime/routes/diagnostics-routes.ts +37 -46
  915. package/src/runtime/routes/disk-pressure-routes.ts +17 -31
  916. package/src/runtime/routes/document-comments-routes.ts +46 -27
  917. package/src/runtime/routes/documents-routes.ts +31 -11
  918. package/src/runtime/routes/domain-routes.ts +61 -28
  919. package/src/runtime/routes/email-routes.ts +33 -0
  920. package/src/runtime/routes/events-routes.ts +114 -9
  921. package/src/runtime/routes/filing-routes.ts +9 -4
  922. package/src/runtime/routes/gateway-log-routes.ts +5 -0
  923. package/src/runtime/routes/global-search-routes.ts +53 -50
  924. package/src/runtime/routes/group-routes.ts +32 -5
  925. package/src/runtime/routes/guardian-action-routes.ts +9 -0
  926. package/src/runtime/routes/guardian-approval-interception.ts +0 -31
  927. package/src/runtime/routes/heartbeat-routes.ts +25 -9
  928. package/src/runtime/routes/home-feed-routes.ts +149 -16
  929. package/src/runtime/routes/home-state-routes.ts +8 -40
  930. package/src/runtime/routes/host-app-control-routes.ts +5 -0
  931. package/src/runtime/routes/host-bash-routes.ts +5 -0
  932. package/src/runtime/routes/host-browser-routes.ts +13 -0
  933. package/src/runtime/routes/host-cu-routes.ts +5 -0
  934. package/src/runtime/routes/host-file-routes.ts +26 -6
  935. package/src/runtime/routes/host-transfer-routes.ts +13 -2
  936. package/src/runtime/routes/http-adapter.ts +1 -2
  937. package/src/runtime/routes/identity-intro-cache.ts +72 -16
  938. package/src/runtime/routes/identity-routes.ts +42 -11
  939. package/src/runtime/routes/image-generation-routes.ts +5 -0
  940. package/src/runtime/routes/inbound-message-handler.ts +15 -11
  941. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +524 -12
  942. package/src/runtime/routes/inbound-stages/background-dispatch.ts +72 -27
  943. package/src/runtime/routes/index.ts +2 -0
  944. package/src/runtime/routes/inference-profile-session-routes.ts +13 -3
  945. package/src/runtime/routes/inference-provider-connection-routes.ts +26 -31
  946. package/src/runtime/routes/inference-send-routes.ts +11 -11
  947. package/src/runtime/routes/integrations/a2a.ts +30 -7
  948. package/src/runtime/routes/integrations/slack/channel.ts +19 -3
  949. package/src/runtime/routes/integrations/slack/share.ts +9 -2
  950. package/src/runtime/routes/integrations/telegram.ts +28 -9
  951. package/src/runtime/routes/integrations/twilio.ts +35 -7
  952. package/src/runtime/routes/integrations/vercel.ts +18 -3
  953. package/src/runtime/routes/internal-oauth-routes.ts +5 -0
  954. package/src/runtime/routes/internal-twilio-routes.ts +13 -0
  955. package/src/runtime/routes/llm-call-sites-routes.ts +39 -4
  956. package/src/runtime/routes/llm-context-normalization.ts +7 -2
  957. package/src/runtime/routes/log-export-routes.ts +28 -10
  958. package/src/runtime/routes/mcp-auth-routes.ts +25 -0
  959. package/src/runtime/routes/memory-item-routes.ts +21 -10
  960. package/src/runtime/routes/memory-v2-routes.ts +90 -36
  961. package/src/runtime/routes/memory-v3-routes.ts +283 -259
  962. package/src/runtime/routes/migration-rollback-routes.ts +5 -1
  963. package/src/runtime/routes/migration-routes.ts +49 -13
  964. package/src/runtime/routes/notification-routes.ts +80 -2
  965. package/src/runtime/routes/oauth-apps.ts +33 -11
  966. package/src/runtime/routes/oauth-commands-routes.ts +43 -15
  967. package/src/runtime/routes/oauth-connect-routes.ts +9 -0
  968. package/src/runtime/routes/oauth-lifecycle-routes.ts +5 -1
  969. package/src/runtime/routes/oauth-providers.ts +35 -10
  970. package/src/runtime/routes/platform-routes.ts +21 -0
  971. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +3 -2
  972. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +37 -16
  973. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +7 -3
  974. package/src/runtime/routes/playground/__tests__/state.test.ts +10 -3
  975. package/src/runtime/routes/playground/force-compact.ts +1 -1
  976. package/src/runtime/routes/playground/helpers.ts +0 -1
  977. package/src/runtime/routes/playground/inject-failures.ts +13 -8
  978. package/src/runtime/routes/playground/reset-circuit.ts +14 -9
  979. package/src/runtime/routes/playground/seed-conversation.ts +1 -1
  980. package/src/runtime/routes/playground/seeded-conversations.ts +3 -3
  981. package/src/runtime/routes/playground/state.ts +4 -3
  982. package/src/runtime/routes/plugins-routes.ts +22 -19
  983. package/src/runtime/routes/profiler-routes.ts +17 -4
  984. package/src/runtime/routes/ps-routes.ts +5 -0
  985. package/src/runtime/routes/publish-routes.ts +13 -3
  986. package/src/runtime/routes/question-routes.ts +5 -0
  987. package/src/runtime/routes/recording-routes.ts +25 -12
  988. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  989. package/src/runtime/routes/sanity-routes.ts +9 -2
  990. package/src/runtime/routes/schedule-routes.ts +137 -47
  991. package/src/runtime/routes/secret-routes.ts +17 -4
  992. package/src/runtime/routes/sequence-routes.ts +33 -0
  993. package/src/runtime/routes/settings-routes.ts +65 -19
  994. package/src/runtime/routes/skills-routes.ts +133 -69
  995. package/src/runtime/routes/slack-channel-routes.ts +5 -0
  996. package/src/runtime/routes/stt-routes.ts +13 -6
  997. package/src/runtime/routes/subagents-routes.ts +24 -18
  998. package/src/runtime/routes/suggest-trust-rule-routes.ts +7 -2
  999. package/src/runtime/routes/surface-action-routes.ts +10 -38
  1000. package/src/runtime/routes/surface-content-routes.ts +21 -6
  1001. package/src/runtime/routes/surface-conversation-resolver.ts +65 -0
  1002. package/src/runtime/routes/task-routes.ts +37 -0
  1003. package/src/runtime/routes/telemetry-routes.ts +9 -0
  1004. package/src/runtime/routes/trace-event-routes.ts +42 -1
  1005. package/src/runtime/routes/trust-rules-routes.ts +5 -0
  1006. package/src/runtime/routes/tts-routes.ts +13 -6
  1007. package/src/runtime/routes/types.ts +17 -8
  1008. package/src/runtime/routes/ui-request-routes.ts +5 -0
  1009. package/src/runtime/routes/upgrade-broadcast-routes.ts +5 -0
  1010. package/src/runtime/routes/usage-routes.ts +71 -3
  1011. package/src/runtime/routes/user-routes-cli.ts +9 -0
  1012. package/src/runtime/routes/user-routes.ts +5 -1
  1013. package/src/runtime/routes/wake-conversation-routes.ts +5 -0
  1014. package/src/runtime/routes/watcher-routes.ts +21 -0
  1015. package/src/runtime/routes/webhook-routes.ts +9 -0
  1016. package/src/runtime/routes/wipe-conversation-routes.ts +8 -0
  1017. package/src/runtime/routes/work-items-routes.ts +47 -19
  1018. package/src/runtime/routes/workspace-commit-routes.ts +5 -0
  1019. package/src/runtime/routes/workspace-routes.test.ts +42 -0
  1020. package/src/runtime/routes/workspace-routes.ts +120 -9
  1021. package/src/runtime/services/__tests__/analyze-conversation.test.ts +4 -4
  1022. package/src/runtime/services/analyze-conversation.ts +3 -6
  1023. package/src/runtime/services/conversation-serializer.ts +24 -2
  1024. package/src/runtime/slack-dm-text-delivery.ts +177 -0
  1025. package/src/runtime/sync/resource-sync-events.ts +17 -3
  1026. package/src/runtime/sync/sync-publisher.ts +2 -2
  1027. package/src/runtime/tool-grant-request-helper.ts +1 -0
  1028. package/src/schedule/run-script.ts +28 -3
  1029. package/src/schedule/schedule-store.ts +16 -1
  1030. package/src/schedule/scheduler.ts +114 -16
  1031. package/src/security/__tests__/provider-key-env-fallback.test.ts +3 -3
  1032. package/src/security/encrypted-store.ts +7 -16
  1033. package/src/security/store-path-override.ts +61 -0
  1034. package/src/signals/user-message.ts +10 -16
  1035. package/src/skills/catalog-files.ts +4 -1
  1036. package/src/skills/clawhub-files.ts +2 -0
  1037. package/src/skills/skillssh-files.ts +2 -0
  1038. package/src/skills/validate-input.ts +177 -0
  1039. package/src/subagent/manager.ts +16 -19
  1040. package/src/subagent/types.ts +6 -0
  1041. package/src/tasks/tool-sanitizer.ts +2 -2
  1042. package/src/telemetry/types.ts +26 -0
  1043. package/src/telemetry/usage-telemetry-reporter.test.ts +138 -1
  1044. package/src/telemetry/usage-telemetry-reporter.ts +31 -0
  1045. package/src/tools/acp/spawn.test.ts +88 -38
  1046. package/src/tools/apps/definitions.ts +42 -24
  1047. package/src/tools/ask-question/ask-question-tool.test.ts +120 -105
  1048. package/src/tools/ask-question/ask-question-tool.ts +85 -90
  1049. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +2 -8
  1050. package/src/tools/computer-use/definitions.ts +295 -289
  1051. package/src/tools/credential-execution/make-authenticated-request.ts +56 -51
  1052. package/src/tools/credential-execution/manage-secure-command-tool.ts +2 -2
  1053. package/src/tools/credential-execution/run-authenticated-command.ts +82 -77
  1054. package/src/tools/credentials/vault.ts +112 -111
  1055. package/src/tools/document/document-tool.ts +131 -8
  1056. package/src/tools/execution-target.ts +3 -6
  1057. package/src/tools/execution-timeout.ts +3 -4
  1058. package/src/tools/executor.ts +18 -55
  1059. package/src/tools/filesystem/edit.ts +45 -42
  1060. package/src/tools/filesystem/list.ts +33 -30
  1061. package/src/tools/filesystem/read.ts +54 -35
  1062. package/src/tools/filesystem/write.ts +34 -31
  1063. package/src/tools/host-filesystem/edit.test.ts +1 -0
  1064. package/src/tools/host-filesystem/edit.ts +44 -42
  1065. package/src/tools/host-filesystem/read.test.ts +1 -0
  1066. package/src/tools/host-filesystem/read.ts +49 -35
  1067. package/src/tools/host-filesystem/transfer.test.ts +31 -6
  1068. package/src/tools/host-filesystem/transfer.ts +121 -108
  1069. package/src/tools/host-filesystem/write.test.ts +1 -0
  1070. package/src/tools/host-filesystem/write.ts +33 -31
  1071. package/src/tools/host-terminal/host-shell.ts +50 -48
  1072. package/src/tools/mcp/mcp-tool-factory.ts +0 -2
  1073. package/src/tools/memory/register.ts +23 -24
  1074. package/src/tools/network/__tests__/managed-search-proxy.test.ts +282 -0
  1075. package/src/tools/network/__tests__/web-search.test.ts +211 -3
  1076. package/src/tools/network/managed-search-proxy.ts +183 -0
  1077. package/src/tools/network/web-fetch.ts +49 -46
  1078. package/src/tools/network/web-search.ts +215 -57
  1079. package/src/tools/policy-context.ts +3 -1
  1080. package/src/tools/registry.ts +184 -118
  1081. package/src/tools/schedule/create.ts +12 -1
  1082. package/src/tools/schedule/update.ts +16 -0
  1083. package/src/tools/shared/filesystem/audio-read.ts +122 -0
  1084. package/src/tools/shared/filesystem/image-read.ts +1 -1
  1085. package/src/tools/skills/execute.ts +34 -31
  1086. package/src/tools/skills/load.ts +29 -23
  1087. package/src/tools/skills/skill-tool-factory.ts +17 -36
  1088. package/src/tools/subagent/notify-parent.ts +35 -32
  1089. package/src/tools/subagent/spawn.ts +3 -0
  1090. package/src/tools/system/avatar-generator.ts +13 -22
  1091. package/src/tools/system/request-permission.ts +30 -27
  1092. package/src/tools/terminal/shell.ts +190 -61
  1093. package/src/tools/tool-approval-handler.ts +10 -4
  1094. package/src/tools/tool-defaults.ts +20 -9
  1095. package/src/tools/tool-manifest.ts +4 -4
  1096. package/src/tools/tool-name-aliases.ts +72 -14
  1097. package/src/tools/types.ts +86 -33
  1098. package/src/tools/ui-surface/definitions.ts +166 -94
  1099. package/src/types/onboarding-context.ts +6 -0
  1100. package/src/usage/attribution.ts +32 -1
  1101. package/src/usage/types.ts +10 -0
  1102. package/src/util/browser.ts +7 -2
  1103. package/src/util/errors.ts +2 -2
  1104. package/src/util/map-limit.ts +27 -0
  1105. package/src/util/platform.ts +15 -12
  1106. package/src/work-items/work-item-runner.ts +7 -2
  1107. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +7 -20
  1108. package/src/workspace/migrations/090-memory-router-cost-optimized-profile.ts +109 -0
  1109. package/src/workspace/migrations/091-retighten-migration-onboarding-thread.ts +41 -0
  1110. package/src/workspace/migrations/092-backfill-v3-leaves.ts +169 -0
  1111. package/src/workspace/migrations/093-backfill-leaf-ids.ts +144 -0
  1112. package/src/workspace/migrations/094-seed-avatar-manifest.ts +155 -0
  1113. package/src/workspace/migrations/__tests__/094-seed-avatar-manifest.test.ts +136 -0
  1114. package/src/workspace/migrations/__tests__/backfill-leaf-ids.test.ts +175 -0
  1115. package/src/workspace/migrations/__tests__/backfill-v3-leaves.test.ts +124 -0
  1116. package/src/workspace/migrations/registry.ts +10 -0
  1117. package/src/workspace/provider-commit-message-generator.ts +15 -17
  1118. package/tsconfig.json +4 -1
  1119. package/src/__tests__/history-repair-pipeline.test.ts +0 -396
  1120. package/src/cli/commands/__tests__/memory-v3-render.test.ts +0 -340
  1121. package/src/cli/commands/memory-v3-render.ts +0 -344
  1122. package/src/daemon/message-types/disk-pressure.ts +0 -9
  1123. package/src/email/feature-gate.ts +0 -23
  1124. package/src/memory/v3/__tests__/coactivation-store.test.ts +0 -422
  1125. package/src/memory/v3/__tests__/consolidation-job.test.ts +0 -468
  1126. package/src/memory/v3/__tests__/edge-learning-job.test.ts +0 -324
  1127. package/src/memory/v3/__tests__/edges.test.ts +0 -563
  1128. package/src/memory/v3/__tests__/filter.test.ts +0 -512
  1129. package/src/memory/v3/__tests__/gate.test.ts +0 -574
  1130. package/src/memory/v3/__tests__/index-composition.test.ts +0 -233
  1131. package/src/memory/v3/__tests__/loop.test.ts +0 -530
  1132. package/src/memory/v3/__tests__/retriever.test.ts +0 -226
  1133. package/src/memory/v3/__tests__/scouts.test.ts +0 -440
  1134. package/src/memory/v3/__tests__/shadow-middleware.test.ts +0 -312
  1135. package/src/memory/v3/__tests__/system-prompts.test.ts +0 -154
  1136. package/src/memory/v3/__tests__/traversal.test.ts +0 -469
  1137. package/src/memory/v3/__tests__/tree-index.test.ts +0 -280
  1138. package/src/memory/v3/__tests__/tree-store.test.ts +0 -529
  1139. package/src/memory/v3/__tests__/tree-walk.test.ts +0 -707
  1140. package/src/memory/v3/__tests__/validate.test.ts +0 -245
  1141. package/src/memory/v3/auto-edges.ts +0 -223
  1142. package/src/memory/v3/coactivation-store.ts +0 -124
  1143. package/src/memory/v3/consolidation-job.ts +0 -323
  1144. package/src/memory/v3/edge-learning-job.ts +0 -160
  1145. package/src/memory/v3/edges.ts +0 -249
  1146. package/src/memory/v3/filter.ts +0 -281
  1147. package/src/memory/v3/gate.ts +0 -334
  1148. package/src/memory/v3/index-composition.ts +0 -113
  1149. package/src/memory/v3/llm-capture.ts +0 -46
  1150. package/src/memory/v3/loop.ts +0 -382
  1151. package/src/memory/v3/maintenance.ts +0 -144
  1152. package/src/memory/v3/prompt-context.ts +0 -33
  1153. package/src/memory/v3/prompts/consolidation.ts +0 -458
  1154. package/src/memory/v3/prompts/system-prompts.ts +0 -196
  1155. package/src/memory/v3/retriever.ts +0 -33
  1156. package/src/memory/v3/scouts.ts +0 -420
  1157. package/src/memory/v3/shadow-middleware.ts +0 -305
  1158. package/src/memory/v3/traversal.ts +0 -206
  1159. package/src/memory/v3/tree-index.ts +0 -237
  1160. package/src/memory/v3/tree-store.ts +0 -394
  1161. package/src/memory/v3/tree-walk.ts +0 -351
  1162. package/src/memory/v3/validate.ts +0 -300
  1163. package/src/plugins/defaults/circuit-breaker.ts +0 -141
  1164. package/src/plugins/defaults/compaction.ts +0 -141
  1165. package/src/plugins/defaults/empty-response.ts +0 -124
  1166. package/src/plugins/defaults/history-repair.ts +0 -83
  1167. package/src/plugins/defaults/persistence.ts +0 -127
  1168. package/src/plugins/defaults/title-generate.ts +0 -90
  1169. package/src/plugins/defaults/token-estimate.ts +0 -101
  1170. package/src/plugins/defaults/tool-error.ts +0 -119
  1171. package/src/plugins/defaults/tool-result-truncate.ts +0 -84
@@ -1,13 +1,25 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
3
3
 
4
+ import { CompactionCircuit } from "../agent/compaction-circuit.js";
4
5
  import type {
5
6
  AgentEvent,
6
- CheckpointDecision,
7
- CheckpointInfo,
7
+ AgentLoopRunOptions,
8
+ AgentLoopRunResult,
9
+ MidLoopCompaction,
8
10
  } from "../agent/loop.js";
11
+ import type { ContextWindowResult } from "../context/window-manager.js";
9
12
  import type { ServerMessage } from "../daemon/message-protocol.js";
13
+ import { defaultCompactionTerminal } from "../plugins/defaults/compaction/terminal.js";
10
14
  import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
15
+ import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
16
+ import { getMiddlewaresFor } from "../plugins/registry.js";
17
+ import type {
18
+ CompactionArgs,
19
+ CompactionResult,
20
+ TurnContext,
21
+ } from "../plugins/types.js";
22
+ import { PluginTimeoutError } from "../plugins/types.js";
11
23
  import type { ContentBlock, Message } from "../providers/types.js";
12
24
 
13
25
  const conversationCrudRealSnapshot = {
@@ -176,6 +188,12 @@ let mockConversationRow: Record<string, unknown> = {
176
188
  title: null,
177
189
  };
178
190
  let mockMessageById: Record<string, unknown> | null = null;
191
+ const deleteMessageByIdMock = mock(() => ({
192
+ segmentIds: [],
193
+ deletedSummaryIds: [],
194
+ }));
195
+ const reserveMessageMock = mock(async () => ({ id: "msg-reserve" }));
196
+ const updateMessageContentMock = mock(() => {});
179
197
  mock.module("../memory/conversation-crud.js", () => ({
180
198
  setConversationOriginChannelIfUnset: () => {},
181
199
  updateConversationUsage: () => {},
@@ -189,7 +207,7 @@ mock.module("../memory/conversation-crud.js", () => ({
189
207
  }),
190
208
  getConversationOriginInterface: () => null,
191
209
  addMessage: () => ({ id: "mock-msg-id" }),
192
- deleteMessageById: () => {},
210
+ deleteMessageById: deleteMessageByIdMock,
193
211
  updateConversationContextWindow: () => {},
194
212
  updateConversationSlackContextWatermark:
195
213
  updateConversationSlackContextWatermarkMock,
@@ -197,6 +215,36 @@ mock.module("../memory/conversation-crud.js", () => ({
197
215
  getConversationOriginChannel: () => null,
198
216
  getMessageById: () => mockMessageById,
199
217
  getLastUserTimestampBefore: () => 0,
218
+ reserveMessage: reserveMessageMock,
219
+ updateMessageContent: updateMessageContentMock,
220
+ // The real schema is a Zod object; tests don't exercise validation,
221
+ // so a passthrough is sufficient — the production code at
222
+ // `handleMessageComplete` only branches on `success` and reads two
223
+ // fields off `data`. `safeParse` of an empty object satisfies the
224
+ // schema (every field is optional).
225
+ messageMetadataSchema: {
226
+ safeParse: (input: unknown) => ({ success: true, data: input ?? {} }),
227
+ },
228
+ }));
229
+
230
+ // The B3 indexing-restoration path imports `indexMessageNow` from
231
+ // `../memory/indexer.js` and `projectAssistantMessage` from
232
+ // `../memory/conversation-attention-store.js`; without these stubs the
233
+ // real modules would try to open a SQLite DB and read a real config.
234
+ const indexMessageNowMock = mock(async () => ({
235
+ indexedSegments: 0,
236
+ enqueuedJobs: 0,
237
+ }));
238
+ const projectAssistantMessageMock = mock(() => false);
239
+ const publishSyncInvalidationMock = mock(async () => {});
240
+ mock.module("../memory/indexer.js", () => ({
241
+ indexMessageNow: indexMessageNowMock,
242
+ }));
243
+ mock.module("../memory/conversation-attention-store.js", () => ({
244
+ projectAssistantMessage: projectAssistantMessageMock,
245
+ }));
246
+ mock.module("../runtime/sync/sync-publisher.js", () => ({
247
+ publishSyncInvalidation: publishSyncInvalidationMock,
200
248
  }));
201
249
 
202
250
  afterAll(() => {
@@ -371,7 +419,7 @@ mock.module("../daemon/date-context.js", () => ({
371
419
  resolveTurnTimezoneContext: resolveTurnTimezoneContextMock,
372
420
  }));
373
421
 
374
- mock.module("../daemon/history-repair.js", () => ({
422
+ mock.module("../plugins/defaults/history-repair/terminal.js", () => ({
375
423
  repairHistory: (msgs: Message[]) => ({
376
424
  messages: msgs,
377
425
  stats: {
@@ -512,13 +560,149 @@ import {
512
560
  type AgentLoopRun = (
513
561
  messages: Message[],
514
562
  onEvent: (event: AgentEvent) => void | Promise<void>,
515
- signal?: AbortSignal,
516
- requestId?: string,
517
- onCheckpoint?: (
518
- checkpoint: CheckpointInfo,
519
- ) => CheckpointDecision | Promise<CheckpointDecision>,
563
+ options?: AgentLoopRunOptions,
520
564
  ) => Promise<Message[]>;
521
565
 
566
+ /**
567
+ * Faithful re-implementation of `AgentLoop.compact()` for the mock loop: run
568
+ * the compaction pipeline against the supplied turn context (which carries the
569
+ * test's `contextWindowManager`), invoke the orchestrator-supplied hooks, and
570
+ * return the continuation history — or `null` on timeout/exhaustion so the
571
+ * caller yields "budget".
572
+ */
573
+ async function simulateInlineCompaction(
574
+ compaction: MidLoopCompaction,
575
+ history: Message[],
576
+ turnContext: TurnContext | undefined,
577
+ signal: AbortSignal | undefined,
578
+ onEvent: (event: AgentEvent) => void | Promise<void>,
579
+ compactionCircuit: CompactionCircuit,
580
+ ): Promise<Message[] | null> {
581
+ await onEvent({ type: "context_compacting" });
582
+ const { rawHistory, options } = compaction.prepare(history);
583
+ let result: CompactionResult;
584
+ try {
585
+ result = await runPipeline<CompactionArgs, CompactionResult>(
586
+ "compaction",
587
+ getMiddlewaresFor("compaction"),
588
+ (args) => defaultCompactionTerminal(args, turnContext as TurnContext),
589
+ { messages: rawHistory, signal, options },
590
+ turnContext as TurnContext,
591
+ DEFAULT_TIMEOUTS.compaction,
592
+ );
593
+ } catch (error) {
594
+ if (error instanceof PluginTimeoutError) {
595
+ await compactionCircuit.recordOutcome(
596
+ {
597
+ currentRequestId: turnContext?.requestId,
598
+ currentTurnTrustContext: turnContext?.trust,
599
+ turnCount: turnContext?.turnIndex ?? 0,
600
+ },
601
+ true,
602
+ onEvent,
603
+ );
604
+ return null;
605
+ }
606
+ throw error;
607
+ }
608
+ const compactResult = result as ContextWindowResult;
609
+ if (compactResult.summaryFailed !== undefined) {
610
+ await compactionCircuit.recordOutcome(
611
+ {
612
+ currentRequestId: turnContext?.requestId,
613
+ currentTurnTrustContext: turnContext?.trust,
614
+ turnCount: turnContext?.turnIndex ?? 0,
615
+ },
616
+ compactResult.summaryFailed,
617
+ onEvent,
618
+ );
619
+ }
620
+ if (compactResult.compacted) {
621
+ await compaction.applyResult(compactResult, rawHistory);
622
+ }
623
+ if (compactResult.exhausted ?? false) {
624
+ return null;
625
+ }
626
+ return compaction.reinject();
627
+ }
628
+
629
+ /**
630
+ * Adapt a `Message[]`-returning mock loop body into `run()`'s real result
631
+ * shape. Mirrors the production loop: the pause-reason carried back is
632
+ * whatever the most recent `onCheckpoint` call yielded with (null when it
633
+ * never yielded), so the orchestrator derives its yield bookkeeping the same
634
+ * way it does against the real loop.
635
+ */
636
+ const asAgentLoopRun = (
637
+ fn: AgentLoopRun,
638
+ compactionCircuit: CompactionCircuit,
639
+ ): ((
640
+ messages: Message[],
641
+ onEvent: (event: AgentEvent) => void | Promise<void>,
642
+ options?: AgentLoopRunOptions,
643
+ ) => Promise<AgentLoopRunResult>) => {
644
+ return async (messages, onEvent, options) => {
645
+ let exitReason: AgentLoopRunResult["exitReason"] = null;
646
+ let wrapped = options;
647
+ if (options?.onCheckpoint) {
648
+ const inner = options.onCheckpoint;
649
+ wrapped = {
650
+ ...options,
651
+ onCheckpoint: async (info) => {
652
+ // Handoff is offered first, mirroring the loop's ordering.
653
+ const decision = await inner(info);
654
+ if (decision !== "continue") {
655
+ exitReason = decision;
656
+ return decision;
657
+ }
658
+ // The mid-loop budget gate and inline compaction both live inside
659
+ // `AgentLoop.run`. Replicate them here — same formula, stubbed
660
+ // estimator, and the loop's own `compact()` ceremony — so these
661
+ // orchestrator tests drive the real escalation path now that the
662
+ // orchestrator's `onCheckpoint` is handoff-only and compaction runs
663
+ // inline rather than via an orchestrator re-entry loop.
664
+ const contextWindow = options.resolveContextWindow?.();
665
+ if (contextWindow?.overflowRecovery.enabled) {
666
+ const { maxInputTokens, overflowRecovery } = contextWindow;
667
+ const safetyMargin =
668
+ info.history.length > 50
669
+ ? Math.max(overflowRecovery.safetyMarginRatio, 0.15)
670
+ : overflowRecovery.safetyMarginRatio;
671
+ const preflightBudget = Math.floor(
672
+ maxInputTokens * (1 - safetyMargin),
673
+ );
674
+ if (mockEstimateTokens > preflightBudget * 0.85) {
675
+ // Mirror `AgentLoop.compact()`: when a compaction path is
676
+ // supplied, run it in place and continue; on timeout or
677
+ // exhaustion it returns null, so the loop yields "budget".
678
+ const compacted = options.compaction
679
+ ? await simulateInlineCompaction(
680
+ options.compaction,
681
+ info.history,
682
+ options.turnContext,
683
+ options.signal,
684
+ onEvent,
685
+ compactionCircuit,
686
+ )
687
+ : null;
688
+ if (compacted) {
689
+ exitReason = null;
690
+ return "continue";
691
+ }
692
+ exitReason = "budget";
693
+ return "budget";
694
+ }
695
+ }
696
+ exitReason = null;
697
+ return "continue";
698
+ },
699
+ };
700
+ }
701
+ const history = await fn(messages, onEvent, wrapped);
702
+ return { history, exitReason };
703
+ };
704
+ };
705
+
522
706
  function makeCtx(
523
707
  overrides?: Partial<AgentLoopConversationContext> & {
524
708
  agentLoopRun?: AgentLoopRun;
@@ -534,6 +718,8 @@ function makeCtx(
534
718
  },
535
719
  ]);
536
720
 
721
+ const compactionCircuit = new CompactionCircuit("test-conv");
722
+
537
723
  return {
538
724
  conversationId: "test-conv",
539
725
  messages: [
@@ -544,12 +730,13 @@ function makeCtx(
544
730
  currentRequestId: "test-req",
545
731
 
546
732
  agentLoop: {
547
- run: agentLoopRun,
733
+ run: asAgentLoopRun(agentLoopRun, compactionCircuit),
548
734
  getToolTokenBudget: () => 0,
549
735
  getResolvedTools: () => [],
550
736
  // Tests here don't exercise calibration; returning undefined makes
551
737
  // the estimator use the per-provider aggregate key.
552
738
  getActiveModel: () => undefined,
739
+ compactionCircuit,
553
740
  } as unknown as AgentLoopConversationContext["agentLoop"],
554
741
  provider: {
555
742
  name: "mock-provider",
@@ -692,6 +879,13 @@ beforeEach(() => {
692
879
  mockSlackChronologicalContext = null;
693
880
  loadSlackChronologicalContextMock.mockClear();
694
881
  getSlackCompactionWatermarkForPrefixMock.mockClear();
882
+ deleteMessageByIdMock.mockClear();
883
+ reserveMessageMock.mockClear();
884
+ updateMessageContentMock.mockClear();
885
+ indexMessageNowMock.mockClear();
886
+ projectAssistantMessageMock.mockClear();
887
+ publishSyncInvalidationMock.mockClear();
888
+ mockMessageById = null;
695
889
  // Orchestrator pipelines (overflowReduce, persistence, …) run through the
696
890
  // plugin registry; reset and re-register every default so the pipelines
697
891
  // dispatch to middleware backed by the mocked collaborators these tests
@@ -762,7 +956,7 @@ describe("session-agent-loop", () => {
762
956
  });
763
957
 
764
958
  describe("proactive artifact trigger", () => {
765
- test("suppresses proactive app build when the foreground turn used app tools", async () => {
959
+ test("does not start proactive artifact jobs after foreground user turns", async () => {
766
960
  mockConversationRow = {
767
961
  ...mockConversationRow,
768
962
  id: "test-conv",
@@ -776,13 +970,10 @@ describe("session-agent-loop", () => {
776
970
  mockHasProactiveArtifactCompleted = false;
777
971
  mockTryClaimProactiveArtifactTrigger = true;
778
972
 
779
- const agentLoopRun: AgentLoopRun = async (
780
- messages,
781
- onEvent,
782
- _signal,
783
- _requestId,
784
- onCheckpoint,
785
- ) => {
973
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent, options) => {
974
+ // Prime the assistant row anchor for LLM call 1 — production code
975
+ // emits this from `AgentLoop.run` just before `provider.sendMessage`.
976
+ await onEvent({ type: "llm_call_started" });
786
977
  await onEvent({
787
978
  type: "message_complete",
788
979
  message: {
@@ -802,12 +993,15 @@ describe("session-agent-loop", () => {
802
993
  content: "{}",
803
994
  isError: false,
804
995
  });
805
- await onCheckpoint?.({
996
+ await options?.onCheckpoint?.({
806
997
  turnIndex: 0,
807
998
  toolCount: 1,
808
999
  hasToolUse: true,
809
1000
  history: messages,
810
1001
  });
1002
+ // Prime the anchor again for LLM call 2 — multi-call agent turns
1003
+ // reserve a fresh assistant row per LLM call.
1004
+ await onEvent({ type: "llm_call_started" });
811
1005
  await onEvent({
812
1006
  type: "message_complete",
813
1007
  message: {
@@ -839,11 +1033,7 @@ describe("session-agent-loop", () => {
839
1033
  );
840
1034
  await new Promise((resolve) => setTimeout(resolve, 0));
841
1035
 
842
- expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(1);
843
- expect(runProactiveArtifactJobMock.mock.calls[0]?.[0]).toMatchObject({
844
- conversationId: "test-conv",
845
- suppressAppBuild: true,
846
- });
1036
+ expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(0);
847
1037
  });
848
1038
  });
849
1039
 
@@ -981,7 +1171,10 @@ describe("session-agent-loop", () => {
981
1171
  },
982
1172
  } as unknown as AgentLoopConversationContext["traceEmitter"],
983
1173
  });
984
- ctx.agentLoop.run = agentLoopRun as AgentLoopRun;
1174
+ ctx.agentLoop.run = asAgentLoopRun(
1175
+ agentLoopRun,
1176
+ ctx.agentLoop.compactionCircuit,
1177
+ );
985
1178
 
986
1179
  await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
987
1180
 
@@ -990,8 +1183,7 @@ describe("session-agent-loop", () => {
990
1183
  expect(activityStates).toContainEqual([
991
1184
  "idle",
992
1185
  "error_terminal",
993
- "global",
994
- "test-req",
1186
+ { anchor: "global", requestId: "test-req" },
995
1187
  ]);
996
1188
  expect(traceEvents[0]).toEqual([
997
1189
  "request_error",
@@ -1053,8 +1245,7 @@ describe("session-agent-loop", () => {
1053
1245
  expect(activityStates).toContainEqual([
1054
1246
  "idle",
1055
1247
  "error_terminal",
1056
- "global",
1057
- "test-req",
1248
+ { anchor: "global", requestId: "test-req" },
1058
1249
  ]);
1059
1250
  });
1060
1251
  });
@@ -1064,6 +1255,9 @@ describe("session-agent-loop", () => {
1064
1255
  const events: ServerMessage[] = [];
1065
1256
 
1066
1257
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1258
+ // Prime the assistant row anchor — production code emits this from
1259
+ // `AgentLoop.run` just before `provider.sendMessage`.
1260
+ await onEvent({ type: "llm_call_started" });
1067
1261
  // Simulate tool_use + error during execution
1068
1262
  onEvent({
1069
1263
  type: "tool_use",
@@ -1113,6 +1307,9 @@ describe("session-agent-loop", () => {
1113
1307
  const events: ServerMessage[] = [];
1114
1308
 
1115
1309
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1310
+ // Prime the assistant row anchor — production code emits this from
1311
+ // `AgentLoop.run` just before `provider.sendMessage`.
1312
+ await onEvent({ type: "llm_call_started" });
1116
1313
  onEvent({
1117
1314
  type: "message_complete",
1118
1315
  message: {
@@ -1173,6 +1370,9 @@ describe("session-agent-loop", () => {
1173
1370
  };
1174
1371
 
1175
1372
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1373
+ // Prime the assistant row anchor — production code emits this from
1374
+ // `AgentLoop.run` just before `provider.sendMessage`.
1375
+ await onEvent({ type: "llm_call_started" });
1176
1376
  onEvent({
1177
1377
  type: "message_complete",
1178
1378
  message: {
@@ -1238,6 +1438,9 @@ describe("session-agent-loop", () => {
1238
1438
  };
1239
1439
 
1240
1440
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1441
+ // Prime the assistant row anchor — production code emits this from
1442
+ // `AgentLoop.run` just before `provider.sendMessage`.
1443
+ await onEvent({ type: "llm_call_started" });
1241
1444
  onEvent({
1242
1445
  type: "message_complete",
1243
1446
  message: {
@@ -1320,6 +1523,9 @@ describe("session-agent-loop", () => {
1320
1523
  };
1321
1524
 
1322
1525
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1526
+ // Prime the assistant row anchor — production code emits this from
1527
+ // `AgentLoop.run` just before `provider.sendMessage`.
1528
+ await onEvent({ type: "llm_call_started" });
1323
1529
  onEvent({
1324
1530
  type: "message_complete",
1325
1531
  message: {
@@ -1388,6 +1594,9 @@ describe("session-agent-loop", () => {
1388
1594
  }> = [];
1389
1595
 
1390
1596
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1597
+ // Prime the assistant row anchor — production code emits this from
1598
+ // `AgentLoop.run` just before `provider.sendMessage`.
1599
+ await onEvent({ type: "llm_call_started" });
1391
1600
  onEvent({ type: "text_delta", text: "Hi." });
1392
1601
  onEvent({
1393
1602
  type: "message_complete",
@@ -1463,6 +1672,9 @@ describe("session-agent-loop", () => {
1463
1672
  }> = [];
1464
1673
 
1465
1674
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1675
+ // Prime the assistant row anchor — production code emits this from
1676
+ // `AgentLoop.run` just before `provider.sendMessage`.
1677
+ await onEvent({ type: "llm_call_started" });
1466
1678
  // No text_delta — pure tool-call response
1467
1679
  onEvent({
1468
1680
  type: "message_complete",
@@ -1526,6 +1738,9 @@ describe("session-agent-loop", () => {
1526
1738
  const events: ServerMessage[] = [];
1527
1739
 
1528
1740
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1741
+ // Prime the assistant row anchor — production code emits this from
1742
+ // `AgentLoop.run` just before `provider.sendMessage`.
1743
+ await onEvent({ type: "llm_call_started" });
1529
1744
  onEvent({
1530
1745
  type: "message_complete",
1531
1746
  message: {
@@ -1638,6 +1853,9 @@ describe("session-agent-loop", () => {
1638
1853
  });
1639
1854
 
1640
1855
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1856
+ // Prime the assistant row anchor — production code emits this from
1857
+ // `AgentLoop.run` just before `provider.sendMessage`.
1858
+ await onEvent({ type: "llm_call_started" });
1641
1859
  onEvent({
1642
1860
  type: "message_complete",
1643
1861
  message: {
@@ -1728,6 +1946,11 @@ describe("session-agent-loop", () => {
1728
1946
  };
1729
1947
 
1730
1948
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1949
+ // Prime the assistant row anchor — production code emits this from
1950
+ // `AgentLoop.run` just before `provider.sendMessage`. Retry branches
1951
+ // need this on every invocation: each agent-loop iteration reserves
1952
+ // its own row.
1953
+ await onEvent({ type: "llm_call_started" });
1731
1954
  callCount++;
1732
1955
  if (callCount === 1) {
1733
1956
  onEvent({
@@ -1855,6 +2078,11 @@ describe("session-agent-loop", () => {
1855
2078
  };
1856
2079
 
1857
2080
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2081
+ // Prime the assistant row anchor — production code emits this from
2082
+ // `AgentLoop.run` just before `provider.sendMessage`. Retry branches
2083
+ // need this on every invocation: each agent-loop iteration reserves
2084
+ // its own row.
2085
+ await onEvent({ type: "llm_call_started" });
1858
2086
  callCount++;
1859
2087
  if (callCount === 1) {
1860
2088
  onEvent({
@@ -1940,6 +2168,11 @@ describe("session-agent-loop", () => {
1940
2168
  mockOverflowAction = "auto_compress_latest_turn";
1941
2169
 
1942
2170
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2171
+ // Prime the assistant row anchor — production code emits this from
2172
+ // `AgentLoop.run` just before `provider.sendMessage`. Retry branches
2173
+ // need this on every invocation: each agent-loop iteration reserves
2174
+ // its own row.
2175
+ await onEvent({ type: "llm_call_started" });
1943
2176
  callCount++;
1944
2177
  if (callCount <= 2) {
1945
2178
  onEvent({
@@ -2059,13 +2292,7 @@ describe("session-agent-loop", () => {
2059
2292
  // call). 90k satisfies both so the path reaches call 3.
2060
2293
  mockEstimateTokens = 90_000;
2061
2294
 
2062
- const agentLoopRun: AgentLoopRun = async (
2063
- messages,
2064
- onEvent,
2065
- _signal,
2066
- _reqId,
2067
- onCheckpoint,
2068
- ) => {
2295
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent, options) => {
2069
2296
  callCount++;
2070
2297
  if (callCount <= 2) {
2071
2298
  // Calls 1 (initial) and 2 (convergence rerun): error so
@@ -2089,8 +2316,8 @@ describe("session-agent-loop", () => {
2089
2316
  // flips `yieldedForBudget` to true, then return without
2090
2317
  // finishing — mirroring what AgentLoop.run does when its
2091
2318
  // checkpoint returns "yield".
2092
- if (onCheckpoint) {
2093
- await onCheckpoint({
2319
+ if (options?.onCheckpoint) {
2320
+ await options.onCheckpoint({
2094
2321
  turnIndex: 0,
2095
2322
  toolCount: 1,
2096
2323
  hasToolUse: true,
@@ -2237,6 +2464,9 @@ describe("session-agent-loop", () => {
2237
2464
 
2238
2465
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2239
2466
  agentLoopCalls++;
2467
+ // Prime the assistant row anchor — production code emits this from
2468
+ // `AgentLoop.run` just before `provider.sendMessage`.
2469
+ await onEvent({ type: "llm_call_started" });
2240
2470
  onEvent({
2241
2471
  type: "message_complete",
2242
2472
  message: {
@@ -2285,6 +2515,11 @@ describe("session-agent-loop", () => {
2285
2515
  let callCount = 0;
2286
2516
 
2287
2517
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2518
+ // Prime the assistant row anchor — production code emits this from
2519
+ // `AgentLoop.run` just before `provider.sendMessage`. Retry branches
2520
+ // need this on every invocation: each agent-loop iteration reserves
2521
+ // its own row.
2522
+ await onEvent({ type: "llm_call_started" });
2288
2523
  callCount++;
2289
2524
  if (callCount === 1) {
2290
2525
  onEvent({
@@ -2362,13 +2597,12 @@ describe("session-agent-loop", () => {
2362
2597
  test("yields at checkpoint when canHandoffAtCheckpoint returns true", async () => {
2363
2598
  const events: ServerMessage[] = [];
2364
2599
 
2365
- const agentLoopRun: AgentLoopRun = async (
2366
- messages,
2367
- onEvent,
2368
- _signal,
2369
- _reqId,
2370
- onCheckpoint,
2371
- ) => {
2600
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent, options) => {
2601
+ // Prime the assistant row anchor — production code emits this from
2602
+ // `AgentLoop.run` just before `provider.sendMessage`. Retry branches
2603
+ // need this on every invocation: each agent-loop iteration reserves
2604
+ // its own row.
2605
+ await onEvent({ type: "llm_call_started" });
2372
2606
  // Simulate tool use followed by checkpoint
2373
2607
  onEvent({ type: "tool_use", id: "tu-1", name: "file_read", input: {} });
2374
2608
  onEvent({
@@ -2391,14 +2625,14 @@ describe("session-agent-loop", () => {
2391
2625
  model: "test-model",
2392
2626
  providerDurationMs: 100,
2393
2627
  });
2394
- if (onCheckpoint) {
2395
- const decision = await onCheckpoint({
2628
+ if (options?.onCheckpoint) {
2629
+ const decision = await options.onCheckpoint({
2396
2630
  turnIndex: 0,
2397
2631
  toolCount: 1,
2398
2632
  hasToolUse: true,
2399
2633
  history: messages,
2400
2634
  });
2401
- if (decision === "yield") {
2635
+ if (decision !== "continue") {
2402
2636
  return [
2403
2637
  ...messages,
2404
2638
  {
@@ -2435,13 +2669,12 @@ describe("session-agent-loop", () => {
2435
2669
  test("continues when canHandoffAtCheckpoint returns false", async () => {
2436
2670
  const events: ServerMessage[] = [];
2437
2671
 
2438
- const agentLoopRun: AgentLoopRun = async (
2439
- messages,
2440
- onEvent,
2441
- _signal,
2442
- _reqId,
2443
- onCheckpoint,
2444
- ) => {
2672
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent, options) => {
2673
+ // Prime the assistant row anchor — production code emits this from
2674
+ // `AgentLoop.run` just before `provider.sendMessage`. Retry branches
2675
+ // need this on every invocation: each agent-loop iteration reserves
2676
+ // its own row.
2677
+ await onEvent({ type: "llm_call_started" });
2445
2678
  onEvent({ type: "tool_use", id: "tu-1", name: "file_read", input: {} });
2446
2679
  onEvent({
2447
2680
  type: "tool_result",
@@ -2463,8 +2696,8 @@ describe("session-agent-loop", () => {
2463
2696
  model: "test-model",
2464
2697
  providerDurationMs: 100,
2465
2698
  });
2466
- if (onCheckpoint) {
2467
- await onCheckpoint({
2699
+ if (options?.onCheckpoint) {
2700
+ await options.onCheckpoint({
2468
2701
  turnIndex: 0,
2469
2702
  toolCount: 1,
2470
2703
  hasToolUse: true,
@@ -2504,6 +2737,9 @@ describe("session-agent-loop", () => {
2504
2737
  const abortController = new AbortController();
2505
2738
 
2506
2739
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2740
+ // Prime the assistant row anchor — production code emits this from
2741
+ // `AgentLoop.run` just before `provider.sendMessage`.
2742
+ await onEvent({ type: "llm_call_started" });
2507
2743
  onEvent({
2508
2744
  type: "message_complete",
2509
2745
  message: {
@@ -2564,6 +2800,9 @@ describe("session-agent-loop", () => {
2564
2800
  resolveAssistantAttachmentsMock.mockClear();
2565
2801
 
2566
2802
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2803
+ // Prime the assistant row anchor — production code emits this from
2804
+ // `AgentLoop.run` just before `provider.sendMessage`.
2805
+ await onEvent({ type: "llm_call_started" });
2567
2806
  onEvent({
2568
2807
  type: "message_complete",
2569
2808
  message: {
@@ -2603,6 +2842,9 @@ describe("session-agent-loop", () => {
2603
2842
  test("increments turnCount after successful run", async () => {
2604
2843
  const ctx = makeCtx({
2605
2844
  agentLoopRun: async (messages, onEvent) => {
2845
+ // Prime the assistant row anchor — production code emits this from
2846
+ // `AgentLoop.run` just before `provider.sendMessage`.
2847
+ await onEvent({ type: "llm_call_started" });
2606
2848
  onEvent({
2607
2849
  type: "message_complete",
2608
2850
  message: {
@@ -2636,6 +2878,9 @@ describe("session-agent-loop", () => {
2636
2878
  test("clears processing state and abort controller", async () => {
2637
2879
  const ctx = makeCtx({
2638
2880
  agentLoopRun: async (messages, onEvent) => {
2881
+ // Prime the assistant row anchor — production code emits this from
2882
+ // `AgentLoop.run` just before `provider.sendMessage`.
2883
+ await onEvent({ type: "llm_call_started" });
2639
2884
  onEvent({
2640
2885
  type: "message_complete",
2641
2886
  message: {
@@ -2699,8 +2944,13 @@ describe("session-agent-loop", () => {
2699
2944
  const ctx = makeCtx({
2700
2945
  agentLoopRun: async (
2701
2946
  messages: Message[],
2702
- onEvent: (event: AgentEvent) => void,
2947
+ onEvent: (event: AgentEvent) => void | Promise<void>,
2703
2948
  ) => {
2949
+ // Prime the assistant row anchor — production code emits this from
2950
+ // `AgentLoop.run` just before `provider.sendMessage`. Must be
2951
+ // awaited so the assistant row is reserved before message_complete
2952
+ // tries to write into it.
2953
+ await onEvent({ type: "llm_call_started" });
2704
2954
  onEvent({
2705
2955
  type: "message_complete",
2706
2956
  message: {
@@ -3044,6 +3294,674 @@ describe("session-agent-loop", () => {
3044
3294
  });
3045
3295
  });
3046
3296
 
3297
+ describe("B3 pre-allocation: indexing + cleanup", () => {
3298
+ test("handleMessageComplete indexes and projects the finalized assistant row", async () => {
3299
+ // The pre-B3 path inserted assistant rows via `addMessage`, which ran
3300
+ // the memory indexer and the conversation-attention projector as
3301
+ // side-effects of the insert. B3 splits the write into
3302
+ // `reserveMessage` + `updateMessageContent`, both of which are CRUD-only,
3303
+ // so the indexing + projection calls had to be re-driven explicitly
3304
+ // after `updateContent` succeeds. Codex P1 caught a regression where
3305
+ // this path was missing entirely; this test pins it down.
3306
+ mockMessageById = {
3307
+ id: "msg-reserve",
3308
+ conversationId: "test-conv",
3309
+ createdAt: 1234567,
3310
+ role: "assistant",
3311
+ content: "[]",
3312
+ metadata: null,
3313
+ };
3314
+ // Force attention projection to report a state change so we also
3315
+ // observe the sync-invalidation publish path on the same turn.
3316
+ projectAssistantMessageMock.mockImplementationOnce(() => true);
3317
+
3318
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3319
+ await onEvent({ type: "llm_call_started" });
3320
+ // `message_complete` is awaited so `handleMessageComplete` (and its
3321
+ // async indexer + projector chain) completes before the next event
3322
+ // or before the loop returns. Without the await the projector's
3323
+ // synchronous call still races against the test's assertion phase
3324
+ // because the indexer's `await` yields microtasks.
3325
+ await onEvent({
3326
+ type: "message_complete",
3327
+ message: {
3328
+ role: "assistant",
3329
+ content: [{ type: "text", text: "indexed reply" }],
3330
+ },
3331
+ });
3332
+ onEvent({
3333
+ type: "usage",
3334
+ inputTokens: 10,
3335
+ outputTokens: 5,
3336
+ model: "test",
3337
+ providerDurationMs: 50,
3338
+ });
3339
+ return [
3340
+ ...messages,
3341
+ {
3342
+ role: "assistant" as const,
3343
+ content: [
3344
+ { type: "text", text: "indexed reply" },
3345
+ ] as ContentBlock[],
3346
+ },
3347
+ ];
3348
+ };
3349
+
3350
+ const ctx = makeCtx({ agentLoopRun });
3351
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3352
+
3353
+ // Indexer fired with the reserved row's id + the finalized content.
3354
+ expect(indexMessageNowMock).toHaveBeenCalledTimes(1);
3355
+ const indexCallArgs = indexMessageNowMock.mock.calls[0] as unknown as [
3356
+ {
3357
+ messageId: string;
3358
+ conversationId: string;
3359
+ role: string;
3360
+ content: string;
3361
+ createdAt: number;
3362
+ scopeId: string;
3363
+ },
3364
+ unknown,
3365
+ ];
3366
+ const indexCall = indexCallArgs[0];
3367
+ expect(indexCall).toMatchObject({
3368
+ messageId: "msg-reserve",
3369
+ conversationId: "test-conv",
3370
+ role: "assistant",
3371
+ createdAt: 1234567,
3372
+ scopeId: "default",
3373
+ });
3374
+ expect(indexCall.content).toContain("indexed reply");
3375
+
3376
+ // Attention projector fired with the same row coordinates.
3377
+ expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
3378
+ const projectCall = projectAssistantMessageMock.mock
3379
+ .calls[0] as unknown as [
3380
+ { conversationId: string; messageId: string; messageAt: number },
3381
+ ];
3382
+ expect(projectCall[0]).toEqual({
3383
+ conversationId: "test-conv",
3384
+ messageId: "msg-reserve",
3385
+ messageAt: 1234567,
3386
+ });
3387
+
3388
+ // Projection reported a state change → sync invalidation fires with
3389
+ // the conversation `:metadata` tag. The mock also receives a
3390
+ // `:messages` invalidation from the orchestrator's
3391
+ // `publishLoopMessagesChanged` post-loop emit, so we filter by tag
3392
+ // rather than asserting a total call count.
3393
+ const metadataPublishes = (
3394
+ publishSyncInvalidationMock.mock.calls as unknown as Array<[string[]]>
3395
+ ).filter((args) => args[0]?.includes("conversation:test-conv:metadata"));
3396
+ expect(metadataPublishes).toHaveLength(1);
3397
+ });
3398
+
3399
+ test("handleMessageComplete skips sync invalidation when attention state unchanged", async () => {
3400
+ // Mirror of the previous test but with the default projector return
3401
+ // (`false`). The projection still runs every turn, but the sync
3402
+ // invalidation publish must be gated on attention-state movement to
3403
+ // avoid flooding clients with no-op metadata refreshes.
3404
+ mockMessageById = {
3405
+ id: "msg-reserve",
3406
+ conversationId: "test-conv",
3407
+ createdAt: 999,
3408
+ role: "assistant",
3409
+ content: "[]",
3410
+ metadata: null,
3411
+ };
3412
+
3413
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3414
+ await onEvent({ type: "llm_call_started" });
3415
+ // See sibling test — `message_complete` must be awaited so the
3416
+ // projector call lands before the assertion phase.
3417
+ await onEvent({
3418
+ type: "message_complete",
3419
+ message: {
3420
+ role: "assistant",
3421
+ content: [{ type: "text", text: "quiet" }],
3422
+ },
3423
+ });
3424
+ onEvent({
3425
+ type: "usage",
3426
+ inputTokens: 1,
3427
+ outputTokens: 1,
3428
+ model: "test",
3429
+ providerDurationMs: 1,
3430
+ });
3431
+ return [
3432
+ ...messages,
3433
+ {
3434
+ role: "assistant" as const,
3435
+ content: [{ type: "text", text: "quiet" }] as ContentBlock[],
3436
+ },
3437
+ ];
3438
+ };
3439
+
3440
+ const ctx = makeCtx({ agentLoopRun });
3441
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3442
+
3443
+ expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
3444
+ // The mock will still receive a `:messages` invalidation from the
3445
+ // orchestrator's `publishLoopMessagesChanged` — filter to the
3446
+ // `:metadata` tag and assert it never landed.
3447
+ const metadataPublishes = (
3448
+ publishSyncInvalidationMock.mock.calls as unknown as Array<[string[]]>
3449
+ ).filter((args) => args[0]?.includes("conversation:test-conv:metadata"));
3450
+ expect(metadataPublishes).toHaveLength(0);
3451
+ });
3452
+
3453
+ test("handleLlmCallStarted deletes a stranded reservation before reserving a new row", async () => {
3454
+ // Simulates a retry path: the first LLM call reserves an assistant row
3455
+ // but exits without `message_complete` (e.g. context-overflow rescue,
3456
+ // ordering-error rescue, image-overflow rescue). The next
3457
+ // `llm_call_started` must delete the stranded row so the transcript
3458
+ // does not accumulate empty assistant bubbles.
3459
+ reserveMessageMock
3460
+ .mockImplementationOnce(async () => ({ id: "msg-strand-A" }))
3461
+ .mockImplementationOnce(async () => ({ id: "msg-strand-B" }));
3462
+ // Indexer/projector mocks default to no-op; no finalized row in this
3463
+ // test, so `mockMessageById` stays null.
3464
+
3465
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3466
+ // First LLM call: reserve msg-strand-A, never finalize.
3467
+ await onEvent({ type: "llm_call_started" });
3468
+ // Second LLM call: should delete msg-strand-A before reserving
3469
+ // msg-strand-B.
3470
+ await onEvent({ type: "llm_call_started" });
3471
+ // Finalize the second one so the loop has a valid assistant message
3472
+ // and exits cleanly.
3473
+ onEvent({
3474
+ type: "message_complete",
3475
+ message: {
3476
+ role: "assistant",
3477
+ content: [{ type: "text", text: "retry succeeded" }],
3478
+ },
3479
+ });
3480
+ onEvent({
3481
+ type: "usage",
3482
+ inputTokens: 5,
3483
+ outputTokens: 3,
3484
+ model: "test",
3485
+ providerDurationMs: 25,
3486
+ });
3487
+ return [
3488
+ ...messages,
3489
+ {
3490
+ role: "assistant" as const,
3491
+ content: [
3492
+ { type: "text", text: "retry succeeded" },
3493
+ ] as ContentBlock[],
3494
+ },
3495
+ ];
3496
+ };
3497
+
3498
+ const ctx = makeCtx({ agentLoopRun });
3499
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3500
+
3501
+ // Exactly one delete fires — for msg-strand-A, before the second
3502
+ // reserve. The second reservation is committed via `updateContent`
3503
+ // (not deleted), and after the run completes
3504
+ // `assistantRowAwaitingFinalization` is false, so no further delete
3505
+ // is attempted on shutdown.
3506
+ expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
3507
+ const strandDeleteCall = deleteMessageByIdMock.mock
3508
+ .calls[0] as unknown as [string];
3509
+ expect(strandDeleteCall[0]).toBe("msg-strand-A");
3510
+ expect(reserveMessageMock).toHaveBeenCalledTimes(2);
3511
+ });
3512
+
3513
+ test("provider-error branch deletes the orphaned reservation and repoints lastAssistantMessageId", async () => {
3514
+ // Codex P2 regression: B3 reserves an empty assistant row at
3515
+ // `llm_call_started`. When the call exits via the provider-error
3516
+ // branch (no `message_complete`), the synthetic error message is
3517
+ // inserted separately. Without cleanup the transcript would carry
3518
+ // both the empty reserved row AND the error message, and
3519
+ // `syncLastAssistantMessageToDisk` (which reads
3520
+ // `state.lastAssistantMessageId`) would mis-target the deleted
3521
+ // reservation id.
3522
+ reserveMessageMock.mockImplementationOnce(async () => ({
3523
+ id: "msg-orphaned-reservation",
3524
+ }));
3525
+
3526
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3527
+ // Reserve the orphan.
3528
+ await onEvent({ type: "llm_call_started" });
3529
+ // Provider rejects — writes the llm_request_log row and arms
3530
+ // `state.providerErrorUserMessage` via `handleError`.
3531
+ onEvent({
3532
+ type: "provider_error",
3533
+ error: new Error("upstream 500"),
3534
+ rawRequest: { model: "gpt-4.1", messages: [] },
3535
+ actualProvider: "openai",
3536
+ });
3537
+ onEvent({
3538
+ type: "error",
3539
+ error: new Error("upstream 500"),
3540
+ });
3541
+ // No assistant message in the result — the synthetic-error branch
3542
+ // below the agent loop fires.
3543
+ return messages;
3544
+ };
3545
+
3546
+ const ctx = makeCtx({ agentLoopRun });
3547
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3548
+
3549
+ // The orphan was deleted exactly once, before the synthetic error
3550
+ // message landed.
3551
+ expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
3552
+ const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
3553
+ string,
3554
+ ];
3555
+ expect(deleteCall[0]).toBe("msg-orphaned-reservation");
3556
+
3557
+ // Post-loop `syncLastAssistantMessageToDisk` targets the synthetic
3558
+ // error row's id (`mock-msg-id` from the mocked `addMessage`), NOT
3559
+ // the deleted reservation id. This is the externally-observable
3560
+ // proof that `state.lastAssistantMessageId` was repointed.
3561
+ expect(syncMessageToDiskMock).toHaveBeenCalled();
3562
+ const syncCalls = syncMessageToDiskMock.mock.calls as unknown as Array<
3563
+ [string, string, number]
3564
+ >;
3565
+ const lastSync = syncCalls[syncCalls.length - 1];
3566
+ expect(lastSync?.[1]).toBe("mock-msg-id");
3567
+ expect(lastSync?.[1]).not.toBe("msg-orphaned-reservation");
3568
+ });
3569
+ });
3570
+
3571
+ describe("partial persistence", () => {
3572
+ // The legacy flow reserves an empty assistant row at `llm_call_started`
3573
+ // (`content: "[]"`) and never touches it again until
3574
+ // `handleMessageComplete` fires the single authoritative
3575
+ // `updateContent`. Between those events the row is empty for the full
3576
+ // duration of a turn — a browser refresh mid-turn sees nothing where
3577
+ // the in-progress assistant reply should be.
3578
+ //
3579
+ // Partial persistence closes that durability gap with a debounced
3580
+ // flush from `handleTextDelta` (250ms timer). `handleToolUse`
3581
+ // intentionally does NOT flush — `AgentLoop.run` emits `tool_use`
3582
+ // strictly AFTER `message_complete`, so any flush from that handler
3583
+ // would land after the authoritative finalize and overwrite the
3584
+ // finalized row. The indexer + projector still fire ONLY at
3585
+ // `message_complete` — partial rows are never indexed.
3586
+ //
3587
+ // These tests pin down the wire-level contract by counting
3588
+ // `updateMessageContent` calls and inspecting the JSON payload of the
3589
+ // partial-flush writes. The indexing / sync-invalidation paths are
3590
+ // covered by the pre-allocation block above.
3591
+
3592
+ test("debounced time gate flushes one partial write after PARTIAL_PERSIST_DEBOUNCE_MS", async () => {
3593
+ mockMessageById = {
3594
+ id: "msg-reserve",
3595
+ conversationId: "test-conv",
3596
+ createdAt: 1234567,
3597
+ role: "assistant",
3598
+ content: "[]",
3599
+ metadata: null,
3600
+ };
3601
+
3602
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3603
+ await onEvent({ type: "llm_call_started" });
3604
+ // Two small deltas — well under the 1024-char size gate — should
3605
+ // schedule a single debounced flush.
3606
+ onEvent({ type: "text_delta", text: "Hello, " });
3607
+ onEvent({ type: "text_delta", text: "world." });
3608
+ // Wait long enough for the 250ms debounce to fire.
3609
+ await new Promise((resolve) => setTimeout(resolve, 1100));
3610
+ await onEvent({
3611
+ type: "message_complete",
3612
+ message: {
3613
+ role: "assistant",
3614
+ content: [{ type: "text", text: "Hello, world." }],
3615
+ },
3616
+ });
3617
+ onEvent({
3618
+ type: "usage",
3619
+ inputTokens: 10,
3620
+ outputTokens: 5,
3621
+ model: "test",
3622
+ providerDurationMs: 50,
3623
+ });
3624
+ return [
3625
+ ...messages,
3626
+ {
3627
+ role: "assistant" as const,
3628
+ content: [
3629
+ { type: "text", text: "Hello, world." },
3630
+ ] as ContentBlock[],
3631
+ },
3632
+ ];
3633
+ };
3634
+
3635
+ const ctx = makeCtx({ agentLoopRun });
3636
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3637
+
3638
+ // Exactly two `updateContent` calls land:
3639
+ // 1. the debounced partial flush after both deltas accumulated, and
3640
+ // 2. the final authoritative flush in `handleMessageComplete`.
3641
+ // Without the debounce gate this would be one-per-delta + one final
3642
+ // (3). Without the partial flush at all it would be just 1.
3643
+ expect(updateMessageContentMock).toHaveBeenCalledTimes(2);
3644
+ const calls = updateMessageContentMock.mock.calls as unknown as Array<
3645
+ [string, string]
3646
+ >;
3647
+ const partialFlush = calls[0];
3648
+ expect(partialFlush?.[0]).toBe("msg-reserve");
3649
+ const partialBlocks = JSON.parse(partialFlush?.[1] ?? "[]") as Array<{
3650
+ type: string;
3651
+ text?: string;
3652
+ }>;
3653
+ expect(partialBlocks).toEqual([{ type: "text", text: "Hello, world." }]);
3654
+ });
3655
+
3656
+ test("handleToolUse does NOT trigger a partial flush of its own", async () => {
3657
+ // `AgentLoop.run` emits `tool_use` strictly AFTER `message_complete`,
3658
+ // so a flush from the tool_use handler would land after the
3659
+ // authoritative final `updateContent` and overwrite the finalized
3660
+ // row (Codex P1 / Vargas review feedback). The handler must be a
3661
+ // no-op for the partial-persist accumulator.
3662
+ mockMessageById = {
3663
+ id: "msg-reserve",
3664
+ conversationId: "test-conv",
3665
+ createdAt: 1234567,
3666
+ role: "assistant",
3667
+ content: "[]",
3668
+ metadata: null,
3669
+ };
3670
+
3671
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3672
+ await onEvent({ type: "llm_call_started" });
3673
+ // No text delta — only a tool_use. If `handleToolUse` were
3674
+ // flushing, this would land a partial write before
3675
+ // `message_complete`.
3676
+ onEvent({
3677
+ type: "tool_use",
3678
+ id: "tu-no-flush",
3679
+ name: "file_read",
3680
+ input: { path: "/foo" },
3681
+ });
3682
+ // Yield a microtask so any (incorrectly) fire-and-forget
3683
+ // pipeline call has a chance to land before message_complete.
3684
+ await new Promise((resolve) => setImmediate(resolve));
3685
+ onEvent({
3686
+ type: "tool_result",
3687
+ toolUseId: "tu-no-flush",
3688
+ content: "ok",
3689
+ isError: false,
3690
+ });
3691
+ await onEvent({
3692
+ type: "message_complete",
3693
+ message: {
3694
+ role: "assistant",
3695
+ content: [
3696
+ {
3697
+ type: "tool_use",
3698
+ id: "tu-no-flush",
3699
+ name: "file_read",
3700
+ input: { path: "/foo" },
3701
+ },
3702
+ ],
3703
+ },
3704
+ });
3705
+ onEvent({
3706
+ type: "usage",
3707
+ inputTokens: 10,
3708
+ outputTokens: 5,
3709
+ model: "test",
3710
+ providerDurationMs: 50,
3711
+ });
3712
+ return [
3713
+ ...messages,
3714
+ {
3715
+ role: "assistant" as const,
3716
+ content: [
3717
+ {
3718
+ type: "tool_use",
3719
+ id: "tu-no-flush",
3720
+ name: "file_read",
3721
+ input: { path: "/foo" },
3722
+ },
3723
+ ] as ContentBlock[],
3724
+ },
3725
+ ];
3726
+ };
3727
+
3728
+ const ctx = makeCtx({ agentLoopRun });
3729
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3730
+
3731
+ // Only the authoritative final flush from `handleMessageComplete`
3732
+ // lands. A partial flush from `handleToolUse` would have made this
3733
+ // 2; that's the regression this test guards against.
3734
+ expect(updateMessageContentMock).toHaveBeenCalledTimes(1);
3735
+ });
3736
+
3737
+ test("handleMessageComplete clears any pending debounce timer before the final flush", async () => {
3738
+ mockMessageById = {
3739
+ id: "msg-reserve",
3740
+ conversationId: "test-conv",
3741
+ createdAt: 1234567,
3742
+ role: "assistant",
3743
+ content: "[]",
3744
+ metadata: null,
3745
+ };
3746
+
3747
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3748
+ await onEvent({ type: "llm_call_started" });
3749
+ // Short delta — schedules a debounce timer but does NOT trip the
3750
+ // size gate. message_complete then arrives immediately after,
3751
+ // before the 250ms timer can fire.
3752
+ onEvent({ type: "text_delta", text: "Quick reply." });
3753
+ await onEvent({
3754
+ type: "message_complete",
3755
+ message: {
3756
+ role: "assistant",
3757
+ content: [{ type: "text", text: "Quick reply." }],
3758
+ },
3759
+ });
3760
+ onEvent({
3761
+ type: "usage",
3762
+ inputTokens: 10,
3763
+ outputTokens: 5,
3764
+ model: "test",
3765
+ providerDurationMs: 50,
3766
+ });
3767
+ // Wait past the original debounce window to prove a late timer
3768
+ // does NOT fire a stray partial flush.
3769
+ await new Promise((resolve) => setTimeout(resolve, 1100));
3770
+ return [
3771
+ ...messages,
3772
+ {
3773
+ role: "assistant" as const,
3774
+ content: [{ type: "text", text: "Quick reply." }] as ContentBlock[],
3775
+ },
3776
+ ];
3777
+ };
3778
+
3779
+ const ctx = makeCtx({ agentLoopRun });
3780
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3781
+
3782
+ // Only the final flush from `handleMessageComplete` lands. The
3783
+ // debounced partial would have fired around T+250ms; the timer-clear
3784
+ // at the top of `handleMessageComplete` cancels it.
3785
+ expect(updateMessageContentMock).toHaveBeenCalledTimes(1);
3786
+ });
3787
+
3788
+ test("partial flushes never trigger the indexer or attention projector", async () => {
3789
+ mockMessageById = {
3790
+ id: "msg-reserve",
3791
+ conversationId: "test-conv",
3792
+ createdAt: 1234567,
3793
+ role: "assistant",
3794
+ content: "[]",
3795
+ metadata: null,
3796
+ };
3797
+
3798
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3799
+ await onEvent({ type: "llm_call_started" });
3800
+ onEvent({ type: "text_delta", text: "hello world" });
3801
+ // Wait past the 250ms debounce so the partial flush definitely
3802
+ // lands BEFORE message_complete fires.
3803
+ await new Promise((resolve) => setTimeout(resolve, 1100));
3804
+ // Snapshot the indexer/projector call counts AFTER the partial
3805
+ // flush has run but BEFORE message_complete. They must be zero.
3806
+ const indexerCallsBeforeComplete =
3807
+ indexMessageNowMock.mock.calls.length;
3808
+ const projectorCallsBeforeComplete =
3809
+ projectAssistantMessageMock.mock.calls.length;
3810
+ // Stash on a side channel the assertion phase can read.
3811
+ (
3812
+ ctx as unknown as { __partialSnapshot?: [number, number] }
3813
+ ).__partialSnapshot = [
3814
+ indexerCallsBeforeComplete,
3815
+ projectorCallsBeforeComplete,
3816
+ ];
3817
+ await onEvent({
3818
+ type: "message_complete",
3819
+ message: {
3820
+ role: "assistant",
3821
+ content: [{ type: "text", text: "hello world" }],
3822
+ },
3823
+ });
3824
+ onEvent({
3825
+ type: "usage",
3826
+ inputTokens: 10,
3827
+ outputTokens: 5,
3828
+ model: "test",
3829
+ providerDurationMs: 50,
3830
+ });
3831
+ return [
3832
+ ...messages,
3833
+ {
3834
+ role: "assistant" as const,
3835
+ content: [{ type: "text", text: "hello world" }] as ContentBlock[],
3836
+ },
3837
+ ];
3838
+ };
3839
+
3840
+ const ctx = makeCtx({ agentLoopRun });
3841
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3842
+
3843
+ const snapshot = (
3844
+ ctx as unknown as { __partialSnapshot?: [number, number] }
3845
+ ).__partialSnapshot;
3846
+ expect(snapshot).toBeDefined();
3847
+ // Indexer + projector were both ZERO during the mid-turn partial
3848
+ // flush — they only fire from `handleMessageComplete` after the
3849
+ // authoritative `updateContent`.
3850
+ expect(snapshot![0]).toBe(0);
3851
+ expect(snapshot![1]).toBe(0);
3852
+ // After the loop completes the indexer + projector each ran exactly
3853
+ // once (the pre-allocation finalize path).
3854
+ expect(indexMessageNowMock).toHaveBeenCalledTimes(1);
3855
+ expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
3856
+ });
3857
+
3858
+ test("partial flushes redact secrets from text blocks before writing", async () => {
3859
+ mockMessageById = {
3860
+ id: "msg-reserve",
3861
+ conversationId: "test-conv",
3862
+ createdAt: 1234567,
3863
+ role: "assistant",
3864
+ content: "[]",
3865
+ metadata: null,
3866
+ };
3867
+ // A GitHub PAT-shaped token mid-stream — the redaction discipline
3868
+ // mirrors `handleMessageComplete`'s final flush so a refresh mid-turn
3869
+ // never sees plaintext credentials in the persisted row.
3870
+ const ghToken = "ghp_" + "a".repeat(36);
3871
+ const payload = "Here's the key: " + ghToken + " enjoy.";
3872
+
3873
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3874
+ await onEvent({ type: "llm_call_started" });
3875
+ onEvent({ type: "text_delta", text: payload });
3876
+ // Wait past the 250ms debounce so the partial flush lands.
3877
+ await new Promise((resolve) => setTimeout(resolve, 1100));
3878
+ await onEvent({
3879
+ type: "message_complete",
3880
+ message: {
3881
+ role: "assistant",
3882
+ content: [{ type: "text", text: payload }],
3883
+ },
3884
+ });
3885
+ onEvent({
3886
+ type: "usage",
3887
+ inputTokens: 10,
3888
+ outputTokens: 5,
3889
+ model: "test",
3890
+ providerDurationMs: 50,
3891
+ });
3892
+ return [
3893
+ ...messages,
3894
+ {
3895
+ role: "assistant" as const,
3896
+ content: [{ type: "text", text: payload }] as ContentBlock[],
3897
+ },
3898
+ ];
3899
+ };
3900
+
3901
+ const ctx = makeCtx({ agentLoopRun });
3902
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3903
+
3904
+ expect(updateMessageContentMock).toHaveBeenCalledTimes(2);
3905
+ const partialPayload = (
3906
+ updateMessageContentMock.mock.calls[0] as unknown as [string, string]
3907
+ )[1];
3908
+ // The raw PAT must never appear in the persisted snapshot. The
3909
+ // redaction substitute is implementation-defined; the contract here
3910
+ // is "the literal token string is gone".
3911
+ expect(partialPayload).not.toContain(ghToken);
3912
+ });
3913
+
3914
+ test("provider-error cleanup deletes a row that has accumulated partial content", async () => {
3915
+ // Regression check: the pre-allocation orphan-cleanup branch
3916
+ // already deletes the reserved row when the LLM call exits via
3917
+ // `provider_error`. Partial-persist writes content to that row
3918
+ // mid-turn; the cleanup must still fire and the row (along with
3919
+ // its partial content) must still be deleted before the synthetic
3920
+ // error message lands.
3921
+ reserveMessageMock.mockImplementationOnce(async () => ({
3922
+ id: "msg-orphan-with-partial",
3923
+ }));
3924
+
3925
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3926
+ await onEvent({ type: "llm_call_started" });
3927
+ // A debounced delta lands a partial flush BEFORE the provider
3928
+ // error fires.
3929
+ onEvent({ type: "text_delta", text: "hello world" });
3930
+ await new Promise((resolve) => setTimeout(resolve, 1100));
3931
+ onEvent({
3932
+ type: "provider_error",
3933
+ error: new Error("upstream 500"),
3934
+ rawRequest: { model: "gpt-4.1", messages: [] },
3935
+ actualProvider: "openai",
3936
+ });
3937
+ onEvent({
3938
+ type: "error",
3939
+ error: new Error("upstream 500"),
3940
+ });
3941
+ return messages;
3942
+ };
3943
+
3944
+ const ctx = makeCtx({ agentLoopRun });
3945
+ await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
3946
+
3947
+ // Partial flush fired exactly once (before the provider error).
3948
+ // The orphan row was then deleted; the synthetic error message is
3949
+ // inserted separately via `addMessage` (`mock-msg-id`) and never
3950
+ // touched by `updateContent`.
3951
+ const partialFlushes = (
3952
+ updateMessageContentMock.mock.calls as unknown as Array<
3953
+ [string, string]
3954
+ >
3955
+ ).filter(([id]) => id === "msg-orphan-with-partial");
3956
+ expect(partialFlushes).toHaveLength(1);
3957
+ expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
3958
+ const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
3959
+ string,
3960
+ ];
3961
+ expect(deleteCall[0]).toBe("msg-orphan-with-partial");
3962
+ });
3963
+ });
3964
+
3047
3965
  describe("pkbSystemReminderBlock metadata persistence", () => {
3048
3966
  test("persists pkbSystemReminderBlock in full mode with PKB active", async () => {
3049
3967
  const reminder = "<system_reminder>\npkb content\n</system_reminder>";
@@ -3539,21 +4457,22 @@ describe("session-agent-loop", () => {
3539
4457
  const agentLoopRun: AgentLoopRun = async (
3540
4458
  messages,
3541
4459
  _onEvent,
3542
- _signal,
3543
- _reqId,
3544
- onCheckpoint,
4460
+ options,
3545
4461
  ) => {
3546
4462
  runCount++;
3547
4463
  if (runCount === 1) {
4464
+ // The loop reaches its mid-loop budget checkpoint with the raw
4465
+ // persistent basis as its in-loop history; the wrapped onCheckpoint
4466
+ // trips the gate and runs inline compaction over that basis.
3548
4467
  mockEstimateTokens = 90_000;
3549
- const decision = await onCheckpoint?.({
4468
+ const decision = await options?.onCheckpoint?.({
3550
4469
  turnIndex: 0,
3551
4470
  toolCount: 1,
3552
4471
  hasToolUse: true,
3553
- history: messages,
4472
+ history: rawMidLoopBasis,
3554
4473
  });
3555
4474
  mockEstimateTokens = 1000;
3556
- if (decision === "yield") {
4475
+ if (decision !== "continue") {
3557
4476
  return rawMidLoopBasis;
3558
4477
  }
3559
4478
  }
@@ -3908,6 +4827,11 @@ describe("session-agent-loop", () => {
3908
4827
  let callCount = 0;
3909
4828
  const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
3910
4829
  callCount++;
4830
+ // Prime the assistant row anchor — production code emits this from
4831
+ // `AgentLoop.run` just before `provider.sendMessage`. Retry branches
4832
+ // need this on every invocation: each agent-loop iteration reserves
4833
+ // its own row.
4834
+ await onEvent({ type: "llm_call_started" });
3911
4835
  if (callCount === 1) {
3912
4836
  // Trigger convergence path: error + appended assistant message so
3913
4837
  // updatedHistory.length > preRunHistoryLength at the strip site.
@@ -3968,5 +4892,86 @@ describe("session-agent-loop", () => {
3968
4892
  );
3969
4893
  expect(stripCalls.length).toBeGreaterThanOrEqual(1);
3970
4894
  });
4895
+
4896
+ test("strip-site marker write is non-fatal when the helper throws", async () => {
4897
+ setConversationHistoryStrippedAtMock.mockImplementation(() => {
4898
+ throw new Error("db write failed");
4899
+ });
4900
+
4901
+ mockReducerStepFn = (msgs: Message[]) => ({
4902
+ messages: msgs,
4903
+ tier: "forced_compaction",
4904
+ state: {
4905
+ appliedTiers: ["forced_compaction"],
4906
+ injectionMode: "full",
4907
+ exhausted: false,
4908
+ },
4909
+ estimatedTokens: 5000,
4910
+ });
4911
+
4912
+ let callCount = 0;
4913
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
4914
+ callCount++;
4915
+ // Prime the assistant row anchor — production code emits this from
4916
+ // `AgentLoop.run` just before `provider.sendMessage`. Retry branches
4917
+ // need this on every invocation: each agent-loop iteration reserves
4918
+ // its own row.
4919
+ await onEvent({ type: "llm_call_started" });
4920
+ if (callCount === 1) {
4921
+ onEvent({
4922
+ type: "error",
4923
+ error: new Error("context_length_exceeded"),
4924
+ });
4925
+ onEvent({
4926
+ type: "usage",
4927
+ inputTokens: 100,
4928
+ outputTokens: 0,
4929
+ model: "test-model",
4930
+ providerDurationMs: 50,
4931
+ });
4932
+ return [
4933
+ ...messages,
4934
+ {
4935
+ role: "assistant" as const,
4936
+ content: [{ type: "text", text: "partial" }] as ContentBlock[],
4937
+ },
4938
+ ];
4939
+ }
4940
+ onEvent({
4941
+ type: "message_complete",
4942
+ message: {
4943
+ role: "assistant",
4944
+ content: [{ type: "text", text: "recovered" }],
4945
+ },
4946
+ });
4947
+ onEvent({
4948
+ type: "usage",
4949
+ inputTokens: 50,
4950
+ outputTokens: 25,
4951
+ model: "test-model",
4952
+ providerDurationMs: 100,
4953
+ });
4954
+ return [
4955
+ ...messages,
4956
+ {
4957
+ role: "assistant" as const,
4958
+ content: [{ type: "text", text: "recovered" }] as ContentBlock[],
4959
+ },
4960
+ ];
4961
+ };
4962
+
4963
+ const ctx = makeCtx({
4964
+ agentLoopRun,
4965
+ contextWindowManager: {
4966
+ shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
4967
+ maybeCompact: async () => ({ compacted: false }),
4968
+ } as unknown as AgentLoopConversationContext["contextWindowManager"],
4969
+ });
4970
+
4971
+ // Must not throw — the strip-site marker write is wrapped in try/catch.
4972
+ await expect(
4973
+ runAgentLoopImpl(ctx, "hello", "msg-1", () => {}),
4974
+ ).resolves.toBeUndefined();
4975
+ });
3971
4976
  });
3972
4977
  });