mixdog 1.0.9 → 1.0.11

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 (165) hide show
  1. package/package.json +3 -3
  2. package/src/rules/agent/40-cycle1-agent.md +19 -23
  3. package/src/rules/lead/LEAD.md +5 -5
  4. package/src/rules/shared/05-tool-calls.md +6 -5
  5. package/src/rules/shared/60-verification.md +2 -2
  6. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +37 -13
  7. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -0
  8. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +6 -0
  9. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +8 -0
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +234 -12
  11. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +22 -5
  12. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +7 -8
  13. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -0
  14. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +9 -7
  15. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +2 -0
  16. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +3 -2
  17. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +77 -0
  18. package/src/runtime/agent/orchestrator/providers/cursor.mjs +11 -5
  19. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +165 -0
  20. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +4 -2
  21. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +4 -6
  22. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  23. package/src/runtime/agent/orchestrator/providers/gemini.mjs +2 -0
  24. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +3 -2
  25. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -0
  26. package/src/runtime/agent/orchestrator/providers/lib/anthropic-image-input.mjs +34 -13
  27. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +94 -0
  28. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +670 -198
  29. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +9 -3
  30. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +23 -4
  31. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +3 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +4 -3
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +4 -0
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +14 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +7 -4
  36. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +24 -9
  37. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -28
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +3 -2
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -0
  40. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -0
  41. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +4 -0
  42. package/src/runtime/agent/orchestrator/providers/provider-base-url.mjs +24 -0
  43. package/src/runtime/agent/orchestrator/providers/registry.mjs +24 -4
  44. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +9 -1
  45. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -2
  46. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +13 -11
  47. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +17 -9
  48. package/src/runtime/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +23 -1
  49. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +40 -71
  50. package/src/runtime/agent/orchestrator/session/cache/tool-result-reuse.mjs +27 -0
  51. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -7
  52. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +54 -12
  53. package/src/runtime/agent/orchestrator/session/context-utils.mjs +27 -5
  54. package/src/runtime/agent/orchestrator/session/eager-dispatch/admission.mjs +8 -51
  55. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +55 -2
  56. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
  57. package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +2 -0
  58. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +62 -0
  59. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +79 -0
  60. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +10 -2
  61. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +8 -1
  62. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +6 -3
  63. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +10 -3
  64. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
  65. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +21 -5
  66. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -4
  67. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +7 -6
  68. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +6 -3
  69. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +13 -0
  70. package/src/runtime/agent/orchestrator/session/tool-batch/execute-call.mjs +9 -19
  71. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +85 -2
  72. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +10 -6
  73. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +3 -0
  74. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
  75. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +3 -3
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +2 -1
  77. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +20 -0
  79. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +51 -28
  80. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +3 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +47 -0
  82. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +1 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +34 -30
  84. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +10 -5
  85. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +18 -7
  86. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +1 -1
  87. package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
  88. package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-roots.mjs +3 -1
  89. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/work.mjs +11 -6
  91. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +13 -1
  92. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +15 -1
  93. package/src/runtime/attachments/limits.mjs +20 -0
  94. package/src/runtime/{agent/orchestrator/tools/builtin/read-office-files.mjs → attachments/office-extract.mjs} +356 -125
  95. package/src/runtime/attachments/pdf-extract.mjs +2 -2
  96. package/src/runtime/attachments/store.mjs +232 -72
  97. package/src/runtime/browser-bridge/client.mjs +3 -3
  98. package/src/runtime/effort-judge/compact-tokenizer.mjs +282 -0
  99. package/src/runtime/effort-judge/judge-client.mjs +194 -0
  100. package/src/runtime/effort-judge/judge-worker.mjs +106 -0
  101. package/src/runtime/local-provider/input-capabilities.mjs +30 -12
  102. package/src/runtime/media/jobs.mjs +0 -1
  103. package/src/runtime/media/media-usage.mjs +2 -1
  104. package/src/runtime/memory/lib/embedding-model-config.mjs +6 -6
  105. package/src/runtime/memory/lib/embedding-model-files.mjs +44 -0
  106. package/src/runtime/memory/lib/embedding-tokenizer.mjs +253 -0
  107. package/src/runtime/memory/lib/embedding-worker.mjs +97 -103
  108. package/src/runtime/memory/lib/memory-chunk-quality.mjs +40 -218
  109. package/src/runtime/memory/lib/memory-embed.mjs +0 -9
  110. package/src/runtime/memory/lib/memory.mjs +34 -23
  111. package/src/runtime/memory/tool-defs.mjs +2 -1
  112. package/src/runtime/shared/automation-attachments.mjs +9 -7
  113. package/src/runtime/shared/environment-error.mjs +14 -0
  114. package/src/runtime/shared/error-presentation.mjs +10 -2
  115. package/src/runtime/shared/inline-file-kind.mjs +28 -2
  116. package/src/runtime/shared/llm/model-catalog.mjs +17 -3
  117. package/src/runtime/shared/llm/quota-value-estimate.mjs +33 -18
  118. package/src/runtime/shared/llm/usage-accounting.mjs +8 -5
  119. package/src/runtime/shared/llm/usage-ledger-quota.mjs +29 -10
  120. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +47 -4
  121. package/src/runtime/shared/model-alias.mjs +5 -0
  122. package/src/runtime/shared/read-image-resize.mjs +56 -0
  123. package/src/runtime/shared/tool-card-model/displayed-result.mjs +7 -5
  124. package/src/runtime/shared/tool-card-model.mjs +2 -1
  125. package/src/runtime/shared/tool-result-summary.mjs +120 -59
  126. package/src/runtime/shared/turn-review-reply.mjs +63 -0
  127. package/src/runtime/shared/turn-snapshot.mjs +8 -5
  128. package/src/runtime/shared/turn-worktree-snapshot.mjs +56 -19
  129. package/src/session-runtime/boot/apis.mjs +11 -0
  130. package/src/session-runtime/boot/providers.mjs +2 -0
  131. package/src/session-runtime/boot/session.mjs +5 -0
  132. package/src/session-runtime/boot/shared.mjs +2 -3
  133. package/src/session-runtime/native-web-search/tool-shape.mjs +8 -8
  134. package/src/session-runtime/native-web-search.mjs +6 -8
  135. package/src/session-runtime/provider-auth/login.mjs +26 -1
  136. package/src/session-runtime/provider-models/row-hydration.mjs +9 -0
  137. package/src/session-runtime/quick-model-rows/provider-rows.mjs +17 -2
  138. package/src/session-runtime/services/provider-admin.mjs +215 -3
  139. package/src/session-runtime/services/usage-stats-model.mjs +37 -2
  140. package/src/session-runtime/settings-builtin-tools-api.mjs +29 -4
  141. package/src/session-runtime/setup-tool/executor.mjs +10 -1
  142. package/src/session-runtime/webhook-session-run.mjs +0 -9
  143. package/src/standalone/daemon-stored-session-views.mjs +15 -0
  144. package/src/standalone/session-protocol.mjs +4 -0
  145. package/src/standalone/session-service/entries/session-loads.mjs +11 -3
  146. package/src/standalone/session-service/entries.mjs +1 -0
  147. package/src/standalone/session-service/session-calls/session-action.mjs +31 -3
  148. package/src/standalone/session-service/session-calls/session-turns.mjs +2 -1
  149. package/src/standalone/session-service.mjs +2 -0
  150. package/src/tui/app/model-options.mjs +10 -1
  151. package/src/tui/app/usage-context-panels/context-model.mjs +4 -1
  152. package/src/tui/app/usage-context-panels.mjs +3 -1
  153. package/src/tui/app/use-prompt-queue-history/restore-to-prompt.mjs +1 -0
  154. package/src/tui/components/ContextPanel.jsx +2 -0
  155. package/src/tui/dist/index.mjs +144 -70
  156. package/src/tui/paste-attachments.mjs +2 -1
  157. package/src/tui/session/session-api/intake/abort.mjs +35 -3
  158. package/src/tui/session/session-api/integrations.mjs +14 -59
  159. package/src/tui/session/session-api/media.mjs +1 -1
  160. package/src/tui/session/session-flow/queue/take-entries.mjs +21 -11
  161. package/src/tui/session/session-flow/steering.mjs +17 -3
  162. package/src/tui/session/session-flow.mjs +1 -0
  163. package/src/tui/session/turn.mjs +18 -4
  164. package/src/vendor/statusline/src/gateway/route-meta.mjs +2 -0
  165. package/scripts/memory-layer2-check.mjs +0 -186
