mixdog 0.8.0 → 0.9.0

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 (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  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 +344 -0
  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 +1223 -95
  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 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. 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