mixdog 0.9.1 → 0.9.3

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 (301) hide show
  1. package/package.json +9 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
@@ -66,7 +66,7 @@ function codePointTokenWeight(cp) {
66
66
  // surrogate-pair emoji are scored once, at the high emoji weight), takes the
67
67
  // max of the weighted sum and the chars/4 ASCII floor, then applies the safety
68
68
  // multiplier. Always overcounts relative to chars/4 for non-ASCII text.
69
- function estimateTokens(text) {
69
+ export function estimateTokens(text) {
70
70
  const s = String(text ?? '');
71
71
  if (s.length === 0) return 0;
72
72
  let weighted = 0;
@@ -74,7 +74,7 @@ function estimateTokens(text) {
74
74
  const asciiFloor = s.length / 4; // never below the legacy chars/4 lower bound
75
75
  return Math.ceil(Math.max(weighted, asciiFloor) * TOKEN_ESTIMATE_SAFETY_MULTIPLIER);
76
76
  }
77
- function messageEstimateText(m) {
77
+ export function messageEstimateText(m) {
78
78
  if (!m || typeof m !== 'object') return '';
79
79
  let text = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
80
80
  if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
@@ -84,13 +84,260 @@ function messageEstimateText(m) {
84
84
  if (m.role === 'tool' && m.toolCallId) text += `\n${m.toolCallId}`;
85
85
  return text;
86
86
  }
87
- function estimateMessageTokens(m) {
87
+ export function estimateMessageTokens(m) {
88
88
  return estimateTokens(messageEstimateText(m)) + 4;
89
89
  }
90
90
  export function estimateMessagesTokens(messages) {
91
91
  return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
92
92
  }
93
93
 
94
+ export const DEFAULT_COMPACTION_BUFFER_TOKENS = 0;
95
+ export const DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
96
+ export const MAX_COMPACTION_BUFFER_RATIO = 0.25;
97
+ export const DEFAULT_COMPACTION_KEEP_TOKENS = 8_000;
98
+ const LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
99
+
100
+ export function positiveTokenInt(value) {
101
+ const n = Number(value);
102
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
103
+ }
104
+
105
+ function envTokenInt(name) {
106
+ return positiveTokenInt(process.env[name]);
107
+ }
108
+
109
+ export function normalizeCompactionBufferRatio(value, fallback = DEFAULT_COMPACTION_BUFFER_RATIO) {
110
+ const n = Number(value);
111
+ if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
112
+ return fallback;
113
+ }
114
+
115
+ // Percent-named inputs (bufferPercent / bufferPct / *_BUFFER_PERCENT) carry a
116
+ // PERCENT: 1 means 1% (0.01). Ratio-named inputs (bufferRatio / bufferFraction)
117
+ // carry a fraction: 0.01 means 1%, and a legacy value > 1 is read as a percent.
118
+ export function resolveBufferRatioCandidate(percentInputs = [], ratioInputs = []) {
119
+ for (const raw of percentInputs) {
120
+ const n = Number(raw);
121
+ if (Number.isFinite(n) && n > 0) return Math.min(1, n / 100);
122
+ }
123
+ for (const raw of ratioInputs) {
124
+ const n = Number(raw);
125
+ if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ export function resolveCompactBufferRatio(cfg = {}) {
131
+ const resolved = resolveBufferRatioCandidate(
132
+ [cfg.bufferPercent, cfg.bufferPct, process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT],
133
+ [cfg.bufferRatio, cfg.bufferFraction, process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO],
134
+ );
135
+ return normalizeCompactionBufferRatio(resolved, DEFAULT_COMPACTION_BUFFER_RATIO);
136
+ }
137
+
138
+ export function compactionBufferTokensForBoundary(boundaryTokens, opts = {}) {
139
+ const boundary = Math.max(0, Math.floor(Number(boundaryTokens) || 0));
140
+ const explicit = Math.max(0, Math.floor(Number(opts.explicitTokens) || 0));
141
+ if (!boundary) return explicit;
142
+ const maxRatio = normalizeCompactionBufferRatio(opts.maxRatio, MAX_COMPACTION_BUFFER_RATIO);
143
+ const cap = Math.max(0, Math.floor(boundary * maxRatio));
144
+ if (explicit > 0) return Math.max(0, Math.min(explicit, cap));
145
+ const ratio = normalizeCompactionBufferRatio(opts.ratio, DEFAULT_COMPACTION_BUFFER_RATIO);
146
+ return Math.max(0, Math.min(Math.floor(boundary * ratio), cap));
147
+ }
148
+
149
+ export function isPersistedZeroBufferTelemetry(cfg = {}, boundaryTokens = 0) {
150
+ const boundary = positiveTokenInt(boundaryTokens);
151
+ if (!boundary) return false;
152
+ if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
153
+ for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
154
+ const n = Number(process.env[envName]);
155
+ if (Number.isFinite(n) && n > 0) return false;
156
+ }
157
+ for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
158
+ const n = Number(cfg?.[key]);
159
+ if (Number.isFinite(n) && n > 0) return false;
160
+ }
161
+ const ratio = Number(cfg?.bufferRatio);
162
+ if (Number.isFinite(ratio) && ratio > 0) return false;
163
+ const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
164
+ if (!Number.isFinite(explicitTokens) || explicitTokens !== 0) return false;
165
+ return true;
166
+ }
167
+
168
+ export function isLegacyDefaultBufferTelemetry(cfg = {}, boundaryTokens = 0) {
169
+ const boundary = positiveTokenInt(boundaryTokens);
170
+ if (!boundary) return false;
171
+ if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
172
+ for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
173
+ const n = Number(process.env[envName]);
174
+ if (Number.isFinite(n) && n > 0) return false;
175
+ }
176
+ // Percent/fraction-named fields are operator config. Legacy/default
177
+ // telemetry persisted bufferTokens + bufferRatio after a check/compact pass.
178
+ for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
179
+ const n = Number(cfg?.[key]);
180
+ if (Number.isFinite(n) && n > 0) return false;
181
+ }
182
+ const explicitTokens = positiveTokenInt(cfg?.bufferTokens ?? cfg?.buffer);
183
+ const ratio = Number(cfg?.bufferRatio);
184
+ if (!explicitTokens || !Number.isFinite(ratio) || Math.abs(ratio - LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO) > 1e-9) return false;
185
+ const expectedTokens = Math.floor(boundary * LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO);
186
+ const cfgBoundary = positiveTokenInt(cfg?.boundaryTokens);
187
+ const cfgTrigger = positiveTokenInt(cfg?.triggerTokens);
188
+ return explicitTokens === expectedTokens
189
+ || (cfgBoundary === boundary && cfgTrigger > 0 && explicitTokens === Math.max(0, boundary - cfgTrigger));
190
+ }
191
+
192
+ export function compactBufferConfigForBoundary(cfg = {}, boundaryTokens = 0) {
193
+ const base = cfg || {};
194
+ if (!isLegacyDefaultBufferTelemetry(base, boundaryTokens)
195
+ && !isPersistedZeroBufferTelemetry(base, boundaryTokens)) {
196
+ return base;
197
+ }
198
+ return {
199
+ ...base,
200
+ bufferTokens: null,
201
+ buffer: null,
202
+ bufferRatio: null,
203
+ };
204
+ }
205
+
206
+ export function resolveCompactBufferTokens(boundaryTokens, cfg = {}, opts = {}) {
207
+ const boundary = positiveTokenInt(boundaryTokens);
208
+ const effectiveCfg = compactBufferConfigForBoundary(cfg, boundary);
209
+ const configured = positiveTokenInt(effectiveCfg.bufferTokens ?? effectiveCfg.buffer)
210
+ || envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')
211
+ || 0;
212
+ if (!boundary) return configured || positiveTokenInt(opts.defaultTokens) || DEFAULT_COMPACTION_BUFFER_TOKENS;
213
+ return compactionBufferTokensForBoundary(boundary, {
214
+ explicitTokens: configured,
215
+ ratio: resolveCompactBufferRatio(effectiveCfg),
216
+ maxRatio: opts.maxRatio ?? MAX_COMPACTION_BUFFER_RATIO,
217
+ });
218
+ }
219
+
220
+ export function resolveCompactTriggerTokens(sessionOrConfig = {}, boundaryTokens = 0) {
221
+ const boundary = positiveTokenInt(boundaryTokens);
222
+ if (!boundary) return null;
223
+ const cfg = sessionOrConfig?.compaction || sessionOrConfig || {};
224
+ const autoLimit = positiveTokenInt(sessionOrConfig?.autoCompactTokenLimit ?? cfg?.autoCompactTokenLimit);
225
+ if (autoLimit && autoLimit < boundary) return Math.max(1, autoLimit);
226
+ const buffer = resolveCompactBufferTokens(boundary, cfg);
227
+ return Math.max(1, boundary - buffer);
228
+ }
229
+
230
+ function stripSystemReminder(text) {
231
+ return String(text || '')
232
+ .replace(/^\s*<system-reminder>\s*/i, '')
233
+ .replace(/\s*<\/system-reminder>\s*$/i, '')
234
+ .trim();
235
+ }
236
+
237
+ function splitMarkdownSections(text) {
238
+ const sections = [];
239
+ let current = [];
240
+ for (const line of String(text || '').split(/\r?\n/)) {
241
+ if (/^#\s+/.test(line) && current.length) {
242
+ const body = current.join('\n').trim();
243
+ if (body) sections.push(body);
244
+ current = [line];
245
+ } else {
246
+ current.push(line);
247
+ }
248
+ }
249
+ const tail = current.join('\n').trim();
250
+ if (tail) sections.push(tail);
251
+ return sections;
252
+ }
253
+
254
+ function reminderSectionBucket(section) {
255
+ const heading = String(section.match(/^#\s+([^\n]+)/)?.[1] || '').trim().toLowerCase();
256
+ if (heading.includes('core memory')) return 'memory';
257
+ if (heading.includes('active workflow') || heading.includes('available agents') || heading.includes('workflow')) return 'workflow';
258
+ if (heading.includes('workspace')) return 'workspace';
259
+ if (heading.includes('environment')) return 'environment';
260
+ return 'other';
261
+ }
262
+
263
+ export function summarizeContextMessages(messages) {
264
+ const rows = {
265
+ system: { count: 0, tokens: 0 },
266
+ user: { count: 0, tokens: 0 },
267
+ assistant: { count: 0, tokens: 0 },
268
+ tool: { count: 0, tokens: 0 },
269
+ other: { count: 0, tokens: 0 },
270
+ };
271
+ const semantic = {
272
+ system: { count: 0, tokens: 0 },
273
+ chat: { count: 0, tokens: 0 },
274
+ assistant: { count: 0, tokens: 0 },
275
+ toolResults: { count: 0, tokens: 0 },
276
+ reminders: { count: 0, tokens: 0, otherTokens: 0 },
277
+ workflow: { tokens: 0 },
278
+ memory: { tokens: 0 },
279
+ workspace: { tokens: 0 },
280
+ environment: { tokens: 0 },
281
+ other: { tokens: 0 },
282
+ };
283
+ let toolCallCount = 0;
284
+ let toolCallTokens = 0;
285
+ let toolResultCount = 0;
286
+ let toolResultTokens = 0;
287
+ for (const message of messages || []) {
288
+ const role = rows[message?.role] ? message.role : 'other';
289
+ const text = messageEstimateText(message);
290
+ const tokens = estimateMessageTokens(message);
291
+ rows[role].count += 1;
292
+ rows[role].tokens += tokens;
293
+ if (role === 'system') {
294
+ semantic.system.count += 1;
295
+ semantic.system.tokens += tokens;
296
+ } else if (role === 'user') {
297
+ if (String(text || '').trim().startsWith('<system-reminder>')) {
298
+ semantic.reminders.count += 1;
299
+ semantic.reminders.tokens += tokens;
300
+ let sectionTokens = 0;
301
+ for (const section of splitMarkdownSections(stripSystemReminder(text))) {
302
+ const bucket = reminderSectionBucket(section);
303
+ const sectionTokenCount = estimateTokens(section);
304
+ semantic[bucket].tokens += sectionTokenCount;
305
+ sectionTokens += sectionTokenCount;
306
+ }
307
+ semantic.reminders.otherTokens += Math.max(0, tokens - sectionTokens);
308
+ } else {
309
+ semantic.chat.count += 1;
310
+ semantic.chat.tokens += tokens;
311
+ }
312
+ } else if (role === 'assistant') {
313
+ semantic.assistant.count += 1;
314
+ semantic.assistant.tokens += tokens;
315
+ } else if (role === 'tool') {
316
+ semantic.toolResults.count += 1;
317
+ semantic.toolResults.tokens += tokens;
318
+ }
319
+ if (message?.role === 'assistant' && Array.isArray(message.toolCalls) && message.toolCalls.length) {
320
+ toolCallCount += message.toolCalls.length;
321
+ try { toolCallTokens += estimateTokens(JSON.stringify(message.toolCalls)); }
322
+ catch { toolCallTokens += estimateTokens(`[${message.toolCalls.length} tool calls]`); }
323
+ }
324
+ if (message?.role === 'tool') {
325
+ toolResultCount += 1;
326
+ toolResultTokens += tokens;
327
+ }
328
+ }
329
+ return {
330
+ count: Array.isArray(messages) ? messages.length : 0,
331
+ estimatedTokens: Array.isArray(messages) ? estimateMessagesTokens(messages) : 0,
332
+ roles: rows,
333
+ semantic,
334
+ toolCallCount,
335
+ toolCallTokens,
336
+ toolResultCount,
337
+ toolResultTokens,
338
+ };
339
+ }
340
+
94
341
  // Per-request overhead the provider injects that never appears in the
95
342
  // `messages` array: function-calling preamble + system-prompt framing the
96
343
  // provider wraps around the request. The chars/4 message estimate misses all
@@ -0,0 +1,28 @@
1
+ // Compaction debug/diagnostic helpers extracted from loop.mjs.
2
+ import { estimateMessagesTokens } from '../context-utils.mjs';
3
+
4
+ export function estimateMessagesTokensSafe(messages) {
5
+ try { return estimateMessagesTokens(messages); }
6
+ catch { return null; }
7
+ }
8
+
9
+ export function compactDebugEnabled() {
10
+ return String(process.env.MIXDOG_COMPACT_DEBUG || '').trim() === '1';
11
+ }
12
+
13
+ export function compactDiagnosticError(err) {
14
+ if (!err) return null;
15
+ const text = String(err?.message || err);
16
+ return text.length > 500 ? `${text.slice(0, 499)}…` : text;
17
+ }
18
+
19
+ export function compactByteLength(text) {
20
+ try { return Buffer.byteLength(String(text || ''), 'utf8'); }
21
+ catch { return String(text || '').length; }
22
+ }
23
+
24
+ export function compactDebugLog(scope, details = {}) {
25
+ if (!compactDebugEnabled()) return;
26
+ try { process.stderr.write(`[compact] ${scope} ${JSON.stringify(details)}\n`); }
27
+ catch { /* best-effort diagnostics only */ }
28
+ }
@@ -0,0 +1,262 @@
1
+ // Compaction policy resolution, pressure/target budgeting, telemetry
2
+ // persistence, and event emission — extracted from loop.mjs.
3
+ // runRecallFastTrackCompact stays in the loop (it drives the recall pipeline
4
+ // against live session state).
5
+ import {
6
+ estimateRequestReserveTokens,
7
+ resolveCompactBufferRatio,
8
+ resolveCompactBufferTokens,
9
+ } from '../context-utils.mjs';
10
+ import {
11
+ compactTypeIsRecallFastTrack,
12
+ compactTypeIsSemantic,
13
+ normalizeCompactType,
14
+ DEFAULT_COMPACT_TYPE,
15
+ DEFAULT_COMPACTION_KEEP_TOKENS,
16
+ } from '../compact.mjs';
17
+ import { positiveTokenInt, envFlag, envTokenInt } from './env.mjs';
18
+
19
+ const COMPACT_SAFETY_PERCENT = 1.00;
20
+ const COMPACT_TARGET_RATIO = 0.02;
21
+ const COMPACT_TARGET_MIN_TOKENS = 4_000;
22
+ const COMPACT_TARGET_MAX_TOKENS = 16_000;
23
+
24
+ function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
25
+ if (process.env.MIXDOG_AGENT_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_AGENT_COMPACT_SEMANTIC', true);
26
+ if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
27
+ if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on') return true;
28
+ // The compact type is already explicit (`semantic` by default). Honor it
29
+ // directly instead of substituting another compaction path.
30
+ return true;
31
+ }
32
+
33
+ function resolveCompactTypeSetting(_sessionRef, cfg = {}) {
34
+ const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
35
+ ?? process.env.MIXDOG_COMPACT_TYPE
36
+ ?? cfg.type
37
+ ?? cfg.compactType
38
+ ?? cfg.compact_type;
39
+ return normalizeCompactType(configured, DEFAULT_COMPACT_TYPE);
40
+ }
41
+
42
+ function resolveCompactTargetRatio(cfg = {}) {
43
+ const raw = cfg.targetPercent
44
+ ?? cfg.targetPct
45
+ ?? cfg.targetRatio
46
+ ?? cfg.targetFraction
47
+ ?? process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
48
+ ?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
49
+ ?? COMPACT_TARGET_RATIO;
50
+ const n = Number(raw);
51
+ if (!Number.isFinite(n) || n <= 0) return COMPACT_TARGET_RATIO;
52
+ return n > 1 ? n / 100 : n;
53
+ }
54
+ function resolveCompactTargetTokens(boundaryTokens, cfg = {}) {
55
+ const boundary = positiveTokenInt(boundaryTokens);
56
+ if (!boundary) return null;
57
+ const explicit = positiveTokenInt(cfg.targetTokens ?? cfg.target)
58
+ || envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_TOKENS')
59
+ || envTokenInt('MIXDOG_COMPACT_TARGET_TOKENS');
60
+ if (explicit) return Math.max(1, Math.min(boundary, explicit));
61
+ const minTarget = Math.min(boundary, positiveTokenInt(cfg.targetMinTokens ?? cfg.minTargetTokens)
62
+ || envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MIN_TOKENS')
63
+ || envTokenInt('MIXDOG_COMPACT_TARGET_MIN_TOKENS')
64
+ || COMPACT_TARGET_MIN_TOKENS);
65
+ const maxTarget = Math.min(boundary, positiveTokenInt(cfg.targetMaxTokens ?? cfg.maxTargetTokens)
66
+ || envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MAX_TOKENS')
67
+ || envTokenInt('MIXDOG_COMPACT_TARGET_MAX_TOKENS')
68
+ || COMPACT_TARGET_MAX_TOKENS);
69
+ const byRatio = Math.max(1, Math.floor(boundary * resolveCompactTargetRatio(cfg)));
70
+ return Math.max(1, Math.min(boundary, maxTarget, Math.max(minTarget, byRatio)));
71
+ }
72
+ function resolveCompactKeepTokens(cfg = {}) {
73
+ return positiveTokenInt(cfg.keepTokens ?? cfg.keep?.tokens ?? cfg.preserveRecentTokens)
74
+ || envTokenInt('MIXDOG_AGENT_COMPACT_KEEP_TOKENS')
75
+ || DEFAULT_COMPACTION_KEEP_TOKENS;
76
+ }
77
+ export function resolveWorkerCompactPolicy(sessionRef, tools) {
78
+ if (!sessionRef) return null;
79
+ const cfg = sessionRef.compaction || {};
80
+ const auto = cfg.auto !== false && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
81
+ if (!auto) return { auto: false };
82
+ const contextWindow = positiveTokenInt(sessionRef.contextWindow ?? cfg.contextWindow);
83
+ const explicitBoundary = positiveTokenInt(sessionRef.compactBoundaryTokens ?? cfg.boundaryTokens);
84
+ const autoLimit = positiveTokenInt(sessionRef.autoCompactTokenLimit ?? cfg.autoCompactTokenLimit);
85
+ const boundaryTokens = explicitBoundary && contextWindow
86
+ ? Math.min(explicitBoundary, contextWindow)
87
+ : (explicitBoundary || contextWindow || autoLimit);
88
+ if (!boundaryTokens) return null;
89
+ const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
90
+ // Only an explicit auto-compact limit STRICTLY BELOW the boundary acts as
91
+ // the trigger. A persisted value == boundary (legacy derived full-window
92
+ // autoCompactTokenLimit) would set autoTriggerTokens == boundary and
93
+ // collapse/override the default trigger, so it is ignored in favor of the
94
+ // default boundary trigger.
95
+ const autoTriggerTokens = autoLimit && autoLimit < compactBoundaryTokens ? Math.max(1, autoLimit) : null;
96
+ // Sanitized explicit limit: only a sub-boundary value is a real auto-compact
97
+ // limit. Anything >= boundary is a legacy derived full-window artifact and
98
+ // is reported as null so rememberCompactTelemetry does not re-persist it
99
+ // back onto the session and re-collapse the buffer on the next turn.
100
+ const explicitAutoCompactTokenLimit = autoTriggerTokens;
101
+ const bufferTokens = autoTriggerTokens
102
+ ? Math.max(0, compactBoundaryTokens - autoTriggerTokens)
103
+ : resolveCompactBufferTokens(compactBoundaryTokens, cfg);
104
+ const bufferRatio = compactBoundaryTokens ? (bufferTokens / compactBoundaryTokens) : resolveCompactBufferRatio(cfg);
105
+ const triggerTokens = autoTriggerTokens || Math.max(1, compactBoundaryTokens - bufferTokens);
106
+ const configuredReserve = positiveTokenInt(cfg.reservedTokens)
107
+ || envTokenInt('MIXDOG_AGENT_COMPACT_RESERVED_TOKENS')
108
+ || 0;
109
+ const requestReserve = estimateRequestReserveTokens(tools);
110
+ const keepTokens = resolveCompactKeepTokens(cfg);
111
+ const compactType = resolveCompactTypeSetting(sessionRef, cfg);
112
+ return {
113
+ auto: true,
114
+ type: compactType,
115
+ compactType,
116
+ prune: cfg.prune === true || envFlag('MIXDOG_AGENT_COMPACT_PRUNE', false),
117
+ boundaryTokens: compactBoundaryTokens,
118
+ triggerTokens,
119
+ bufferTokens,
120
+ bufferRatio,
121
+ contextWindow,
122
+ rawContextWindow: positiveTokenInt(sessionRef.rawContextWindow ?? cfg.rawContextWindow) || contextWindow,
123
+ effectiveContextWindowPercent: Number.isFinite(Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent))
124
+ ? Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent)
125
+ : null,
126
+ autoCompactTokenLimit: explicitAutoCompactTokenLimit,
127
+ semantic: compactTypeIsSemantic(compactType) && resolveSemanticCompactSetting(sessionRef, cfg),
128
+ recallFastTrack: compactTypeIsRecallFastTrack(compactType),
129
+ semanticTimeoutMs: positiveTokenInt(cfg.timeoutMs) || envTokenInt('MIXDOG_AGENT_COMPACT_TIMEOUT_MS') || 30_000,
130
+ tailTurns: positiveTokenInt(cfg.tailTurns) || envTokenInt('MIXDOG_AGENT_COMPACT_TAIL_TURNS') || 2,
131
+ keepTokens,
132
+ preserveRecentTokens: positiveTokenInt(cfg.preserveRecentTokens) || envTokenInt('MIXDOG_AGENT_COMPACT_PRESERVE_RECENT_TOKENS') || keepTokens,
133
+ reserveTokens: requestReserve + configuredReserve,
134
+ requestReserveTokens: requestReserve,
135
+ configuredReserveTokens: configuredReserve,
136
+ };
137
+ }
138
+ /** Transcript + request reserve only (never provider lastContextTokens). */
139
+ function compactPressureTokens(messageTokensEst, policy) {
140
+ if (messageTokensEst === null) return 0;
141
+ return Math.max(0, messageTokensEst + (policy?.reserveTokens || 0));
142
+ }
143
+
144
+ /** Telemetry pressure when a reactive overflow retry forces the next compact. */
145
+ export function compactionTelemetryPressureTokens(messageTokensEst, policy, { reactivePending = false } = {}) {
146
+ const base = compactPressureTokens(messageTokensEst, policy);
147
+ if (!reactivePending) return base;
148
+ const floor = positiveTokenInt(policy?.triggerTokens) || positiveTokenInt(policy?.boundaryTokens) || 0;
149
+ return floor ? Math.max(base, floor) : base;
150
+ }
151
+ export function compactTargetBudget(policy) {
152
+ const boundary = positiveTokenInt(policy?.boundaryTokens);
153
+ if (!boundary) return null;
154
+ const reserve = Math.max(0, Number(policy?.reserveTokens) || 0);
155
+ const targetEffective = resolveCompactTargetTokens(boundary, policy) || boundary;
156
+ return Math.max(1, Math.min(boundary, targetEffective + reserve));
157
+ }
158
+ export function shouldCompactForSession(messageTokensEst, policy, { forceReactive = false } = {}) {
159
+ if (!policy?.auto || !policy.boundaryTokens) return false;
160
+ if (forceReactive) return true;
161
+ if (messageTokensEst === null) return true;
162
+ return compactPressureTokens(messageTokensEst, policy) >= (policy.triggerTokens || policy.boundaryTokens);
163
+ }
164
+ export function countPrunedToolOutputs(before, after) {
165
+ if (!Array.isArray(before) || !Array.isArray(after)) return 0;
166
+ let count = 0;
167
+ const n = Math.min(before.length, after.length);
168
+ for (let i = 0; i < n; i += 1) {
169
+ if (before[i]?.role !== 'tool' || after[i]?.role !== 'tool') continue;
170
+ if (before[i]?.content !== after[i]?.content && after[i]?.compactedKind === 'tool_output_prune') count += 1;
171
+ }
172
+ return count;
173
+ }
174
+ export function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
175
+ if (!sessionRef || !policy) return;
176
+ const prev = sessionRef.compaction && typeof sessionRef.compaction === 'object'
177
+ ? sessionRef.compaction
178
+ : {};
179
+ const changed = meta.compactChanged === true || meta.pruneCount > 0;
180
+ sessionRef.compaction = {
181
+ ...prev,
182
+ auto: policy.auto !== false,
183
+ prune: policy.prune === true,
184
+ reservedTokens: policy.configuredReserveTokens || prev.reservedTokens || null,
185
+ requestReserveTokens: policy.requestReserveTokens || 0,
186
+ reserveTokens: policy.reserveTokens || 0,
187
+ boundaryTokens: policy.boundaryTokens || null,
188
+ triggerTokens: policy.triggerTokens || null,
189
+ bufferTokens: policy.bufferTokens || 0,
190
+ bufferRatio: policy.bufferRatio ?? prev.bufferRatio ?? null,
191
+ contextWindow: policy.contextWindow || null,
192
+ rawContextWindow: policy.rawContextWindow || null,
193
+ effectiveContextWindowPercent: policy.effectiveContextWindowPercent ?? null,
194
+ autoCompactTokenLimit: policy.autoCompactTokenLimit || null,
195
+ type: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
196
+ compactType: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
197
+ semantic: policy.semantic === true ? 'auto' : false,
198
+ recallFastTrack: policy.recallFastTrack === true,
199
+ semanticModel: policy.semanticModel || null,
200
+ semanticTimeoutMs: policy.semanticTimeoutMs || null,
201
+ tailTurns: policy.tailTurns || null,
202
+ keepTokens: policy.keepTokens || null,
203
+ preserveRecentTokens: policy.preserveRecentTokens || null,
204
+ lastCheckedAt: Date.now(),
205
+ lastBeforeTokens: meta.beforeTokens ?? null,
206
+ lastAfterTokens: meta.afterTokens ?? null,
207
+ lastPressureTokens: meta.pressureTokens ?? null,
208
+ currentEstimatedTokens: meta.pressureTokens ?? prev.currentEstimatedTokens ?? null,
209
+ lastApiRequestTokens: positiveTokenInt(sessionRef?.lastContextTokens) || prev.lastApiRequestTokens || null,
210
+ lastStage: meta.stage || prev.lastStage || null,
211
+ lastChanged: changed,
212
+ lastTrigger: meta.trigger || prev.lastTrigger || null,
213
+ lastSemantic: meta.semanticCompact === true,
214
+ lastSemanticError: Object.hasOwn(meta, 'semanticError')
215
+ ? (meta.semanticError ?? null)
216
+ : (prev.lastSemanticError ?? null),
217
+ lastRecallFastTrack: meta.recallFastTrack === true,
218
+ lastRecallFastTrackError: Object.hasOwn(meta, 'recallFastTrackError')
219
+ ? (meta.recallFastTrackError ?? null)
220
+ : (prev.lastRecallFastTrackError ?? null),
221
+ lastError: Object.hasOwn(meta, 'compactError') || Object.hasOwn(meta, 'lastError')
222
+ ? (meta.compactError ?? meta.lastError ?? null)
223
+ : (prev.lastError ?? null),
224
+ lastPruneCount: meta.pruneCount || 0,
225
+ lastDurationMs: meta.durationMs != null && Number.isFinite(Number(meta.durationMs))
226
+ ? Math.max(0, Math.round(Number(meta.durationMs)))
227
+ : null,
228
+ compactCount: (prev.compactCount || 0) + (changed ? 1 : 0),
229
+ };
230
+ if (changed) {
231
+ const changedAt = Date.now();
232
+ sessionRef.compaction.lastChangedAt = changedAt;
233
+ sessionRef.compaction.lastCompactAt = changedAt;
234
+ sessionRef.lastContextTokensStaleAfterCompact = true;
235
+ }
236
+ sessionRef.contextWindow = policy.contextWindow || sessionRef.contextWindow;
237
+ sessionRef.rawContextWindow = policy.rawContextWindow || sessionRef.rawContextWindow;
238
+ sessionRef.compactBoundaryTokens = policy.boundaryTokens || sessionRef.compactBoundaryTokens || null;
239
+ // Persist only the sanitized (sub-boundary) explicit limit. policy.autoCompactTokenLimit
240
+ // is already null for legacy derived full-window values, so a stale
241
+ // boundary-sized autoCompactTokenLimit on the session is cleared here rather
242
+ // than carried forward to re-collapse the buffer next turn.
243
+ {
244
+ const _boundary = positiveTokenInt(sessionRef.compactBoundaryTokens);
245
+ const _prevLimit = positiveTokenInt(sessionRef.autoCompactTokenLimit);
246
+ const _keepPrev = _prevLimit && (!_boundary || _prevLimit < _boundary) ? _prevLimit : null;
247
+ sessionRef.autoCompactTokenLimit = policy.autoCompactTokenLimit || _keepPrev || null;
248
+ }
249
+ if (policy.effectiveContextWindowPercent !== null) {
250
+ sessionRef.effectiveContextWindowPercent = policy.effectiveContextWindowPercent;
251
+ }
252
+ }
253
+
254
+ export function emitCompactEvent(opts, event = {}) {
255
+ if (!opts || typeof opts.onCompactEvent !== 'function') return;
256
+ try { opts.onCompactEvent({ ts: Date.now(), ...event }); }
257
+ catch { /* best-effort UI/log hook */ }
258
+ }
259
+
260
+ export function compactEventType(policy, fallback = DEFAULT_COMPACT_TYPE) {
261
+ return policy?.compactType || policy?.type || fallback;
262
+ }
@@ -0,0 +1,38 @@
1
+ // Agent context-overflow error, extracted from loop.mjs. Raised when the latest
2
+ // turn cannot fit the target context budget even after compaction.
3
+
4
+ export class AgentContextOverflowError extends Error {
5
+ constructor({ stage, sessionId, provider, model, contextWindow, budgetTokens, reserveTokens, messageTokensEst }, cause) {
6
+ const target = [provider, model].filter(Boolean).join('/') || 'target model';
7
+ const causeMsg = cause && cause.message ? `: ${cause.message}` : '';
8
+ super(
9
+ `agent context overflow (${target}, stage=${stage || 'compact'}): ` +
10
+ `latest turn cannot fit target context budget=${budgetTokens ?? 'unknown'} ` +
11
+ `reserve=${reserveTokens ?? 'unknown'} contextWindow=${contextWindow ?? 'unknown'} ` +
12
+ `messageTokensEst=${messageTokensEst ?? 'unknown'}${causeMsg}`,
13
+ );
14
+ this.name = 'AgentContextOverflowError';
15
+ this.code = 'AGENT_CONTEXT_OVERFLOW';
16
+ this.sessionId = sessionId || null;
17
+ this.provider = provider || null;
18
+ this.model = model || null;
19
+ this.contextWindow = contextWindow ?? null;
20
+ this.budgetTokens = budgetTokens ?? null;
21
+ this.reserveTokens = reserveTokens ?? null;
22
+ this.messageTokensEst = messageTokensEst ?? null;
23
+ if (cause) this.cause = cause;
24
+ }
25
+ }
26
+
27
+ export function agentContextOverflowError({ stage, sessionId, sessionRef, model, budgetTokens, reserveTokens, messageTokensEst }, cause) {
28
+ return new AgentContextOverflowError({
29
+ stage,
30
+ sessionId,
31
+ provider: sessionRef?.provider || null,
32
+ model: sessionRef?.model || model || null,
33
+ contextWindow: sessionRef?.contextWindow ?? null,
34
+ budgetTokens,
35
+ reserveTokens,
36
+ messageTokensEst,
37
+ }, cause);
38
+ }
@@ -0,0 +1,14 @@
1
+ // Env-var / token parsing helpers extracted from loop.mjs.
2
+
3
+ export function positiveTokenInt(value) {
4
+ const n = Number(value);
5
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
6
+ }
7
+ export function envFlag(name, fallback = false) {
8
+ const v = process.env[name];
9
+ if (v === undefined) return fallback;
10
+ return !['0', 'false', 'off', 'no'].includes(String(v).trim().toLowerCase());
11
+ }
12
+ export function envTokenInt(name) {
13
+ return positiveTokenInt(process.env[name]);
14
+ }
@@ -0,0 +1,21 @@
1
+ // Hidden (sub-agent) role registry, extracted from loop.mjs.
2
+ // Source of truth: defaults/agents.json — read once and cached. HIDDEN_AGENT_NAMES
3
+ // is built eagerly so it stays in sync with the declarative registry (no
4
+ // hardcoded duplicate).
5
+ import { readFileSync as _readFileSync } from 'fs';
6
+ import { fileURLToPath } from 'url';
7
+ import { dirname, resolve as resolvePath } from 'path';
8
+
9
+ const _AGENTS_JSON = resolvePath(dirname(fileURLToPath(import.meta.url)), '../../../../../defaults/agents.json');
10
+ let _hiddenAgentsCache = null;
11
+ function _getHiddenAgents() {
12
+ if (_hiddenAgentsCache) return _hiddenAgentsCache;
13
+ try {
14
+ _hiddenAgentsCache = JSON.parse(_readFileSync(_AGENTS_JSON, 'utf8'));
15
+ } catch { _hiddenAgentsCache = { agents: [] }; }
16
+ return _hiddenAgentsCache;
17
+ }
18
+
19
+ export const HIDDEN_AGENT_NAMES = new Set(
20
+ (_getHiddenAgents().agents || []).map((r) => r && r.agent).filter((n) => typeof n === 'string' && n.length > 0)
21
+ );