mixdog 0.9.51 → 0.9.53

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 (178) hide show
  1. package/package.json +5 -3
  2. package/scripts/abort-recovery-test.mjs +17 -1
  3. package/scripts/agent-model-liveness-test.mjs +79 -2
  4. package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
  5. package/scripts/anthropic-transport-policy-test.mjs +466 -0
  6. package/scripts/atomic-lock-tryonce-test.mjs +60 -1
  7. package/scripts/bench-run.mjs +2 -2
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/channel-daemon-smoke.mjs +630 -10
  10. package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
  11. package/scripts/code-graph-disk-hit-test.mjs +11 -0
  12. package/scripts/code-graph-root-federation-test.mjs +273 -0
  13. package/scripts/compact-pressure-test.mjs +159 -1
  14. package/scripts/compact-smoke.mjs +80 -0
  15. package/scripts/context-mcp-metering-test.mjs +1350 -19
  16. package/scripts/deferred-tool-loading-test.mjs +17 -0
  17. package/scripts/desktop-session-bridge-test.mjs +704 -0
  18. package/scripts/freevar-smoke.mjs +7 -4
  19. package/scripts/gemini-provider-test.mjs +1053 -0
  20. package/scripts/interrupted-turn-history-test.mjs +371 -0
  21. package/scripts/lifecycle-api-test.mjs +137 -0
  22. package/scripts/max-output-recovery-test.mjs +86 -0
  23. package/scripts/mcp-grace-deferred-test.mjs +89 -13
  24. package/scripts/memory-core-input-test.mjs +10 -0
  25. package/scripts/memory-pg-recovery-test.mjs +59 -0
  26. package/scripts/openai-oauth-ws-1006-retry-test.mjs +387 -6
  27. package/scripts/openai-ws-early-settle-test.mjs +40 -0
  28. package/scripts/parent-abort-link-test.mjs +24 -0
  29. package/scripts/process-lifecycle-test.mjs +447 -0
  30. package/scripts/provider-admission-scheduler-test.mjs +582 -0
  31. package/scripts/provider-contract-test.mjs +525 -0
  32. package/scripts/provider-toolcall-test.mjs +492 -11
  33. package/scripts/reactive-compact-persist-smoke.mjs +59 -0
  34. package/scripts/resource-admission-test.mjs +789 -0
  35. package/scripts/session-orphan-sweep-test.mjs +27 -1
  36. package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
  37. package/scripts/steering-drain-buckets-test.mjs +18 -0
  38. package/scripts/toolcall-args-test.mjs +14 -6
  39. package/scripts/tui-transcript-perf-test.mjs +5 -7
  40. package/src/cli.mjs +15 -2
  41. package/src/lib/keychain-cjs.cjs +36 -23
  42. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +27 -13
  43. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
  45. package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
  46. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
  47. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
  48. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +114 -308
  49. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +31 -21
  50. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -286
  51. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +42 -5
  52. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
  53. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
  54. package/src/runtime/agent/orchestrator/providers/gemini.mjs +84 -150
  55. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -119
  56. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
  57. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
  58. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
  59. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
  60. package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
  61. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
  62. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +28 -74
  64. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
  65. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +136 -20
  66. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +94 -33
  67. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +201 -123
  68. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +37 -28
  69. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
  70. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
  71. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
  72. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +50 -45
  73. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
  74. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
  75. package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
  76. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +229 -111
  77. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +130 -63
  78. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
  79. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +37 -224
  81. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
  82. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +81 -42
  83. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
  84. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
  85. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
  86. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
  87. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +30 -7
  88. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  89. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
  90. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
  91. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
  92. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
  93. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -4
  94. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +12 -1
  96. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
  97. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
  98. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
  99. package/src/runtime/agent/orchestrator/session/store.mjs +353 -108
  100. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
  101. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
  102. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
  103. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
  107. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
  108. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
  109. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +317 -250
  112. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
  113. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
  114. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
  115. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
  116. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
  117. package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
  118. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +125 -85
  119. package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
  120. package/src/runtime/channels/backends/discord.mjs +6 -6
  121. package/src/runtime/channels/lib/config.mjs +15 -2
  122. package/src/runtime/channels/lib/memory-client.mjs +20 -3
  123. package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
  124. package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
  125. package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
  126. package/src/runtime/channels/lib/worker-main.mjs +16 -5
  127. package/src/runtime/memory/index.mjs +46 -202
  128. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
  129. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
  130. package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
  131. package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
  132. package/src/runtime/memory/lib/pg/process.mjs +91 -47
  133. package/src/runtime/memory/lib/pg/supervisor.mjs +50 -17
  134. package/src/runtime/memory/lib/query-handlers.mjs +2 -122
  135. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
  136. package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
  137. package/src/runtime/search/lib/http-fetch.mjs +1 -1
  138. package/src/runtime/shared/atomic-file.mjs +28 -150
  139. package/src/runtime/shared/config.mjs +58 -13
  140. package/src/runtime/shared/llm/cost.mjs +14 -4
  141. package/src/runtime/shared/memory-snapshot.mjs +236 -0
  142. package/src/runtime/shared/process-lifecycle.mjs +436 -0
  143. package/src/runtime/shared/process-shutdown.mjs +33 -4
  144. package/src/runtime/shared/resource-admission.mjs +364 -0
  145. package/src/runtime/shared/staged-child-result.mjs +19 -0
  146. package/src/session-runtime/channel-config-api.mjs +7 -7
  147. package/src/session-runtime/config-lifecycle.mjs +20 -17
  148. package/src/session-runtime/context-status.mjs +38 -27
  149. package/src/session-runtime/cwd-plugins.mjs +9 -7
  150. package/src/session-runtime/env.mjs +1 -2
  151. package/src/session-runtime/hitch-profile.mjs +45 -0
  152. package/src/session-runtime/lifecycle-api.mjs +60 -13
  153. package/src/session-runtime/mcp-glue.mjs +6 -11
  154. package/src/session-runtime/provider-init-key.mjs +17 -0
  155. package/src/session-runtime/provider-request-tools.mjs +72 -0
  156. package/src/session-runtime/runtime-core.mjs +49 -105
  157. package/src/session-runtime/runtime-paths.mjs +20 -0
  158. package/src/session-runtime/runtime-tool-routing.mjs +55 -0
  159. package/src/session-runtime/session-turn-api.mjs +5 -4
  160. package/src/session-runtime/tool-catalog-data.mjs +51 -0
  161. package/src/session-runtime/tool-catalog.mjs +390 -104
  162. package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
  163. package/src/standalone/agent-tool/worker-rows.mjs +93 -0
  164. package/src/standalone/agent-tool.mjs +29 -200
  165. package/src/standalone/channel-admin.mjs +29 -0
  166. package/src/standalone/channel-daemon-client.mjs +200 -38
  167. package/src/standalone/channel-daemon-transport.mjs +136 -9
  168. package/src/standalone/channel-worker.mjs +79 -12
  169. package/src/tui/App.jsx +11 -5
  170. package/src/tui/dist/index.mjs +275 -343
  171. package/src/tui/engine/session-api-ext.mjs +50 -6
  172. package/src/tui/engine/session-api.mjs +1 -1
  173. package/src/tui/engine/turn.mjs +10 -6
  174. package/src/tui/engine.mjs +13 -4
  175. package/src/tui/index.jsx +5 -1
  176. package/src/tui/lib/voice-setup.mjs +21 -5
  177. package/scripts/_devtools-stub.mjs +0 -1
  178. package/src/runtime/lib/keychain-cjs.cjs +0 -288
