mixdog 0.9.1 → 0.9.2

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 (214) hide show
  1. package/package.json +8 -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/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/task-bench.mjs +207 -0
  23. package/scripts/tool-failures.mjs +6 -6
  24. package/scripts/tool-smoke.mjs +306 -66
  25. package/scripts/toolcall-args-test.mjs +81 -0
  26. package/src/agents/debugger/AGENT.md +4 -4
  27. package/src/agents/heavy-worker/AGENT.md +4 -2
  28. package/src/agents/reviewer/AGENT.md +4 -4
  29. package/src/agents/worker/AGENT.md +4 -2
  30. package/src/app.mjs +10 -6
  31. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  32. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  33. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  34. package/src/headless-role.mjs +14 -14
  35. package/src/help.mjs +1 -0
  36. package/src/lib/rules-builder.cjs +32 -54
  37. package/src/mixdog-session-runtime.mjs +710 -318
  38. package/src/output-styles/default.md +12 -7
  39. package/src/output-styles/minimal.md +25 -0
  40. package/src/output-styles/oneline.md +21 -0
  41. package/src/output-styles/simple.md +10 -9
  42. package/src/repl.mjs +12 -2
  43. package/src/rules/agent/00-common.md +7 -5
  44. package/src/rules/agent/30-explorer.md +7 -8
  45. package/src/rules/lead/01-general.md +3 -1
  46. package/src/rules/lead/lead-tool.md +7 -0
  47. package/src/rules/shared/01-tool.md +17 -12
  48. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  51. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  52. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  53. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  54. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  55. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  56. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  57. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  58. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  59. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  60. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  62. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +332 -57
  63. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +59 -32
  64. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  65. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  66. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  67. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  68. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  69. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +78 -3
  70. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +202 -98
  71. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +183 -20
  72. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  73. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  74. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  75. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -9
  76. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  77. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  78. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  80. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  81. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  82. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  83. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  84. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  85. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  86. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  87. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  88. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  89. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  90. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  91. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  92. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  93. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  95. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  96. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  97. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  99. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  100. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  101. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  102. package/src/runtime/channels/backends/discord.mjs +99 -9
  103. package/src/runtime/channels/backends/telegram.mjs +501 -0
  104. package/src/runtime/channels/index.mjs +441 -1224
  105. package/src/runtime/channels/lib/config.mjs +54 -2
  106. package/src/runtime/channels/lib/format.mjs +4 -2
  107. package/src/runtime/channels/lib/output-forwarder.mjs +80 -67
  108. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  109. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  110. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  111. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  112. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  113. package/src/runtime/channels/lib/webhook.mjs +59 -31
  114. package/src/runtime/channels/tool-defs.mjs +1 -1
  115. package/src/runtime/memory/index.mjs +184 -19
  116. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  117. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  118. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  119. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  120. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  121. package/src/runtime/memory/lib/memory.mjs +101 -4
  122. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  123. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  124. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  125. package/src/runtime/memory/tool-defs.mjs +6 -3
  126. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  127. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  128. package/src/runtime/shared/config.mjs +9 -0
  129. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  130. package/src/runtime/shared/schedules-store.mjs +21 -19
  131. package/src/runtime/shared/tool-surface.mjs +98 -13
  132. package/src/runtime/shared/transcript-writer.mjs +129 -0
  133. package/src/runtime/shared/update-checker.mjs +214 -0
  134. package/src/standalone/agent-tool.mjs +255 -109
  135. package/src/standalone/channel-admin.mjs +133 -40
  136. package/src/standalone/channel-worker.mjs +8 -291
  137. package/src/standalone/explore-tool.mjs +2 -2
  138. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  139. package/src/standalone/provider-admin.mjs +11 -0
  140. package/src/standalone/usage-dashboard.mjs +1 -1
  141. package/src/tui/App.jsx +2096 -732
  142. package/src/tui/components/ConfirmBar.jsx +47 -0
  143. package/src/tui/components/ContextPanel.jsx +5 -3
  144. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  145. package/src/tui/components/Markdown.jsx +22 -98
  146. package/src/tui/components/Message.jsx +14 -35
  147. package/src/tui/components/Picker.jsx +87 -12
  148. package/src/tui/components/PromptInput.jsx +83 -7
  149. package/src/tui/components/QueuedCommands.jsx +1 -1
  150. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  151. package/src/tui/components/Spinner.jsx +7 -7
  152. package/src/tui/components/StatusLine.jsx +40 -21
  153. package/src/tui/components/TextEntryPanel.jsx +51 -7
  154. package/src/tui/components/ToolExecution.jsx +170 -98
  155. package/src/tui/components/TurnDone.jsx +4 -4
  156. package/src/tui/components/UsagePanel.jsx +1 -1
  157. package/src/tui/components/tool-output-format.mjs +159 -21
  158. package/src/tui/components/tool-output-format.test.mjs +87 -0
  159. package/src/tui/display-width.mjs +69 -0
  160. package/src/tui/display-width.test.mjs +35 -0
  161. package/src/tui/dist/index.mjs +6965 -2391
  162. package/src/tui/engine.mjs +287 -126
  163. package/src/tui/index.jsx +117 -7
  164. package/src/tui/keyboard-protocol.mjs +42 -0
  165. package/src/tui/lib/voice-recorder.mjs +453 -0
  166. package/src/tui/markdown/format-token.mjs +129 -76
  167. package/src/tui/markdown/format-token.test.mjs +61 -19
  168. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  169. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  170. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  171. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  172. package/src/tui/markdown/table-layout.mjs +9 -9
  173. package/src/tui/paste-attachments.mjs +0 -11
  174. package/src/tui/prompt-history-store.mjs +129 -0
  175. package/src/tui/prompt-history-store.test.mjs +52 -0
  176. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  177. package/src/tui/theme.mjs +41 -657
  178. package/src/tui/themes/base.mjs +86 -0
  179. package/src/tui/themes/basic.mjs +85 -0
  180. package/src/tui/themes/catppuccin.mjs +72 -0
  181. package/src/tui/themes/dracula.mjs +70 -0
  182. package/src/tui/themes/everforest.mjs +71 -0
  183. package/src/tui/themes/gruvbox.mjs +71 -0
  184. package/src/tui/themes/index.mjs +71 -0
  185. package/src/tui/themes/indigo.mjs +78 -0
  186. package/src/tui/themes/kanagawa.mjs +80 -0
  187. package/src/tui/themes/light.mjs +81 -0
  188. package/src/tui/themes/nord.mjs +72 -0
  189. package/src/tui/themes/onedark.mjs +16 -0
  190. package/src/tui/themes/rosepine.mjs +70 -0
  191. package/src/tui/themes/teal.mjs +81 -0
  192. package/src/tui/themes/tokyonight.mjs +79 -0
  193. package/src/tui/themes/utils.mjs +106 -0
  194. package/src/tui/themes/warm.mjs +79 -0
  195. package/src/tui/transcript-tool-failures.mjs +13 -2
  196. package/src/ui/markdown.mjs +1 -1
  197. package/src/ui/model-display.mjs +2 -2
  198. package/src/ui/statusline.mjs +26 -27
  199. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  200. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  201. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  202. package/src/workflows/default/WORKFLOW.md +39 -12
  203. package/src/workflows/sequential/WORKFLOW.md +46 -0
  204. package/src/workflows/solo/WORKFLOW.md +7 -0
  205. package/vendor/ink/build/display-width.js +62 -0
  206. package/vendor/ink/build/ink.js +100 -12
  207. package/vendor/ink/build/measure-text.js +4 -1
  208. package/vendor/ink/build/output.js +115 -9
  209. package/vendor/ink/build/render-node-to-output.js +4 -1
  210. package/vendor/ink/build/render.js +4 -0
  211. package/src/output-styles/extreme-simple.md +0 -20
  212. package/src/rules/lead/04-workflow.md +0 -51
  213. package/src/workflows/default/workflow.json +0 -13
  214. package/src/workflows/solo/workflow.json +0 -7
