mixdog 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,9 +1,78 @@
1
1
  import { isOffloadedToolResultText } from './tool-result-offload.mjs';
2
2
  import { createHash } from 'node:crypto';
3
3
 
4
- // Rough token estimate: ~4 chars per token
4
+ // ---------------------------------------------------------------------------
5
+ // Conservative, Unicode-aware token estimator.
6
+ //
7
+ // This is a SAFETY estimator, NOT exact tokenizer parity. We cannot run the
8
+ // provider's real BPE tokenizer here (no network, no bundled vocab), so the
9
+ // goal is to never UNDERcount: a transcript the estimator says "fits" must
10
+ // genuinely fit the model context once compaction has run. The legacy chars/4
11
+ // heuristic badly undercounts Korean/CJK/kana/emoji and dense JSON/tool-call
12
+ // payloads (which BPE often splits into >=1 token per character or per byte),
13
+ // so a "fits" verdict was optimistic exactly where it mattered most.
14
+ //
15
+ // Strategy: weight each code point by how expensive it tends to be under a
16
+ // modern BPE tokenizer (cl100k/o200k-class), then take the MAX of that weighted
17
+ // sum and the chars/4 ASCII lower bound, then apply a small safety multiplier.
18
+ // Weights deliberately lean high (overcount) for CJK/Hangul/emoji.
19
+ //
20
+ // MIXDOG_TOKEN_ESTIMATE_SAFETY_MULTIPLIER (default 1.1, clamped 1.0..2.0) lets
21
+ // operators dial extra headroom without code changes.
22
+ function readSafetyMultiplier() {
23
+ const raw = Number(process.env.MIXDOG_TOKEN_ESTIMATE_SAFETY_MULTIPLIER);
24
+ if (Number.isFinite(raw)) return Math.min(2.0, Math.max(1.0, raw));
25
+ return 1.1;
26
+ }
27
+ const TOKEN_ESTIMATE_SAFETY_MULTIPLIER = readSafetyMultiplier();
28
+
29
+ // Per-code-point token-cost weight. Tuned to overcount, not match exactly.
30
+ function codePointTokenWeight(cp) {
31
+ // ASCII (latin letters, digits, punctuation, whitespace, control): the one
32
+ // region where chars/4 is roughly right — keep the cheap 0.25/char cost.
33
+ if (cp < 0x80) return 0.25;
34
+ // Hangul syllables + Jamo + compatibility Jamo. Korean is the worst case
35
+ // for chars/4: a single syllable frequently costs 1.5–3 BPE tokens, and
36
+ // rarer syllables fall back to multi-byte splits. Weight high for safety.
37
+ if (cp >= 0xAC00 && cp <= 0xD7A3) return 1.5;
38
+ if (cp >= 0x1100 && cp <= 0x11FF) return 1.5;
39
+ if (cp >= 0x3130 && cp <= 0x318F) return 1.5;
40
+ if (cp >= 0xA960 && cp <= 0xA97F) return 1.5;
41
+ if (cp >= 0xD7B0 && cp <= 0xD7FF) return 1.5;
42
+ // Hiragana / Katakana / Katakana phonetic extensions.
43
+ if (cp >= 0x3040 && cp <= 0x30FF) return 1.2;
44
+ if (cp >= 0x31F0 && cp <= 0x31FF) return 1.2;
45
+ // CJK unified ideographs (incl. Ext A) + compatibility ideographs.
46
+ if (cp >= 0x3400 && cp <= 0x4DBF) return 1.2;
47
+ if (cp >= 0x4E00 && cp <= 0x9FFF) return 1.2;
48
+ if (cp >= 0xF900 && cp <= 0xFAFF) return 1.2;
49
+ // CJK Extension B and beyond (supplementary ideographic plane).
50
+ if (cp >= 0x20000 && cp <= 0x2FA1F) return 1.2;
51
+ // Emoji / pictographs / dingbats / symbols — these explode under BPE
52
+ // (surrogate pairs, ZWJ sequences, variation selectors), so weight highest.
53
+ if (cp >= 0x2600 && cp <= 0x27BF) return 2.0;
54
+ if (cp >= 0x1F000 && cp <= 0x1FAFF) return 2.0;
55
+ if (cp >= 0x2190 && cp <= 0x21FF) return 1.5; // arrows
56
+ if (cp >= 0x2300 && cp <= 0x23FF) return 1.5; // technical symbols
57
+ // Latin-1 supplement / extended latin / IPA — pricier than ASCII (often a
58
+ // token per accented char) but cheaper than CJK.
59
+ if (cp < 0x0400) return 0.6;
60
+ // Everything else non-ASCII (Cyrillic, Greek, Arabic, Hebrew, Thai, …):
61
+ // multi-byte UTF-8, typically ~0.5–1 token/char. Stay conservative.
62
+ return 0.8;
63
+ }
64
+
65
+ // Conservative Unicode-aware token estimate. Iterates by code point (so
66
+ // surrogate-pair emoji are scored once, at the high emoji weight), takes the
67
+ // max of the weighted sum and the chars/4 ASCII floor, then applies the safety
68
+ // multiplier. Always overcounts relative to chars/4 for non-ASCII text.
5
69
  function estimateTokens(text) {
6
- return Math.ceil(String(text ?? '').length / 4);
70
+ const s = String(text ?? '');
71
+ if (s.length === 0) return 0;
72
+ let weighted = 0;
73
+ for (const ch of s) weighted += codePointTokenWeight(ch.codePointAt(0));
74
+ const asciiFloor = s.length / 4; // never below the legacy chars/4 lower bound
75
+ return Math.ceil(Math.max(weighted, asciiFloor) * TOKEN_ESTIMATE_SAFETY_MULTIPLIER);
7
76
  }