@@ -1,7 +1,27 @@
1
1
  import { isOffloadedToolResultText } from './tool-result-offload.mjs';
2
2
  import { createHash } from 'node:crypto';
3
- import { isAgentOwner } from '../agent-owner.mjs';
3
+ import { providerNativeToolPrefixCount } from '../../../../session-runtime/provider-request-tools.mjs';
4
4
  import { contentImageDescriptors, contentToText } from '../providers/media-normalization.mjs';
5
+ export {
6
+ DEFAULT_COMPACTION_BUFFER_TOKENS,
7
+ DEFAULT_COMPACTION_BUFFER_RATIO,
8
+ DEFAULT_MAIN_COMPACTION_BUFFER_RATIO,
9
+ MAX_COMPACTION_BUFFER_RATIO,
10
+ DEFAULT_COMPACTION_KEEP_TOKENS,
11
+ positiveTokenInt,
12
+ normalizeCompactionBufferRatio,
13
+ resolveBufferRatioCandidate,
14
+ resolveCompactBufferRatio,
15
+ resolveMainCompactBufferRatio,
16
+ compactionBufferTokensForBoundary,
17
+ isPersistedZeroBufferTelemetry,
18
+ isLegacyDefaultBufferTelemetry,
19
+ compactBufferConfigForBoundary,
20
+ resolveCompactBufferTokens,
21
+ resolveMainCompactBufferTokens,
22
+ resolveCompactTriggerTokens,
23
+ resolveSessionCompactPolicy,
24
+ } from './context-compaction-policy.mjs';
5
25
 
6
26
  // ---------------------------------------------------------------------------
7
27
  // Conservative, Unicode-aware token estimator.
@@ -148,6 +168,14 @@ export function messageEstimateText(m) {
148
168
  if (m.role === 'assistant' && Array.isArray(m.reasoningItems) && m.reasoningItems.length) {
149
169
  text += `\n${nativeBlocksEstimateText(m.reasoningItems)}`;
150
170
  }
171
+ // Provider-scoped replay metadata is never sent to a different provider,
172
+ // but Gemini replays signed thought parts on subsequent Gemini turns.
173
+ const geminiThoughtParts = m.role === 'assistant'
174
+ ? m.providerMetadata?.gemini?.thoughtParts
175
+ : null;
176
+ if (Array.isArray(geminiThoughtParts) && geminiThoughtParts.length) {
177
+ text += `\n${nativeBlocksEstimateText(geminiThoughtParts)}`;
178
+ }
151
179
  if (m.role === 'tool' && m.toolCallId) text += `\n${m.toolCallId}`;
152
180
  return text;
153
181
  }
@@ -212,6 +240,7 @@ function contextMessageFingerprint(message) {
212
240
  thinkingBlocks: contextValueFingerprint(null),
213
241
  assistantBlocks: contextValueFingerprint(null),
214
242
  reasoningItems: contextValueFingerprint(null),
243
+ providerMetadata: contextValueFingerprint(null),
215
244
  toolCallId: null,
216
245
  };
217
246
  }
@@ -222,6 +251,7 @@ function contextMessageFingerprint(message) {
222
251
  thinkingBlocks: contextValueFingerprint(message.thinkingBlocks),
223
252
  assistantBlocks: contextValueFingerprint(message.assistantBlocks),
224
253
  reasoningItems: contextValueFingerprint(message.reasoningItems),
254
+ providerMetadata: contextValueFingerprint(message.providerMetadata),
225
255
  toolCallId: message?.toolCallId || null,
226
256
  };
227
257
  }
