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
@@ -1,77 +1,90 @@
1
1
  import { createRequire } from 'module';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { randomBytes, createHash } from 'crypto';
4
- import { join } from 'path';
5
4
  import { getProvider, providerInputExcludesCache } from '../providers/registry.mjs';
6
- import { getModelMetadataSync } from '../providers/model-catalog.mjs';
7
5
  import { fetchOAuthUsageSnapshot } from '../providers/oauth-usage.mjs';
8
6
  // Image content is kept in-memory and in the model-visible history so multi-turn
9
- // recognition matches reference agent behavior (live transcript always retains images). The
7
+ // recognition works reliably (live transcript always retains images). The
10
8
  // stored-history placeholder swap now happens only at disk-serialization time
11
9
  // inside the session store, so it is no longer imported here.
12
10
  import {
13
11
  recallFastTrackCompactMessages,
14
12
  semanticCompactMessages,
13
+ pruneToolOutputsUnanchored,
14
+ effectiveBudget as compactEffectiveBudget,
15
15
  compactTypeIsRecallFastTrack,
16
16
  compactTypeIsSemantic,
17
17
  normalizeCompactType,
18
18
  DEFAULT_COMPACT_TYPE,
19
19
  SUMMARY_PREFIX,
20
- DEFAULT_COMPACTION_BUFFER_RATIO,
21
- compactionBufferTokensForBoundary,
22
- normalizeCompactionBufferRatio,
23
20
  drainSessionCycle1,
24
21
  } from './compact.mjs';
25
22
  import { estimateMessagesTokens, estimateRequestReserveTokens, estimateTranscriptContextUsage } from './context-utils.mjs';
26
- import { getMcpTools } from '../mcp/client.mjs';
27
- import { getInternalTools, executeInternalTool } from '../internal-tools.mjs';
28
- import { BUILTIN_TOOLS } from '../tools/builtin/builtin-tools.mjs';
29
- import { PATCH_TOOL_DEFS } from '../tools/patch-tool-defs.mjs';
30
- import { CODE_GRAPH_TOOL_DEFS } from '../tools/code-graph-tool-defs.mjs';
31
- import { collectSkillsCached, buildSkillManifest, buildSkillToolDefs, composeSystemPrompt } from '../context/collect.mjs';
23
+ import { executeInternalTool } from '../internal-tools.mjs';
24
+ import { collectSkillsCached, buildSkillManifest, composeSystemPrompt } from '../context/collect.mjs';
32
25
  import { saveSession, saveSessionAsync, loadSession, listStoredSessionSummaries, sweepStaleSessions, markSessionClosed, publishHeartbeat, deleteHeartbeat, setLiveSession } from './store.mjs';
33
26
  import { clearReadDedupSession, tryPrefetchCached, setPrefetchCached } from './read-dedup.mjs';
34
27
  import { clearOffloadSession } from './tool-result-offload.mjs';
35
28
  import { classifyResultKind } from './result-classification.mjs';
36
29
  import { createAbortController } from '../../../shared/abort-controller.mjs';
37
- import { isInternalRuntimeNotificationText as contractIsInternalRuntimeNotificationText } from '../../../shared/tool-execution-contract.mjs';
38
30
  import { logLlmCall } from '../../../shared/llm/usage-log.mjs';
39
- import { resolvePluginData, mixdogRoot } from '../../../shared/plugin-paths.mjs';
40
- import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
41
31
  import { appendAgentTrace } from '../agent-trace.mjs';
42
32
  import { isAgentOwner } from '../agent-owner.mjs';
43
- import { maxMtimeRecursive } from '../cache-mtime.mjs';
44
- import { getHiddenRole, getRoleInstructionDir, listHiddenRoleNames } from '../internal-roles.mjs';
33
+ import { getHiddenAgent } from '../internal-agents.mjs';
45
34
  import { DEFAULT_ACTIVITY_HEARTBEAT_MS } from '../stall-policy.mjs';
46
35
  import {
47
36
  buildGatewayLimits,
48
37
  recordGatewayUsageEvent,
49
38
  summarizeGatewayUsage,
50
39
  } from '../providers/statusline-route-meta.mjs';
51
- // Phase B: Pool B Tier 2 content builder (common rules only).
52
- // Loaded once per process via createRequire so the CJS module reaches us.
53
- const _require = createRequire(import.meta.url);
54
- const _rulesBuilder = (() => {
55
- const candidates = [
56
- join(mixdogRoot(), 'lib', 'rules-builder.cjs'),
57
- ].filter(Boolean);
58
- for (const p of candidates) {
59
- try { return _require(p); } catch { /* fall through */ }
60
- }
61
- // Fallback: walk up from this file's location to find lib/rules-builder.cjs.
62
- try { return _require('../../../../lib/rules-builder.cjs'); } catch { return null; }
63
- })();
64
-
65
- // BP1/BP2/BP3 prompt-layer caches — invalidated by source file mtime, not a
66
- // timer. Cheap: O(sentinel-count) stat calls on each session creation, no file
67
- // I/O when warm.
68
- let _sharedRulesCache = null;
69
- let _sharedRulesMtime = 0;
70
- const _agentRulesCacheByProfile = new Map();
71
- let _leadRulesCache = null;
72
- let _leadRulesMtime = 0;
73
- let _leadMetaCache = null;
74
- let _leadMetaMtime = 0;
40
+ // Split modules see manager/ directory. manager.mjs is now a thin facade that
41
+ // orchestrates these cohesive units while keeping the exact public surface.
42
+ import {
43
+ _buildSharedRules,
44
+ _buildAgentRules,
45
+ _buildLeadRules,
46
+ _buildLeadMetaContext,
47
+ _buildAgentSpecific,
48
+ } from './manager/rules-cache.mjs';
49
+ import {
50
+ applyToolPermissionNarrowing,
51
+ finalizeSessionToolList,
52
+ resolveSessionTools,
53
+ previewSessionTools,
54
+ permissionFromToolSpec,
55
+ } from './manager/tool-resolution.mjs';
56
+ import {
57
+ guessContextWindow,
58
+ positiveContextWindow,
59
+ preserveBufferConfigFields,
60
+ resolveSessionContextMeta,
61
+ compactTriggerForSession,
62
+ compactTargetBudget,
63
+ semanticCompactionEnabledForSession,
64
+ compactTypeForSession,
65
+ } from './manager/context-meta.mjs';
66
+ import {
67
+ promptContentText,
68
+ hasModelVisiblePromptContent,
69
+ promptContentBytes,
70
+ prefixUserTurnContent,
71
+ prefixSessionStartContent,
72
+ buildCurrentTimeBlock,
73
+ buildSessionStartBlock,
74
+ hasUserConversationMessage,
75
+ isInternalRuntimeNotificationText,
76
+ } from './manager/prompt-utils.mjs';
77
+ import {
78
+ _mergePendingMessageEntries,
79
+ enqueuePendingMessage,
80
+ drainPendingMessages,
81
+ _dropPendingMessageState,
82
+ } from './manager/pending-messages.mjs';
83
+ // Re-export split-module public/test surface unchanged so every importer of the
84
+ // old symbol names keeps resolving through the facade.
85
+ export { previewSessionTools };
86
+ export { _mergePendingMessageEntries, enqueuePendingMessage, drainPendingMessages };
87
+ export { isInternalRuntimeNotificationText as _isInternalRuntimeNotificationText };
75
88
  let _codeGraphRuntimePromise = null;
76
89
  let _agentLoopPromise = null;
77
90
  let _bashSessionRuntimePromise = null;
@@ -94,127 +107,9 @@ function _closeBashSessionLazy(sessionId, reason) {
94
107
  .then((mod) => { if (typeof mod.closeBashSession === 'function') mod.closeBashSession(sessionId, reason); })
95
108
  .catch(() => {});
96
109
  }
97
- function _buildSharedRules() {
98
- if (!_rulesBuilder || typeof _rulesBuilder.buildSharedToolContent !== 'function') return '';
99
- const PLUGIN_ROOT = mixdogRoot();
100
- const RULES_DIR = join(PLUGIN_ROOT, 'rules');
101
- const mtime = maxMtimeRecursive([
102
- join(RULES_DIR, 'shared'),
103
- ]);
104
- if (_sharedRulesCache !== null && mtime <= _sharedRulesMtime) {
105
- return _sharedRulesCache;
106
- }
107
- try {
108
- const built = _rulesBuilder.buildSharedToolContent({ PLUGIN_ROOT, DATA_DIR: resolvePluginData() });
109
- _sharedRulesCache = built;
110
- _sharedRulesMtime = mtime;
111
- return built;
112
- } catch (e) {
113
- throw new Error(`[session] shared tool rules build failed: ${e.message}`);
114
- }
115
- }
116
-
117
- function _buildAgentRules(profile = 'full') {
118
- if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleContent !== 'function') return '';
119
- const key = String(profile || 'full');
120
- const PLUGIN_ROOT = mixdogRoot();
121
- const DATA_DIR = resolvePluginData();
122
- const RULES_DIR = join(PLUGIN_ROOT, 'rules');
123
- const mtime = maxMtimeRecursive([
124
- join(RULES_DIR, 'agent'),
125
- join(DATA_DIR, 'mixdog-config.json'),
126
- ]);
127
- const cached = _agentRulesCacheByProfile.get(key);
128
- if (cached && mtime <= cached.mtime) {
129
- return cached.value;
130
- }
131
- try {
132
- const built = _rulesBuilder.buildAgentRoleContent({ PLUGIN_ROOT, DATA_DIR, profile: key });
133
- _agentRulesCacheByProfile.set(key, { mtime, value: built });
134
- return built;
135
- } catch (e) {
136
- throw new Error(`[session] agent role rules build failed: ${e.message}`);
137
- }
138
- }
139
-
140
- function _buildLeadRules() {
141
- if (!_rulesBuilder || typeof _rulesBuilder.buildLeadRoleContent !== 'function') return '';
142
- const PLUGIN_ROOT = mixdogRoot();
143
- const DATA_DIR = resolvePluginData();
144
- const RULES_DIR = join(PLUGIN_ROOT, 'rules');
145
- const mtime = maxMtimeRecursive([
146
- join(RULES_DIR, 'lead'),
147
- join(DATA_DIR, 'mixdog-config.json'),
148
- ]);
149
- if (_leadRulesCache !== null && mtime <= _leadRulesMtime) {
150
- return _leadRulesCache;
151
- }
152
- try {
153
- const built = _rulesBuilder.buildLeadRoleContent({ PLUGIN_ROOT, DATA_DIR });
154
- _leadRulesCache = built;
155
- _leadRulesMtime = mtime;
156
- return built;
157
- } catch (e) {
158
- throw new Error(`[session] lead role rules build failed: ${e.message}`);
159
- }
160
- }
161
-
162
- function _buildLeadMetaContext() {
163
- if (!_rulesBuilder || typeof _rulesBuilder.buildLeadMetaContent !== 'function') return '';
164
- const PLUGIN_ROOT = mixdogRoot();
165
- const DATA_DIR = resolvePluginData();
166
- const RULES_DIR = join(PLUGIN_ROOT, 'rules');
167
- const mtime = maxMtimeRecursive([
168
- join(RULES_DIR, 'lead'),
169
- join(DATA_DIR, 'history'),
170
- join(DATA_DIR, 'mixdog-config.json'),
171
- join(DATA_DIR, 'user-workflow.md'),
172
- join(PLUGIN_ROOT, 'output-styles'),
173
- join(DATA_DIR, 'output-styles'),
174
- ]);
175
- if (_leadMetaCache !== null && mtime <= _leadMetaMtime) {
176
- return _leadMetaCache;
177
- }
178
- try {
179
- const built = _rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT, DATA_DIR });
180
- _leadMetaCache = built;
181
- _leadMetaMtime = mtime;
182
- return built;
183
- } catch (e) {
184
- throw new Error(`[session] lead meta context build failed: ${e.message}`);
185
- }
186
- }
187
-
188
- // BP4-adjacent role-specific data cache — keyed by role. webhook / schedule
189
- // roles each have their own scoped instruction set; other roles return ''.
190
- const _roleSpecificCache = new Map(); // role → { value, mtime }
191
- function _buildRoleSpecific(currentRole) {
192
- if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleSpecificContent !== 'function') return '';
193
- if (!currentRole) return '';
194
- const PLUGIN_ROOT = mixdogRoot();
195
- const DATA_DIR = resolvePluginData();
196
- const RULES_DIR = join(PLUGIN_ROOT, 'rules');
197
- const roleInstructionDir = getRoleInstructionDir(currentRole);
198
- const mtime = maxMtimeRecursive([
199
- join(RULES_DIR, 'shared'),
200
- join(DATA_DIR, 'mixdog-config.json'),
201
- join(DATA_DIR, 'webhooks'),
202
- join(DATA_DIR, 'schedules'),
203
- ...(roleInstructionDir ? [join(DATA_DIR, roleInstructionDir)] : []),
204
- join(PLUGIN_ROOT, 'defaults', 'hidden-roles.json'),
205
- ]);
206
- const entry = _roleSpecificCache.get(currentRole);
207
- if (entry && mtime <= entry.mtime) {
208
- return entry.value;
209
- }
210
- try {
211
- const built = _rulesBuilder.buildAgentRoleSpecificContent({ PLUGIN_ROOT, DATA_DIR, currentRole });
212
- _roleSpecificCache.set(currentRole, { mtime, value: built });
213
- return built;
214
- } catch (e) {
215
- throw new Error(`[session] role-specific rules build failed (role: ${currentRole}): ${e.message}`);
216
- }
217
- }
110
+ // Re-export the rules builders so any deep importer of the old symbol names
111
+ // keeps working through the facade.
112
+ export { _buildSharedRules, _buildAgentRules, _buildLeadRules, _buildLeadMetaContext, _buildAgentSpecific };
218
113
 