8
77
  function messageEstimateText(m) {
9
78
  if (!m || typeof m !== 'object') return '';
@@ -46,56 +115,122 @@ export function estimateToolSchemaTokens(tools) {
46
115
 
47
116
  /**
48
117
  * Total headroom the caller should reserve out of the context window before
49
- * compaction: tool-schema bytes + fixed request framing overhead. Pass the
50
- * result as `opts.reserveTokens` to compactMessages so the working budget
51
- * accounts for request-side bytes the message estimate cannot see.
118
+ * compaction: tool-schema bytes + fixed request framing overhead. Pass this as
119
+ * `opts.reserveTokens` so semantic/recall compaction budgets account for
120
+ * request-side bytes the message estimate cannot see.
52
121
  */
53
122
  export function estimateRequestReserveTokens(tools) {
54
123
  return estimateToolSchemaTokens(tools) + REQUEST_OVERHEAD_TOKENS;
55
124
  }
125
+
126
+ /**
127
+ * Live/current context numerator SSOT: transcript estimate + request reserve.
128
+ * Provider-reported usage is excluded (secondary metadata only).
129
+ * Empty / no-activity transcript returns 0 so fresh sessions do not show
130
+ * reserve-only phantom usage.
131
+ *
132
+ * @param {unknown[]} messages
133
+ * @param {unknown[]|number} toolsOrReserve tool list or precomputed reserve tokens
134
+ * @param {{ messageCount?: number }} [opts]
135
+ */
136
+ export function estimateTranscriptContextUsage(messages, toolsOrReserve, opts = {}) {
137
+ const list = Array.isArray(messages) ? messages : [];
138
+ const count = Number.isFinite(Number(opts.messageCount)) ? Number(opts.messageCount) : list.length;
139
+ if (count <= 0 || list.length === 0) return 0;
140
+ const messageTokens = estimateMessagesTokens(list);
141
+ const reserve = typeof toolsOrReserve === 'number' && Number.isFinite(toolsOrReserve)
142
+ ? Math.max(0, toolsOrReserve)
143
+ : estimateRequestReserveTokens(toolsOrReserve);
144
+ return messageTokens + reserve;
145
+ }
146
+
56
147
  const TOOL_MISSING_STUB = '[Older tool result unavailable after context compaction]';
148
+ function collectAssistantToolCallIds(message) {
149
+ if (!message || message.role !== 'assistant') return [];
150
+ const ids = [];
151
+ const seen = new Set();
152
+ const add = (id) => {
153
+ if (!id || seen.has(id)) return;
154
+ seen.add(id);
155
+ ids.push(id);
156
+ };
157
+ if (Array.isArray(message.toolCalls)) {
158
+ for (const tc of message.toolCalls) add(tc?.id);
159
+ }
160
+ const blocksFrom = (blocks) => {
161
+ if (!Array.isArray(blocks)) return;
162
+ for (const b of blocks) {
163
+ if (b?.type === 'tool_use' && b.id) add(b.id);
164
+ }
165
+ };
166
+ blocksFrom(message.assistantBlocks);
167
+ blocksFrom(message.content);
168
+ return ids;
169
+ }
57
170
  /**
58
- * Tool-pair sanitization (Hermes `_sanitize_tool_pairs`):
171
+ * Tool-pair sanitization (unmatched tool_use / tool_result repair):
172
+ * - Drop malformed `tool` messages without toolCallId.
59
173
  * - Drop `tool` messages whose toolCallId has no surviving assistant tool_call.
60
- * - For surviving assistant tool_calls whose results are missing, insert a
61
- * stub tool message so the provider doesn't reject the request for
62
- * unmatched tool_use_id.
63
- * Messages ordering is preserved; stubs are inserted immediately after the
64
- * assistant message so the tool pair sits adjacent.
174
+ * - For each surviving assistant tool_call, reattach the matching `tool`
175
+ * message (if any) immediately after that assistant; duplicate ids prefer
176
+ * the contiguous post-assistant block, then later matches, then earlier.
177
+ * - For tool_calls with no matching result, insert a stub tool message so
178
+ * the provider doesn't reject the request for unmatched tool_use_id.
179
+ * Non-tool message order is preserved; tool results are not duplicated.
65
180
  */
66
181
  export function sanitizeToolPairs(messages) {
67
182
  if (!Array.isArray(messages) || messages.length === 0) return messages;
68
183
  const assistantCallIds = new Set();
69
184
  for (const m of messages) {
70
- if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {
71
- for (const tc of m.toolCalls) {
72
- if (tc && tc.id) assistantCallIds.add(tc.id);
73
- }
74
- }
75
- }
76
- const toolById = new Map();
77
- for (const m of messages) {
78
- if (m.role === 'tool' && m.toolCallId) toolById.set(m.toolCallId, m);
185
+ for (const id of collectAssistantToolCallIds(m)) assistantCallIds.add(id);
79
186
  }
80
- const filtered = messages.filter(m => {
81
- if (m.role !== 'tool') return true;
82
- if (!m.toolCallId) return true;
83
- return assistantCallIds.has(m.toolCallId);
84
- });
187
+ const pickToolResultForAssistant = (assistantIdx, toolCallId) => {
188
+ let i = assistantIdx + 1;
189
+ while (i < messages.length && messages[i]?.role === 'tool') {
190
+ const tm = messages[i];
191
+ if (tm.toolCallId === toolCallId) return tm;
192
+ i += 1;
193
+ }
194
+ let afterBlock = assistantIdx + 1;
195
+ while (afterBlock < messages.length && messages[afterBlock]?.role === 'tool') afterBlock += 1;
196
+ for (let j = afterBlock; j < messages.length; j += 1) {
197
+ const tm = messages[j];
198
+ if (tm?.role === 'tool' && tm.toolCallId === toolCallId) return tm;
199
+ }
200
+ for (let j = 0; j < assistantIdx; j += 1) {
201
+ const tm = messages[j];
202
+ if (tm?.role === 'tool' && tm.toolCallId === toolCallId) return tm;
203
+ }
204
+ return null;
205
+ };
206
+ const placedToolIds = new Set();
85
207
  const result = [];
86
- for (const m of filtered) {
208
+ for (let idx = 0; idx < messages.length; idx += 1) {
209
+ const m = messages[idx];
210
+ if (m.role === 'tool') {
211
+ if (!m.toolCallId) continue;
212
+ if (!assistantCallIds.has(m.toolCallId)) continue;
213
+ if (placedToolIds.has(m.toolCallId)) continue;
214
+ continue;
215
+ }
87
216
  result.push(m);
88
- if (m.role !== 'assistant' || !Array.isArray(m.toolCalls)) continue;
89
- for (const tc of m.toolCalls) {
90
- if (!tc?.id) continue;
91
- const existing = toolById.get(tc.id);
92
- if (existing && filtered.includes(existing)) continue;
93
- const preserved = existing?.content;
217
+ if (m.role !== 'assistant') continue;
218
+ const callIds = collectAssistantToolCallIds(m);
219
+ if (callIds.length === 0) continue;
220
+ for (const callId of callIds) {
221
+ if (placedToolIds.has(callId)) continue;
222
+ const existing = pickToolResultForAssistant(idx, callId);
223
+ if (existing) {
224
+ result.push(existing);
225
+ placedToolIds.add(callId);
226
+ continue;
227
+ }
94
228
  result.push({
95
229
  role: 'tool',
96
- content: isOffloadedToolResultText(preserved) ? preserved : TOOL_MISSING_STUB,
97
- toolCallId: tc.id,
230
+ content: TOOL_MISSING_STUB,
231
+ toolCallId: callId,
98
232
  });
233
+ placedToolIds.add(callId);
99
234
  }
100
235
  }
101
236
  return result;
@@ -177,8 +312,27 @@ export function sanitizeAnthropicContentPairs(messages) {
177
312
  if (!Array.isArray(messages)) return messages;
178
313
  const work = messages.slice();
179
314
  const out = [];
315
+ let pendingToolUseIds = new Set();
316
+ const stripOrphanToolResults = (userMsg, allowedIds) => {
317
+ if (userMsg?.role !== 'user' || !Array.isArray(userMsg.content)) return userMsg;
318
+ const hasToolResults = userMsg.content.some((b) => b?.type === 'tool_result');
319
+ if (!hasToolResults) return userMsg;
320
+ const filtered = userMsg.content.filter((b) => {
321
+ if (b?.type !== 'tool_result') return true;
322
+ if (!b.tool_use_id) return false;
323
+ return allowedIds.size > 0 && allowedIds.has(b.tool_use_id);
324
+ });
325
+ if (filtered.length === userMsg.content.length) return userMsg;
326
+ return { ...userMsg, content: filtered };
327
+ };
180
328
  for (let i = 0; i < work.length; i++) {
181
329
  let m = work[i];
330
+ if (m?.role === 'user' && Array.isArray(m.content)) {
331
+ const hadToolResults = m.content.some((b) => b?.type === 'tool_result');
332
+ m = stripOrphanToolResults(m, pendingToolUseIds);
333
+ work[i] = m;
334
+ if (hadToolResults) pendingToolUseIds = new Set();
335
+ }
182
336
  // Drop tool_use blocks without an id from assistant messages — these
183
337
  // come from partial streaming chunks that never finalised, and the
184
338
  // provider rejects them as `tool_use ids were found without
@@ -192,13 +346,22 @@ export function sanitizeAnthropicContentPairs(messages) {
192
346
  work[i] = m;
193
347
  }
194
348
  }
349
+ if (m?.role === 'user' && Array.isArray(m.content) && m.content.length === 0) continue;
195
350
  out.push(m);
196
351
  if (m?.role !== 'assistant' || !Array.isArray(m.content)) continue;
197
352
  const toolUseIds = m.content
198
353
  .filter((b) => b?.type === 'tool_use' && b.id)
199
354
  .map((b) => b.id);
200
- if (toolUseIds.length === 0) continue;
201
- const next = work[i + 1];
355
+ if (toolUseIds.length === 0) {
356
+ pendingToolUseIds = new Set();
357
+ continue;
358
+ }
359
+ pendingToolUseIds = new Set(toolUseIds);
360
+ let next = work[i + 1];
361
+ if (next?.role === 'user' && Array.isArray(next.content)) {
362
+ next = stripOrphanToolResults(next, pendingToolUseIds);
363
+ work[i + 1] = next;
364
+ }
202
365
  const nextResultIds = (next?.role === 'user' && Array.isArray(next.content))
203
366
  ? new Set(
204
367
  next.content