@@ -12,17 +12,16 @@ import { fetchOAuthUsageSnapshot } from '../providers/oauth-usage.mjs';
12
12
  import {
13
13
  recallFastTrackCompactMessages,
14
14
  semanticCompactMessages,
15
+ pruneToolOutputsUnanchored,
16
+ effectiveBudget as compactEffectiveBudget,
15
17
  compactTypeIsRecallFastTrack,
16
18
  compactTypeIsSemantic,
17
19
  normalizeCompactType,
18
20
  DEFAULT_COMPACT_TYPE,
19
21
  SUMMARY_PREFIX,
20
- DEFAULT_COMPACTION_BUFFER_RATIO,
21
- compactionBufferTokensForBoundary,
22
- normalizeCompactionBufferRatio,
23
22
  drainSessionCycle1,
24
23
  } from './compact.mjs';
25
- import { estimateMessagesTokens, estimateRequestReserveTokens, estimateTranscriptContextUsage } from './context-utils.mjs';
24
+ import { estimateMessagesTokens, estimateRequestReserveTokens, estimateTranscriptContextUsage, resolveCompactTriggerTokens } from './context-utils.mjs';
26
25
  import { getMcpTools } from '../mcp/client.mjs';
27
26
  import { getInternalTools, executeInternalTool } from '../internal-tools.mjs';
28
27
  import { BUILTIN_TOOLS } from '../tools/builtin/builtin-tools.mjs';
@@ -41,7 +40,7 @@ import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
41
40
  import { appendAgentTrace } from '../agent-trace.mjs';
42
41
  import { isAgentOwner } from '../agent-owner.mjs';
43
42
  import { maxMtimeRecursive } from '../cache-mtime.mjs';
44
- import { getHiddenRole, getRoleInstructionDir, listHiddenRoleNames } from '../internal-roles.mjs';
43
+ import { getHiddenAgent, getAgentInstructionDir, listHiddenAgentNames } from '../internal-agents.mjs';
45
44
  import { DEFAULT_ACTIVITY_HEARTBEAT_MS } from '../stall-policy.mjs';
46
45
  import {
47
46
  buildGatewayLimits,
@@ -185,34 +184,34 @@ function _buildLeadMetaContext() {
185
184
  }
186
185
  }