219
114
  // Agent Runtime is optional — injected via setAgentRuntime() during plugin init
220
115
  // so session creation never depends on a circular import. If never injected,
@@ -259,34 +154,10 @@ const HEARTBEAT_THROTTLE_MS = 60_000; // 60s
259
154
  // notification never fires. A slow write finishes in the background.
260
155
  const TERMINAL_SAVE_TIMEOUT_MS = nonNegativeIntEnv('MIXDOG_TERMINAL_SAVE_TIMEOUT_MS', 5_000);
261
156
 
262
- // Merge externally-connected MCP tools with the plugin's in-process tools
263
- // (registered by agent's toolExecutor adapter). Internal tools are exposed
264
- // under their bare names no mcp__ prefix, since the dispatcher in
265
- // server.mjs handles them directly without a transport.
266
- // Sorted deterministically by name — protects BP_1 hash stability from
267
- // listTools() ordering churn. Anthropic / OpenAI / Gemini all hash the
268
- // tools array verbatim, so any reorder rewrites the prefix.
269
- // No cache: getMcpTools() and getInternalTools() are O(n) in-memory reads;
270
- // the sort overhead on ~30 tools is negligible.
271
- function _getMcpTools() {
272
- const mcp = getMcpTools() || [];
273
- const internalRaw = getInternalTools() || [];
274
- const internal = internalRaw.map(t => ({
275
- name: t.name,
276
- description: typeof t.description === 'string' ? t.description : '',
277
- inputSchema: t.inputSchema || { type: 'object', properties: {} },
278
- // Keep annotations so the permission filter / role invariants can
279
- // tell read-only from write-capable internal tools, and so
280
- // agentHidden can be read during deny filtering.
281
- annotations: t.annotations || {},
282
- }));
283
- return [...mcp, ...internal].sort((a, b) => {
284
- const an = a?.name || '';
285
- const bn = b?.name || '';
286
- return an < bn ? -1 : an > bn ? 1 : 0;
287
- });
288
- }
289
-
157
+ // Session tool resolution + permission narrowing moved to
158
+ // manager/tool-resolution.mjs (imported above). The following section-level
159
+ // docs describe the toolSpec contract those helpers implement.
160
+ //
290
161
  // Phase D-2 — profile.tools resolution.
291
162
  //
292
163
  // `toolSpec` may be:
@@ -309,549 +180,7 @@ function _getMcpTools() {
309
180
  // surface intentionally tiny; runtime permission guards in loop.mjs remain
310
181
  // the fail-safe either way.
311
182
 
312
- const SESSION_ROUTE_TOOL_ORDER = [
313
- 'code_graph',
314
- 'find',
315
- 'glob',
316
- 'list',
317
- 'grep',
318
- 'read',
319
- 'apply_patch',
320
- 'shell',
321
- 'task',
322
- ];
323
- const SESSION_ROUTE_TOOL_RANK = new Map(SESSION_ROUTE_TOOL_ORDER.map((name, index) => [name, index]));
324
- const FILESYSTEM_TOOL_NAMES = new Set([
325
- 'code_graph',
326
- 'find',
327
- 'glob',
328
- 'list',
329
- 'grep',
330
- 'read',
331
- 'apply_patch',
332
- ]);
333
- const READONLY_TOOL_NAMES = new Set([
334
- 'code_graph',
335
- 'find',
336
- 'glob',
337
- 'list',
338
- 'grep',
339
- 'read',
340
- ]);
341
-
342
- const AGENT_STRING_PERMISSION_READ_ALLOW = Object.freeze([
343
- 'code_graph',
344
- 'find',
345
- 'glob',
346
- 'list',
347
- 'grep',
348
- 'read',
349
- 'explore',
350
- 'search',
351
- 'web_fetch',
352
- ]);
353
-
354
- function stringToolPermissionAllowList(toolPermission) {
355
- if (toolPermission === 'read') return AGENT_STRING_PERMISSION_READ_ALLOW;
356
- if (toolPermission === 'read-write') return AGENT_STRING_PERMISSION_READ_WRITE_ALLOW;
357
- if (toolPermission === 'none') return [];
358
- return null;
359
- }
360
-
361
- const AGENT_STRING_PERMISSION_READ_WRITE_ALLOW = Object.freeze([
362
- 'code_graph',
363
- 'find',
364
- 'glob',
365
- 'list',
366
- 'grep',
367
- 'read',
368
- 'apply_patch',
369
- 'explore',
370
- 'search',
371
- 'web_fetch',
372
- ]);
373
-
374
- function applyToolPermissionNarrowing(tools, toolPermission, warnRole = null) {
375
- if (toolPermission === 'none') return [];
376
- const allowList = stringToolPermissionAllowList(toolPermission);
377
- if (allowList) {
378
- const allowSet = new Set(allowList.map((n) => String(n).toLowerCase()));
379
- return tools.filter((t) => allowSet.has(String(t?.name || '').toLowerCase()));
380
- }
381
- if (toolPermission && typeof toolPermission === 'object') {
382
- const allowSet = Array.isArray(toolPermission.allow) && toolPermission.allow.length > 0
383
- ? new Set(toolPermission.allow.map(n => String(n).toLowerCase()))
384
- : null;
385
- const denySet = Array.isArray(toolPermission.deny) && toolPermission.deny.length > 0
386
- ? new Set(toolPermission.deny.map(n => String(n).toLowerCase()))
387
- : null;
388
- if (allowSet || denySet) {
389
- const filtered = tools.filter(t => {
390
- const name = String(t?.name || '').toLowerCase();
391
- if (denySet && denySet.has(name)) return false;
392
- if (allowSet && !allowSet.has(name)) return false;
393
- return true;
394
- });
395
- if (filtered.length === 0) {
396
- process.stderr.write(`[session] WARN: role permission intersection produced 0 tools — failing closed (role=${warnRole || 'unknown'})\n`);
397
- }
398
- return filtered;
399
- }
400
- }
401
- return tools;
402
- }
403
-
404
- function recursiveWrapperToolNameForPublicAgentRole(role) {
405
- if (!role) return null;
406
- const key = String(role).trim();
407
- for (const hiddenName of listHiddenRoleNames()) {
408
- const def = getHiddenRole(hiddenName);
409
- const invokedBy = typeof def?.invokedBy === 'string' ? def.invokedBy.trim() : '';
410
- if (invokedBy && invokedBy === key) return invokedBy;
411
- }
412
- return null;
413
- }
414
-
415
- function finalizeSessionToolList(tools, {
416
- schemaAllowedTools = null,
417
- disallowedTools = null,
418
- ownerIsAgent = false,
419
- resolvedRole = null,
420
- } = {}) {
421
- let out = Array.isArray(tools) ? tools : [];
422
- const hasCallerAllow = Array.isArray(schemaAllowedTools);
423
- if (hasCallerAllow) {
424
- const allowSet = new Set(schemaAllowedTools.map(n => String(n).toLowerCase()));
425
- out = out.filter(t => allowSet.has(String(t?.name || '').toLowerCase()));
426
- }
427
- const callerDeny = Array.isArray(disallowedTools) ? disallowedTools.map(n => String(n)) : [];
428
- if (callerDeny.length) {
429
- const denySet = new Set(callerDeny.map(n => n.toLowerCase()));
430
- out = out.filter(t => !denySet.has(String(t?.name || '').toLowerCase()));
431
- }
432
- const recursiveDeny = ownerIsAgent ? recursiveWrapperToolNameForPublicAgentRole(resolvedRole) : null;
433
- if (recursiveDeny) {
434
- const deny = recursiveDeny.toLowerCase();
435
- out = out.filter(t => String(t?.name || '').toLowerCase() !== deny);
436
- }
437
- if (ownerIsAgent) {
438
- out = out.filter(t => !t?.annotations?.agentHidden);
439
- out = orderSessionTools(out);
440
- }
441
- return out;
442
- }
443
-
444
- function orderSessionTools(tools) {
445
- return tools.map((tool, index) => ({ tool, index }))
446
- .sort((a, b) => {
447
- const ar = SESSION_ROUTE_TOOL_RANK.get(a.tool?.name) ?? 10_000;
448
- const br = SESSION_ROUTE_TOOL_RANK.get(b.tool?.name) ?? 10_000;
449
- if (ar !== br) return ar - br;
450
- return a.index - b.index;
451
- })
452
- .map((entry) => entry.tool);
453
- }
454
-
455
- const ALL_BUILTIN_SESSION_TOOLS = orderSessionTools(_dedupByName([
456
- ...BUILTIN_TOOLS,
457
- ...PATCH_TOOL_DEFS,
458
- ...CODE_GRAPH_TOOL_DEFS,
459
- ]));
460
-
461
- function resolveSessionTools(toolSpec, skills, { ownerIsAgentSession = false } = {}) {
462
- const mcp = _getMcpTools();
463
- // Agent sessions freeze the skill meta-tool into the schema
464
- // unconditionally — concrete skill resolution is cwd-scoped at tool-call
465
- // time (loop.mjs), so the schema bytes stay bit-identical across roles /
466
- // cwds and the provider cache shard does not fragment.
467
- const skillTools = buildSkillToolDefs(skills, { ownerIsAgentSession });
468
- return _computeBaseTools(toolSpec, mcp, skillTools);
469
- }
470
-
471
- export function previewSessionTools(toolSpec, skills = [], options = {}) {
472
- return resolveSessionTools(toolSpec, skills, options);
473
- }
474
-
475
- // Dedup by name, first occurrence wins. BUILTIN_TOOLS is passed in ahead
476
- // of the MCP-registered internal tools so plugin-side definitions take
477
- // precedence when both surfaces declare the same name (e.g. read / grep / glob).
478
- // Without this merge, Anthropic rejected the request with
479
- // "tools: Tool names must be unique" and the orchestrator burned up to
480
- // 20 iterations retrying before the final answer landed.
481
- function _dedupByName(tools) {
482
- const seen = new Map();
483
- for (const t of tools) {
484
- const n = t?.name;
485
- if (!n || seen.has(n)) continue;
486
- seen.set(n, t);
487
- }
488
- return [...seen.values()];
489
- }
490
-
491
- // Agent visibility is declared per-tool via annotations.agentHidden.
492
- // Tools with agentHidden:true are stripped from agent sessions at schema
493
- // build time (see deny filtering below). No code-level name list needed.
494
-
495
- function _computeBaseTools(toolSpec, mcp, skillTools) {
496
- if (Array.isArray(toolSpec)) {
497
- if (toolSpec.length === 0) {
498
- // Explicit "no tools" — skill meta tools still travel so the model
499
- // can at least discover and invoke skills if that is the one
500
- // dynamic surface the profile retains.
501
- return _dedupByName([...skillTools]);
502
- }
503
- if (toolSpec.includes('full')) {
504
- return _dedupByName([...ALL_BUILTIN_SESSION_TOOLS, ...mcp, ...skillTools]);
505
- }
506
- const byName = new Map();
507
- const add = (tool) => { if (tool?.name && !byName.has(tool.name)) byName.set(tool.name, tool); };
508
- const addMany = (arr) => { for (const t of arr) add(t); };
509
- for (const tagRaw of toolSpec) {
510
- const tag = String(tagRaw || '').trim();
511
- switch (tag) {
512
- case 'tools:filesystem':
513
- addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => FILESYSTEM_TOOL_NAMES.has(t.name)));
514
- break;
515
- case 'tools:readonly':
516
- addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => READONLY_TOOL_NAMES.has(t.name)));
517
- break;
518
- case 'tools:shell':
519
- case 'tools:git':
520
- case 'tools:analysis':
521
- // Shell-class toolset. `tools:git` / `tools:analysis` exist so
522
- // profile authors can name the intent (git workflows / data
523
- // analysis) without inventing new toolset ids.
524
- addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => t.name === 'shell' || t.name === 'task'));
525
- break;
526
- case 'tools:mcp':
527
- addMany(mcp);
528
- break;
529
- case 'tools:search':
530
- // Name-pattern match: picks up `search` and any future tool
531
- // whose name contains `search`. `recall` and `explore` deliberately do NOT match
532
- // — they need `tools:mcp` (full mcp surface) or their own
533
- // toolset id if a role wants targeted retrieval. Public agent
534
- // roles never reach the wrapper bodies regardless: see the
535
- // isBlockedPublicWrapperCall guard in session/loop.mjs.
536
- addMany(mcp.filter(t => /search/i.test(t?.name || '')));
537
- break;
538
- default:
539
- process.stderr.write(`[session] unknown toolset id "${tag}" (profile.tools); skipping\n`);
540
- }
541
- }
542
- return _dedupByName([...byName.values(), ...skillTools]);
543
- }
544
-
545
- switch (toolSpec) {
546
- case 'mcp':
547
- return _dedupByName([...mcp, ...skillTools]);
548
- case 'readonly': {
549
- const readTools = ALL_BUILTIN_SESSION_TOOLS.filter(t => READONLY_TOOL_NAMES.has(t.name));
550
- return _dedupByName([...readTools, ...mcp, ...skillTools]);
551
- }
552
- case 'full':
553
- default:
554
- return _dedupByName([...ALL_BUILTIN_SESSION_TOOLS, ...mcp, ...skillTools]);
555
- }
556
- }
557
-
558
- function permissionFromToolSpec(toolSpec) {
559
- if (toolSpec === 'readonly') return 'read';
560
- if (toolSpec === 'mcp') return 'mcp';
561
- if (Array.isArray(toolSpec)) {
562
- const tags = new Set(toolSpec.map(t => String(t || '').trim()));
563
- const hasWriteOrShell = tags.has('full')
564
- || tags.has('tools:filesystem')
565
- || tags.has('tools:shell')
566
- || tags.has('tools:git')
567
- || tags.has('tools:analysis');
568
- if (tags.has('tools:readonly') && !hasWriteOrShell) return 'read';
569
- }
570
- return null;
571
- }
572
-
573
183
  let nextId = Date.now();
