mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,749 @@
1
+ // Compaction window selection, recall-tail preservation, and the semantic +
2
+ // recall fast-track compaction entry points. Extracted verbatim from
3
+ // compact.mjs (behavior-preserving).
4
+ import {
5
+ sanitizeToolPairs,
6
+ dedupToolResultBodies,
7
+ reconcileDedupStubs,
8
+ estimateMessagesTokens,
9
+ DEFAULT_COMPACTION_KEEP_TOKENS,
10
+ } from '../context-utils.mjs';
11
+ import {
12
+ SUMMARY_OUTPUT_TOKENS,
13
+ COMPACT_SUMMARY_MIN_ROOM_TOKENS,
14
+ COMPACT_TYPE_SEMANTIC,
15
+ COMPACT_TYPE_RECALL_FASTTRACK,
16
+ compactDebugLog,
17
+ } from './constants.mjs';
18
+ import {
19
+ extractText,
20
+ safeEstimateMessagesTokens,
21
+ textByteLength,
22
+ messageContentHasMarker,
23
+ redactToolCallSecretsInMessages,
24
+ } from './text-utils.mjs';
25
+ import {
26
+ isSummaryMessage,
27
+ splitProtectedContext,
28
+ } from './messages.mjs';
29
+ import {
30
+ effectiveBudget,
31
+ extractPreservedFacts,
32
+ preserveRecentBudget,
33
+ } from './budget.mjs';
34
+ import {
35
+ COMPACTION_SYSTEM_PROMPT,
36
+ fitCompactionPrompt,
37
+ extractResponseText,
38
+ enforceSemanticSummarySchema,
39
+ fitSemanticSummaryMessage,
40
+ fitRecallFastTrackSummaryMessage,
41
+ stripNestedSummaryHeaderLines,
42
+ RECALL_TAIL_TRUNCATION_MARKER,
43
+ RECALL_TAIL_SHORT_TRUNCATION_MARKER,
44
+ } from './summary.mjs';
45
+
46
+ const DEFAULT_TAIL_TURNS = 2;
47
+ const COMPACTION_PROMPT_HEADROOM = 0.85;
48
+
49
+ function userIndexes(messages) {
50
+ const out = [];
51
+ for (let i = 0; i < messages.length; i += 1) {
52
+ if (messages[i]?.role === 'user') out.push(i);
53
+ }
54
+ return out;
55
+ }
56
+
57
+ function splitTailIntoTurns(messages) {
58
+ const turns = [];
59
+ let current = null;
60
+ for (const m of messages) {
61
+ if (isSummaryMessage(m)) continue;
62
+ if (m?.role === 'user') {
63
+ if (current) turns.push(current);
64
+ current = [m];
65
+ } else {
66
+ if (!current) current = [];
67
+ current.push(m);
68
+ }
69
+ }
70
+ if (current && current.length) turns.push(current);
71
+ return turns;
72
+ }
73
+
74
+ function indexLiveTurns(live) {
75
+ const turns = splitTailIntoTurns(live);
76
+ const indexed = [];
77
+ let scan = 0;
78
+ for (const messages of turns) {
79
+ while (scan < live.length && live[scan] !== messages[0]) scan += 1;
80
+ const start = scan;
81
+ const end = start + messages.length;
82
+ indexed.push({ start, end, messages });
83
+ scan = end;
84
+ }
85
+ return indexed;
86
+ }
87
+
88
+ function splitTurnStartIndexForBudget(turn, budget) {
89
+ const { start, end, messages } = turn;
90
+ for (let i = 0; i < messages.length; i += 1) {
91
+ const suffixStart = start + i;
92
+ const suffix = messages.slice(i);
93
+ if (suffix.length > 0 && estimateMessagesTokens(suffix) <= budget) {
94
+ return suffixStart;
95
+ }
96
+ }
97
+ return end;
98
+ }
99
+
100
+ function splitLiveCompactionContext(messages) {
101
+ const sanitized = reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs(messages)));
102
+ const { protectedPrefix, conversation: nonSystem } = splitProtectedContext(sanitized);
103
+ let previousSummary = null;
104
+ for (let i = nonSystem.length - 1; i >= 0; i -= 1) {
105
+ if (isSummaryMessage(nonSystem[i])) {
106
+ previousSummary = nonSystem[i].content;
107
+ break;
108
+ }
109
+ }
110
+ const live = nonSystem.filter((m) => !isSummaryMessage(m));
111
+ return { system: protectedPrefix, live, previousSummary, sanitized };
112
+ }
113
+
114
+ // A tail may begin ONLY at an index that is not a tool result: a tool result
115
+ // must stay paired with the assistant tool_call that precedes it, so it can
116
+ // never be the first message of the preserved tail. Every other role (user /
117
+ // assistant / developer / ...) is a valid tail boundary. This replaces the old
118
+ // "the tail must begin at a real user turn" rule, which threw whenever the
119
+ // recent window carried no user message (single-turn agent sessions whose tail
120
+ // is assistant/tool only).
121
+ function findValidCutIndices(live) {
122
+ const out = [];
123
+ for (let i = 0; i < live.length; i += 1) {
124
+ if (live[i]?.role === 'tool') continue;
125
+ out.push(i);
126
+ }
127
+ return out;
128
+ }
129
+
130
+ // User-anchored path (unchanged behaviour): keep up to tailTurns recent turns
131
+ // bounded by recentBudget, splitting the newest turn's suffix when it alone is
132
+ // too large. Preserved verbatim so Lead / normal sessions with real user turns
133
+ // compact exactly as before.
134
+ function selectTailStartByTurns(live, recentBudget, tailTurns, previousSummary, opts) {
135
+ const indexedTurns = indexLiveTurns(live);
136
+ if (indexedTurns.length === 0) return live.length;
137
+
138
+ let tailStartIdx = live.length;
139
+ let keptTurns = 0;
140
+
141
+ for (let t = indexedTurns.length - 1; t >= 0; t -= 1) {
142
+ if (keptTurns >= tailTurns) break;
143
+ const turn = indexedTurns[t];
144
+ const tailFromTurn = live.slice(turn.start);
145
+ if (keptTurns === 0) {
146
+ if (estimateMessagesTokens(tailFromTurn) <= recentBudget) {
147
+ tailStartIdx = turn.start;
148
+ keptTurns += 1;
149
+ continue;
150
+ }
151
+ const splitIdx = splitTurnStartIndexForBudget(turn, recentBudget);
152
+ if (splitIdx < turn.end) {
153
+ tailStartIdx = splitIdx;
154
+ keptTurns += 1;
155
+ break;
156
+ }
157
+ // Newest turn has no fitting suffix: keep entire live transcript in head for summarization.
158
+ tailStartIdx = live.length;
159
+ keptTurns = 0;
160
+ break;
161
+ }
162
+ const candidateStart = turn.start;
163
+ const candidateTail = live.slice(candidateStart);
164
+ if (estimateMessagesTokens(candidateTail) <= recentBudget) {
165
+ tailStartIdx = candidateStart;
166
+ keptTurns += 1;
167
+ continue;
168
+ }
169
+ break;
170
+ }
171
+
172
+ if (opts.force === true && !previousSummary && tailStartIdx <= 0) {
173
+ if (indexedTurns.length >= 2) {
174
+ tailStartIdx = indexedTurns[1].start;
175
+ } else if (indexedTurns.length === 1) {
176
+ const onlyTurn = indexedTurns[0];
177
+ const splitIdx = splitTurnStartIndexForBudget(onlyTurn, recentBudget);
178
+ if (splitIdx > onlyTurn.start && splitIdx < onlyTurn.end) {
179
+ tailStartIdx = splitIdx;
180
+ } else if (onlyTurn.end > onlyTurn.start + 1) {
181
+ tailStartIdx = onlyTurn.start + 1;
182
+ }
183
+ }
184
+ }
185
+ return tailStartIdx;
186
+ }
187
+
188
+ // No-user path: pick the tail boundary from valid cut points. Walk newest ->
189
+ // oldest, growing the tail across valid cut points while its suffix still fits
190
+ // recentBudget, and stop before it overflows. Never anchors on a user turn, so
191
+ // an assistant/tool-only single-turn transcript still yields a head to
192
+ // summarize and a paired tail to keep.
193
+ function selectTailStartByCutPoint(live, recentBudget, previousSummary) {
194
+ const validCuts = findValidCutIndices(live);
195
+ if (validCuts.length === 0) return live.length; // degenerate: only tool results
196
+
197
+ let chosen = null;
198
+ for (let k = validCuts.length - 1; k >= 0; k -= 1) {
199
+ const idx = validCuts[k];
200
+ if (estimateMessagesTokens(live.slice(idx)) <= recentBudget) {
201
+ chosen = idx; // fits — try to grow the tail toward an older cut
202
+ continue;
203
+ }
204
+ break; // this cut overflows recentBudget; keep the previous (newer) choice
205
+ }
206
+
207
+ if (chosen === null) {
208
+ // Even the newest valid cut's suffix exceeds recentBudget (a single huge
209
+ // message run). Keep the minimal tail from the newest valid cut so a head
210
+ // remains to summarize; if that cut is at index 0 there is nothing to
211
+ // split off, so keep everything in the head instead. The oversized tail
212
+ // is tolerated downstream (mandatory-cost budget raise) rather than
213
+ // throwing.
214
+ const newestCut = validCuts[validCuts.length - 1];
215
+ return newestCut > 0 ? newestCut : live.length;
216
+ }
217
+
218
+ if (chosen <= 0) {
219
+ // Whole transcript would become the tail => nothing to compact. With no
220
+ // prior summary to build on, pull the tail start forward to the next
221
+ // valid cut so the leading message(s) become the compactable head.
222
+ if (!previousSummary && validCuts.length >= 2) return validCuts[1];
223
+ // Only ONE valid cut (or a leading tool run before it) and no prior
224
+ // summary: there is no older cut to pull forward to. Returning 0 would
225
+ // make the whole transcript the tail with an empty head, and
226
+ // semanticCompactMessages throws on head.length===0 && !previousSummary.
227
+ // Keep everything in the HEAD instead (empty tail) so a head remains to
228
+ // summarize; an empty tail is valid downstream (mandatory = system+tail).
229
+ if (!previousSummary && validCuts.length < 2) return live.length;
230
+ return chosen;
231
+ }
232
+ return chosen;
233
+ }
234
+
235
+ function selectCompactionWindow(messages, budget, opts = {}) {
236
+ const { system, live, previousSummary } = splitLiveCompactionContext(messages);
237
+ const tailTurns = Math.max(1, Number(opts.tailTurns) || DEFAULT_TAIL_TURNS);
238
+ const recentBudget = preserveRecentBudget(budget, opts);
239
+
240
+ const tailStartIdx = userIndexes(live).length
241
+ ? selectTailStartByTurns(live, recentBudget, tailTurns, previousSummary, opts)
242
+ : selectTailStartByCutPoint(live, recentBudget, previousSummary);
243
+
244
+ const head = live.slice(0, tailStartIdx);
245
+ let tail = live.slice(tailStartIdx);
246
+ // sanitizeToolPairs/dedup/reconcile repairs any orphan tool_result the cut
247
+ // may have left; because valid cut points never start on a tool result, an
248
+ // assistant tool_call and its trailing tool_results always land on the same
249
+ // side of the boundary, so pairing stays provider-valid.
250
+ tail = reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs(tail)));
251
+
252
+ // Only a genuinely empty live window is unrecoverable. Absence of a user
253
+ // turn in the tail is no longer an error.
254
+ if (!head.length && !tail.length) {
255
+ throw new Error('semanticCompactMessages: nothing to compact (empty live window)');
256
+ }
257
+
258
+ const preservedFacts = extractPreservedFacts(head);
259
+ const originalHead = head;
260
+ return {
261
+ system,
262
+ head,
263
+ tail,
264
+ previousSummary,
265
+ originalHead,
266
+ preservedFacts,
267
+ };
268
+ }
269
+
270
+ function combinedSignal(parent, timeoutMs) {
271
+ const ms = Number(timeoutMs);
272
+ if (!Number.isFinite(ms) || ms <= 0) return parent || undefined;
273
+ const timeout = AbortSignal.timeout(Math.floor(ms));
274
+ if (parent && typeof AbortSignal.any === 'function') return AbortSignal.any([parent, timeout]);
275
+ return timeout;
276
+ }
277
+
278
+ export async function semanticCompactMessages(provider, messages, model, budgetTokens, opts = {}) {
279
+ if (!provider || typeof provider.send !== 'function') {
280
+ throw new Error('semanticCompactMessages: provider.send is required');
281
+ }
282
+ const startedAt = Date.now();
283
+ let budget = effectiveBudget(budgetTokens, opts);
284
+ const baseSanitized = reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs(messages)));
285
+ const baseTokens = safeEstimateMessagesTokens(baseSanitized);
286
+ // No-op fast path: if the original sanitized transcript already fits and we
287
+ // are not forced, return it UNCHANGED (no preserved-tail redaction applied)
288
+ // to keep prior no-compaction semantics.
289
+ if (baseTokens != null && baseTokens <= budget && opts.force !== true) {
290
+ return {
291
+ messages: baseSanitized,
292
+ usage: null,
293
+ semantic: false,
294
+ compactType: COMPACT_TYPE_SEMANTIC,
295
+ diagnostics: {
296
+ noOp: true,
297
+ reason: 'fits_budget',
298
+ inputMessages: Array.isArray(messages) ? messages.length : 0,
299
+ baseMessages: baseSanitized.length,
300
+ baseTokens,
301
+ budgetTokens: budget,
302
+ durationMs: Date.now() - startedAt,
303
+ },
304
+ };
305
+ }
306
+ // Compaction will proceed: redact sensitive tool-call argument VALUES before
307
+ // window selection so the preserved tail/system that survive verbatim are
308
+ // measured AND emitted in their redacted form. Head prompt normalizers
309
+ // (toolCallSummary/normalizeToolArgValue) still apply on top for the
310
+ // summarized head. Redaction is shape-preserving, so tool-pair structure
311
+ // stays provider-valid.
312
+ const sanitized = redactToolCallSecretsInMessages(baseSanitized);
313
+
314
+ const selected = selectCompactionWindow(sanitized, budget, opts);
315
+ if (selected.head.length === 0 && !selected.previousSummary) {
316
+ throw new Error('semanticCompactMessages: no compactable prior history before preserved tail');
317
+ }
318
+
319
+ const mandatory = reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs([...selected.system, ...selected.tail])));
320
+ const mandatoryCost = estimateMessagesTokens(mandatory);
321
+ const originalBudget = budget;
322
+ // The preserved tail is kept verbatim and the head is replaced by a much
323
+ // smaller summary, so the compacted result is always smaller than the
324
+ // input regardless of how the configured target budget compares to the
325
+ // mandatory cost. When the budget cannot even hold what we must keep, raise
326
+ // it to fit (mandatory + summary room) rather than refusing — a refusal
327
+ // here was the source of auto-clear / overflow compact failures.
328
+ if (mandatoryCost + COMPACT_SUMMARY_MIN_ROOM_TOKENS > budget) {
329
+ budget = mandatoryCost + COMPACT_SUMMARY_MIN_ROOM_TOKENS;
330
+ }
331
+ const budgetRaisedBy = Math.max(0, budget - originalBudget);
332
+
333
+ const callBudget = Math.max(1, Math.floor((opts.compactionInputBudgetTokens || budget) * COMPACTION_PROMPT_HEADROOM));
334
+ const prompt = fitCompactionPrompt(selected, callBudget);
335
+ if (!prompt) {
336
+ throw new Error(`semanticCompactMessages: compaction prompt cannot fit call budget=${callBudget}`);
337
+ }
338
+ const compactModel = model;
339
+ const sendOpts = {
340
+ ...(opts.sendOpts || {}),
341
+ thinkingBudgetTokens: undefined,
342
+ xaiReasoningEffort: undefined,
343
+ reasoningEffort: undefined,
344
+ effort: 'low',
345
+ fast: opts.fast ?? opts.sendOpts?.fast ?? true,
346
+ maxOutputTokens: opts.maxOutputTokens || SUMMARY_OUTPUT_TOKENS,
347
+ providerState: undefined,
348
+ onToolCall: undefined,
349
+ onToolResult: undefined,
350
+ onTextDelta: undefined,
351
+ onReasoningDelta: undefined,
352
+ onUsageDelta: undefined,
353
+ onStreamDelta: undefined,
354
+ onStageChange: undefined,
355
+ drainSteering: undefined,
356
+ onSteerMessage: undefined,
357
+ signal: combinedSignal(opts.signal || opts.sendOpts?.signal || null, opts.timeoutMs || 30_000),
358
+ };
359
+ if (opts.sessionId) sendOpts.sessionId = `${opts.sessionId}:compact`;
360
+ if (opts.promptCacheKey || opts.sendOpts?.promptCacheKey) {
361
+ sendOpts.promptCacheKey = `${opts.promptCacheKey || opts.sendOpts.promptCacheKey}:compact`;
362
+ }
363
+ if (opts.providerCacheKey || opts.sendOpts?.providerCacheKey) {
364
+ sendOpts.providerCacheKey = `${opts.providerCacheKey || opts.sendOpts.providerCacheKey}:compact`;
365
+ }
366
+
367
+ const response = await provider.send([
368
+ { role: 'system', content: COMPACTION_SYSTEM_PROMPT },
369
+ { role: 'user', content: prompt },
370
+ ], compactModel, undefined, sendOpts);
371
+ const rawSummary = extractResponseText(response);
372
+ if (!rawSummary) throw new Error('semanticCompactMessages: compaction agent returned empty summary');
373
+ // Lightweight schema enforcement: a non-empty but malformed provider
374
+ // response (missing the required template sections) is deterministically
375
+ // repaired into the structured anchored shape rather than injected blindly.
376
+ const enforced = enforceSemanticSummarySchema(rawSummary, { head: selected.head, tail: selected.tail });
377
+ const summary = enforced.summary;
378
+
379
+ const oldHistory = selected.originalHead;
380
+ const semanticMeta = {
381
+ provider: opts.providerName || provider.name || null,
382
+ model: compactModel,
383
+ };
384
+ const summaryMessage = fitSemanticSummaryMessage(oldHistory, summary, budget - mandatoryCost, semanticMeta, selected.preservedFacts);
385
+ if (!summaryMessage) {
386
+ throw new Error(`semanticCompactMessages: summary cannot fit remaining budget=${budget - mandatoryCost}`);
387
+ }
388
+
389
+ // selected.system / selected.tail already carry redacted tool-call args
390
+ // (sanitized was redacted before window selection), so the preserved tail
391
+ // is both measured and emitted in redacted form.
392
+ let result = sanitizeToolPairs([...selected.system, summaryMessage, ...selected.tail]);
393
+ result = reconcileDedupStubs(dedupToolResultBodies(result));
394
+ const finalTokens = estimateMessagesTokens(result);
395
+ if (finalTokens > budget) {
396
+ throw new Error(`semanticCompactMessages: compacted result exceeds budget=${budget} (result=${finalTokens})`);
397
+ }
398
+ const diagnostics = {
399
+ noOp: false,
400
+ inputMessages: Array.isArray(messages) ? messages.length : 0,
401
+ baseMessages: baseSanitized.length,
402
+ baseTokens,
403
+ systemMessages: selected.system.length,
404
+ headMessages: selected.head.length,
405
+ originalHeadMessages: selected.originalHead.length,
406
+ tailMessages: selected.tail.length,
407
+ mandatoryMessages: mandatory.length,
408
+ finalMessages: result.length,
409
+ systemTokens: safeEstimateMessagesTokens(selected.system),
410
+ headTokens: safeEstimateMessagesTokens(selected.head),
411
+ tailTokens: safeEstimateMessagesTokens(selected.tail),
412
+ mandatoryCost,
413
+ finalTokens,
414
+ originalBudgetTokens: originalBudget,
415
+ budgetTokens: budget,
416
+ budgetRaised: budgetRaisedBy > 0,
417
+ budgetRaisedBy,
418
+ remainingTokens: budget - mandatoryCost,
419
+ callBudgetTokens: callBudget,
420
+ promptChars: String(prompt || '').length,
421
+ promptBytes: textByteLength(prompt),
422
+ promptTokens: safeEstimateMessagesTokens([
423
+ { role: 'system', content: COMPACTION_SYSTEM_PROMPT },
424
+ { role: 'user', content: prompt },
425
+ ]),
426
+ summaryChars: String(summary || '').length,
427
+ rawSummaryChars: String(rawSummary || '').length,
428
+ summaryRepaired: enforced.repaired === true,
429
+ previousSummary: !!selected.previousSummary,
430
+ durationMs: Date.now() - startedAt,
431
+ };
432
+ compactDebugLog('semantic result', diagnostics);
433
+ return {
434
+ messages: result,
435
+ usage: response?.usage || null,
436
+ providerState: response?.providerState,
437
+ semantic: true,
438
+ compactType: COMPACT_TYPE_SEMANTIC,
439
+ summary,
440
+ summaryRepaired: enforced.repaired === true,
441
+ diagnostics,
442
+ };
443
+ }
444
+
445
+ export function recallFastTrackCompactMessages(messages, budgetTokens, opts = {}) {
446
+ return _recallFastTrackCompactMessages(messages, budgetTokens, opts);
447
+ }
448
+
449
+ // Recall fast-track (type 2) tail policy: preserve the most recent turns of the
450
+ // live conversation VERBATIM and STRUCTURED, keeping role semantics for
451
+ // user / assistant / tool / system / developer instead of collapsing the tail
452
+ // to user-only. The chunk summary anchors older history; the preserved tail
453
+ // keeps recent assistant reasoning, tool_calls, and tool_results so fresh
454
+ // state is not silently dropped.
455
+ //
456
+ // Turns are anchored on user-role boundaries: each turn = a user message plus
457
+ // the assistant/tool/system/developer messages that follow it (a leading run of
458
+ // non-user messages before the first user boundary is treated as its own
459
+ // partial turn so nothing is lost). We keep the newest RECALL_TAIL_USER_MAX
460
+ // turns; if the kept set exceeds RECALL_TAIL_TOKEN_CAP we drop whole oldest
461
+ // turns first, then middle-truncate the oldest surviving messages' string
462
+ // content so the set fits while leaving the newest message whole.
463
+ //
464
+ // Partial tool_call/tool_result pairs that truncation might leave behind are
465
+ // repaired by sanitizeToolPairs/reconcileDedupStubs in the caller, so pairing
466
+ // stays valid even after trimming.
467
+ const RECALL_TAIL_USER_MAX = 2;
468
+ const RECALL_TAIL_TOKEN_CAP = DEFAULT_COMPACTION_KEEP_TOKENS; // 8k
469
+ // Rough chars-per-token used only to size a truncation target; the real fit is
470
+ // re-checked with estimateMessagesTokens below.
471
+ const RECALL_TAIL_CHARS_PER_TOKEN = 4;
472
+
473
+ function truncateMessageForRecallTail(text, maxChars) {
474
+ const marker = RECALL_TAIL_TRUNCATION_MARKER;
475
+ const value = String(text ?? '').replace(/\r\n/g, '\n');
476
+ if (value.length <= maxChars) return value;
477
+ if (maxChars <= 0) return RECALL_TAIL_SHORT_TRUNCATION_MARKER;
478
+ if (maxChars < marker.length) return RECALL_TAIL_SHORT_TRUNCATION_MARKER;
479
+ const room = maxChars - marker.length;
480
+ const head = Math.ceil(room * 0.35);
481
+ const tailPart = Math.floor(room * 0.65);
482
+ return `${value.slice(0, head)}${marker}${value.slice(value.length - tailPart)}`;
483
+ }
484
+
485
+ function fitRecallUserMessageToCap(userMsg, cap, following = []) {
486
+ const followCost = estimateMessagesTokens(following);
487
+ const room = Math.max(1, cap - followCost);
488
+ const base = { ...userMsg };
489
+ const raw = typeof base.content === 'string' ? base.content : extractText(base);
490
+ if (estimateMessagesTokens([{ ...base, content: raw }]) <= room) return { ...base, content: raw };
491
+
492
+ let lo = 0;
493
+ let hi = raw.length;
494
+ let best = truncateMessageForRecallTail(raw, 0);
495
+ while (lo <= hi) {
496
+ const mid = Math.floor((lo + hi) / 2);
497
+ const candidateText = truncateMessageForRecallTail(raw, mid);
498
+ const candidate = { ...base, content: candidateText };
499
+ if (estimateMessagesTokens([candidate]) <= room) {
500
+ best = candidateText;
501
+ lo = mid + 1;
502
+ } else {
503
+ hi = mid - 1;
504
+ }
505
+ }
506
+ return { ...base, content: best };
507
+ }
508
+
509
+ function fitSingleRecallTurnToCap(turn, cap) {
510
+ const userIdx = turn.findIndex((m) => m?.role === 'user');
511
+ if (userIdx < 0) {
512
+ return truncateTailToCap(turn, cap);
513
+ }
514
+ const userMsg = turn[userIdx];
515
+ const following = turn.slice(userIdx + 1);
516
+ const fittedUser = fitRecallUserMessageToCap(userMsg, cap, following);
517
+ let out = [fittedUser];
518
+ for (const m of following) {
519
+ const candidate = [...out, m];
520
+ if (estimateMessagesTokens(candidate) <= cap) out.push(m);
521
+ }
522
+ return reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs(out)));
523
+ }
524
+
525
+ function truncateTailToCap(messages, cap) {
526
+ const turn = Array.isArray(messages) ? messages : [];
527
+ if (turn.length === 0) return [];
528
+ // No user anchor in this turn: keep the NEWEST messages that fit `cap`,
529
+ // walking backward. (Previously this delegated back to
530
+ // fitSingleRecallTurnToCap, which re-entered here on a no-user turn —
531
+ // infinite mutual recursion. Unreachable while a no-user tail threw upstream;
532
+ // now that a no-user tail is allowed, this path must terminate on its own.)
533
+ let out = [];
534
+ let startIdx = turn.length; // index in `turn` where `out` begins
535
+ for (let i = turn.length - 1; i >= 0; i -= 1) {
536
+ const candidate = [turn[i], ...out];
537
+ if (estimateMessagesTokens(candidate) <= cap) {
538
+ out = candidate;
539
+ startIdx = i;
540
+ continue;
541
+ }
542
+ if (out.length === 0) {
543
+ // Even the newest single message exceeds cap: middle-truncate its
544
+ // string content so at least one message survives.
545
+ const m = turn[i];
546
+ const text = typeof m?.content === 'string' ? m.content : extractText(m);
547
+ const truncated = truncateMessageForRecallTail(text, Math.max(1, cap * RECALL_TAIL_CHARS_PER_TOKEN));
548
+ out = [{ ...m, content: truncated }];
549
+ startIdx = i;
550
+ }
551
+ break;
552
+ }
553
+ // A leading tool_result with no preceding assistant tool_call is an orphan
554
+ // that sanitizeToolPairs drops — which could empty the whole tail. Extend the
555
+ // window backward to swallow the preceding non-tool boundary (the assistant
556
+ // that owns the tool_call), so the pair survives sanitize. Bounded by
557
+ // startIdx so it always terminates.
558
+ while (startIdx > 0 && out[0]?.role === 'tool') {
559
+ startIdx -= 1;
560
+ out = [turn[startIdx], ...out];
561
+ }
562
+ let sanitized = reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs(out)));
563
+ // Final guard: if sanitize still emptied the tail but the turn has a non-tool
564
+ // message, rebuild from the newest non-tool message forward so the tail is
565
+ // never empty when preservable content exists.
566
+ if (sanitized.length === 0) {
567
+ let nt = -1;
568
+ for (let i = turn.length - 1; i >= 0; i -= 1) {
569
+ if (turn[i]?.role !== 'tool') { nt = i; break; }
570
+ }
571
+ if (nt >= 0) {
572
+ sanitized = reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs(turn.slice(nt))));
573
+ }
574
+ }
575
+ return sanitized;
576
+ }
577
+
578
+ function splitRecallFitInputs(recallText, previousSummary) {
579
+ return {
580
+ recall: String(recallText || '').trim(),
581
+ prior: previousSummary ? stripNestedSummaryHeaderLines(previousSummary) : '',
582
+ };
583
+ }
584
+
585
+ function recallTailStartIndex(live, tail) {
586
+ if (!tail.length) return live.length;
587
+ const first = tail[0];
588
+ const idx = live.indexOf(first);
589
+ if (idx >= 0) return idx;
590
+ return Math.max(0, live.length - tail.length);
591
+ }
592
+
593
+ function selectRecallPreservedTail(live, opts = {}) {
594
+ const msgs = (Array.isArray(live) ? live : []).filter((m) => m && !isSummaryMessage(m));
595
+ if (msgs.length === 0) return { tail: [], head: [], tailStartIdx: 0 };
596
+ const maxTurns = Math.max(1, Number(opts.maxUsers) || RECALL_TAIL_USER_MAX);
597
+ const cap = Math.max(1, Number(opts.tokenCap) || RECALL_TAIL_TOKEN_CAP);
598
+ const turns = splitTailIntoTurns(msgs);
599
+ if (turns.length === 0) return { tail: [], head: msgs, tailStartIdx: 0 };
600
+
601
+ let kept = turns.slice(-maxTurns);
602
+ while (kept.length > 1 && estimateMessagesTokens(kept.flat()) > cap) {
603
+ kept = kept.slice(1);
604
+ }
605
+
606
+ let tail;
607
+ if (estimateMessagesTokens(kept.flat()) <= cap) {
608
+ tail = reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs(kept.flat())));
609
+ } else {
610
+ tail = fitSingleRecallTurnToCap(kept[kept.length - 1], cap);
611
+ }
612
+
613
+ // A no-user tail is valid: a single-turn agent session may keep only
614
+ // assistant/tool structure recently. Mirror the semantic cut-point model —
615
+ // preserve the recent structured turn(s) verbatim without demanding a user
616
+ // anchor rather than throwing. tool-pairing is already reconciled above.
617
+ const tailStartIdx = recallTailStartIndex(msgs, tail);
618
+ const head = msgs.slice(0, tailStartIdx);
619
+ return { tail, head, tailStartIdx };
620
+ }
621
+
622
+ function _recallFastTrackCompactMessages(messages, budgetTokens, opts = {}) {
623
+ const startedAt = Date.now();
624
+ let budget = effectiveBudget(budgetTokens, opts);
625
+ const baseSanitized = reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs(messages)));
626
+ const baseTokens = safeEstimateMessagesTokens(baseSanitized);
627
+ if (baseTokens != null && baseTokens <= budget && opts.force !== true) {
628
+ return {
629
+ messages: baseSanitized,
630
+ recallFastTrack: false,
631
+ compactType: COMPACT_TYPE_RECALL_FASTTRACK,
632
+ query: opts.query || '',
633
+ diagnostics: {
634
+ noOp: true,
635
+ reason: 'fits_budget',
636
+ inputMessages: Array.isArray(messages) ? messages.length : 0,
637
+ baseMessages: baseSanitized.length,
638
+ baseTokens,
639
+ budgetTokens: budget,
640
+ durationMs: Date.now() - startedAt,
641
+ },
642
+ };
643
+ }
644
+ const sanitized = redactToolCallSecretsInMessages(baseSanitized);
645
+
646
+ const { system: safeSystem, live, previousSummary } = splitLiveCompactionContext(sanitized);
647
+ const recallTailOpts = {
648
+ maxUsers: opts.recallTailMaxUsers ?? opts.tailTurns ?? RECALL_TAIL_USER_MAX,
649
+ tokenCap: opts.recallTailTokenCap ?? preserveRecentBudget(budget, opts),
650
+ };
651
+ const { tail: recallTail, head: recallHead } = selectRecallPreservedTail(live, recallTailOpts);
652
+ const recallFit = splitRecallFitInputs(opts.recallText, previousSummary);
653
+ if (recallHead.length === 0 && !previousSummary
654
+ && !(recallFit.recall || recallFit.prior || opts.allowEmptyRecall === true)) {
655
+ throw new Error('recallFastTrackCompactMessages: no compactable prior history before preserved tail');
656
+ }
657
+
658
+ const mandatory = reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs([...safeSystem, ...recallTail])));
659
+ const mandatoryCost = estimateMessagesTokens(mandatory);
660
+ const originalBudget = budget;
661
+ if (mandatoryCost + COMPACT_SUMMARY_MIN_ROOM_TOKENS > budget) {
662
+ budget = mandatoryCost + COMPACT_SUMMARY_MIN_ROOM_TOKENS;
663
+ }
664
+ const budgetRaisedBy = Math.max(0, budget - originalBudget);
665
+
666
+ if (!recallFit.recall && !recallFit.prior && opts.allowEmptyRecall !== true) {
667
+ throw new Error('recallFastTrackCompactMessages: recall text is empty');
668
+ }
669
+ const oldHistory = recallHead;
670
+ const recallMeta = {
671
+ querySha: opts.querySha || null,
672
+ };
673
+ // Recall injection room is capped separately from the mandatory
674
+ // (system + preserved tail) budget: recall text never gets more than
675
+ // opts.recallTokenCap (5% of context window, floor 2048 tokens, set by
676
+ // the caller); the remaining budget stays reserved for live
677
+ // conversation. Only applied when the cap is a positive finite number,
678
+ // preserving current uncapped behavior otherwise.
679
+ const recallRoomUncapped = budget - mandatoryCost;
680
+ const recallTokenCap = Number(opts.recallTokenCap);
681
+ const recallRoom = (Number.isFinite(recallTokenCap) && recallTokenCap > 0)
682
+ ? Math.min(recallRoomUncapped, recallTokenCap)
683
+ : recallRoomUncapped;
684
+ const summaryMessage = fitRecallFastTrackSummaryMessage(
685
+ oldHistory,
686
+ recallFit.recall,
687
+ recallRoom,
688
+ recallMeta,
689
+ recallFit.prior,
690
+ );
691
+ if (!summaryMessage) {
692
+ throw new Error(`recallFastTrackCompactMessages: summary cannot fit remaining budget=${recallRoom}`);
693
+ }
694
+
695
+ let result = sanitizeToolPairs([...safeSystem, summaryMessage, ...recallTail]);
696
+ result = reconcileDedupStubs(dedupToolResultBodies(result));
697
+ const finalTokens = estimateMessagesTokens(result);
698
+ if (finalTokens > budget) {
699
+ throw new Error(`recallFastTrackCompactMessages: compacted result exceeds budget=${budget} (result=${finalTokens})`);
700
+ }
701
+ const summaryContent = String(summaryMessage?.content || '');
702
+ const diagnostics = {
703
+ noOp: false,
704
+ inputMessages: Array.isArray(messages) ? messages.length : 0,
705
+ baseMessages: baseSanitized.length,
706
+ baseTokens,
707
+ systemMessages: safeSystem.length,
708
+ liveMessages: live.length,
709
+ headMessages: recallHead.length,
710
+ tailMessages: recallTail.length,
711
+ mandatoryMessages: mandatory.length,
712
+ finalMessages: result.length,
713
+ systemTokens: safeEstimateMessagesTokens(safeSystem),
714
+ liveTokens: safeEstimateMessagesTokens(live),
715
+ headTokens: safeEstimateMessagesTokens(recallHead),
716
+ tailTokens: safeEstimateMessagesTokens(recallTail),
717
+ mandatoryCost,
718
+ finalTokens,
719
+ originalBudgetTokens: originalBudget,
720
+ budgetTokens: budget,
721
+ budgetRaised: budgetRaisedBy > 0,
722
+ budgetRaisedBy,
723
+ remainingTokens: budget - mandatoryCost,
724
+ recallTokenCap: (Number.isFinite(recallTokenCap) && recallTokenCap > 0) ? recallTokenCap : null,
725
+ recallRoom,
726
+ recallChars: recallFit.recall.length,
727
+ recallBytes: textByteLength(recallFit.recall),
728
+ priorChars: recallFit.prior.length,
729
+ priorBytes: textByteLength(recallFit.prior),
730
+ summaryMessageChars: summaryContent.length,
731
+ summaryMessageBytes: textByteLength(summaryContent),
732
+ recallEmpty: !recallFit.recall,
733
+ priorEmpty: !recallFit.prior,
734
+ recallTruncatedInSummary: !!recallFit.recall && !summaryContent.includes(recallFit.recall),
735
+ priorTruncatedInSummary: !!recallFit.prior && !summaryContent.includes(recallFit.prior),
736
+ tailTruncated: recallTail.some((m) => messageContentHasMarker(m, RECALL_TAIL_TRUNCATION_MARKER) || messageContentHasMarker(m, RECALL_TAIL_SHORT_TRUNCATION_MARKER)),
737
+ tailOptions: recallTailOpts,
738
+ previousSummary: !!previousSummary,
739
+ durationMs: Date.now() - startedAt,
740
+ };
741
+ compactDebugLog('recall-fasttrack result', diagnostics);
742
+ return {
743
+ messages: result,
744
+ recallFastTrack: true,
745
+ compactType: COMPACT_TYPE_RECALL_FASTTRACK,
746
+ query: opts.query || '',
747
+ diagnostics,
748
+ };
749
+ }