@@ -237,6 +267,7 @@ function sameContextMessageFingerprint(a, b) {
237
267
  && sameContextValueFingerprint(a.thinkingBlocks, b.thinkingBlocks)
238
268
  && sameContextValueFingerprint(a.assistantBlocks, b.assistantBlocks)
239
269
  && sameContextValueFingerprint(a.reasoningItems, b.reasoningItems)
270
+ && sameContextValueFingerprint(a.providerMetadata, b.providerMetadata)
240
271
  && a.toolCallId === b.toolCallId;
241
272
  }
242
273
 
@@ -365,228 +396,6 @@ function contextSummaryResult(state, count) {
365
396
  };
366
397
  }
367
398
 
368
- export const DEFAULT_COMPACTION_BUFFER_TOKENS = 0;
369
- export const DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
370
- // Main/user recall-fasttrack compacts at 95% by default. Lower triggers remain
371
- // opt-in through the mainBuffer* configuration and MIXDOG_MAIN_COMPACT_BUFFER_*.
372
- export const DEFAULT_MAIN_COMPACTION_BUFFER_RATIO = 0.05;
373
- export const MAX_COMPACTION_BUFFER_RATIO = 0.25;
374
- const MAX_BUFFER_INPUT_RATIO = 0.999_999;
375
- export const DEFAULT_COMPACTION_KEEP_TOKENS = 8_000;
376
- const LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
377
-
378
- export function positiveTokenInt(value) {
379
- const n = Number(value);
380
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
381
- }
382
-
383
- function envTokenInt(name) {
384
- return positiveTokenInt(process.env[name]);
385
- }
386
-
387
- export function normalizeCompactionBufferRatio(value, fallback = DEFAULT_COMPACTION_BUFFER_RATIO) {
388
- const n = Number(value);
389
- if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
390
- return fallback;
391
- }
392
-
393
- // Percent-named inputs (bufferPercent / bufferPct / *_BUFFER_PERCENT) carry a
394
- // PERCENT: 1 means 1% (0.01). Ratio-named inputs (bufferRatio / bufferFraction)
395
- // carry a fraction: 0.01 means 1%, and a legacy value > 1 is read as a percent.
396
- export function resolveBufferRatioCandidate(percentInputs = [], ratioInputs = []) {
397
- for (const raw of percentInputs) {
398
- const n = Number(raw);
399
- if (Number.isFinite(n) && n > 0) return Math.min(MAX_BUFFER_INPUT_RATIO, n / 100);
400
- }
401
- for (const raw of ratioInputs) {
402
- const n = Number(raw);
403
- if (Number.isFinite(n) && n > 0) return Math.min(MAX_BUFFER_INPUT_RATIO, n > 1 ? n / 100 : n);
404
- }
405
- return null;
406
- }
407
-
408
- export function resolveCompactBufferRatio(cfg = {}) {
409
- const resolved = resolveBufferRatioCandidate(
410
- [cfg.bufferPercent, cfg.bufferPct, process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT],
411
- [cfg.bufferRatio, cfg.bufferFraction, process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO],
412
- );
413
- return normalizeCompactionBufferRatio(resolved, DEFAULT_COMPACTION_BUFFER_RATIO);
414
- }
415
-
416
- // Main/user settings are deliberately separate from the semantic-agent buffer:
417
- // `buffer*` and MIXDOG_AGENT_COMPACT_BUFFER_* remain agent-only controls.
418
- // Source precedence is config before environment; within each source the fixed
419
- // unit precedence is tokens, percent, then ratio. Invalid/zero values are
420
- // ignored, and percent/ratio inputs are capped below 100%/1.0.
421
- function positiveTokenCandidate(values = []) {
422
- for (const value of values) {
423
- const tokens = positiveTokenInt(value);
424
- if (tokens) return tokens;
425
- }
426
- return null;
427
- }
428
-
429
- function resolveMainBufferSetting(cfg = {}) {
430
- const configTokens = positiveTokenCandidate([cfg.mainBufferTokens, cfg.mainBuffer]);
431
- if (configTokens) return { tokens: configTokens };
432
- const configRatio = resolveBufferRatioCandidate(
433
- [cfg.mainBufferPercent, cfg.mainBufferPct],
434
- [cfg.mainBufferRatio, cfg.mainBufferFraction],
435
- );
436
- if (configRatio !== null) return { ratio: configRatio };
437
-
438
- const envTokens = positiveTokenCandidate([process.env.MIXDOG_MAIN_COMPACT_BUFFER_TOKENS]);
439
- if (envTokens) return { tokens: envTokens };
440
- const envRatio = resolveBufferRatioCandidate(
441
- [process.env.MIXDOG_MAIN_COMPACT_BUFFER_PERCENT],
442
- [process.env.MIXDOG_MAIN_COMPACT_BUFFER_RATIO],
443
- );
444
- return envRatio === null ? null : { ratio: envRatio };
445
- }
446
-
447
- export function resolveMainCompactBufferRatio(cfg = {}) {
448
- const setting = resolveMainBufferSetting(cfg);
449
- return setting?.ratio ?? DEFAULT_MAIN_COMPACTION_BUFFER_RATIO;
450
- }
451
-
452
- export function compactionBufferTokensForBoundary(boundaryTokens, opts = {}) {
453
- const boundary = Math.max(0, Math.floor(Number(boundaryTokens) || 0));
454
- const explicit = Math.max(0, Math.floor(Number(opts.explicitTokens) || 0));
455
- if (!boundary) return explicit;
456
- const maxRatio = normalizeCompactionBufferRatio(opts.maxRatio, MAX_COMPACTION_BUFFER_RATIO);
457
- const cap = Math.max(0, Math.floor(boundary * maxRatio));
458
- if (explicit > 0) return Math.max(0, Math.min(explicit, cap));
459
- const ratio = normalizeCompactionBufferRatio(opts.ratio, DEFAULT_COMPACTION_BUFFER_RATIO);
460
- return Math.max(0, Math.min(Math.floor(boundary * ratio), cap));
461
- }
462
-
463
- export function isPersistedZeroBufferTelemetry(cfg = {}, boundaryTokens = 0) {
464
- const boundary = positiveTokenInt(boundaryTokens);
465
- if (!boundary) return false;
466
- if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
467
- for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
468
- const n = Number(process.env[envName]);
469
- if (Number.isFinite(n) && n > 0) return false;
470
- }
471
- for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
472
- const n = Number(cfg?.[key]);
473
- if (Number.isFinite(n) && n > 0) return false;
474
- }
475
- const ratio = Number(cfg?.bufferRatio);
476
- if (Number.isFinite(ratio) && ratio > 0) return false;
477
- const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
478
- if (!Number.isFinite(explicitTokens) || explicitTokens !== 0) return false;
479
- return true;
480
- }
481
-
482
- export function isLegacyDefaultBufferTelemetry(cfg = {}, boundaryTokens = 0) {
483
- const boundary = positiveTokenInt(boundaryTokens);
484
- if (!boundary) return false;
485
- if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
486
- for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
487
- const n = Number(process.env[envName]);
488
- if (Number.isFinite(n) && n > 0) return false;
489
- }
490
- // Percent/fraction-named fields are operator config. Legacy/default
491
- // telemetry persisted bufferTokens + bufferRatio after a check/compact pass.
492
- for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
493
- const n = Number(cfg?.[key]);
494
- if (Number.isFinite(n) && n > 0) return false;
495
- }
496
- const explicitTokens = positiveTokenInt(cfg?.bufferTokens ?? cfg?.buffer);
497
- const ratio = Number(cfg?.bufferRatio);
498
- if (!explicitTokens || !Number.isFinite(ratio) || Math.abs(ratio - LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO) > 1e-9) return false;
499
- const expectedTokens = Math.floor(boundary * LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO);
500
- const cfgBoundary = positiveTokenInt(cfg?.boundaryTokens);
501
- const cfgTrigger = positiveTokenInt(cfg?.triggerTokens);
502
- return explicitTokens === expectedTokens
503
- || (cfgBoundary === boundary && cfgTrigger > 0 && explicitTokens === Math.max(0, boundary - cfgTrigger));
504
- }
505
-
506
- export function compactBufferConfigForBoundary(cfg = {}, boundaryTokens = 0) {
507
- const base = cfg || {};
508
- if (!isLegacyDefaultBufferTelemetry(base, boundaryTokens)
509
- && !isPersistedZeroBufferTelemetry(base, boundaryTokens)) {
510
- return base;
511
- }
512
- return {
513
- ...base,
514
- bufferTokens: null,
515
- buffer: null,
516
- bufferRatio: null,
517
- };
518
- }
519
-
520
- export function resolveCompactBufferTokens(boundaryTokens, cfg = {}, opts = {}) {
521
- const boundary = positiveTokenInt(boundaryTokens);
522
- const effectiveCfg = compactBufferConfigForBoundary(cfg, boundary);
523
- const configured = positiveTokenInt(effectiveCfg.bufferTokens ?? effectiveCfg.buffer)
524
- || envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')
525
- || 0;
526
- if (!boundary) return configured || positiveTokenInt(opts.defaultTokens) || DEFAULT_COMPACTION_BUFFER_TOKENS;
527
- return compactionBufferTokensForBoundary(boundary, {
528
- explicitTokens: configured,
529
- ratio: resolveCompactBufferRatio(effectiveCfg),
530
- maxRatio: opts.maxRatio ?? MAX_COMPACTION_BUFFER_RATIO,
531
- });
532
- }
533
-
534
- export function resolveMainCompactBufferTokens(boundaryTokens, cfg = {}, opts = {}) {
535
- const boundary = positiveTokenInt(boundaryTokens);
536
- const setting = resolveMainBufferSetting(cfg);
537
- const configured = setting?.tokens || 0;
538
- if (!boundary) return configured || positiveTokenInt(opts.defaultTokens) || DEFAULT_COMPACTION_BUFFER_TOKENS;
539
- return compactionBufferTokensForBoundary(boundary, {
540
- explicitTokens: configured,
541
- ratio: setting?.ratio ?? DEFAULT_MAIN_COMPACTION_BUFFER_RATIO,
542
- maxRatio: opts.maxRatio ?? MAX_COMPACTION_BUFFER_RATIO,
543
- });
544
- }
545
-
546
- export function resolveCompactTriggerTokens(sessionOrConfig = {}, boundaryTokens = 0) {
547
- return resolveSessionCompactPolicy(sessionOrConfig, boundaryTokens).triggerTokens;
548
- }
549
-
550
- // Single source of truth for per-session compaction policy math. Manager
551
- // (compactTriggerForSession), the turn loop (resolveWorkerCompactPolicy), and
552
- // the /context gauge all derive their trigger/buffer from here so the numbers
553
- // never diverge. Rules:
554
- // - a truly-explicit sub-boundary auto-compact limit always wins
555
- // (trigger = limit) for every session type;
556
- // - agent-owned semantic sessions otherwise keep the default early-trigger
557
- // buffer (config-driven, default 10% -> compact at 90% of the boundary);
558
- // - main/user recall-fasttrack sessions use their independently configurable
559
- // buffer (default 25% -> compact at 75% of the effective boundary).
560
- // Returns the sanitized explicit limit (null when absent/legacy full-window)
561
- // plus triggerTokens / bufferTokens / bufferRatio for the given boundary.
562
- export function resolveSessionCompactPolicy(sessionOrConfig = {}, boundaryTokens = 0) {
563
- const cfg = sessionOrConfig?.compaction || sessionOrConfig || {};
564
- const boundary = positiveTokenInt(boundaryTokens);
565
- if (!boundary) {
566
- return {
567
- autoCompactTokenLimit: null,
568
- triggerTokens: null,
569
- bufferTokens: 0,
570
- bufferRatio: isAgentOwner(sessionOrConfig)
571
- ? resolveCompactBufferRatio(cfg)
572
- : resolveMainCompactBufferRatio(cfg),
573
- };
574
- }
575
- const rawLimit = positiveTokenInt(sessionOrConfig?.autoCompactTokenLimit ?? cfg?.autoCompactTokenLimit);
576
- const explicitLimit = rawLimit && rawLimit < boundary ? rawLimit : null;
577
- let triggerTokens;
578
- if (explicitLimit) {
579
- triggerTokens = explicitLimit;
580
- } else if (isAgentOwner(sessionOrConfig)) {
581
- triggerTokens = Math.max(1, boundary - resolveCompactBufferTokens(boundary, cfg));
582
- } else {
583
- triggerTokens = Math.max(1, boundary - resolveMainCompactBufferTokens(boundary, cfg));
584
- }
585
- const bufferTokens = Math.max(0, boundary - triggerTokens);
586
- const bufferRatio = bufferTokens / boundary;
587
- return { autoCompactTokenLimit: explicitLimit, triggerTokens, bufferTokens, bufferRatio };
588
- }
589
-
590
399
  function stripSystemReminder(text) {
591
400
  return String(text || '')
592
401
  .replace(/^\s*<system-reminder>\s*/i, '')
@@ -680,8 +489,12 @@ const requestReserveTokenMemo = new WeakMap();
680
489
 
681
490
  function serializeToolSchemas(tools) {
682
491
  const list = Array.isArray(tools) ? tools : [];
492
+ const nativePrefixCount = providerNativeToolPrefixCount(list);
683
493
  try {
684
- return JSON.stringify(list.map(tool => {
494
+ return JSON.stringify(list.map((tool, index) => {
495
+ if (index < nativePrefixCount) {
496
+ return tool;
497
+ }
685
498
  const wireTool = {
686
499
  name: tool?.name,
687
500
  description: tool?.description,
@@ -28,16 +28,6 @@ import { providerInputExcludesCache } from '../../providers/registry.mjs';
28
28
  // the recall-fasttrack injection cap uses (loop.mjs recallTokenCap). One
29
29
  // number governs every "share of model context" budget.
30
30
 
31
- function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
32
- // Types are hard-locked (agent -> semantic, main/user -> recall-fasttrack).
33
- // Semantic must always be available as a compact path or agent sessions
34
- // would have none (loop.mjs throws when no type is available). Env/cfg
35
- // off-switches no longer apply.
36
- void sessionRef;
37
- void cfg;
38
- return true;
39
- }
40
-
41
31
  function resolveCompactTypeSetting(sessionRef, cfg = {}) {
42
32
  // Agent-owned sessions are ALWAYS semantic. recall-fasttrack rebuilds
43
33
  // context from Memory recall, which is scoped to the user's main-session
@@ -171,7 +161,7 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
171
161
  ? Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent)
172
162
  : null,
173
163
  autoCompactTokenLimit: explicitAutoCompactTokenLimit,
174
- semantic: compactTypeIsSemantic(compactType) && resolveSemanticCompactSetting(sessionRef, cfg),
164
+ semantic: compactTypeIsSemantic(compactType),
175
165
  recallFastTrack: compactTypeIsRecallFastTrack(compactType),
176
166
  semanticTimeoutMs: positiveTokenInt(cfg.timeoutMs) || envTokenInt('MIXDOG_AGENT_COMPACT_TIMEOUT_MS') || 30_000,
177
167
  tailTurns: positiveTokenInt(cfg.tailTurns) || envTokenInt('MIXDOG_AGENT_COMPACT_TAIL_TURNS') || 2,
@@ -191,15 +181,15 @@ function compactPressureTokens(messageTokensEst, policy) {
191
181
 
192
182
  function providerPressureTokens(sessionRef, usage) {
193
183
  if (!usage || typeof usage !== 'object') return 0;
194
- const input = Math.max(0, Number(usage.inputTokens) || 0);
195
- const cachedRead = Math.max(0, Number(usage.cachedTokens) || 0);
196
- const cacheWrite = Math.max(0, Number(usage.cacheWriteTokens) || 0);
197
- const explicitPrompt = Math.max(0, Number(usage.promptTokens) || 0);
184
+ const input = Math.max(0, Number(usage.mainInputTokens ?? usage.inputTokens) || 0);
185
+ const cachedRead = Math.max(0, Number(usage.mainCachedTokens ?? usage.cachedTokens) || 0);
186
+ const cacheWrite = Math.max(0, Number(usage.mainCacheWriteTokens ?? usage.cacheWriteTokens) || 0);
187
+ const explicitPrompt = Math.max(0, Number(usage.mainPromptTokens ?? usage.promptTokens) || 0);
198
188
  const normalizedPrompt = providerInputExcludesCache(sessionRef?.provider)
199
189
  ? input + cachedRead + cacheWrite
200
190
  : input;
201
191
  const prompt = Math.max(explicitPrompt, normalizedPrompt);
202
- const output = Math.max(0, Number(usage.outputTokens) || 0);
192
+ const output = Math.max(0, Number(usage.mainOutputTokens ?? usage.outputTokens) || 0);
203
193
  return Math.max(0, Math.round(prompt + output));
204
194
  }
205
195
 
@@ -213,10 +203,14 @@ export function recordProviderContextBaseline(sessionRef, messages, usage, {
213
203
  sendTools = sessionRef?.tools,
214
204
  } = {}) {
215
205
  if (!sessionRef || !Array.isArray(messages)) return false;
206
+ if (usage?.mainUsageAvailable === false) {
207
+ invalidateProviderContextBaseline(sessionRef);
208
+ return false;
209
+ }
216
210
  const tokens = providerPressureTokens(sessionRef, usage);
217
211
  if (!tokens) return false;
218
212
  sessionRef.contextPressureBaselineTokens = tokens;
219
- sessionRef.contextPressureBaselineOutputTokens = Math.max(0, Math.round(Number(usage?.outputTokens) || 0));
213
+ sessionRef.contextPressureBaselineOutputTokens = Math.max(0, Math.round(Number(usage?.mainOutputTokens ?? usage?.outputTokens) || 0));
220
214
  sessionRef.contextPressureBaselineMessageCount = messages.length;
221
215
  sessionRef.contextPressureBaselinePrefixSignature = contextMessagesSignature(messages);
222
216
  sessionRef.contextPressureBaselineProvider = sessionRef.provider || null;
@@ -22,7 +22,6 @@ import {
22
22
  buildCurrentTimeBlock,
23
23
  buildSessionStartBlock,
24
24
  hasUserConversationMessage,
25
- isInternalRuntimeNotificationText,
26
25
  } from './prompt-utils.mjs';
27
26
  import { _mergePendingMessageEntries, drainPendingMessages } from './pending-messages.mjs';
28
27
  import { persistIterationMetrics, applyAskTerminalUsageTotals } from './usage-metrics.mjs';
@@ -38,11 +37,16 @@ import {
38
37
  _touchRuntime,
39
38
  _unlinkParentAbortListener,
40
39
  _getRuntimeEntry,
40
+ _evictTerminalSessionRuntime,
41
41
  } from './runtime-liveness.mjs';
42
42
  import { SessionClosedError } from './session-errors.mjs';
43
43
  import { acquireSessionLock } from './session-lock.mjs';
44
44
  import { _tryBridgeExplicitPrefetch } from './prefetch-bridge.mjs';
45
- import { sanitizeSessionMessagesForModel, persistCompactedOutgoingAfterAskFailure } from './message-sanitize.mjs';
45
+ import {
46
+ filterModelVisibleSessionMessages,
47
+ persistCompactedOutgoingAfterAskFailure,
48
+ } from './message-sanitize.mjs';
49
+ import { createTurnInterruptionTracker } from './turn-interruption.mjs';
46
50
  import { _getAgentLoop } from './runtime-loaders.mjs';
47
51
  import { getAgentRuntimeSync } from './agent-runtime-singleton.mjs';
48
52
  import { recordProviderContextBaseline } from '../loop/compact-policy.mjs';
@@ -188,6 +192,8 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
188
192
  let activeSession = preSession;
189
193
  let cancelledUserTurnContent = '';
190
194
  let _turnOutgoing = null;
195
+ const _turnInterruption = createTurnInterruptionTracker();
196
+ const _sessionStartMetaInjectedBeforeTurn = preSession.sessionStartMetaInjected === true;
191
197
  try {
192
198
  const session = activeSession;
193
199
  const provider = getProvider(session.provider);
@@ -244,7 +250,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
244
250
  if (explicitPrefetchResult) {
245
251
  _contextBlock += `# Prefetch\n${_capCtx(explicitPrefetchResult)}\n\n`;
246
252
  }
247
- const historyMessages = sanitizeSessionMessagesForModel(session.messages);
253
+ const historyMessages = filterModelVisibleSessionMessages(session.messages);
248
254
  const beforeCount = historyMessages.length + 1;
249
255
  const promptTextForMetrics = promptContentText(prompt);
250
256
  // Soft warning only; real size management (compaction primary,
@@ -291,7 +297,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
291
297
  // below overwrites this provisional transcript with the fully mutated
292
298
  // outgoing history and appends the assistant result, so no duplicate
293
299
  // user turn is introduced.
294
- session.messages = sanitizeSessionMessagesForModel(outgoing);
300
+ session.messages = filterModelVisibleSessionMessages(outgoing);
295
301
  session.liveTurnMessages = outgoing;
296
302
  saveSessionAsync(session, { expectedGeneration: askGeneration }).catch((err) => {
297
303
  try { process.stderr.write(`[session] preflight user-turn save failed: ${err?.message || err}\n`); } catch {}
@@ -324,6 +330,27 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
324
330
  });
325
331
  } catch { /* trace must never break the ask path */ }
326
332
  const agentLoop = await _getAgentLoop();
333
+ const _trackTextDelta = (chunk) => {
334
+ _turnInterruption.recordTextDelta(chunk);
335
+ if (typeof askOpts?.onTextDelta === 'function') askOpts.onTextDelta(chunk);
336
+ };
337
+ const _trackReasoningDelta = (chunk) => {
338
+ _turnInterruption.recordReasoningDelta(chunk);
339
+ if (typeof askOpts?.onReasoningDelta === 'function') askOpts.onReasoningDelta(chunk);
340
+ };
341
+ const _trackAssistantText = (text) => {
342
+ _turnInterruption.recordAssistantText(text);
343
+ if (typeof askOpts?.onAssistantText === 'function') askOpts.onAssistantText(text);
344
+ };
345
+ const _trackedOnToolCall = async (iteration, calls) => {
346
+ _turnInterruption.recordToolCalls(calls);
347
+ if (typeof onToolCall === 'function') return await onToolCall(iteration, calls);
348
+ return undefined;
349
+ };
350
+ const _trackToolResult = (message) => {
351
+ _turnInterruption.recordToolResult(message);
352
+ if (typeof askOpts?.onToolResult === 'function') askOpts.onToolResult(message);
353
+ };
327
354
  const priorToolApprovalHook = session.toolApprovalHook;
328
355
  if (typeof askOpts?.onToolApproval === 'function') {
329
356
  session.toolApprovalHook = askOpts.onToolApproval;
@@ -331,13 +358,17 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
331
358
  let result;
332
359
  try {
333
360
  result = await _api_call_with_interrupt(sessionId, (signal) =>
334
- agentLoop(provider, outgoing, session.model, session.tools, onToolCall, effectiveCwd, {
361
+ agentLoop(provider, outgoing, session.model, session.tools, _trackedOnToolCall, effectiveCwd, {
335
362
  effort: session.effort || null,
336
363
  fast: session.fast === true,
337
364
  sessionId,
338
- onTextDelta: typeof askOpts?.onTextDelta === 'function' ? askOpts.onTextDelta : undefined,
339
- onReasoningDelta: typeof askOpts?.onReasoningDelta === 'function' ? askOpts.onReasoningDelta : undefined,
340
- onAssistantText: typeof askOpts?.onAssistantText === 'function' ? askOpts.onAssistantText : undefined,
365
+ onTextDelta: _trackTextDelta,
366
+ onReasoningDelta: _trackReasoningDelta,
367
+ onAssistantText: _trackAssistantText,
368
+ onAssistantMessageCommitted: () => _turnInterruption.markAssistantMessageCommitted(),
369
+ onAssistantToolCallObserved: (call, detail) => _turnInterruption.recordToolCalls([call], detail),
370
+ onProviderSendStarted: () => _turnInterruption.markProviderSendStarted(),
371
+ onToolPhaseStarted: () => _turnInterruption.markToolPhaseStarted(),
341
372
  onUsageDelta: (d) => {
342
373
  persistIterationMetrics(d).catch(() => {});
343
374
  // provider_send usage arrives before agentLoop appends
@@ -348,16 +379,17 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
348
379
  // tool results/steering.
349
380
  if (d?.source === 'provider_send') {
350
381
  recordProviderContextBaseline(session, outgoing, {
351
- inputTokens: d.deltaInput,
352
- outputTokens: d.deltaOutput,
353
- promptTokens: d.deltaPrompt,
354
- cachedTokens: d.deltaCachedRead,
355
- cacheWriteTokens: d.deltaCacheWrite,
382
+ mainUsageAvailable: d.contextUsageAvailable,
383
+ inputTokens: d.contextInputTokens ?? d.deltaInput,
384
+ outputTokens: d.contextOutputTokens ?? d.deltaOutput,
385
+ promptTokens: d.contextPromptTokens ?? d.deltaPrompt,
386
+ cachedTokens: d.contextCachedReadTokens ?? d.deltaCachedRead,
387
+ cacheWriteTokens: d.contextCacheWriteTokens ?? d.deltaCacheWrite,
356
388
  }, { boundary: 'request', sendTools: d.sendTools });
357
389
  }
358
390
  try { askOpts?.onUsageDelta?.(d); } catch {}
359
391
  },
360
- onToolResult: typeof askOpts?.onToolResult === 'function' ? askOpts.onToolResult : undefined,
392
+ onToolResult: _trackToolResult,
361
393
  onToolApproval: typeof askOpts?.onToolApproval === 'function' ? askOpts.onToolApproval : undefined,
362
394
  onCompactEvent: typeof askOpts?.onCompactEvent === 'function' ? askOpts.onCompactEvent : undefined,
363
395
  // Claude Code parity: mid-chain queued prompt/notification
@@ -429,7 +461,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
429
461
  // Update and save. outgoing is mutated in place by agentLoop
430
462
  // (compaction + safety trim), so its length reflects post-loop state.
431
463
  const messagesDropped = Math.max(0, beforeCount - outgoing.length);
432
- session.messages = sanitizeSessionMessagesForModel(outgoing);
464
+ session.messages = filterModelVisibleSessionMessages(outgoing);
433
465
  // Turn committed into session.messages; drop the live-turn alias so
434
466
  // contextStatus() reverts to the authoritative committed transcript.
435
467
  session.liveTurnMessages = null;
@@ -451,6 +483,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
451
483
  ...(typeof result.reasoningContent === 'string' && result.reasoningContent
452
484
  ? { reasoningContent: result.reasoningContent }
453
485
  : {}),
486
+ ...(result.providerMetadata && typeof result.providerMetadata === 'object'
487
+ ? { providerMetadata: result.providerMetadata }
488
+ : {}),
454
489
  });
455
490
  } else {
456
491
  // Empty terminal turn: still persist a forensic record so
@@ -576,12 +611,13 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
576
611
  });
577
612
  recordStandaloneStatusTelemetry(session, result, Date.now() - _askStartedAt);
578
613
  }
579
- // Persist opaque providerState for future stateful providers.
580
- // No provider currently emits it (openai-oauth is stateless per
581
- // contract), so this branch is dormant — kept so a future
582
- // Responses-API provider with stable continuation can plug in
583
- // without reworking the session shape.
584
- if (result.providerState !== undefined) {
614
+ // Persist opaque providerState for stateful providers. The update
615
+ // bit distinguishes an adapter that emitted no state update from
616
+ // an explicit clear caused by compaction/provider reset.
617
+ if (result.providerStateUpdated === true
618
+ && (result.providerState === undefined || result.providerState === null)) {
619
+ delete session.providerState;
620
+ } else if (result.providerStateUpdated === true || result.providerState !== undefined) {
585
621
  session.providerState = result.providerState;
586
622
  }
587
623
  const terminalResultPreview = {
@@ -634,28 +670,26 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
634
670
  const currentRuntime = _getRuntimeEntry(sessionId);
635
671
  if (!currentRuntime?.closed) {
636
672
  if (activeSession) {
637
- const originalMessages = Array.isArray(activeSession.messages) ? activeSession.messages : [];
638
- const cleanedMessages = sanitizeSessionMessagesForModel(originalMessages);
639
- const nextMessages = cleanedMessages.slice();
640
- // In-memory cancelled turn keeps its original content
641
- // (images intact for the next model send); the store
642
- // layer placeholders image bytes on disk serialization.
643
- const cancelledStoredContent = cancelledUserTurnContent;
644
- const shouldPreserveUserTurn = cancelledStoredContent && !isInternalRuntimeNotificationText(cancelledStoredContent);
645
- const lastMessage = nextMessages[nextMessages.length - 1];
646
- if (shouldPreserveUserTurn && !(lastMessage?.role === 'user' && promptContentText(lastMessage.content) === promptContentText(cancelledStoredContent))) {
647
- nextMessages.push({ role: 'user', content: cancelledStoredContent });
648
- }
649
- const messagesChanged = nextMessages.length !== originalMessages.length
650
- || nextMessages.some((message, index) => message !== originalMessages[index]);
651
- if (messagesChanged) {
652
- activeSession.messages = nextMessages;
653
- activeSession.updatedAt = Date.now();
654
- activeSession.lastUsedAt = Date.now();
655
- try {
656
- await saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
657
- } catch { /* cancellation cleanup is best-effort */ }
673
+ const finalized = _turnInterruption.finalize({
674
+ turnOutgoing: _turnOutgoing || activeSession.messages,
675
+ currentUserContent: cancelledUserTurnContent,
676
+ abortReason: err.reason,
677
+ });
678
+ activeSession.messages = finalized.messages;
679
+ if (!finalized.responsePreserved) {
680
+ activeSession.sessionStartMetaInjected = _sessionStartMetaInjectedBeforeTurn;
681
+ } else {
682
+ // The opaque provider continuation now points at a
683
+ // request that ended mid-turn. Force full transcript
684
+ // replay on the next send instead of reusing it.
685
+ activeSession.providerState = undefined;
658
686
  }
687
+ activeSession.updatedAt = Date.now();
688
+ activeSession.lastUsedAt = Date.now();
689
+ try {
690
+ await saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
691
+ } catch { /* cancellation cleanup is best-effort */ }
692
+ if (currentRuntime) currentRuntime.session = activeSession;
659
693
  }
660
694
  markSessionCancelled(sessionId);
661
695
  }
@@ -715,6 +749,11 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
715
749
  // Detach the live session reference; ask is over.
716
750
  entry.session = null;
717
751
  }
752
+ // Final-stage runtime diagnostics are useful only while the turn is
753
+ // unwinding. Once its controller is detached, retaining the full entry
754
+ // (and any accidental references hanging from it) for the host lifetime
755
+ // turns one-shot agent traffic into an unbounded manager Map.
756
+ _evictTerminalSessionRuntime(sessionId);
718
757
  unlock();
719
758
  }
720
759
  }
@@ -12,6 +12,7 @@ import {
12
12
  effectiveBudget as compactEffectiveBudget,
13
13
  compactTypeIsRecallFastTrack,
14
14
  compactTypeIsSemantic,
15
+ normalizeCompactType,
15
16
  CONTEXT_SHARE_RATIO,
16
17
  RECALL_TOKEN_CAP_FLOOR_TOKENS,
17
18
  } from '../compact.mjs';
@@ -271,7 +272,13 @@ export async function runSessionCompaction(session, opts = {}) {
271
272
  : boundary;
272
273
  const pressureTokens = estimateTranscriptContextUsage(messages, session.tools || []);
273
274
  const beforeTokens = pressureTokens;
274
- const compactType = compactTypeForSession(session);
275
+ // Manual /compact may explicitly request the original semantic path:
276
+ // summarize THIS session's transcript directly without hydrating/searching
277
+ // Memory first. Automatic and auto-clear compaction keep their configured
278
+ // recall-fasttrack behavior unless their caller explicitly changes it.
279
+ const compactType = mode === 'manual' && opts.compactType != null
280
+ ? normalizeCompactType(opts.compactType)
281
+ : compactTypeForSession(session);
275
282
  if (!force && pressureTokens < triggerTokens) return {
276
283
  changed: false,
277
284
  reason: 'below threshold',
@@ -358,6 +365,7 @@ export async function runSessionCompaction(session, opts = {}) {
358
365
  tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
359
366
  keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
360
367
  preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
368
+ filterOldHistoryForIngest: opts.filterOldHistoryForIngest === true,
361
369
  force: true,
362
370
  },
363
371
  );
@@ -401,6 +409,7 @@ export async function runSessionCompaction(session, opts = {}) {
401
409
  tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
402
410
  keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
403
411
  preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
412
+ filterOldHistoryForIngest: opts.filterOldHistoryForIngest === true,
404
413
  force: true,
405
414
  },
406
415
  );