574
- // Known context windows for the current-generation models this plugin
575
- // routes to. Anything not listed falls through to guessContextWindow() —
576
- // local llama/mistral/phi default to 8192, everything else 128000. Keep
577
- // this map trimmed to live models; older generations slow down reads
578
- // without buying anything.
579
- const CONTEXT_WINDOWS = {
580
- // OpenAI GPT-5.x family (openai / openai-oauth)
581
- 'gpt-5.5': 272000,
582
- 'gpt-5.4': 272000,
583
- 'gpt-5.4-mini': 272000,
584
- 'gpt-5.4-nano': 272000,
585
- // Anthropic Claude 4.x
586
- 'claude-opus-4-8': 1000000,
587
- 'claude-opus-4-7': 1000000,
588
- 'claude-sonnet-4-6': 1000000,
589
- 'claude-haiku-4-5-20251001': 200000,
590
- // Google Gemini 3.x
591
- 'gemini-3.1-pro': 1000000,
592
- 'gemini-3-pro': 1000000,
593
- 'gemini-3.5-flash': 1000000,
594
- 'gemini-3-flash': 1000000,
595
- // xAI Grok (catalog polyfill mirror — model-catalog PRICING_OVERRIDES)
596
- 'grok-build-0.1': 256000,
597
- 'grok-4.20': 1000000,
598
- };
599
- // Family-pattern fallback used only when both the provider catalog and the
600
- // exact-id table miss (cold metadata, before the LiteLLM/models.dev catalog
601
- // warms). Keep these aligned with the catalog so /context, gateway, and the
602
- // runtime agree on the boundary the first time a model is routed. Local models
603
- // (llama/mistral/phi/qwen/gemma) stay small so an unknown local id never claims
604
- // a giant window.
605
- function guessContextWindow(model) {
606
- if (CONTEXT_WINDOWS[model])
607
- return CONTEXT_WINDOWS[model];
608
- const m = String(model || '').toLowerCase();
609
- // Local/self-hosted families — never inflate an unknown local id.
610
- if (m.includes('llama') || m.includes('mistral') || m.includes('mixtral')
611
- || m.includes('phi') || m.includes('qwen') || m.includes('gemma')
612
- || m.includes('deepseek-r1') || m.includes('codellama'))
613
- return 8192;
614
- // Current hosted families by name pattern.
615
- if (m.startsWith('claude-opus') || m.startsWith('claude-sonnet')) return 1000000;
616
- if (m.startsWith('claude-haiku') || m.startsWith('claude-')) return 200000;
617
- if (m.startsWith('gemini-3') || m.startsWith('gemini-2')) return 1000000;
618
- if (m.startsWith('gpt-5')) return 272000;
619
- if (m.startsWith('grok-build')) return 256000;
620
- if (m.startsWith('grok-')) return 1000000;
621
- if (m.startsWith('deepseek-v')) return 1000000;
622
- return 128000;
623
- }
624
- function positiveContextWindow(value) {
625
- const n = Number(value);
626
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
627
- }
628
- function envFlag(name, fallback = false) {
629
- const v = process.env[name];
630
- if (v === undefined) return fallback;
631
- return !['0', 'false', 'off', 'no'].includes(String(v).trim().toLowerCase());
632
- }
633
- function boundedPercent(value, fallback = null) {
634
- const n = Number(value);
635
- if (Number.isFinite(n) && n > 0 && n <= 100) return n;
636
- return fallback;
637
- }
638
- function providerNameOf(provider) {
639
- if (typeof provider === 'string') return provider.toLowerCase();
640
- return String(provider?.name || provider?.id || '').toLowerCase();
641
- }
642
- // Buffer-percent parsing trap: normalizeCompactionBufferRatio treats any value
643
- // > 1 as a percent (n/100) and any value <= 1 as a literal ratio. That is wrong
644
- // for percent-NAMED inputs: bufferPercent / bufferPct / *_BUFFER_PERCENT = 1
645
- // means 1% (0.01), but the shared normalizer would read it as the literal ratio
646
- // 1.0 (100%). Resolve percent-named and ratio-named inputs with the correct
647
- // semantics here, before handing a finished ratio to the shared helper:
648
- // percent inputs: n -> n/100 (1 -> 0.01, 10 -> 0.10)
649
- // ratio inputs: n -> n>1 ? n/100 : n (0.01 -> 0.01, 10 -> 0.10 legacy)
650
- function resolveBufferRatioCandidate(percentInputs, ratioInputs) {
651
- for (const raw of percentInputs) {
652
- const n = Number(raw);
653
- if (Number.isFinite(n) && n > 0) return Math.min(1, n / 100);
654
- }
655
- for (const raw of ratioInputs) {
656
- const n = Number(raw);
657
- if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
658
- }
659
- return null;
660
- }
661
- function compactBufferRatioForConfig(cfg = {}) {
662
- const resolved = resolveBufferRatioCandidate(
663
- [cfg.bufferPercent, cfg.bufferPct, process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT],
664
- [cfg.bufferRatio, cfg.bufferFraction, process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO],
665
- );
666
- return normalizeCompactionBufferRatio(resolved, DEFAULT_COMPACTION_BUFFER_RATIO);
667
- }
668
- function compactBufferRatioForSession(session) {
669
- return compactBufferRatioForConfig(session?.compaction || {});
670
- }
671
- // Carry the percent/ratio-named buffer config from a compaction config object
672
- // onto session.compaction so downstream parsers (compactBufferRatioForSession,
673
- // loop.resolveCompactBufferRatio, contextStatus) honor a configured buffer
674
- // percent/ratio. Only finite positive values are copied; absent fields stay
675
- // undefined so the default-ratio fallback still applies.
676
- function preserveBufferConfigFields(cfg = {}) {
677
- const out = {};
678
- for (const key of ['bufferPercent', 'bufferPct', 'bufferRatio', 'bufferFraction']) {
679
- const n = Number(cfg?.[key]);
680
- if (Number.isFinite(n) && n > 0) out[key] = n;
681
- }
682
- return out;
683
- }
684
- const LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
685
- function isPersistedZeroBufferTelemetry(cfg = {}, boundaryTokens = 0) {
686
- const boundary = positiveContextWindow(boundaryTokens);
687
- if (!boundary) return false;
688
- if (positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_BUFFER_TOKENS)) return false;
689
- if (Number.isFinite(Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT)) && Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT) > 0) return false;
690
- if (Number.isFinite(Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO)) && Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO) > 0) return false;
691
- for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
692
- const n = Number(cfg?.[key]);
693
- if (Number.isFinite(n) && n > 0) return false;
694
- }
695
- const ratio = Number(cfg?.bufferRatio);
696
- if (Number.isFinite(ratio) && ratio > 0) return false;
697
- const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
698
- if (!Number.isFinite(explicitTokens) || explicitTokens !== 0) return false;
699
- return true;
700
- }
701
- function isLegacyDefaultBufferTelemetry(cfg = {}, boundaryTokens = 0) {
702
- const boundary = positiveContextWindow(boundaryTokens);
703
- if (!boundary) return false;
704
- if (positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_BUFFER_TOKENS)) return false;
705
- if (Number.isFinite(Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT)) && Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT) > 0) return false;
706
- if (Number.isFinite(Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO)) && Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO) > 0) return false;
707
- // Percent/fraction-named fields are treated as operator config. The legacy
708
- // default telemetry always persisted bufferTokens + bufferRatio after a
709
- // check/compact pass, so only that shape is migrated away.
710
- for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
711
- const n = Number(cfg?.[key]);
712
- if (Number.isFinite(n) && n > 0) return false;
713
- }
714
- const explicitTokens = positiveContextWindow(cfg?.bufferTokens ?? cfg?.buffer);
715
- const ratio = Number(cfg?.bufferRatio);
716
- if (!explicitTokens || !Number.isFinite(ratio) || Math.abs(ratio - LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO) > 1e-9) return false;
717
- const expectedTokens = Math.floor(boundary * LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO);
718
- const cfgBoundary = positiveContextWindow(cfg?.boundaryTokens);
719
- const cfgTrigger = positiveContextWindow(cfg?.triggerTokens);
720
- return explicitTokens === expectedTokens
721
- || (cfgBoundary === boundary && cfgTrigger > 0 && explicitTokens === Math.max(0, boundary - cfgTrigger));
722
- }
723
- function compactBufferConfigForBoundary(cfg = {}, boundaryTokens = 0) {
724
- const base = cfg || {};
725
- if (!isLegacyDefaultBufferTelemetry(base, boundaryTokens)
726
- && !isPersistedZeroBufferTelemetry(base, boundaryTokens)) {
727
- return base;
728
- }
729
- return {
730
- ...base,
731
- bufferTokens: null,
732
- buffer: null,
733
- bufferRatio: null,
734
- };
735
- }
736
- function compactBufferTokensForSession(session, boundaryTokens) {
737
- const cfg = compactBufferConfigForBoundary(session?.compaction || {}, boundaryTokens);
738
- const explicit = positiveContextWindow(cfg.bufferTokens ?? cfg.buffer)
739
- || positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_BUFFER_TOKENS)
740
- || 0;
741
- return compactionBufferTokensForBoundary(boundaryTokens, {
742
- explicitTokens: explicit,
743
- ratio: compactBufferRatioForConfig(cfg),
744
- maxRatio: 0.25,
745
- });
746
- }
747
- const COMPACT_TARGET_RATIO = 0.02;
748
- const COMPACT_TARGET_MIN_TOKENS = 4_000;
749
- const COMPACT_TARGET_MAX_TOKENS = 16_000;
750
- function compactTargetRatio() {
751
- const raw = process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
752
- ?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
753
- ?? COMPACT_TARGET_RATIO;
754
- const n = Number(raw);
755
- if (!Number.isFinite(n) || n <= 0) return COMPACT_TARGET_RATIO;
756
- return n > 1 ? n / 100 : n;
757
- }
758
- function compactTargetTokensForBoundary(boundaryTokens) {
759
- const boundary = positiveContextWindow(boundaryTokens);
760
- if (!boundary) return null;
761
- const explicit = positiveContextWindow(
762
- process.env.MIXDOG_AGENT_COMPACT_TARGET_TOKENS
763
- ?? process.env.MIXDOG_COMPACT_TARGET_TOKENS,
764
- );
765
- if (explicit) return Math.max(1, Math.min(boundary, explicit));
766
- const minTarget = Math.min(boundary, positiveContextWindow(process.env.MIXDOG_COMPACT_TARGET_MIN_TOKENS) || COMPACT_TARGET_MIN_TOKENS);
767
- const maxTarget = Math.min(boundary, positiveContextWindow(process.env.MIXDOG_COMPACT_TARGET_MAX_TOKENS) || COMPACT_TARGET_MAX_TOKENS);
768
- const byRatio = Math.max(1, Math.floor(boundary * compactTargetRatio()));
769
- return Math.max(1, Math.min(boundary, maxTarget, Math.max(minTarget, byRatio)));
770
- }
771
- function defaultEffectiveContextWindowPercent(provider) {
772
- // Gateway/statusline route metadata reserves a universal 10% headroom from
773
- // the raw catalog window. Keep session compaction on the same effective
774
- // capacity so /context, the TUI statusline, and gateway telemetry agree.
775
- return 90;
776
- }
777
- function resolveSessionContextMeta(provider, model, seed = {}) {
778
- const info = typeof provider?.getCachedModelInfo === 'function'
779
- ? provider.getCachedModelInfo(model)
780
- : null;
781
- const catalogInfo = getModelMetadataSync(model, providerNameOf(provider));
782
- const rawContextWindow = positiveContextWindow(info?.contextWindow)
783
- || positiveContextWindow(info?.maxContextWindow)
784
- || positiveContextWindow(info?.context_window)
785
- || positiveContextWindow(info?.max_context_window)
786
- || positiveContextWindow(catalogInfo?.contextWindow)
787
- || positiveContextWindow(catalogInfo?.maxContextWindow)
788
- || positiveContextWindow(catalogInfo?.context_window)
789
- || positiveContextWindow(catalogInfo?.max_context_window)
790
- || positiveContextWindow(seed.rawContextWindow)
791
- || positiveContextWindow(seed.raw_context_window)
792
- || positiveContextWindow(seed.contextWindow)
793
- || guessContextWindow(model);
794
- const effectiveContextWindowPercent = boundedPercent(
795
- seed.effectiveContextWindowPercent
796
- ?? seed.effective_context_window_percent
797
- ?? info?.effectiveContextWindowPercent
798
- ?? info?.effective_context_window_percent
799
- ?? catalogInfo?.effectiveContextWindowPercent
800
- ?? catalogInfo?.effective_context_window_percent,
801
- defaultEffectiveContextWindowPercent(provider),
802
- );
803
- const pct = boundedPercent(effectiveContextWindowPercent, 100);
804
- const contextWindow = Math.max(1, Math.floor(rawContextWindow * pct / 100));
805
- const compactBoundaryTokens = contextWindow;
806
- const rawCompactLimit = positiveContextWindow(
807
- seed.autoCompactTokenLimit
808
- ?? seed.auto_compact_token_limit
809
- ?? info?.autoCompactTokenLimit
810
- ?? info?.auto_compact_token_limit
811
- ?? catalogInfo?.autoCompactTokenLimit
812
- ?? catalogInfo?.auto_compact_token_limit,
813
- );
814
- // Legacy-data migration: old implementations derived autoCompactTokenLimit
815
- // from the full effective/raw window and persisted it onto the session.
816
- // A resumed session therefore re-seeds autoCompactTokenLimit == boundary
817
- // (or the raw window), which compactTriggerForSession / loop policy used to
818
- // honor as an explicit trigger, collapsing the compaction buffer to 0. Only
819
- // accept an explicit limit that is STRICTLY BELOW the boundary; a value at
820
- // or above the boundary is a derived full-window artifact and is dropped to
821
- // null so the trigger falls back to the default boundary trigger.
822
- const explicitCompactLimit = rawCompactLimit && rawCompactLimit < compactBoundaryTokens
823
- ? rawCompactLimit
824
- : null;
825
- // Do NOT derive the auto-compact limit from the full effective window.
826
- // Setting it to contextWindow makes autoTriggerTokens == boundary and the
827
- // compaction buffer collapse to 0 (loop.mjs:708-713 / compactTriggerForSession),
828
- // so auto-compact only fires when the context is already at the limit —
829
- // at which point semantic compact fails ("result exceeds budget" /
830
- // "summary cannot fit") and the turn can no longer be resumed.
831
- // Leave it null unless the provider/catalog/seed supplies an explicit
832
- // limit; the downstream buffer logic (default 10%, capped 25%) then
833
- // triggers compaction with headroom, matching the reference auto-compact threshold.
834
- const autoCompactTokenLimit = explicitCompactLimit || null;
835
- return {
836
- contextWindow,
837
- rawContextWindow,
838
- effectiveContextWindowPercent,
839
- autoCompactTokenLimit: autoCompactTokenLimit || null,
840
- compactBoundaryTokens,
841
- };
842
- }
843
- function compactTriggerForSession(session, boundaryTokens) {
844
- const boundary = positiveContextWindow(boundaryTokens);
845
- if (!boundary) return null;
846
- const autoLimit = positiveContextWindow(session?.autoCompactTokenLimit ?? session?.compaction?.autoCompactTokenLimit);
847
- // Only honor an explicit auto-compact limit that sits STRICTLY BELOW the
848
- // boundary. A persisted value == boundary (or >=) is a legacy derived
849
- // full-window artifact; honoring it collapses the compaction buffer to 0,
850
- // so fall through to the default boundary trigger instead.
851
- if (autoLimit && autoLimit < boundary) return Math.max(1, autoLimit);
852
- const buffer = compactBufferTokensForSession(session, boundary);
853
- return Math.max(1, boundary - buffer);
854
- }
855
184
  // Test-only exports for the legacy auto-compact-limit migration + buffer-config