@@ -16,8 +16,8 @@ import { HANDOFF_TIMEOUT_MAX_MS } from '../compact/constants.mjs';
16
16
  import { envFlag, envPositiveInt } from '../../../../shared/env.mjs';
17
17
  import { positiveInt } from '../../../../shared/numbers.mjs';
18
18
  import { isAgentOwner } from '../../agent-owner.mjs';
19
- import { providerInputExcludesCache } from '../../providers/registry.mjs';
20
- import { contentMediaBytes } from '../../providers/media-normalization.mjs';
19
+ import { providerInputExcludesCache, providerNativePdf } from '../../providers/registry.mjs';
20
+ import { contentMediaBytes, contentMediaCount } from '../../providers/media-normalization.mjs';
21
21
 
22
22
  // Unified context-share rule (compact/constants.mjs CONTEXT_SHARE_RATIO): the
23
23
  // post-compaction target is 25% of the boundary/context window. One
@@ -547,6 +547,17 @@ export function resolveGaugeContextTokens(messageTokensEst, policy, { messages,
547
547
  return resolveContextTokens(messageTokensEst, policy, { messages, sessionRef });
548
548
  }
549
549
 
550
+ /**
551
+ * The "before" number a compaction row shows: the context the provider last
552
+ * measured, as the gauge displayed it. Falls back to the pass's own value only
553
+ * when no live reading exists (none yet, or invalidated by a prior compaction).
554
+ */
555
+ export function compactDisplayBeforeTokens(sessionRef, fallbackTokens) {
556
+ const lastContextTokens = positiveInt(sessionRef?.lastContextTokens);
557
+ if (lastContextTokens && sessionRef.lastContextTokensStaleAfterCompact !== true) return lastContextTokens;
558
+ return fallbackTokens;
559
+ }
560
+
550
561
  export function resolveCompactionPressureTokens(messageTokensEst, policy, { messages, sessionRef } = {}) {
551
562
  return resolveContextTokens(messageTokensEst, policy, { messages, sessionRef });
552
563
  }
@@ -591,13 +602,20 @@ export function compactTargetBudget(policy) {
591
602
  // part compaction drops, so it alone arms this trigger; attachments the user
592
603
  // sent survive compaction and would only make it repeat.
593
604
  const REQUEST_MEDIA_COMPACT_BYTES = 24_000_000;
605
+ // Providers also cap the number of images plus native documents one request
606
+ // may carry (100 on Anthropic); the same tool-result media arms the trigger.
607
+ const REQUEST_MEDIA_COMPACT_COUNT = 100;
594
608
 
595
- export function shouldCompactForRequestMedia(messages) {
609
+ export function shouldCompactForRequestMedia(messages, provider = '') {
610
+ // A PDF that lowers to text is neither bytes nor a document on the wire.
611
+ const nativePdf = providerNativePdf(provider);
596
612
  let bytes = 0;
613
+ let count = 0;
597
614
  for (const message of messages) {
598
615
  if (message?.role !== 'tool') continue;
599
- bytes += contentMediaBytes(message.content);
600
- if (bytes >= REQUEST_MEDIA_COMPACT_BYTES) return true;
616
+ bytes += contentMediaBytes(message.content, { nativePdf });
617
+ count += contentMediaCount(message.content, { nativePdf });
618
+ if (bytes >= REQUEST_MEDIA_COMPACT_BYTES || count > REQUEST_MEDIA_COMPACT_COUNT) return true;
601
619
  }
602
620
  return false;
603
621
  }
@@ -48,6 +48,7 @@ export async function prepareProviderRequest(state) {
48
48
  cwd,
49
49
  opts,
50
50
  signal,
51
+ crossTurnCalls,
51
52
  loopUsageMetricsTurnId,
52
53
  loopUsageMetricsEpoch,
53
54
  } = state;
@@ -101,6 +102,7 @@ export async function prepareProviderRequest(state) {
101
102
  firstTurnUsage,
102
103
  providerState,
103
104
  reactiveOverflowRetryPending,
105
+ crossTurnCalls,
104
106
  loopUsageMetricsTurnId,
105
107
  loopUsageMetricsEpoch,
106
108
  skipProactiveCompact: compactAttempted,
@@ -64,6 +64,7 @@ export async function beginIteration(state) {
64
64
  providerState: state.providerState,
65
65
  reactiveOverflowRetryPending: state.reactiveOverflowRetryPending,
66
66
  fixedProviderToolSurface: state.fixedProviderToolSurface,
67
+ crossTurnCalls: state.crossTurnCalls,
67
68
  loopUsageMetricsTurnId: state.usageMetricsTurnId,
68
69
  loopUsageMetricsEpoch: state.usageMetricsEpoch,
69
70
  });
@@ -234,17 +235,78 @@ function settleImageStrip(state, round, sent) {
234
235
  state.imageStrip = null;
235
236
  }
236
237
 
238
+ const MESSAGES_CACHE_TTL_MS = { '5m': 5 * 60 * 1000, '1h': 60 * 60 * 1000 };
239
+ const MISS_RATIO = 0.5;
240
+ const MISS_MIN_DROP_TOKENS = 4096;
241
+
242
+ /** Usage-based prompt-cache miss detection for Anthropic sessions. The
243
+ * baseline (previous request's cache read + write) lives on the session
244
+ * record so it survives a reload, unlike the in-memory prefix guard. */
245
+ function traceActualCacheMiss(state, sent, response, { previousGuardState, previousSendAt, cacheBreakIntent }) {
246
+ const { sessionRef, sessionId, model } = state;
247
+ const providerName = sessionRef?.provider || state.provider?.name || null;
248
+ if (!sessionRef || (providerName !== 'anthropic' && providerName !== 'anthropic-oauth')) return;
249
+ const usage = response?.usage || {};
250
+ const cachedTokens = Number(usage.cachedTokens) || 0;
251
+ const cachePrefixTokens = cachedTokens + (Number(usage.cacheWriteTokens) || 0);
252
+ const baseline = Number(sessionRef.lastProviderCachedPrefixTokens);
253
+ const ttlMs = MESSAGES_CACHE_TTL_MS[sessionRef.providerCacheOpts?.cacheStrategy?.messages];
254
+ const idleMs = sent.sendStartedAt - previousSendAt;
255
+ const requestPrefixChanged =
256
+ !!previousGuardState &&
257
+ previousGuardState.requestPrefixHash !== sent.prefixGuardCandidate?.requestPrefixHash;
258
+ if (
259
+ baseline > 0 &&
260
+ ttlMs &&
261
+ Number.isFinite(idleMs) &&
262
+ idleMs >= 0 &&
263
+ idleMs < ttlMs &&
264
+ cachedTokens < baseline * MISS_RATIO &&
265
+ baseline - cachedTokens >= MISS_MIN_DROP_TOKENS &&
266
+ !cacheBreakIntent &&
267
+ !requestPrefixChanged
268
+ ) {
269
+ traceCacheBreak(
270
+ {
271
+ sessionId,
272
+ iteration: state.iterations + 1,
273
+ classification: 'unexpected',
274
+ reason: 'actual_cache_miss',
275
+ source: 'provider_usage',
276
+ provider: providerName,
277
+ model: model || null,
278
+ actualCacheMiss: true,
279
+ cachedTokens,
280
+ promptTokens: Number(usage.promptTokens) || null,
281
+ previousCachedPrefixTokens: baseline,
282
+ idleMs,
283
+ missingPrefixGuardBaseline: !previousGuardState,
284
+ },
285
+ state.cacheBreakTraceOptions
286
+ );
287
+ }
288
+ sessionRef.lastProviderCachedPrefixTokens = cachePrefixTokens;
289
+ }
290
+
237
291
  /** Fold a completed send into the state: prefix guard, image-strip
238
292
  * rebaseline, per-request budgets, provider state, usage and diagnostics. */
239
293
  export function settleSendResult(state, round, sent) {
240
294
  const { opts, sessionRef, sessionId, model, messages } = state;
241
295
  const response = sent.result.response;
242
296
  state.response = response;
297
+ const previousGuardState = state.prefixGuardState;
298
+ const previousSendAt = Number(sessionRef?.lastProviderSendAt);
299
+ const cacheBreakIntent = opts.cacheBreakIntent;
243
300
  state.prefixGuardState = sent.prefixGuardCandidate;
244
301
  if (sessionRef) sessionRef._providerPrefixGuardState = state.prefixGuardState;
245
302
  // The provider read and refreshed its prompt cache when this request was
246
303
  // sent; agent cache-expiry compaction measures idle time from here.
247
304
  if (sessionRef) sessionRef.lastProviderSendAt = sent.sendStartedAt;
305
+ traceActualCacheMiss(state, sent, response, {
306
+ previousGuardState,
307
+ previousSendAt,
308
+ cacheBreakIntent,
309
+ });
248
310
  if (state.imageStrip) settleImageStrip(state, round, sent);
249
311
  opts.onToolCall = undefined;
250
312
  delete opts.cacheBreakIntent;
@@ -0,0 +1,79 @@
1
+ // Auto effort for one user turn. The Auto effort built-in (Extensions →
2
+ // Built-in) turns it `on`: the judged effort applies to this turn. The
3
+ // MIXDOG_AUTO_EFFORT env value (off/observe/on) overrides it for diagnostics;
4
+ // `observe` judges and logs without applying. Applies only to user-written
5
+ // prompts on models whose effort can change mid-conversation without
6
+ // invalidating the prompt cache.
7
+ import { readSection } from '../../../../shared/config.mjs';
8
+ import { effortConfigurationMode } from '../../providers/effort-configuration.mjs';
9
+ import { autoEffortBase, normalizeAutoEffortMode, resolveAutoEffort } from '../../providers/auto-effort.mjs';
10
+ import { builtinFeatureActive } from '../../runtime-core/builtin-features.mjs';
11
+ import { effortOptionsFor } from '../../runtime-core/effort.mjs';
12
+ import { judgeTurn, recordEffortDecision } from '../../../../effort-judge/judge-client.mjs';
13
+ import { promptContentText } from './prompt-utils.mjs';
14
+
15
+ export function autoEffortMode() {
16
+ if (process.env.MIXDOG_AUTO_EFFORT) return normalizeAutoEffortMode(process.env.MIXDOG_AUTO_EFFORT);
17
+ try {
18
+ return builtinFeatureActive(readSection('agent'), 'autoEffort') ? 'on' : 'off';
19
+ } catch {
20
+ return 'off';
21
+ }
22
+ }
23
+
24
+ function lastAssistantText(messages) {
25
+ for (let i = (messages?.length || 0) - 1; i >= 0; i--) {
26
+ const message = messages[i];
27
+ if (message?.role !== 'assistant') continue;
28
+ const text = promptContentText(message.content).trim();
29
+ if (text) return text;
30
+ }
31
+ return '';
32
+ }
33
+
34
+ /**
35
+ * `{ base, effort, step, level, confidence, applied }` for this turn, or null
36
+ * when auto effort does not apply. The judge never delays the turn: a missing,
37
+ * warming, or slow model yields null and the default effort.
38
+ */
39
+ export async function resolveTurnAutoEffort({ sessionId, session, provider, input }) {
40
+ const mode = autoEffortMode();
41
+ // promptSource marks runtime-injected turns (completion notices, queued work);
42
+ // agent-owned sessions (workers, maintenance) receive runtime-written briefs.
43
+ if (mode === 'off' || input?.promptSource || session?.owner === 'agent') return null;
44
+ const request = promptContentText(input?.prompt).trim();
45
+ // Skill bodies and runtime blocks arrive as tagged text, not as a request.
46
+ if (!request || request.startsWith('<')) return null;
47
+ const opts = { ...(provider?.config || {}), modelParameters: session.modelParameters || {} };
48
+ if (!effortConfigurationMode(session.provider, session.model, opts)) return null;
49
+ const chosen = String(session.effort || '').toLowerCase();
50
+ const base = autoEffortBase(chosen);
51
+ const judged = await judgeTurn({ request, prev: lastAssistantText(session.messages) });
52
+ const record = {
53
+ at: new Date().toISOString(),
54
+ sessionId,
55
+ provider: session.provider,
56
+ model: session.model,
57
+ mode,
58
+ chosen,
59
+ base,
60
+ requestChars: request.length,
61
+ };
62
+ if (!judged.probs) {
63
+ recordEffortDecision({ ...record, skipped: judged.skipped });
64
+ return null;
65
+ }
66
+ const resolved = resolveAutoEffort({
67
+ base,
68
+ options: effortOptionsFor(session.provider, { id: session.model }),
69
+ probs: judged.probs,
70
+ });
71
+ recordEffortDecision({
72
+ ...record,
73
+ ...(resolved || { skipped: 'default-outside-auto-range' }),
74
+ ms: judged.ms,
75
+ probs: judged.probs.map((value) => Number(value.toFixed(3))),
76
+ });
77
+ if (!resolved) return null;
78
+ return { ...resolved, applied: mode === 'on' && resolved.effort !== chosen };
79
+ }
@@ -7,6 +7,7 @@
7
7
  import { getProvider } from '../../providers/registry.mjs';
8
8
  import { prepareExplicitSkills } from '../explicit-skills.mjs';
9
9
  import { prepareTurnEffortConfiguration } from '../../providers/effort-configuration.mjs';
10
+ import { resolveTurnAutoEffort } from './ask-turn-auto-effort.mjs';
10
11
  import { saveSessionAsync } from '../store.mjs';
11
12
  import { hasUserConversationMessage, refreshSessionBp3Environment } from './prompt-utils.mjs';
12
13
  import { filterModelVisibleSessionMessages } from './message-sanitize.mjs';
@@ -26,8 +27,13 @@ export async function prepareAskTurn({ sessionId, opened, input, cwdOverride, tr
26
27
  // Register the live session object for synchronous close snapshots.
27
28
  runtime.session = session;
28
29
  if (!provider) throw new Error(`Provider "${session.provider}" not available`);
29
- const turnEffort = session.effort || null;
30
- const effortConfiguration = prepareTurnEffortConfiguration(session, provider);
30
+ const autoEffort = await resolveTurnAutoEffort({ sessionId, session, provider, input });
31
+ const turnEffort = autoEffort?.applied ? autoEffort.effort : session.effort || null;
32
+ const effortConfiguration = prepareTurnEffortConfiguration(
33
+ session,
34
+ provider,
35
+ autoEffort?.applied ? autoEffort.effort : undefined
36
+ );
31
37
  applyTurnContextMeta(session, provider);
32
38
  const effectiveCwd = cwdOverride || session.cwd;
33
39
  // A failed-turn retry resubmits the prompt the failure left unanswered;
@@ -113,6 +119,8 @@ export async function prepareAskTurn({ sessionId, opened, input, cwdOverride, tr
113
119
  effectiveCwd,
114
120
  turnEffort,
115
121
  effortConfiguration,
122
+ // The effort Auto picked for this turn, shown next to the turn's result.
123
+ autoEffort: autoEffort ? autoEffort.effort : null,
116
124
  outgoing,
117
125
  beforeCount: historyMessages.length + 1,
118
126
  deferredToolDelta: userTurn.deferredToolDelta,
@@ -59,7 +59,12 @@ function attachAssistantTranscriptCompletion(messages, completion, turnStartedAt
59
59
  ...meta,
60
60
  transcript: {
61
61
  ...transcript,
62
- completion: { status, verb, elapsedMs },
62
+ completion: {
63
+ status,
64
+ verb,
65
+ elapsedMs,
66
+ ...(typeof completion.autoEffort === 'string' && completion.autoEffort ? { autoEffort: completion.autoEffort } : {}),
67
+ },
63
68
  },
64
69
  },
65
70
  };
@@ -122,6 +127,7 @@ export async function commitAskTurn({ sessionId, opened, prepared, result, rawTr
122
127
  ...result,
123
128
  trimmed: messagesDropped > 0,
124
129
  messagesDropped,
130
+ ...(prepared.autoEffort ? { autoEffort: prepared.autoEffort } : {}),
125
131
  };
126
132
  }
127
133
 
@@ -144,6 +150,7 @@ export function publishAskTurn({ sessionId, opened, terminalResultPreview, askOp
144
150
  status: 'done',
145
151
  verb: rawTranscriptMeta?.completionVerb,
146
152
  elapsedMs: Date.now() - turnStartedAt,
153
+ autoEffort: terminalResultPreview?.autoEffort,
147
154
  },
148
155
  turnStartedAt
149
156
  );
@@ -23,6 +23,7 @@ import { isSessionCompactionBlocked } from './runtime-liveness.mjs';
23
23
  import { resetReadStateAfterCompaction } from '../read-dedup.mjs';
24
24
  import {
25
25
  compactTargetBudget as compactTargetBudgetForPolicy,
26
+ compactDisplayBeforeTokens,
26
27
  currentContextEstimateTokens,
27
28
  invalidateProviderContextBaseline,
28
29
  recordContextUsageSnapshot,
@@ -233,10 +234,12 @@ function compactionPlanNumbers({ session, messages, boundary, alignedPolicy, for
233
234
  (boundary ? bufferTokens / boundary : resolveCompactBufferRatio(session.compaction || {}));
234
235
  const budget = alignedPolicy ? compactTargetBudgetForPolicy({ ...alignedPolicy, force }) || boundary : boundary;
235
236
  const pressureTokens = estimateTranscriptContextUsage(messages, session.tools || [], { provider: session.provider });
236
- const beforeTokens =
237
+ const beforeTokens = compactDisplayBeforeTokens(
238
+ session,
237
239
  (alignedPolicy
238
240
  ? resolveGaugeContextTokens(beforeMessageTokens, alignedPolicy, { messages, sessionRef: session })
239
- : 0) || pressureTokens;
241
+ : 0) || pressureTokens
242
+ );
240
243
  return { beforeMessageTokens, triggerTokens, bufferTokens, bufferRatio, budget, pressureTokens, beforeTokens };
241
244
  }
242
245
 
@@ -689,7 +692,7 @@ export async function runSessionCompaction(session, opts = {}) {
689
692
  });
690
693
  }
691
694
  plan.requireReduction =
692
- (plan.mode === 'auto' || opts.requireReduction === true) && !shouldCompactForRequestMedia(plan.messages);
695
+ (plan.mode === 'auto' || opts.requireReduction === true) && !shouldCompactForRequestMedia(plan.messages, session.provider);
693
696
  const skipped = () =>
694
697
  compactionResult(plan, { changed: false, skipped: true, reason: 'no_token_reduction' }, unchangedAfter(plan), {
695
698
  freshContext: false,
@@ -9,7 +9,11 @@ import { _isActivelyOwnedElsewhere, _recoverTurnCheckpointDurably } from './sess
9
9
  import { isAgentOwner } from '../../agent-owner.mjs';
10
10
  import { getHiddenAgent } from '../../internal-agents.mjs';
11
11
  import { loadConfig } from '../../config.mjs';
12
- import { buildProviderCacheOpts, cacheCapabilityForProvider } from '../../agent-runtime/cache-strategy.mjs';
12
+ import {
13
+ buildProviderCacheOpts,
14
+ cacheCapabilityForProvider,
15
+ roleProviderCacheOpts,
16
+ } from '../../agent-runtime/cache-strategy.mjs';
13
17
  import { normalizeAutoClearConfig, resolveAutoClearIdleMs } from '../../runtime-core/config-helpers.mjs';
14
18
  import { _buildBaseRules } from './rules-cache.mjs';
15
19
  import { composeSessionSystem, seedSessionMessages } from './session-prompt-composition.mjs';
@@ -43,8 +47,11 @@ export { prefetchSession, prepareSessionProjection } from './prepared-resume-cac
43
47
  function buildSessionProviderCacheOpts(providerName, sessionId, agent = null) {
44
48
  // Keep this in sync with createSession's provider-cache policy: only
45
49
  // explicit-breakpoint providers get BP cache opts here; OpenAI/key-prefix
46
- // providers use promptCacheKey and request-time strategy instead.
47
- if (cacheCapabilityForProvider(providerName) !== 'explicit-breakpoint') return null;
50
+ // providers use promptCacheKey and request-time strategy instead, plus the
51
+ // shared prompt-cache scope of a one-shot role.
52
+ if (cacheCapabilityForProvider(providerName) !== 'explicit-breakpoint') {
53
+ return roleProviderCacheOpts(providerName, agent);
54
+ }
48
55
  try {
49
56
  let autoClear = null;
50
57
  if (!agent || agent === 'lead') {
@@ -88,7 +88,11 @@ export function composeSessionSystem(opts, { profile, providerName, modelName, s
88
88
  // Lead-only response language; closes the environment block so no English
89
89
  // system text (session/shell/git lines) follows it.
90
90
  const languageContext = leadOnly ? _buildLeadLanguageContext() : '';
91
- const shellEnvironmentContext = buildShellEnvironmentContext(opts, ownerIsAgent, surface.toolsForRouting);
91
+ // Cwd, shell and repository facts only serve tool calls; a tool-free role
92
+ // gets none, which also keeps its prompt identical across projects.
93
+ const shellEnvironmentContext = toolFreeRole
94
+ ? ''
95
+ : buildShellEnvironmentContext(opts, ownerIsAgent, surface.toolsForRouting);
92
96
  // Persisted env tail: refreshSessionBp3Environment rebuilds the env block
93
97
  // as [session, bp3EnvironmentContext], so the language block must
94
98
  // already sit at the end of this string to stay last after a refresh.
@@ -5,6 +5,7 @@
5
5
  import {
6
6
  resolveWorkerCompactPolicy,
7
7
  compactionTelemetryPressureTokens,
8
+ compactDisplayBeforeTokens,
8
9
  currentContextEstimateTokens,
9
10
  compactTargetBudget,
10
11
  shouldCompactForRequestMedia,
@@ -17,6 +18,8 @@ import {
17
18
  } from './loop/compact-policy.mjs';
18
19
  import { previewFreshContextCompaction, runFreshContextCompact } from './loop/fresh-context.mjs';
19
20
  import { estimateMessagesTokensSafe } from './loop/compact-debug.mjs';
21
+ import { textPdfAllowanceDiscount } from './context-utils.mjs';
22
+ import { providerNativePdf } from '../providers/registry.mjs';
20
23
  import { messagesArrayChanged } from './loop/tool-helpers.mjs';
21
24
  import { normalizeUsage, addUsage } from './loop/usage.mjs';
22
25
  import { reasoningUsage } from '../../../shared/llm/reasoning-usage.mjs';
@@ -66,6 +69,13 @@ function transcriptPrefixHash(messages) {
66
69
  }
67
70
  }
68
71
 
72
+ // The transcript estimate prices every PDF as a native document; a provider
73
+ // that takes PDFs as text sends at most the extraction budget instead.
74
+ function providerAwareTokens(tokens, messages, sessionRef) {
75
+ if (tokens == null || providerNativePdf(sessionRef?.provider)) return tokens;
76
+ return Math.max(0, tokens - textPdfAllowanceDiscount(messages));
77
+ }
78
+
69
79
  // The compaction decision for this send. `pressureTokens` is the exact
70
80
  // canonical value the decision uses; reactive overflow recovery floors it at
71
81
  // the trigger so the gauge, telemetry, and forced compact still describe the
@@ -76,7 +86,7 @@ function transcriptPrefixHash(messages) {
76
86
  // proactive triggers still need a smaller predicted transcript.
77
87
  function preSendCompactDecision(state, compactPolicy) {
78
88
  const { messages, sessionRef } = state;
79
- const messageTokensEst = estimateMessagesTokensSafe(messages);
89
+ const messageTokensEst = providerAwareTokens(estimateMessagesTokensSafe(messages), messages, sessionRef);
80
90
  const reactivePending = state.reactiveOverflowRetryPending === true;
81
91
  const pressureTokens = compactionTelemetryPressureTokens(messageTokensEst, compactPolicy, {
82
92
  reactivePending,
@@ -84,7 +94,7 @@ function preSendCompactDecision(state, compactPolicy) {
84
94
  sessionRef,
85
95
  });
86
96
  const cacheExpired = shouldCompactForExpiredAgentCache(sessionRef, state.opts);
87
- const mediaPressure = shouldCompactForRequestMedia(messages);
97
+ const mediaPressure = shouldCompactForRequestMedia(messages, sessionRef?.provider);
88
98
  const requireReduction = !reactivePending && !mediaPressure;
89
99
  let shouldCompact =
90
100
  state.skipProactiveCompact !== true &&
@@ -209,7 +219,7 @@ function emitCompact(ctx, extra) {
209
219
  sessionId: state.sessionId,
210
220
  stage: 'pre_send',
211
221
  trigger: decision.compactTrigger,
212
- beforeTokens: decision.pressureTokens,
222
+ beforeTokens: compactDisplayBeforeTokens(state.sessionRef, decision.pressureTokens),
213
223
  beforeMessages: before.count,
214
224
  afterMessages: state.messages.length,
215
225
  pressureTokens: decision.pressureTokens,
@@ -369,11 +379,17 @@ function throwCompactFailure(ctx, run, compactErr) {
369
379
  // Replaces the live transcript with the compacted one and resets everything
370
380
  // keyed to the old transcript shape.
371
381
  function adoptCompactedTranscript(ctx, run) {
372
- const { messages, sessionRef, sessionId, opts } = ctx.state;
382
+ const { state } = ctx;
383
+ const { messages, sessionRef, sessionId, opts } = state;
373
384
  messages.length = 0;
374
385
  messages.push(...run.compacted);
375
386
  acknowledgeGoalReminder(sessionRef, run);
376
387
  resetReadStateAfterCompaction(sessionId);
388
+ // Cross-turn receipts reference tool results that compaction just removed
389
+ // from context; a stub for them would point at nothing. Clear in place so
390
+ // the eager dispatcher sharing this map observes it. Cached bodies stay
391
+ // reusable and are re-delivered (and re-recorded) on the next identical call.
392
+ state.crossTurnCalls?.clear();
377
393
  // This attempt's provider-tool scope was keyed to the old transcript
378
394
  // shape. Invalidate it synchronously before any post-compact callback or
379
395
  // subsequent async continuation.
@@ -402,7 +418,7 @@ function reportCompactOutcome(ctx, run) {
402
418
  const { messages, sessionRef } = state;
403
419
  const changed = ctx.compactChanged || run.summaryChanged;
404
420
  const { freshContextResult } = run;
405
- const afterMessageTokensEst = estimateMessagesTokensSafe(messages);
421
+ const afterMessageTokensEst = providerAwareTokens(estimateMessagesTokensSafe(messages), messages, sessionRef);
406
422
  // Same scale as the pre-compact gauge: compaction invalidated the provider
407
423
  // baseline, so the post-compact gauge number is the calibrated transcript
408
424
  // estimate plus the request reserve.
@@ -25,6 +25,8 @@ import {
25
25
  persistsConfirmedImageRejection,
26
26
  promptHasInlineImages,
27
27
  shouldStripImagesForRetry,
28
+ isImagePreparationFailed,
29
+ stripFailingImage,
28
30
  stripInlineImagesFromLatestTurn,
29
31
  } from './image-strip-recovery.mjs';
30
32
  import {
@@ -375,7 +377,13 @@ async function recoverFromSendError(sendErr, state) {
375
377
  // only when no tool was dispatched. Text exposure must be retracted first;
376
378
  // reasoning-only high-effort streams can retry without a text reset.
377
379
  if (
378
- imageStripEligible(sendErr, outcome, { recoveryMessages, imageStripUsed, transportRetriesUsed, relayWitness }) &&
380
+ imageStripEligible(sendErr, outcome, {
381
+ recoveryMessages,
382
+ imageStripUsed,
383
+ transportRetriesUsed,
384
+ relayWitness,
385
+ isGrok: (sessionRef?.provider || state.provider?.name) === 'grok-oauth',
386
+ }) &&
379
387
  (outcome.replaySafe === true || (await retract()))
380
388
  ) {
381
389
  const stripped = retryWithImageStrip(sendErr, recoveryMessages, state);
@@ -684,7 +692,7 @@ function repairThinkingReplay(recoveryMessages, state) {
684
692
  function imageStripEligible(
685
693
  sendErr,
686
694
  outcome,
687
- { recoveryMessages, imageStripUsed, transportRetriesUsed, relayWitness }
695
+ { recoveryMessages, imageStripUsed, transportRetriesUsed, relayWitness, isGrok }
688
696
  ) {
689
697
  return (
690
698
  transportRetriesUsed < TRANSPORT_RETRY_MAX &&
@@ -692,7 +700,13 @@ function imageStripEligible(
692
700
  hasImages: promptHasInlineImages(recoveryMessages),
693
701
  alreadyStripped: imageStripUsed === true,
694
702
  }) ||
695
- (imageStripUsed !== true && promptHasInlineImages(recoveryMessages) && isRetryableStreamErrorEvent(sendErr))) &&
703
+ // Grok Build RetryWithImageStrip only: a bare stream error on a Grok
704
+ // upload may be the image body being rejected. Other providers' transient
705
+ // stream errors are not evidence about images and use normal transport retry.
706
+ (isGrok &&
707
+ imageStripUsed !== true &&
708
+ promptHasInlineImages(recoveryMessages) &&
709
+ isRetryableStreamErrorEvent(sendErr))) &&
696
710
  outcome.sideEffectDispatched !== true &&
697
711
  Number(outcome.toolCallsDispatched) === 0 &&
698
712
  Number(outcome.toolCallsComplete) === 0 &&
@@ -702,7 +716,9 @@ function imageStripEligible(
702
716
 
703
717
  function retryWithImageStrip(sendErr, recoveryMessages, state) {
704
718
  const { opts, transportRetriesUsed = 0 } = state;
705
- const stripped = stripInlineImagesFromLatestTurn(recoveryMessages);
719
+ const stripped = isImagePreparationFailed(sendErr)
720
+ ? stripFailingImage(recoveryMessages, sendErr)
721
+ : stripInlineImagesFromLatestTurn(recoveryMessages);
706
722
  if (!(stripped.stripped > 0)) return null;
707
723
  const attempt = transportRetriesUsed + 1;
708
724
  logLoop(
@@ -1,4 +1,4 @@
1
- import { sanitizeContentForStoredHistory } from '../../providers/media-normalization.mjs';
1
+ import { contentCarriesLiveMedia } from '../../providers/media-normalization.mjs';
2
2
 
3
3
  const _lastSaveError = new Map(); // id -> { message, at }
4
4
 
@@ -153,10 +153,11 @@ export function _clearLiveSession(id) {
153
153
  if (id) _liveSessions.delete(id);
154
154
  }
155
155
 
156
- // Live snapshots that still carry raw media bytes (images are placeholder'd
157
- // in the persisted JSON) stay resident for this long after their last use so
158
- // multi-turn image recognition keeps working across an idle gap. Beyond the
159
- // TTL the memory cost wins and the snapshot is reclaimed like any other.
156
+ // Live snapshots that still carry inline media bytes (the persisted JSON holds
157
+ // content-addressed refs, or a placeholder for media the store refused) stay
158
+ // resident for this long after their last use so multi-turn image recognition
159
+ // keeps working across an idle gap. Beyond the TTL the memory cost wins and the
160
+ // snapshot is reclaimed like any other.
160
161
  export const LIVE_MEDIA_RETENTION_MS = Math.max(
161
162
  60_000,
162
163
  Number(process.env.MIXDOG_LIVE_MEDIA_RETENTION_MS) || 10 * 60 * 1000
@@ -166,7 +167,7 @@ export function _messagesCarryLiveMedia(messages) {
166
167
  if (!Array.isArray(messages)) return false;
167
168
  for (const m of messages) {
168
169
  if (!m || typeof m !== 'object') continue;
169
- if (sanitizeContentForStoredHistory(m.content) !== m.content) return true;
170
+ if (contentCarriesLiveMedia(m.content)) return true;
170
171
  }
171
172
  return false;
172
173
  }
@@ -4,9 +4,12 @@ import { renameWithRetrySync } from '../../../../shared/atomic-file.mjs';
4
4
  import { sanitizeContentForStoredHistory } from '../../providers/media-normalization.mjs';
5
5
  import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from '../lifecycle-scan.mjs';
6
6
 
7
- // Inline legacy media is replaced with placeholders on disk. New prompt media
8
- // already arrives as durable content-addressed refs, so session JSON stays small
9
- // while provider lowering can still resolve it across turns.
7
+ // Inline base64 images (tool screenshots included) are written as
8
+ // content-addressed refs, like prompt media, so session JSON stays small while
9
+ // provider lowering resolves them to the same bytes across turns and reloads.
10
+ // Inline documents (PDF, Office, text) are stored the same way as `file` refs.
11
+ // Only media the store refuses (over the size cap, disk error) or that has no
12
+ // inline bytes (URL/file-id images) is replaced with a placeholder on disk.
10
13
  // Per-message disk projection shared by _sessionForDisk and the save-worker
11
14
  // delta path (tail-only projection). Returns the SAME array reference when no
12
15
  // message changed. Idempotent: already-sanitized content passes through
@@ -5,6 +5,14 @@
5
5
  // and then flow through the normal post-tool steering boundary. The background
6
6
  // task itself keeps running.
7
7
  const waitersBySession = new Map();
8
+ const steeringChecks = new Set();
9
+
10
+ // Scoped to an active turn. Read the live queue at wait entry rather than
11
+ // retaining a wake signal that could outlive a consumed or reclaimed prompt.
12
+ export function registerTaskWaitSteeringCheck(check) {
13
+ steeringChecks.add(check);
14
+ return () => steeringChecks.delete(check);
15
+ }
8
16
 
9
17
  function sessionKey(sessionId) {
10
18
  return String(sessionId || '').trim();
@@ -45,6 +53,11 @@ export function beginInterruptibleTaskWait(sessionId, parentSignal = null) {
45
53
  }
46
54
  }
47
55
 
56
+ if (!controller.signal.aborted && [...steeringChecks].some((check) => check(key))) {
57
+ waiter.interruptedByUser = true;
58
+ controller.abort('user-message');
59
+ }
60
+
48
61
  return {
49
62
  signal: controller.signal,
50
63
  get interruptedByUser() {
@@ -4,9 +4,10 @@
4
4
  import { markSessionToolCall } from '../manager/runtime-liveness.mjs';
5
5
  import { classifyResultKind } from '../result-classification.mjs';
6
6
  import { normalizeToolEnvelope } from '../tool-envelope.mjs';
7
- import { captureReadCacheState, tryReadCached, tryScopedToolCached } from '../read-dedup.mjs';
7
+ import { captureReadCacheState } from '../read-dedup.mjs';
8
+ import { lookupToolResultReuse } from '../cache/tool-result-reuse.mjs';
8
9
  import { isInvalidToolArgsMarker, formatInvalidToolArgsResult } from '../../providers/openai-compat-stream.mjs';
9
- import { _stripMcpPrefix, _isReadTool, _isScopedCacheableTool } from '../loop/tool-classify.mjs';
10
+ import { _isReadTool } from '../loop/tool-classify.mjs';
10
11
  import { preDispatchDenyForSession } from '../loop/pre-dispatch-deny.mjs';
11
12
  import { getToolKind, isEagerDispatchable, isParallelDispatchable } from '../loop/tool-helpers.mjs';
12
13
  import { scopedCacheGeneration } from '../cache/scoped-cache.mjs';
@@ -73,24 +74,13 @@ export async function executeBatchCall(batch, call, callIndex) {
73
74
  return exec;
74
75
  }
75
76
 
76
- // Cross-turn read dedup: an unchanged stat tuple (mtime/ctime/size/ino/dev)
77
- // since a prior read in THIS session returns the cached body instead of
78
- // executing. Scoped-tool cache (grep/glob/list + graph lookups): keyed by
79
- // (toolName, canonical args) without per-file stat, since these tools scan
80
- // many files; write-class tools evict entries whose registered dependency
81
- // root contains the touched path.
77
+ // Use the same freshness policy as eager admission and cross-turn references.
78
+ // Read entries are stat-validated; scoped entries retain dependency
79
+ // invalidation and their bounded TTL. A miss proceeds to execution.
82
80
  function lookupCaches(batch, call, exec) {
83
- const { sessionId, cwd } = batch;
84
- if (sessionId && _isReadTool(call.name)) {
85
- exec.readCacheHit = tryReadCached({ sessionId, args: call.arguments, cwd });
86
- } else if (sessionId && _isScopedCacheableTool(call.name)) {
87
- exec.scopedCacheHit = tryScopedToolCached({
88
- sessionId,
89
- toolName: _stripMcpPrefix(call.name),
90
- args: call.arguments,
91
- cwd,
92
- });
93
- }
81
+ const cached = lookupToolResultReuse({ sessionId: batch.sessionId, call, cwd: batch.cwd });
82
+ if (cached?.kind === 'read') exec.readCacheHit = cached.entry;
83
+ else if (cached?.kind === 'scoped') exec.scopedCacheHit = cached.entry;
94
84
  }
95
85
 
96
86
  function markInstant(exec) {