187
186
 
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) {
187
+ // BP4-adjacent agent-specific data cache — keyed by agent. webhook / schedule
188
+ // agents each have their own scoped instruction set; other agents return ''.
189
+ const _roleSpecificCache = new Map(); // agent → { value, mtime }
190
+ function _buildAgentSpecific(currentAgent) {
192
191
  if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleSpecificContent !== 'function') return '';
193
- if (!currentRole) return '';
192
+ if (!currentAgent) return '';
194
193
  const PLUGIN_ROOT = mixdogRoot();
195
194
  const DATA_DIR = resolvePluginData();
196
195
  const RULES_DIR = join(PLUGIN_ROOT, 'rules');
197
- const roleInstructionDir = getRoleInstructionDir(currentRole);
196
+ const roleInstructionDir = getAgentInstructionDir(currentAgent);
198
197
  const mtime = maxMtimeRecursive([
199
198
  join(RULES_DIR, 'shared'),
200
199
  join(DATA_DIR, 'mixdog-config.json'),
201
200
  join(DATA_DIR, 'webhooks'),
202
201
  join(DATA_DIR, 'schedules'),
203
202
  ...(roleInstructionDir ? [join(DATA_DIR, roleInstructionDir)] : []),
204
- join(PLUGIN_ROOT, 'defaults', 'hidden-roles.json'),
203
+ join(PLUGIN_ROOT, 'defaults', 'agents.json'),
205
204
  ]);
206
- const entry = _roleSpecificCache.get(currentRole);
205
+ const entry = _roleSpecificCache.get(currentAgent);
207
206
  if (entry && mtime <= entry.mtime) {
208
207
  return entry.value;
209
208
  }
210
209
  try {
211
- const built = _rulesBuilder.buildAgentRoleSpecificContent({ PLUGIN_ROOT, DATA_DIR, currentRole });
212
- _roleSpecificCache.set(currentRole, { mtime, value: built });
210
+ const built = _rulesBuilder.buildAgentRoleSpecificContent({ PLUGIN_ROOT, DATA_DIR, currentAgent });
211
+ _roleSpecificCache.set(currentAgent, { mtime, value: built });
213
212
  return built;
214
213
  } catch (e) {
215
- throw new Error(`[session] role-specific rules build failed (role: ${currentRole}): ${e.message}`);
214
+ throw new Error(`[session] agent-specific rules build failed (agent: ${currentAgent}): ${e.message}`);
216
215
  }
217
216
  }
218
217
 
@@ -349,6 +348,7 @@ const AGENT_STRING_PERMISSION_READ_ALLOW = Object.freeze([
349
348
  'explore',
350
349
  'search',
351
350
  'web_fetch',
351
+ 'Skill',
352
352
  ]);
353
353
 