856
185
  // preservation (see scripts/compact-trigger-migration-smoke.mjs).
857
186
  export const _resolveSessionContextMeta = resolveSessionContextMeta;
@@ -874,40 +203,18 @@ function normalizeStaleCompactingStage(session) {
874
203
  c.lastCheckedAt = Date.now();
875
204
  return true;
876
205
  }
877
- function compactTargetBudget(boundaryTokens, reserveTokens, _sourceTokens = null, _ratio = null) {
878
- const boundary = positiveContextWindow(boundaryTokens);
879
- if (!boundary) return null;
880
- const reserve = Math.max(0, Number(reserveTokens) || 0);
881
- const targetEffective = compactTargetTokensForBoundary(boundary) || boundary;
882
- return Math.max(1, Math.min(boundary, targetEffective + reserve));
883
- }
884
- function semanticCompactionEnabledForSession(session) {
885
- const cfg = session?.compaction || {};
886
- if (process.env.MIXDOG_AGENT_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_AGENT_COMPACT_SEMANTIC', true);
887
- if (process.env.MIXDOG_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_COMPACT_SEMANTIC', true);
888
- if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
889
- if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on' || cfg.semantic === 'auto') return true;
890
- return true;
891
- }
892
- function compactTypeForSession(session) {
893
- const cfg = session?.compaction || {};
894
- const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
895
- ?? process.env.MIXDOG_COMPACT_TYPE
896
- ?? cfg.type
897
- ?? cfg.compactType
898
- ?? cfg.compact_type;
899
- return normalizeCompactType(configured, DEFAULT_COMPACT_TYPE);
900
- }
901
206
  function addCompactUsageToSession(session, usage) {
902
207
  if (!session || !usage) return;
903
208
  const inputTokens = usage.inputTokens || 0;
904
209
  const outputTokens = usage.outputTokens || 0;
905
210
  const cachedTokens = usage.cachedTokens || 0;
906
211
  const cacheWriteTokens = usage.cacheWriteTokens || 0;
212
+ const uncachedInputTokens = uncachedInputTokensForProvider(session.provider, inputTokens, cachedTokens, cacheWriteTokens);
907
213
  session.totalInputTokens = (session.totalInputTokens || 0) + inputTokens;
908
214
  session.totalOutputTokens = (session.totalOutputTokens || 0) + outputTokens;
909
215
  session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + cachedTokens;
910
216
  session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + cacheWriteTokens;
217
+ session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
911
218
  session.tokensCumulative = (session.tokensCumulative || 0) + inputTokens + outputTokens;
912
219
  }
913
220
  async function runRecallFastTrackForSession(session, messages, opts = {}) {
@@ -977,6 +284,18 @@ async function runRecallFastTrackForSession(session, messages, opts = {}) {
977
284
  }
978
285
  return { query, querySha, recallText: [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n') };
979
286
  }
287
+ // Element-identity change detection (same approach as loop.mjs messagesArrayChanged): two
288
+ // arrays are "unchanged" only when same length AND every slot is the same object
289
+ // reference. Used to reject a no-op prune (which returns a fresh array whose
290
+ // elements are the untouched originals) from being accepted as a recovery.
291
+ function messagesChanged(before, after) {
292
+ if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
293
+ if (before.length !== after.length) return true;
294
+ for (let i = 0; i < before.length; i += 1) {
295
+ if (before[i] !== after[i]) return true;
296
+ }
297
+ return false;
298
+ }
980
299
  async function runSessionCompaction(session, opts = {}) {
981
300
  if (!session || session.closed === true) return null;
982
301
  const mode = opts.mode === 'auto' ? 'auto' : 'manual';
@@ -991,7 +310,16 @@ async function runSessionCompaction(session, opts = {}) {
991
310
  if (force) throw new Error('compact: no context window is available for this session');
992
311
  return null;
993
312
  }
994
- const reserveTokens = estimateRequestReserveTokens(session.tools || []);
313
+ // Reserve must mirror loop.mjs (buildCompactPolicy): request reserve (tool
314
+ // schema) PLUS the configured reserve (session.compaction.reservedTokens or
315
+ // MIXDOG_AGENT_COMPACT_RESERVED_TOKENS env). The old request-only value left
316
+ // the manual / auto-clear compact budget without the configured headroom the
317
+ // loop path reserves, so a compacted transcript could overflow on next send.
318
+ const requestReserveTokens = estimateRequestReserveTokens(session.tools || []);
319
+ const configuredReserveTokens = positiveContextWindow(session.compaction?.reservedTokens)
320
+ || positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_RESERVED_TOKENS)
321
+ || 0;
322
+ const reserveTokens = requestReserveTokens + configuredReserveTokens;
995
323
  const beforeMessageTokens = estimateMessagesTokens(messages);
996
324
  const triggerTokens = compactTriggerForSession(session, boundary)
997
325
  || positiveContextWindow(session.compaction?.triggerTokens)
@@ -1041,7 +369,11 @@ async function runSessionCompaction(session, opts = {}) {
1041
369
  recallText: recallPayload.recallText,
1042
370
  query: recallPayload.query,
1043
371
  querySha: recallPayload.querySha,
1044
- allowEmptyRecall: true,
372
+ // Ingest just ran on the live transcript, so an empty recall dump
373
+ // means the memory pipeline is broken — do NOT erase history
374
+ // behind an empty summary shell. Empty recall now throws and is
375
+ // handled by the semantic fallback below (or recorded failure).
376
+ allowEmptyRecall: false,
1045
377
  tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
1046
378
  keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
1047
379
  preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
@@ -1055,6 +387,48 @@ async function runSessionCompaction(session, opts = {}) {
1055
387
  try {
1056
388
  process.stderr.write(`[session] recall-fasttrack ${mode} compact failed (sess=${session.id || 'unknown'}): ${err?.message || err}\n`);
1057
389
  } catch { /* best-effort */ }
390
+ // Degraded-compact fallback: recall-fasttrack failed (empty recall,
391
+ // ingest error, fit failure). Before recording a hard failure, try
392
+ // the semantic path once so auto-clear/manual compaction still makes
393
+ // progress WITHOUT shipping an empty-recall summary. History is only
394
+ // replaced when the semantic summary actually succeeds.
395
+ if (semanticCompactionEnabledForSession(session)
396
+ && provider && typeof provider.send === 'function') {
397
+ try {
398
+ semanticCompactResult = await semanticCompactMessages(
399
+ provider,
400
+ messages,
401
+ opts.model || session.model,
402
+ budget,
403
+ {
404
+ reserveTokens,
405
+ providerName: session.provider || provider?.name || null,
406
+ sessionId: opts.sessionId || session.id || null,
407
+ signal: opts.signal || null,
408
+ promptCacheKey: session.promptCacheKey || null,
409
+ providerCacheKey: session.promptCacheKey || null,
410
+ timeoutMs: positiveContextWindow(session.compaction?.timeoutMs) || 30_000,
411
+ tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
412
+ keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
413
+ preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
414
+ force: true,
415
+ },
416
+ );
417
+ if (Array.isArray(semanticCompactResult?.messages)) {
418
+ compacted = semanticCompactResult.messages;
419
+ compactError = null;
420
+ addCompactUsageToSession(session, semanticCompactResult.usage);
421
+ try {
422
+ process.stderr.write(`[session] degraded compact: recall-fasttrack failed, semantic fallback succeeded (sess=${session.id || 'unknown'}, mode=${mode})\n`);
423
+ } catch { /* best-effort */ }
424
+ }
425
+ } catch (fallbackErr) {
426
+ semanticCompactError = fallbackErr;
427
+ try {
428
+ process.stderr.write(`[session] degraded compact: semantic fallback also failed (sess=${session.id || 'unknown'}): ${fallbackErr?.message || fallbackErr}\n`);
429
+ } catch { /* best-effort */ }
430
+ }
431
+ }
1058
432
  }
1059
433
  } else if (compactTypeIsSemantic(compactType)) {
1060
434
  try {
@@ -1098,6 +472,32 @@ async function runSessionCompaction(session, opts = {}) {
1098
472
  if (!compacted && !compactError) {
1099
473
  compactError = new Error(`${compactType} compact produced no messages`);
1100
474
  }
475
+ // Anchor-independent prune safety net (mirror loop.mjs compact catch): when a
476
+ // non-recall (semantic) compact failed, try one non-LLM prune that needs no
477
+ // user anchor before recording failure, so Lead manual / auto-clear paths
478
+ // recover the same transcripts the loop path does. Gated off the recall
479
+ // path — a recall failure keeps its original contract (no silent prune).
480
+ if (!compacted && !recallFastTrackError) {
481
+ try {
482
+ const acceptThreshold = compactEffectiveBudget(budget, { reserveTokens });
483
+ const salvaged = pruneToolOutputsUnanchored(messages, budget, { reserveTokens });
484
+ // pruneToolOutputsUnanchored ALWAYS returns a fresh reconciled array
485
+ // (never the input identity), so `salvaged !== messages` is always
486
+ // true and cannot detect a no-op. Compare by element identity so a
487
+ // transcript that already fit (nothing pruned) is NOT falsely accepted
488
+ // as a recovery — that would clear compactError and unconditionally
489
+ // invalidate providerState for an unchanged transcript.
490
+ if (Array.isArray(salvaged)
491
+ && messagesChanged(messages, salvaged)
492
+ && estimateMessagesTokens(salvaged) <= acceptThreshold) {
493
+ compacted = salvaged;
494
+ compactError = null;
495
+ try {
496
+ process.stderr.write(`[session] compact fallback prune recovered (sess=${session.id || 'unknown'}, mode=${mode})\n`);
497
+ } catch { /* best-effort */ }
498
+ }
499
+ } catch { /* fall through to failure record */ }
500
+ }
1101
501
  if (!compacted) {
1102
502
  const now = Date.now();
1103
503
  session.compaction = {
@@ -1506,7 +906,7 @@ async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
1506
906
  // Preset shape: { name, provider, model, effort?, fast?, tools? }
1507
907
  //
1508
908
  // Agent Runtime integration:
1509
- // opts.taskType / opts.role / opts.profileId — enables profile-aware routing.
909
+ // opts.taskType / opts.agent / opts.profileId — enables profile-aware routing.
1510
910
  // Rule-based SmartRouter resolves these synchronously; the resolved
1511
911
  // profile controls context filtering (skip.skills/memory/etc) and cache
1512
912
  // strategy. If no rule matches, falls back to classic preset behavior.
@@ -1519,13 +919,13 @@ export function createSession(opts) {
1519
919
  // --- Agent Runtime profile resolution (best-effort, sync) ---
1520
920
  let profile = opts.profile || null;
1521
921
  let providerCacheOpts = opts.providerCacheOpts || null;
1522
- if (!profile && (opts.taskType || opts.role || opts.profileId)) {
922
+ if (!profile && (opts.taskType || opts.agent || opts.profileId)) {
1523
923
  const agentRuntime = getAgentRuntimeSync();
1524
924
  if (agentRuntime) {
1525
925
  try {
1526
926
  const resolved = agentRuntime.resolveSync({
1527
927
  taskType: opts.taskType,
1528
- role: opts.role,
928
+ agent: opts.agent,
1529
929
  profileId: opts.profileId,
1530
930
  preset: presetObj?.name || (typeof opts.preset === 'string' ? opts.preset : null),
1531
931
  provider: opts.provider || presetObj?.provider,
@@ -1567,25 +967,26 @@ export function createSession(opts) {
1567
967
  const id = `sess_${process.pid}_${nextId++}_${Date.now()}_${randomBytes(16).toString('hex')}`;
1568
968
  const messages = [];
1569
969
  const ownerIsAgent = isAgentOwner(opts.owner);
1570
- const resolvedRole = opts.role || profile?.taskType || null;
1571
- const hiddenRole = getHiddenRole(resolvedRole);
1572
- const isRetrievalRole = hiddenRole?.kind === 'retrieval';
1573
- // Skill schema is fixed; the compact manifest is discovered once through
1574
- // the mtime-cached frontmatter index so BP1 tells every role which Skill()
1575
- // names are available without loading full SKILL.md bodies.
1576
- const skills = opts.skipSkills ? [] : collectSkillsCached(opts.cwd);
970
+ const resolvedAgent = opts.agent || opts.role || profile?.taskType || null;
971
+ const hiddenAgent = getHiddenAgent(resolvedAgent);
972
+ const isRetrievalAgent = hiddenAgent?.kind === 'retrieval';
973
+ // Skill schema is fixed for public agent sessions, but hidden retrieval /
974
+ // maintenance roles are deliberately narrowed away from the Skill tool.
975
+ // Do not leak a Skill manifest into those hidden prompts when no Skill()
976
+ // loader is available.
977
+ const skills = (opts.skipSkills || hiddenAgent) ? [] : collectSkillsCached(opts.cwd);
1577
978
 
1578
979
  // BP1 is shared tool policy (+ compact skill manifest in compose). BP2 is
1579
980
  // role/system rules. User-defined schedules/webhooks ride as normal user
1580
981
  // context below so event data does not rewrite BP3 memory/meta.
1581
- const agentRulesRole = opts.role || profile?.taskType || null;
1582
- const agentRulesProfile = isRetrievalRole ? 'retrieval' : 'full';
982
+ const agentRulesAgent = opts.agent || opts.role || profile?.taskType || null;
983
+ const agentRulesProfile = isRetrievalAgent ? 'retrieval' : 'full';
1583
984
  const skipAgentRules = opts.skipAgentRules === true;
1584
985
  // Retrieval roles already inject a compact # Tool Use via BP2; skip the full BP1 shared tool policy to avoid duplicating it.
1585
- const injectedRules = (skipAgentRules || isRetrievalRole) ? '' : _buildSharedRules();
986
+ const injectedRules = (skipAgentRules || isRetrievalAgent) ? '' : _buildSharedRules();
1586
987
  const roleRules = skipAgentRules ? '' : (ownerIsAgent ? _buildAgentRules(agentRulesProfile) : _buildLeadRules());
1587
988
  const metaContext = skipAgentRules ? '' : (ownerIsAgent ? '' : _buildLeadMetaContext());
1588
- const roleSpecific = ownerIsAgent && !skipAgentRules ? _buildRoleSpecific(agentRulesRole) : '';
989
+ const roleSpecific = ownerIsAgent && !skipAgentRules ? _buildAgentSpecific(agentRulesAgent) : '';
1589
990
  // Agent sessions must not inherit role/profile/preset tool narrowing: Pool
1590
991
  // B and Pool C share one bit-identical tool schema to maximize provider
1591
992
  // prefix reuse, and permission differences are enforced only at call time. Raw
@@ -1612,7 +1013,7 @@ export function createSession(opts) {
1612
1013
  // fail closed (zero tools) rather than silently falling back to the full
1613
1014
  // preset, which would grant the role more surface than declared.
1614
1015
  if (ownerIsAgent) {
1615
- toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, toolPermission, opts.role || null);
1016
+ toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, toolPermission, opts.agent || null);
1616
1017
  }
1617
1018
 
1618
1019
  const { baseRules, stableSystemContext, sessionMarker, volatileTail } = composeSystemPrompt({
@@ -1622,7 +1023,7 @@ export function createSession(opts) {
1622
1023
  metaContext: metaContext || undefined,
1623
1024
  skipRoleCatalog: !ownerIsAgent,
1624
1025
  profile: profile || undefined,
1625
- role: resolvedRole,
1026
+ agent: resolvedAgent,
1626
1027
  workflowContext: opts.workflowContext || null,
1627
1028
  workspaceContext: opts.workspaceContext || null,
1628
1029
  coreMemoryContext: opts.coreMemoryContext || null,
@@ -1678,17 +1079,17 @@ export function createSession(opts) {
1678
1079
  const hasCallerAllow = Array.isArray(opts.schemaAllowedTools);
1679
1080
  const tools = finalizeSessionToolList(toolsForRouting, {
1680
1081
  schemaAllowedTools: hasCallerAllow ? opts.schemaAllowedTools : null,
1681
- disallowedTools: opts.disallowedTools,
1082
+ disallowedTools: hiddenAgent ? [...(Array.isArray(opts.disallowedTools) ? opts.disallowedTools : []), 'Skill'] : opts.disallowedTools,
1682
1083
  ownerIsAgent,
1683
- resolvedRole,
1084
+ resolvedAgent,
1684
1085
  });
1685
1086
 
1686
1087
  // Unified-shard policy — no broad role-specific schema filter. Keep
1687
1088
  // agent schemas shared unless a hidden-role schema profile explicitly
1688
1089
  // passes schemaAllowedTools for a small specialist; broad role
1689
1090
  // whitelists would fragment the cache shard.
1690
- if (resolvedRole && process.env.MIXDOG_DEBUG_SESSION_LOG) {
1691
- process.stderr.write(`[session] role=${resolvedRole} permission=${permission || 'full'} toolPermission=${toolPermission || 'full'} tools=${tools.length}\n`);
1091
+ if (resolvedAgent && process.env.MIXDOG_DEBUG_SESSION_LOG) {
1092
+ process.stderr.write(`[session] agent=${resolvedAgent} permission=${permission || 'full'} toolPermission=${toolPermission || 'full'} tools=${tools.length}\n`);
1692
1093
  }
1693
1094
  const contextMeta = resolveSessionContextMeta(provider, modelName);
1694
1095
  const workflowMeta = opts.workflow && typeof opts.workflow === 'object' && String(opts.workflow.id || '').trim()
@@ -1758,7 +1159,6 @@ export function createSession(opts) {
1758
1159
  // agent sessions past RUNNING_STALL_MS.
1759
1160
  lastUsedAt: Date.now(),
1760
1161
  tokensCumulative: 0,
1761
- role: opts.role || null,
1762
1162
  taskType: opts.taskType || null,
1763
1163
  maxLoopIterations: Number.isFinite(opts.maxLoopIterations) ? opts.maxLoopIterations : null,
1764
1164
  // Agent tag (auto worker{n} on spawn) persisted so the forked status
@@ -2203,6 +1603,18 @@ export function usageMetricsIdempotencyKey(sessionId, session, delta = {}) {
2203
1603
  return `${sessionId}:${turnId}:${epoch}:${delta.iterationIndex}:${source}`;
2204
1604
  }
2205
1605
 
1606
+ function uncachedInputTokensForProvider(provider, inputTokens, cachedReadTokens = 0, cacheWriteTokens = 0) {
1607
+ const input = Number(inputTokens) || 0;
1608
+ if (input <= 0) return 0;
1609
+ // Anthropic-style providers report input_tokens excluding cache reads; OpenAI
1610
+ // Responses/Gemini-style providers report input_tokens inclusive of cached
1611
+ // prefix tokens. Keep both views so UI can show the real context footprint
1612
+ // and the fresh/new token portion without mistaking cache hits for a cache
1613
+ // break.
1614
+ if (providerInputExcludesCache(provider)) return input + (Number(cacheWriteTokens) || 0);
1615
+ return Math.max(input - (Number(cachedReadTokens) || 0) - (Number(cacheWriteTokens) || 0), 0);
1616
+ }
1617
+
2206
1618
  /**
2207
1619
  * Apply terminal ask usage to session totals. Skips lifetime totals when incremental
2208
1620
  * per-iteration persistence already counted this turn (askSession path).
@@ -2211,23 +1623,38 @@ export function applyAskTerminalUsageTotals(session, result, options = {}) {
2211
1623
  if (!session || !result?.usage) return;
2212
1624
  const skipTotals = options.skipTotalsIfIncremental === true;
2213
1625
  if (!skipTotals) {
2214
- session.totalInputTokens = (session.totalInputTokens || 0) + (result.usage.inputTokens || 0);
2215
- session.totalOutputTokens = (session.totalOutputTokens || 0) + (result.usage.outputTokens || 0);
1626
+ const inputTokens = result.usage.inputTokens || 0;
1627
+ const outputTokens = result.usage.outputTokens || 0;
1628
+ const cachedTokens = result.usage.cachedTokens || 0;
1629
+ const cacheWriteTokens = result.usage.cacheWriteTokens || 0;
1630
+ const uncachedInputTokens = uncachedInputTokensForProvider(session.provider, inputTokens, cachedTokens, cacheWriteTokens);
1631
+ session.totalInputTokens = (session.totalInputTokens || 0) + inputTokens;
1632
+ session.totalOutputTokens = (session.totalOutputTokens || 0) + outputTokens;
2216
1633
  session.tokensCumulative = (session.tokensCumulative || 0)
2217
- + (result.usage.inputTokens || 0)
2218
- + (result.usage.outputTokens || 0);
2219
- session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + (result.usage.cachedTokens || 0);
2220
- session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + (result.usage.cacheWriteTokens || 0);
1634
+ + inputTokens
1635
+ + outputTokens;
1636
+ session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + cachedTokens;
1637
+ session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + cacheWriteTokens;
1638
+ session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
2221
1639
  }
2222
1640
  const _lastTurn = result.lastTurnUsage || result.usage || {};
2223
- session.lastInputTokens = _lastTurn.inputTokens || 0;
1641
+ const _lastInputTokens = _lastTurn.inputTokens || 0;
1642
+ const _lastCachedReadTokens = _lastTurn.cachedTokens || 0;
1643
+ const _lastCacheWriteTokens = _lastTurn.cacheWriteTokens || 0;
1644
+ session.lastInputTokens = _lastInputTokens;
2224
1645
  session.lastOutputTokens = _lastTurn.outputTokens || 0;
2225
- session.lastCachedReadTokens = _lastTurn.cachedTokens || 0;
2226
- session.lastCacheWriteTokens = _lastTurn.cacheWriteTokens || 0;
1646
+ session.lastCachedReadTokens = _lastCachedReadTokens;
1647
+ session.lastCacheWriteTokens = _lastCacheWriteTokens;
1648
+ session.lastUncachedInputTokens = uncachedInputTokensForProvider(
1649
+ session.provider,
1650
+ _lastInputTokens,
1651
+ _lastCachedReadTokens,
1652
+ _lastCacheWriteTokens,
1653
+ );
2227
1654
  const _inputExcludesCache = providerInputExcludesCache(session.provider);
2228
1655
  session.lastContextTokens = _inputExcludesCache
2229
- ? (_lastTurn.inputTokens || 0) + (_lastTurn.cachedTokens || 0)
2230
- : (_lastTurn.inputTokens || 0);
1656
+ ? _lastInputTokens + _lastCachedReadTokens + _lastCacheWriteTokens
1657
+ : _lastInputTokens;
2231
1658
  session.lastContextTokensUpdatedAt = Date.now();
2232
1659
  session.lastContextTokensStaleAfterCompact = false;
2233
1660
  }
@@ -2257,6 +1684,9 @@ export async function persistIterationMetrics(delta) {
2257
1684
  seen.add(ikey);
2258
1685
  if (!isReplay) {
2259
1686
  if (runtimeEntry) runtimeEntry.usageMetricsTurnIncremental = true;
1687
+ const deltaUncachedInput = delta.deltaUncachedInput != null
1688
+ ? Number(delta.deltaUncachedInput) || 0
1689
+ : uncachedInputTokensForProvider(session.provider, deltaInput, deltaCachedRead, deltaCacheWrite);
2260
1690
  session.totalInputTokens = (session.totalInputTokens || 0) + (deltaInput || 0);
2261
1691
  session.totalOutputTokens = (session.totalOutputTokens || 0) + (deltaOutput || 0);
2262
1692
  session.tokensCumulative = (session.tokensCumulative || 0) + (deltaInput || 0) + (deltaOutput || 0);
@@ -2266,12 +1696,15 @@ export async function persistIterationMetrics(delta) {
2266
1696
  // includes cached_read / cache_write in its terminal usage rollup).
2267
1697
  session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + (deltaCachedRead || 0);
2268
1698
  session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + (deltaCacheWrite || 0);
1699
+ session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + deltaUncachedInput;
2269
1700
  // Window snapshot updated per iteration so agent type=list reflects the
2270
1701
  // most-recent provider-reported input size even for short dispatches
2271
1702
  // that finish before askSession's terminal save lands.
2272
1703
  session.lastInputTokens = deltaInput || 0;
2273
1704
  session.lastOutputTokens = deltaOutput || 0;
2274
1705
  session.lastCachedReadTokens = deltaCachedRead || 0;
1706
+ session.lastCacheWriteTokens = deltaCacheWrite || 0;
1707
+ session.lastUncachedInputTokens = deltaUncachedInput;
2275
1708
  // Normalized last-call context footprint: how many prompt tokens the
2276
1709
  // model actually saw on the most-recent send, comparable ACROSS
2277
1710
  // providers. Anthropic reports input_tokens EXCLUDING cache (cache_read
@@ -2280,7 +1713,7 @@ export async function persistIterationMetrics(delta) {
2280
1713
  // tokens INTO the input count, so input alone is the footprint.
2281
1714
  const _inputExcludesCache = providerInputExcludesCache(session.provider);
2282
1715
  session.lastContextTokens = _inputExcludesCache
2283
- ? (deltaInput || 0) + (deltaCachedRead || 0)
1716
+ ? (deltaInput || 0) + (deltaCachedRead || 0) + (deltaCacheWrite || 0)
2284
1717
  : (deltaInput || 0);
2285
1718
  session.lastContextTokensUpdatedAt = ts || Date.now();
2286
1719
  session.lastContextTokensStaleAfterCompact = false;
@@ -2515,363 +1948,6 @@ const _sessionLocks = new Map();
2515
1948
  // queue contract, two call sites. Rich content is kept in memory for the live
2516
1949
  // relay path; the disk mirror stores only a text fallback so image bytes do not
2517
1950
  // leak into the pending-message JSON.
2518
- const _sessionPendingMessages = new Map();
2519
- const PENDING_MESSAGES_FILE = 'session-pending-messages.json';
2520
- const PENDING_MESSAGES_MODE = 0o600;
2521
- const _pendingPersistBuffers = new Map();
2522
- let _pendingPersistImmediate = null;
2523
-
2524
- function pendingMessagesPath() {
2525
- return join(resolvePluginData(), PENDING_MESSAGES_FILE);
2526
- }
2527
-
2528
- function isValidPendingSessionId(sessionId) {
2529
- return typeof sessionId === 'string' && /^[A-Za-z0-9_-]+$/.test(sessionId);
2530
- }
2531
-
2532
- function normalizePendingStore(raw) {
2533
- const sessions = raw && typeof raw === 'object' && raw.sessions && typeof raw.sessions === 'object'
2534
- ? raw.sessions
2535
- : {};
2536
- const out = { version: 1, updatedAt: Date.now(), sessions: {} };
2537
- for (const [sid, value] of Object.entries(sessions)) {
2538
- if (!isValidPendingSessionId(sid) || !Array.isArray(value)) continue;
2539
- const q = value
2540
- .map((entry) => {
2541
- if (typeof entry === 'string') return entry;
2542
- if (entry && typeof entry === 'object' && typeof entry.message === 'string') return entry.message;
2543
- return '';
2544
- })
2545
- .filter(Boolean);
2546
- if (q.length > 0) out.sessions[sid] = q;
2547
- }
2548
- return out;
2549
- }
2550
-
2551
- function normalizePendingMessageEntry(entry) {
2552
- if (typeof entry === 'string') {
2553
- const text = entry.trim();
2554
- return text ? { content: text, text } : null;
2555
- }
2556
- if (Array.isArray(entry)) {
2557
- if (entry.length === 0) return null;
2558
- const text = promptContentText(entry).trim();
2559
- return { content: entry, text };
2560
- }
2561
- if (!entry || typeof entry !== 'object') return null;
2562
- const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : null;
2563
- if (content == null) return null;
2564
- const text = typeof entry.text === 'string' ? entry.text.trim() : promptContentText(content).trim();
2565
- if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
2566
- if (typeof content === 'string') {
2567
- const value = content.trim();
2568
- return value ? { content: value, text: text || value } : null;
2569
- }
2570
- const fallback = promptContentText(content).trim();
2571
- return fallback ? { content: fallback, text: text || fallback } : null;
2572
- }
2573
-
2574
- function pendingMessageText(entry) {
2575
- const normalized = normalizePendingMessageEntry(entry);
2576
- return normalized ? String(normalized.text || promptContentText(normalized.content) || '').trim() : '';
2577
- }
2578
-
2579
- function pendingMessageQueueEntry(entry) {
2580
- const normalized = normalizePendingMessageEntry(entry);
2581
- if (!normalized) return null;
2582
- if (typeof normalized.content === 'string' && normalized.content === normalized.text) return normalized.content;
2583
- return { content: normalized.content, text: normalized.text || promptContentText(normalized.content).trim() };
2584
- }
2585
-
2586
- function persistPendingMessages(sessionId, messages) {
2587
- if (!isValidPendingSessionId(sessionId)) return 0;
2588
- const persistedMessages = (Array.isArray(messages) ? messages : [messages])
2589
- .map(pendingMessageText)
2590
- .filter(Boolean);
2591
- if (persistedMessages.length === 0) return 0;
2592
- let depth = 0;
2593
- try {
2594
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
2595
- const next = normalizePendingStore(raw);
2596
- const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
2597
- q.push(...persistedMessages);
2598
- next.sessions[sessionId] = q;
2599
- next.updatedAt = Date.now();
2600
- depth = q.length;
2601
- return next;
2602
- }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
2603
- } catch (err) {
2604
- try { process.stderr.write(`[session] pending-message persist failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
2605
- }
2606
- return depth;
2607
- }
2608
-
2609
- function flushPendingMessagePersistsSync() {
2610
- if (_pendingPersistImmediate) {
2611
- try { clearImmediate(_pendingPersistImmediate); } catch {}
2612
- _pendingPersistImmediate = null;
2613
- }
2614
- if (_pendingPersistBuffers.size === 0) return;
2615
- const batches = [..._pendingPersistBuffers.entries()];
2616
- _pendingPersistBuffers.clear();
2617
- for (const [sid, messages] of batches) {
2618
- persistPendingMessages(sid, messages);
2619
- }
2620
- }
2621
-
2622
- function schedulePendingMessagePersist(sessionId, message) {
2623
- if (!isValidPendingSessionId(sessionId)) return 0;
2624
- const persistedMessage = pendingMessageText(message);
2625
- if (!persistedMessage) return 0;
2626
- const q = _pendingPersistBuffers.get(sessionId) || [];
2627
- q.push(persistedMessage);
2628
- _pendingPersistBuffers.set(sessionId, q);
2629
- if (!_pendingPersistImmediate) {
2630
- _pendingPersistImmediate = setImmediate(() => {
2631
- _pendingPersistImmediate = null;
2632
- flushPendingMessagePersistsSync();
2633
- });
2634
- }
2635
- return q.length;
2636
- }
2637
-
2638
- function takeBufferedPendingMessages(sessionId) {
2639
- if (!isValidPendingSessionId(sessionId)) return [];
2640
- const buffered = _pendingPersistBuffers.get(sessionId);
2641
- if (!buffered || buffered.length === 0) return [];
2642
- _pendingPersistBuffers.delete(sessionId);
2643
- return buffered.slice();
2644
- }
2645
-
2646
- function drainPersistedPendingMessages(sessionId) {
2647
- if (!isValidPendingSessionId(sessionId)) return [];
2648
- let drained = [];
2649
- try {
2650
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
2651
- const next = normalizePendingStore(raw);
2652
- const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
2653
- drained = q.filter((m) => typeof m === 'string' && m.length > 0);
2654
- if (drained.length === 0) return undefined;
2655
- delete next.sessions[sessionId];
2656
- next.updatedAt = Date.now();
2657
- return next;
2658
- }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
2659
- } catch (err) {
2660
- try { process.stderr.write(`[session] pending-message drain failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
2661
- }
2662
- return drained;
2663
- }
2664
-
2665
- export function enqueuePendingMessage(sessionId, message) {
2666
- const entry = pendingMessageQueueEntry(message);
2667
- if (!sessionId || !entry) return 0;
2668
- let q = _sessionPendingMessages.get(sessionId);
2669
- if (!q) { q = []; _sessionPendingMessages.set(sessionId, q); }
2670
- q.push(entry);
2671
- const bufferedDepth = schedulePendingMessagePersist(sessionId, entry);
2672
- return Math.max(q.length, bufferedDepth || 0);
2673
- }
2674
- export function drainPendingMessages(sessionId) {
2675
- const q = _sessionPendingMessages.get(sessionId);
2676
- const memory = q && q.length > 0 ? q.slice() : [];
2677
- _sessionPendingMessages.delete(sessionId);
2678
- const persisted = [...takeBufferedPendingMessages(sessionId), ...drainPersistedPendingMessages(sessionId)];
2679
- const memoryVisible = modelVisiblePendingMessages(memory);
2680
- const persistedVisible = modelVisiblePendingMessages(persisted);
2681
- if (memoryVisible.length === 0) return persistedVisible;
2682
- if (persistedVisible.length === 0) return memoryVisible;
2683
- const persistedTexts = persistedVisible.map(pendingMessageText);
2684
- const prefixMatches = memoryVisible.every((m, i) => persistedTexts[i] === pendingMessageText(m));
2685
- if (prefixMatches) return [...memoryVisible, ...persistedVisible.slice(memoryVisible.length)];
2686
- const out = persistedVisible.slice();
2687
- const seen = new Set(persistedTexts);
2688
- for (const m of memoryVisible) {
2689
- const text = pendingMessageText(m);
2690
- if (!text || seen.has(text)) continue;
2691
- out.push(m);
2692
- seen.add(text);
2693
- }
2694
- return out;
2695
- }
2696
-
2697
- function promptContentText(content) {
2698
- if (typeof content === 'string') return content;
2699
- if (Array.isArray(content)) {
2700
- return content.map((part) => {
2701
- if (typeof part === 'string') return part;
2702
- if (part?.type === 'text') return part.text || '';
2703
- if (part?.type === 'image') return '[Image]';
2704
- return part?.text || '';
2705
- }).filter(Boolean).join('\n');
2706
- }
2707
- return String(content ?? '');
2708
- }
2709
-
2710
- function hasModelVisiblePromptContent(prompt) {
2711
- return !!promptContentText(prompt).trim();
2712
- }
2713
-
2714
- function promptContentBytes(content) {
2715
- try {
2716
- if (typeof content === 'string') return Buffer.byteLength(content, 'utf8');
2717
- return Buffer.byteLength(JSON.stringify(content), 'utf8');
2718
- } catch {
2719
- return Buffer.byteLength(promptContentText(content), 'utf8');
2720
- }
2721
- }
2722
-
2723
- function prefixUserTurnContent(content, contextBlock) {
2724
- if (!contextBlock) return content;
2725
- if (Array.isArray(content)) {
2726
- return [{ type: 'text', text: `${contextBlock}# Task\n` }, ...content];
2727
- }
2728
- return `${contextBlock}# Task\n${content}`;
2729
- }
2730
-
2731
- function prefixSessionStartContent(content, sessionBlock) {
2732
- if (!sessionBlock) return content;
2733
- if (Array.isArray(content)) {
2734
- return [{ type: 'text', text: `${sessionBlock}\n\n` }, ...content];
2735
- }
2736
- return `${sessionBlock}\n\n${content}`;
2737
- }
2738
-
2739
- function localIsoDate(date = new Date()) {
2740
- const year = date.getFullYear();
2741
- const month = String(date.getMonth() + 1).padStart(2, '0');
2742
- const day = String(date.getDate()).padStart(2, '0');
2743
- return `${year}-${month}-${day}`;
2744
- }
2745
-
2746
- function localDateTimeWithZone(date = new Date()) {
2747
- const datePart = localIsoDate(date);
2748
- const hh = String(date.getHours()).padStart(2, '0');
2749
- const mm = String(date.getMinutes()).padStart(2, '0');
2750
- const ss = String(date.getSeconds()).padStart(2, '0');
2751
- let zone = '';
2752
- try { zone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch {}
2753
- return zone ? `${datePart} ${hh}:${mm}:${ss} ${zone}` : `${datePart} ${hh}:${mm}:${ss}`;
2754
- }
2755
-
2756
- function temporalPromptText(content) {
2757
- const text = promptContentText(content)
2758
- .replace(/\s+/g, ' ')
2759
- .trim()
2760
- .toLowerCase();
2761
- return text;
2762
- }
2763
-
2764
- function promptNeedsDateReminder(content) {
2765
- const text = temporalPromptText(content);
2766
- if (!text) return false;
2767
- return /(?:오늘|내일|어제|모레|그저께|요즘|최근|방금|아까|현재\s*(?:날짜|시간|시각)|지금\s*(?:몇\s*시|시간|날짜|요일)|몇\s*월\s*몇\s*일|몇\s*시|무슨\s*요일|요일|날짜|이번\s*(?:주|달|월|년)|지난\s*(?:주|달|월|년)|다음\s*(?:주|달|월|년)|올해|작년|내년|today|tomorrow|yesterday|recently|current\s+(?:date|time)|what\s+(?:date|time)|which\s+day|weekday|this\s+(?:week|month|year)|last\s+(?:week|month|year)|next\s+(?:week|month|year))/i.test(text);
2768
- }
2769
-
2770
- function promptNeedsTimeReminder(content) {
2771
- const text = temporalPromptText(content);
2772
- if (!text) return false;
2773
- return /(?:현재\s*(?:시간|시각)|지금\s*(?:몇\s*시|시간)|몇\s*시|시각|시간|current\s+time|what\s+time|time\s+is\s+it)/i.test(text);
2774
- }
2775
-
2776
- function buildCurrentTimeBlock(content) {
2777
- const needsTime = promptNeedsTimeReminder(content);
2778
- if (!needsTime && !promptNeedsDateReminder(content)) return '';
2779
- return localDateTimeWithZone(new Date());
2780
- }
2781
-
2782
- function sessionModelDisplay(model) {
2783
- const text = String(model || '').trim();
2784
- if (!text) return '';
2785
- return text
2786
- .replace(/-\d{4}-\d{2}-\d{2}$/, '')
2787
- .replace(/^gpt-/i, 'GPT-')
2788
- .replace(/(?:^|-)([a-z])/g, (m) => m.toUpperCase());
2789
- }
2790
-
2791
- function buildSessionStartBlock(session, cwd) {
2792
- if (!session || session.owner === 'agent') return '';
2793
- const lines = ['# Session'];
2794
- const effectiveCwd = String(cwd || session.cwd || '').trim();
2795
- if (effectiveCwd) lines.push(`Cwd: ${effectiveCwd}`);
2796
- const modelBits = [
2797
- sessionModelDisplay(session.model),
2798
- session.effort ? String(session.effort).trim().toUpperCase() : '',
2799
- session.fast === true ? 'FAST' : '',
2800
- ].filter(Boolean);
2801
- if (modelBits.length) lines.push(`Model: ${modelBits.join(' · ')}`);
2802
- const workflowName = String(session.workflow?.name || session.workflow?.id || '').trim();
2803
- if (workflowName) lines.push(`Workflow: ${workflowName}`);
2804
- return lines.length > 1 ? lines.join('\n') : '';
2805
- }
2806
-
2807
- function isReferenceFilesMessage(message) {
2808
- return message?.role === 'user'
2809
- && typeof message.content === 'string'
2810
- && /^Reference files:\s*/i.test(message.content.trimStart());
2811
- }
2812
-
2813
- function isProtectedContextUserMessage(message) {
2814
- return message?.role === 'user'
2815
- && typeof message.content === 'string'
2816
- && message.content.trimStart().startsWith('<system-reminder>');
2817
- }
2818
-
2819
- function hasUserConversationMessage(messages) {
2820
- return (Array.isArray(messages) ? messages : []).some((message) => (
2821
- message?.role === 'user'
2822
- && !isProtectedContextUserMessage(message)
2823
- && !isReferenceFilesMessage(message)
2824
- ));
2825
- }
2826
-
2827
- function modelVisiblePendingMessages(messages) {
2828
- return (Array.isArray(messages) ? messages : [])
2829
- .map(pendingMessageQueueEntry)
2830
- .filter(Boolean)
2831
- .filter((message) => !isInternalRuntimeNotificationText(
2832
- message && typeof message === 'object' && Object.prototype.hasOwnProperty.call(message, 'content')
2833
- ? message.content
2834
- : message,
2835
- ));
2836
- }
2837
-
2838
- export function _mergePendingMessageEntries(entries) {
2839
- const normalized = (Array.isArray(entries) ? entries : [])
2840
- .map(normalizePendingMessageEntry)
2841
- .filter(Boolean);
2842
- if (normalized.length === 0) return null;
2843
- const displayText = normalized.map((entry) => entry.text || promptContentText(entry.content))
2844
- .filter((text) => String(text || '').trim())
2845
- .join('\n');
2846
- if (normalized.every((entry) => typeof entry.content === 'string')) {
2847
- return {
2848
- content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
2849
- text: displayText,
2850
- count: normalized.length,
2851
- };
2852
- }
2853
- const parts = [];
2854
- for (const entry of normalized) {
2855
- if (typeof entry.content === 'string') {
2856
- if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
2857
- } else if (Array.isArray(entry.content)) {
2858
- parts.push(...entry.content);
2859
- } else {
2860
- const text = promptContentText(entry.content);
2861
- if (text.trim()) parts.push({ type: 'text', text });
2862
- }
2863
- parts.push({ type: 'text', text: '\n' });
2864
- }
2865
- while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
2866
- return { content: parts, text: displayText || promptContentText(parts), count: normalized.length };
2867
- }
2868
-
2869
- function isInternalRuntimeNotificationText(content) {
2870
- return contractIsInternalRuntimeNotificationText(promptContentText(content));
2871
- }
2872
-
2873
- export const _isInternalRuntimeNotificationText = isInternalRuntimeNotificationText;
2874
-
2875
1951
  function isInternalCancelledAssistantMessage(message) {
2876
1952
  if (!message || message.role !== 'assistant') return false;
2877
1953
  if (message.cancelled === true) return true;
@@ -3439,7 +2515,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
3439
2515
  logLlmCall({
3440
2516
  ts: new Date().toISOString(),
3441
2517
  sourceType: session.sourceType || 'lead',
3442
- sourceName: session.sourceName || session.role || null,
2518
+ sourceName: session.sourceName || session.agent || null,
3443
2519
  preset: session.presetName || null,
3444
2520
  model: session.model,
3445
2521
  provider: session.provider,
@@ -3660,13 +2736,13 @@ export async function resumeSession(sessionId, preset) {
3660
2736
  }
3661
2737
  let toolsForRouting = resolveSessionTools(toolSpec, skills, { ownerIsAgentSession: ownerIsAgent });
3662
2738
  if (ownerIsAgent) {
3663
- toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.role || null);
2739
+ toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.agent || null);
3664
2740
  }
3665
2741
  session.tools = finalizeSessionToolList(toolsForRouting, {
3666
2742
  schemaAllowedTools: Array.isArray(session.schemaAllowedTools) ? session.schemaAllowedTools : null,
3667
- disallowedTools: null,
2743
+ disallowedTools: getHiddenAgent(session.agent || null) ? ['Skill'] : null,
3668
2744
  ownerIsAgent,
3669
- resolvedRole: session.role || null,
2745
+ resolvedAgent: session.agent || null,
3670
2746
  });
3671
2747
  const newTools = session.tools;
3672
2748
  const missing = oldTools.filter(t => !newTools.find(n => n.name === t.name));
@@ -3911,6 +2987,10 @@ export function closeSession(id, reason = 'manual') {
3911
2987
  // or Map entries across session lifetime. Fire-and-forget — close path
3912
2988
  // should not await disk IO; errors are swallowed inside.
3913
2989
  try { clearOffloadSession(id); } catch { /* ignore */ }
2990
+ // Drop the in-memory pending-message queue and any buffered-persist entry
2991
+ // for this session — otherwise both Maps accumulate one entry per closed
2992
+ // session for the life of the mcp-server.
2993
+ _dropPendingMessageState(id);
3914
2994
  // 4. Defer runtime map clear to next tick so any settling askSession can
3915
2995
  // observe `closed=true` / bumped generation before we yank the entry.
3916
2996
  // Disk tombstone remains — that's what blocks resurrection.