354
354
  function stringToolPermissionAllowList(toolPermission) {
@@ -369,6 +369,7 @@ const AGENT_STRING_PERMISSION_READ_WRITE_ALLOW = Object.freeze([
369
369
  'explore',
370
370
  'search',
371
371
  'web_fetch',
372
+ 'Skill',
372
373
  ]);
373
374
 
374
375
  function applyToolPermissionNarrowing(tools, toolPermission, warnRole = null) {
@@ -401,12 +402,14 @@ function applyToolPermissionNarrowing(tools, toolPermission, warnRole = null) {
401
402
  return tools;
402
403
  }
403
404
 
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);
405
+ function recursiveWrapperToolNameForPublicAgent(agent) {
406
+ if (!agent) return null;
407
+ const key = String(agent).trim();
408
+ if (key === 'explore') return 'explore';
409
+ for (const hiddenName of listHiddenAgentNames()) {
410
+ const def = getHiddenAgent(hiddenName);
409
411
  const invokedBy = typeof def?.invokedBy === 'string' ? def.invokedBy.trim() : '';
412
+ if (hiddenName === key && invokedBy) return invokedBy;
410
413
  if (invokedBy && invokedBy === key) return invokedBy;
411
414
  }
412
415
  return null;
@@ -416,7 +419,7 @@ function finalizeSessionToolList(tools, {
416
419
  schemaAllowedTools = null,
417
420
  disallowedTools = null,
418
421
  ownerIsAgent = false,
419
- resolvedRole = null,
422
+ resolvedAgent = null,
420
423
  } = {}) {
421
424
  let out = Array.isArray(tools) ? tools : [];
422
425
  const hasCallerAllow = Array.isArray(schemaAllowedTools);
@@ -429,7 +432,7 @@ function finalizeSessionToolList(tools, {
429
432
  const denySet = new Set(callerDeny.map(n => n.toLowerCase()));
430
433
  out = out.filter(t => !denySet.has(String(t?.name || '').toLowerCase()));
431
434
  }
432
- const recursiveDeny = ownerIsAgent ? recursiveWrapperToolNameForPublicAgentRole(resolvedRole) : null;
435
+ const recursiveDeny = ownerIsAgent ? recursiveWrapperToolNameForPublicAgent(resolvedAgent) : null;
433
436
  if (recursiveDeny) {
434
437
  const deny = recursiveDeny.toLowerCase();
435
438
  out = out.filter(t => String(t?.name || '').toLowerCase() !== deny);
@@ -639,38 +642,9 @@ function providerNameOf(provider) {
639
642
  if (typeof provider === 'string') return provider.toLowerCase();
640
643
  return String(provider?.name || provider?.id || '').toLowerCase();
641
644
  }
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
645
  // 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
646
+ // onto session.compaction so the shared compact-policy parser honors configured
647
+ // buffer
674
648
  // percent/ratio. Only finite positive values are copied; absent fields stay
675
649
  // undefined so the default-ratio fallback still applies.
676
650
  function preserveBufferConfigFields(cfg = {}) {
@@ -681,69 +655,6 @@ function preserveBufferConfigFields(cfg = {}) {
681
655
  }
682
656
  return out;
683
657
  }
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
658
  const COMPACT_TARGET_RATIO = 0.02;
748
659
  const COMPACT_TARGET_MIN_TOKENS = 4_000;
749
660
  const COMPACT_TARGET_MAX_TOKENS = 16_000;
@@ -774,15 +685,32 @@ function defaultEffectiveContextWindowPercent(provider) {
774
685
  // capacity so /context, the TUI statusline, and gateway telemetry agree.
775
686
  return 90;
776
687
  }
688
+ const PROVIDER_SYNTHETIC_CONTEXT_DEFAULT = 1_000_000;
689
+ function providerRawContextWindow(info, catalogInfo) {
690
+ if (!info || typeof info !== 'object') return null;
691
+ const fromApiFields = positiveContextWindow(info.context_window)
692
+ || positiveContextWindow(info.max_context_window);
693
+ if (fromApiFields) return fromApiFields;
694
+ const fromCache = positiveContextWindow(info.contextWindow)
695
+ || positiveContextWindow(info.maxContextWindow);
696
+ if (!fromCache) return null;
697
+ const catalogWindow = positiveContextWindow(catalogInfo?.contextWindow)
698
+ || positiveContextWindow(catalogInfo?.maxContextWindow)
699
+ || positiveContextWindow(catalogInfo?.context_window)
700
+ || positiveContextWindow(catalogInfo?.max_context_window);
701
+ if (fromCache === PROVIDER_SYNTHETIC_CONTEXT_DEFAULT
702
+ && catalogWindow
703
+ && fromCache !== catalogWindow) {
704
+ return null;
705
+ }
706
+ return fromCache;
707
+ }
777
708
  function resolveSessionContextMeta(provider, model, seed = {}) {
778
709
  const info = typeof provider?.getCachedModelInfo === 'function'
779
710
  ? provider.getCachedModelInfo(model)
780
711
  : null;
781
712
  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)
713
+ const rawContextWindow = providerRawContextWindow(info, catalogInfo)
786
714
  || positiveContextWindow(catalogInfo?.contextWindow)
787
715
  || positiveContextWindow(catalogInfo?.maxContextWindow)
788
716
  || positiveContextWindow(catalogInfo?.context_window)
@@ -841,16 +769,7 @@ function resolveSessionContextMeta(provider, model, seed = {}) {
841
769
  };
842
770
  }
843
771
  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);
772
+ return resolveCompactTriggerTokens(session, boundaryTokens);
854
773
  }
855
774
  // Test-only exports for the legacy auto-compact-limit migration + buffer-config
856
775
  // preservation (see scripts/compact-trigger-migration-smoke.mjs).
@@ -904,10 +823,12 @@ function addCompactUsageToSession(session, usage) {
904
823
  const outputTokens = usage.outputTokens || 0;
905
824
  const cachedTokens = usage.cachedTokens || 0;
906
825
  const cacheWriteTokens = usage.cacheWriteTokens || 0;
826
+ const uncachedInputTokens = uncachedInputTokensForProvider(session.provider, inputTokens, cachedTokens, cacheWriteTokens);
907
827
  session.totalInputTokens = (session.totalInputTokens || 0) + inputTokens;
908
828
  session.totalOutputTokens = (session.totalOutputTokens || 0) + outputTokens;
909
829
  session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + cachedTokens;
910
830
  session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + cacheWriteTokens;
831
+ session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
911
832
  session.tokensCumulative = (session.tokensCumulative || 0) + inputTokens + outputTokens;
912
833
  }
913
834
  async function runRecallFastTrackForSession(session, messages, opts = {}) {
@@ -977,6 +898,18 @@ async function runRecallFastTrackForSession(session, messages, opts = {}) {
977
898
  }
978
899
  return { query, querySha, recallText: [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n') };
979
900
  }
901
+ // Element-identity change detection (mirrors loop.mjs messagesArrayChanged): two
902
+ // arrays are "unchanged" only when same length AND every slot is the same object
903
+ // reference. Used to reject a no-op prune (which returns a fresh array whose
904
+ // elements are the untouched originals) from being accepted as a recovery.
905
+ function messagesChanged(before, after) {
906
+ if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
907
+ if (before.length !== after.length) return true;
908
+ for (let i = 0; i < before.length; i += 1) {
909
+ if (before[i] !== after[i]) return true;
910
+ }
911
+ return false;
912
+ }
980
913
  async function runSessionCompaction(session, opts = {}) {
981
914
  if (!session || session.closed === true) return null;
982
915
  const mode = opts.mode === 'auto' ? 'auto' : 'manual';
@@ -991,7 +924,16 @@ async function runSessionCompaction(session, opts = {}) {
991
924
  if (force) throw new Error('compact: no context window is available for this session');
992
925
  return null;
993
926
  }
994
- const reserveTokens = estimateRequestReserveTokens(session.tools || []);
927
+ // Reserve must mirror loop.mjs (buildCompactPolicy): request reserve (tool
928
+ // schema) PLUS the configured reserve (session.compaction.reservedTokens or
929
+ // MIXDOG_AGENT_COMPACT_RESERVED_TOKENS env). The old request-only value left
930
+ // the manual / auto-clear compact budget without the configured headroom the
931
+ // loop path reserves, so a compacted transcript could overflow on next send.
932
+ const requestReserveTokens = estimateRequestReserveTokens(session.tools || []);
933
+ const configuredReserveTokens = positiveContextWindow(session.compaction?.reservedTokens)
934
+ || positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_RESERVED_TOKENS)
935
+ || 0;
936
+ const reserveTokens = requestReserveTokens + configuredReserveTokens;
995
937
  const beforeMessageTokens = estimateMessagesTokens(messages);
996
938
  const triggerTokens = compactTriggerForSession(session, boundary)
997
939
  || positiveContextWindow(session.compaction?.triggerTokens)
@@ -1041,7 +983,11 @@ async function runSessionCompaction(session, opts = {}) {
1041
983
  recallText: recallPayload.recallText,
1042
984
  query: recallPayload.query,
1043
985
  querySha: recallPayload.querySha,
1044
- allowEmptyRecall: true,
986
+ // Ingest just ran on the live transcript, so an empty recall dump
987
+ // means the memory pipeline is broken — do NOT erase history
988
+ // behind an empty summary shell. Empty recall now throws and is
989
+ // handled by the semantic fallback below (or recorded failure).
990
+ allowEmptyRecall: false,
1045
991
  tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
1046
992
  keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
1047
993
  preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
@@ -1055,6 +1001,48 @@ async function runSessionCompaction(session, opts = {}) {
1055
1001
  try {
1056
1002
  process.stderr.write(`[session] recall-fasttrack ${mode} compact failed (sess=${session.id || 'unknown'}): ${err?.message || err}\n`);
1057
1003
  } catch { /* best-effort */ }
1004
+ // Degraded-compact fallback: recall-fasttrack failed (empty recall,
1005
+ // ingest error, fit failure). Before recording a hard failure, try
1006
+ // the semantic path once so auto-clear/manual compaction still makes
1007
+ // progress WITHOUT shipping an empty-recall summary. History is only
1008
+ // replaced when the semantic summary actually succeeds.
1009
+ if (semanticCompactionEnabledForSession(session)
1010
+ && provider && typeof provider.send === 'function') {
1011
+ try {
1012
+ semanticCompactResult = await semanticCompactMessages(
1013
+ provider,
1014
+ messages,
1015
+ opts.model || session.model,
1016
+ budget,
1017
+ {
1018
+ reserveTokens,
1019
+ providerName: session.provider || provider?.name || null,
1020
+ sessionId: opts.sessionId || session.id || null,
1021
+ signal: opts.signal || null,
1022
+ promptCacheKey: session.promptCacheKey || null,
1023
+ providerCacheKey: session.promptCacheKey || null,
1024
+ timeoutMs: positiveContextWindow(session.compaction?.timeoutMs) || 30_000,
1025
+ tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
1026
+ keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
1027
+ preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
1028
+ force: true,
1029
+ },
1030
+ );
1031
+ if (Array.isArray(semanticCompactResult?.messages)) {
1032
+ compacted = semanticCompactResult.messages;
1033
+ compactError = null;
1034
+ addCompactUsageToSession(session, semanticCompactResult.usage);
1035
+ try {
1036
+ process.stderr.write(`[session] degraded compact: recall-fasttrack failed, semantic fallback succeeded (sess=${session.id || 'unknown'}, mode=${mode})\n`);
1037
+ } catch { /* best-effort */ }
1038
+ }
1039
+ } catch (fallbackErr) {
1040
+ semanticCompactError = fallbackErr;
1041
+ try {
1042
+ process.stderr.write(`[session] degraded compact: semantic fallback also failed (sess=${session.id || 'unknown'}): ${fallbackErr?.message || fallbackErr}\n`);
1043
+ } catch { /* best-effort */ }
1044
+ }
1045
+ }
1058
1046
  }
1059
1047
  } else if (compactTypeIsSemantic(compactType)) {
1060
1048
  try {
@@ -1098,6 +1086,32 @@ async function runSessionCompaction(session, opts = {}) {
1098
1086
  if (!compacted && !compactError) {
1099
1087
  compactError = new Error(`${compactType} compact produced no messages`);
1100
1088
  }
1089
+ // Anchor-independent prune safety net (mirror loop.mjs compact catch): when a
1090
+ // non-recall (semantic) compact failed, try one non-LLM prune that needs no
1091
+ // user anchor before recording failure, so Lead manual / auto-clear paths
1092
+ // recover the same transcripts the loop path does. Gated off the recall
1093
+ // path — a recall failure keeps its original contract (no silent prune).
1094
+ if (!compacted && !recallFastTrackError) {
1095
+ try {
1096
+ const acceptThreshold = compactEffectiveBudget(budget, { reserveTokens });
1097
+ const salvaged = pruneToolOutputsUnanchored(messages, budget, { reserveTokens });
1098
+ // pruneToolOutputsUnanchored ALWAYS returns a fresh reconciled array
1099
+ // (never the input identity), so `salvaged !== messages` is always
1100
+ // true and cannot detect a no-op. Compare by element identity so a
1101
+ // transcript that already fit (nothing pruned) is NOT falsely accepted
1102
+ // as a recovery — that would clear compactError and unconditionally
1103
+ // invalidate providerState for an unchanged transcript.
1104
+ if (Array.isArray(salvaged)
1105
+ && messagesChanged(messages, salvaged)
1106
+ && estimateMessagesTokens(salvaged) <= acceptThreshold) {
1107
+ compacted = salvaged;
1108
+ compactError = null;
1109
+ try {
1110
+ process.stderr.write(`[session] compact fallback prune recovered (sess=${session.id || 'unknown'}, mode=${mode})\n`);
1111
+ } catch { /* best-effort */ }
1112
+ }
1113
+ } catch { /* fall through to failure record */ }
1114
+ }
1101
1115
  if (!compacted) {
1102
1116
  const now = Date.now();
1103
1117
  session.compaction = {
@@ -1506,7 +1520,7 @@ async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
1506
1520
  // Preset shape: { name, provider, model, effort?, fast?, tools? }
1507
1521
  //
1508
1522
  // Agent Runtime integration:
1509
- // opts.taskType / opts.role / opts.profileId — enables profile-aware routing.
1523
+ // opts.taskType / opts.agent / opts.profileId — enables profile-aware routing.
1510
1524
  // Rule-based SmartRouter resolves these synchronously; the resolved
1511
1525
  // profile controls context filtering (skip.skills/memory/etc) and cache
1512
1526
  // strategy. If no rule matches, falls back to classic preset behavior.
@@ -1519,13 +1533,13 @@ export function createSession(opts) {
1519
1533
  // --- Agent Runtime profile resolution (best-effort, sync) ---
1520
1534
  let profile = opts.profile || null;
1521
1535
  let providerCacheOpts = opts.providerCacheOpts || null;
1522
- if (!profile && (opts.taskType || opts.role || opts.profileId)) {
1536
+ if (!profile && (opts.taskType || opts.agent || opts.profileId)) {
1523
1537
  const agentRuntime = getAgentRuntimeSync();
1524
1538
  if (agentRuntime) {
1525
1539
  try {
1526
1540
  const resolved = agentRuntime.resolveSync({
1527
1541
  taskType: opts.taskType,
1528
- role: opts.role,
1542
+ agent: opts.agent,
1529
1543
  profileId: opts.profileId,
1530
1544
  preset: presetObj?.name || (typeof opts.preset === 'string' ? opts.preset : null),
1531
1545
  provider: opts.provider || presetObj?.provider,
@@ -1567,25 +1581,26 @@ export function createSession(opts) {
1567
1581
  const id = `sess_${process.pid}_${nextId++}_${Date.now()}_${randomBytes(16).toString('hex')}`;
1568
1582
  const messages = [];
1569
1583
  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);
1584
+ const resolvedAgent = opts.agent || opts.role || profile?.taskType || null;
1585
+ const hiddenAgent = getHiddenAgent(resolvedAgent);
1586
+ const isRetrievalAgent = hiddenAgent?.kind === 'retrieval';
1587
+ // Skill schema is fixed for public agent sessions, but hidden retrieval /
1588
+ // maintenance roles are deliberately narrowed away from the Skill tool.
1589
+ // Do not leak a Skill manifest into those hidden prompts when no Skill()
1590
+ // loader is available.
1591
+ const skills = (opts.skipSkills || hiddenAgent) ? [] : collectSkillsCached(opts.cwd);
1577
1592
 
1578
1593
  // BP1 is shared tool policy (+ compact skill manifest in compose). BP2 is
1579
1594
  // role/system rules. User-defined schedules/webhooks ride as normal user
1580
1595
  // 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';
1596
+ const agentRulesAgent = opts.agent || opts.role || profile?.taskType || null;
1597
+ const agentRulesProfile = isRetrievalAgent ? 'retrieval' : 'full';
1583
1598
  const skipAgentRules = opts.skipAgentRules === true;
1584
1599
  // 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();
1600
+ const injectedRules = (skipAgentRules || isRetrievalAgent) ? '' : _buildSharedRules();
1586
1601
  const roleRules = skipAgentRules ? '' : (ownerIsAgent ? _buildAgentRules(agentRulesProfile) : _buildLeadRules());
1587
1602
  const metaContext = skipAgentRules ? '' : (ownerIsAgent ? '' : _buildLeadMetaContext());
1588
- const roleSpecific = ownerIsAgent && !skipAgentRules ? _buildRoleSpecific(agentRulesRole) : '';
1603
+ const roleSpecific = ownerIsAgent && !skipAgentRules ? _buildAgentSpecific(agentRulesAgent) : '';
1589
1604
  // Agent sessions must not inherit role/profile/preset tool narrowing: Pool
1590
1605
  // B and Pool C share one bit-identical tool schema to maximize provider
1591
1606
  // prefix reuse, and permission differences are enforced only at call time. Raw
@@ -1612,7 +1627,7 @@ export function createSession(opts) {
1612
1627
  // fail closed (zero tools) rather than silently falling back to the full
1613
1628
  // preset, which would grant the role more surface than declared.
1614
1629
  if (ownerIsAgent) {
1615
- toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, toolPermission, opts.role || null);
1630
+ toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, toolPermission, opts.agent || null);
1616
1631
  }
1617
1632
 
1618
1633
  const { baseRules, stableSystemContext, sessionMarker, volatileTail } = composeSystemPrompt({
@@ -1622,7 +1637,7 @@ export function createSession(opts) {
1622
1637
  metaContext: metaContext || undefined,
1623
1638
  skipRoleCatalog: !ownerIsAgent,
1624
1639
  profile: profile || undefined,
1625
- role: resolvedRole,
1640
+ agent: resolvedAgent,
1626
1641
  workflowContext: opts.workflowContext || null,
1627
1642
  workspaceContext: opts.workspaceContext || null,
1628
1643
  coreMemoryContext: opts.coreMemoryContext || null,
@@ -1678,17 +1693,17 @@ export function createSession(opts) {
1678
1693
  const hasCallerAllow = Array.isArray(opts.schemaAllowedTools);
1679
1694
  const tools = finalizeSessionToolList(toolsForRouting, {
1680
1695
  schemaAllowedTools: hasCallerAllow ? opts.schemaAllowedTools : null,
1681
- disallowedTools: opts.disallowedTools,
1696
+ disallowedTools: hiddenAgent ? [...(Array.isArray(opts.disallowedTools) ? opts.disallowedTools : []), 'Skill'] : opts.disallowedTools,
1682
1697
  ownerIsAgent,
1683
- resolvedRole,
1698
+ resolvedAgent,
1684
1699
  });
1685
1700
 
1686
1701
  // Unified-shard policy — no broad role-specific schema filter. Keep
1687
1702
  // agent schemas shared unless a hidden-role schema profile explicitly
1688
1703
  // passes schemaAllowedTools for a small specialist; broad role
1689
1704
  // 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`);
1705
+ if (resolvedAgent && process.env.MIXDOG_DEBUG_SESSION_LOG) {
1706
+ process.stderr.write(`[session] agent=${resolvedAgent} permission=${permission || 'full'} toolPermission=${toolPermission || 'full'} tools=${tools.length}\n`);
1692
1707
  }
1693
1708
  const contextMeta = resolveSessionContextMeta(provider, modelName);
1694
1709
  const workflowMeta = opts.workflow && typeof opts.workflow === 'object' && String(opts.workflow.id || '').trim()
@@ -1758,7 +1773,6 @@ export function createSession(opts) {
1758
1773
  // agent sessions past RUNNING_STALL_MS.
1759
1774
  lastUsedAt: Date.now(),
1760
1775
  tokensCumulative: 0,
1761
- role: opts.role || null,
1762
1776
  taskType: opts.taskType || null,
1763
1777
  maxLoopIterations: Number.isFinite(opts.maxLoopIterations) ? opts.maxLoopIterations : null,
1764
1778
  // Agent tag (auto worker{n} on spawn) persisted so the forked status
@@ -2203,6 +2217,18 @@ export function usageMetricsIdempotencyKey(sessionId, session, delta = {}) {
2203
2217
  return `${sessionId}:${turnId}:${epoch}:${delta.iterationIndex}:${source}`;
2204
2218
  }
2205
2219
 
2220
+ function uncachedInputTokensForProvider(provider, inputTokens, cachedReadTokens = 0, cacheWriteTokens = 0) {
2221
+ const input = Number(inputTokens) || 0;
2222
+ if (input <= 0) return 0;
2223
+ // Anthropic-style providers report input_tokens excluding cache reads; OpenAI
2224
+ // Responses/Gemini-style providers report input_tokens inclusive of cached
2225
+ // prefix tokens. Keep both views so UI can show the real context footprint
2226
+ // and the fresh/new token portion without mistaking cache hits for a cache
2227
+ // break.
2228
+ if (providerInputExcludesCache(provider)) return input + (Number(cacheWriteTokens) || 0);
2229
+ return Math.max(input - (Number(cachedReadTokens) || 0) - (Number(cacheWriteTokens) || 0), 0);
2230
+ }
2231
+
2206
2232
  /**
2207
2233
  * Apply terminal ask usage to session totals. Skips lifetime totals when incremental
2208
2234
  * per-iteration persistence already counted this turn (askSession path).
@@ -2211,23 +2237,38 @@ export function applyAskTerminalUsageTotals(session, result, options = {}) {
2211
2237
  if (!session || !result?.usage) return;
2212
2238
  const skipTotals = options.skipTotalsIfIncremental === true;
2213
2239
  if (!skipTotals) {
2214
- session.totalInputTokens = (session.totalInputTokens || 0) + (result.usage.inputTokens || 0);
2215
- session.totalOutputTokens = (session.totalOutputTokens || 0) + (result.usage.outputTokens || 0);
2240
+ const inputTokens = result.usage.inputTokens || 0;
2241
+ const outputTokens = result.usage.outputTokens || 0;
2242
+ const cachedTokens = result.usage.cachedTokens || 0;
2243
+ const cacheWriteTokens = result.usage.cacheWriteTokens || 0;
2244
+ const uncachedInputTokens = uncachedInputTokensForProvider(session.provider, inputTokens, cachedTokens, cacheWriteTokens);
2245
+ session.totalInputTokens = (session.totalInputTokens || 0) + inputTokens;
2246
+ session.totalOutputTokens = (session.totalOutputTokens || 0) + outputTokens;
2216
2247
  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);
2248
+ + inputTokens
2249
+ + outputTokens;
2250
+ session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + cachedTokens;
2251
+ session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + cacheWriteTokens;
2252
+ session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
2221
2253
  }
2222
2254
  const _lastTurn = result.lastTurnUsage || result.usage || {};
2223
- session.lastInputTokens = _lastTurn.inputTokens || 0;
2255
+ const _lastInputTokens = _lastTurn.inputTokens || 0;
2256
+ const _lastCachedReadTokens = _lastTurn.cachedTokens || 0;
2257
+ const _lastCacheWriteTokens = _lastTurn.cacheWriteTokens || 0;
2258
+ session.lastInputTokens = _lastInputTokens;
2224
2259
  session.lastOutputTokens = _lastTurn.outputTokens || 0;
2225
- session.lastCachedReadTokens = _lastTurn.cachedTokens || 0;
2226
- session.lastCacheWriteTokens = _lastTurn.cacheWriteTokens || 0;
2260
+ session.lastCachedReadTokens = _lastCachedReadTokens;
2261
+ session.lastCacheWriteTokens = _lastCacheWriteTokens;
2262
+ session.lastUncachedInputTokens = uncachedInputTokensForProvider(
2263
+ session.provider,
2264
+ _lastInputTokens,
2265
+ _lastCachedReadTokens,
2266
+ _lastCacheWriteTokens,
2267
+ );
2227
2268
  const _inputExcludesCache = providerInputExcludesCache(session.provider);
2228
2269
  session.lastContextTokens = _inputExcludesCache
2229
- ? (_lastTurn.inputTokens || 0) + (_lastTurn.cachedTokens || 0)
2230
- : (_lastTurn.inputTokens || 0);
2270
+ ? _lastInputTokens + _lastCachedReadTokens + _lastCacheWriteTokens
2271
+ : _lastInputTokens;
2231
2272
  session.lastContextTokensUpdatedAt = Date.now();
2232
2273
  session.lastContextTokensStaleAfterCompact = false;
2233
2274
  }
@@ -2257,6 +2298,9 @@ export async function persistIterationMetrics(delta) {
2257
2298
  seen.add(ikey);
2258
2299
  if (!isReplay) {
2259
2300
  if (runtimeEntry) runtimeEntry.usageMetricsTurnIncremental = true;
2301
+ const deltaUncachedInput = delta.deltaUncachedInput != null
2302
+ ? Number(delta.deltaUncachedInput) || 0
2303
+ : uncachedInputTokensForProvider(session.provider, deltaInput, deltaCachedRead, deltaCacheWrite);
2260
2304
  session.totalInputTokens = (session.totalInputTokens || 0) + (deltaInput || 0);
2261
2305
  session.totalOutputTokens = (session.totalOutputTokens || 0) + (deltaOutput || 0);
2262
2306
  session.tokensCumulative = (session.tokensCumulative || 0) + (deltaInput || 0) + (deltaOutput || 0);
@@ -2266,12 +2310,15 @@ export async function persistIterationMetrics(delta) {
2266
2310
  // includes cached_read / cache_write in its terminal usage rollup).
2267
2311
  session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + (deltaCachedRead || 0);
2268
2312
  session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + (deltaCacheWrite || 0);
2313
+ session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + deltaUncachedInput;
2269
2314
  // Window snapshot updated per iteration so agent type=list reflects the
2270
2315
  // most-recent provider-reported input size even for short dispatches
2271
2316
  // that finish before askSession's terminal save lands.
2272
2317
  session.lastInputTokens = deltaInput || 0;
2273
2318
  session.lastOutputTokens = deltaOutput || 0;
2274
2319
  session.lastCachedReadTokens = deltaCachedRead || 0;
2320
+ session.lastCacheWriteTokens = deltaCacheWrite || 0;
2321
+ session.lastUncachedInputTokens = deltaUncachedInput;
2275
2322
  // Normalized last-call context footprint: how many prompt tokens the
2276
2323
  // model actually saw on the most-recent send, comparable ACROSS
2277
2324
  // providers. Anthropic reports input_tokens EXCLUDING cache (cache_read
@@ -2280,7 +2327,7 @@ export async function persistIterationMetrics(delta) {
2280
2327
  // tokens INTO the input count, so input alone is the footprint.
2281
2328
  const _inputExcludesCache = providerInputExcludesCache(session.provider);
2282
2329
  session.lastContextTokens = _inputExcludesCache
2283
- ? (deltaInput || 0) + (deltaCachedRead || 0)
2330
+ ? (deltaInput || 0) + (deltaCachedRead || 0) + (deltaCacheWrite || 0)
2284
2331
  : (deltaInput || 0);
2285
2332
  session.lastContextTokensUpdatedAt = ts || Date.now();
2286
2333
  session.lastContextTokensStaleAfterCompact = false;
@@ -3439,7 +3486,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
3439
3486
  logLlmCall({
3440
3487
  ts: new Date().toISOString(),
3441
3488
  sourceType: session.sourceType || 'lead',
3442
- sourceName: session.sourceName || session.role || null,
3489
+ sourceName: session.sourceName || session.agent || null,
3443
3490
  preset: session.presetName || null,
3444
3491
  model: session.model,
3445
3492
  provider: session.provider,
@@ -3660,13 +3707,13 @@ export async function resumeSession(sessionId, preset) {
3660
3707
  }
3661
3708
  let toolsForRouting = resolveSessionTools(toolSpec, skills, { ownerIsAgentSession: ownerIsAgent });
3662
3709
  if (ownerIsAgent) {
3663
- toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.role || null);
3710
+ toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.agent || null);
3664
3711
  }
3665
3712
  session.tools = finalizeSessionToolList(toolsForRouting, {
3666
3713
  schemaAllowedTools: Array.isArray(session.schemaAllowedTools) ? session.schemaAllowedTools : null,
3667
- disallowedTools: null,
3714
+ disallowedTools: getHiddenAgent(session.agent || null) ? ['Skill'] : null,
3668
3715
  ownerIsAgent,
3669
- resolvedRole: session.role || null,
3716
+ resolvedAgent: session.agent || null,
3670
3717
  });
3671
3718
  const newTools = session.tools;
3672
3719
  const missing = oldTools.filter(t => !newTools.find(n => n.name === t.name));