mixdog 0.9.52 → 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 (111) hide show
  1. package/package.json +1 -1
  2. package/scripts/bench-run.mjs +2 -2
  3. package/scripts/compact-pressure-test.mjs +104 -0
  4. package/scripts/desktop-session-bridge-test.mjs +704 -0
  5. package/scripts/freevar-smoke.mjs +7 -4
  6. package/scripts/lifecycle-api-test.mjs +65 -4
  7. package/scripts/max-output-recovery-test.mjs +31 -0
  8. package/scripts/memory-core-input-test.mjs +10 -0
  9. package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
  10. package/scripts/openai-ws-early-settle-test.mjs +40 -0
  11. package/scripts/parent-abort-link-test.mjs +24 -0
  12. package/scripts/process-lifecycle-test.mjs +80 -22
  13. package/scripts/provider-contract-test.mjs +257 -0
  14. package/scripts/provider-toolcall-test.mjs +172 -10
  15. package/scripts/session-orphan-sweep-test.mjs +27 -1
  16. package/src/lib/keychain-cjs.cjs +36 -23
  17. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
  18. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
  19. package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
  20. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
  22. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
  24. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
  25. package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
  26. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
  27. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
  28. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
  29. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
  30. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
  41. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
  42. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
  43. package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
  44. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
  45. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
  46. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
  47. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
  48. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
  49. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
  50. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
  53. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
  55. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
  57. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
  59. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
  61. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
  62. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
  63. package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
  64. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
  65. package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
  66. package/src/runtime/channels/backends/discord.mjs +6 -6
  67. package/src/runtime/channels/lib/config.mjs +15 -2
  68. package/src/runtime/channels/lib/memory-client.mjs +20 -3
  69. package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
  70. package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
  72. package/src/runtime/channels/lib/worker-main.mjs +16 -5
  73. package/src/runtime/memory/index.mjs +24 -198
  74. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
  75. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
  76. package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
  77. package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
  78. package/src/runtime/memory/lib/query-handlers.mjs +2 -122
  79. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
  80. package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
  81. package/src/runtime/search/lib/http-fetch.mjs +1 -1
  82. package/src/runtime/shared/config.mjs +58 -13
  83. package/src/runtime/shared/llm/cost.mjs +14 -4
  84. package/src/runtime/shared/memory-snapshot.mjs +236 -0
  85. package/src/runtime/shared/process-lifecycle.mjs +92 -19
  86. package/src/runtime/shared/process-shutdown.mjs +6 -0
  87. package/src/runtime/shared/resource-admission.mjs +7 -2
  88. package/src/session-runtime/channel-config-api.mjs +7 -7
  89. package/src/session-runtime/config-lifecycle.mjs +20 -17
  90. package/src/session-runtime/cwd-plugins.mjs +9 -7
  91. package/src/session-runtime/env.mjs +1 -2
  92. package/src/session-runtime/hitch-profile.mjs +45 -0
  93. package/src/session-runtime/lifecycle-api.mjs +36 -9
  94. package/src/session-runtime/mcp-glue.mjs +6 -11
  95. package/src/session-runtime/provider-init-key.mjs +17 -0
  96. package/src/session-runtime/runtime-core.mjs +44 -103
  97. package/src/session-runtime/runtime-paths.mjs +20 -0
  98. package/src/session-runtime/runtime-tool-routing.mjs +55 -0
  99. package/src/session-runtime/tool-catalog-data.mjs +51 -0
  100. package/src/session-runtime/tool-catalog.mjs +15 -89
  101. package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
  102. package/src/standalone/agent-tool/worker-rows.mjs +93 -0
  103. package/src/standalone/agent-tool.mjs +12 -162
  104. package/src/standalone/channel-admin.mjs +29 -0
  105. package/src/tui/App.jsx +11 -5
  106. package/src/tui/dist/index.mjs +202 -65
  107. package/src/tui/engine/session-api-ext.mjs +37 -4
  108. package/src/tui/index.jsx +1 -0
  109. package/src/tui/lib/voice-setup.mjs +5 -5
  110. package/scripts/_devtools-stub.mjs +0 -1
  111. package/src/runtime/lib/keychain-cjs.cjs +0 -288
@@ -14,14 +14,27 @@ function _usageNum(value) {
14
14
  return Number.isFinite(n) ? n : 0;
15
15
  }
16
16
 
17
- export function _combineUsageWithWarmup(actual, warmup) {
17
+ export function _combineUsageWithWarmup(actual, warmup, { separateMainContext = false } = {}) {
18
18
  if (!warmup) return actual;
19
- if (!actual) return warmup;
19
+ if (!actual) {
20
+ // A warmup-only settle is billable, but has no corresponding main
21
+ // request whose context footprint can be measured.
22
+ const combined = {
23
+ ...warmup,
24
+ warmupInputTokens: _usageNum(warmup.inputTokens),
25
+ warmupCachedTokens: _usageNum(warmup.cachedTokens),
26
+ warmupOutputTokens: _usageNum(warmup.outputTokens),
27
+ warmupPromptTokens: _usageNum(warmup.promptTokens),
28
+ warmupCacheWriteTokens: _usageNum(warmup.cacheWriteTokens),
29
+ };
30
+ if (separateMainContext) combined.mainUsageAvailable = false;
31
+ return combined;
32
+ }
20
33
  const actualRaw = actual.raw || {};
21
34
  const warmupRaw = warmup.raw || {};
22
35
  const actualTicks = _usageNum(actualRaw.cost_in_usd_ticks);
23
36
  const warmupTicks = _usageNum(warmupRaw.cost_in_usd_ticks);
24
- return {
37
+ const combined = {
25
38
  ...actual,
26
39
  inputTokens: _usageNum(actual.inputTokens) + _usageNum(warmup.inputTokens),
27
40
  outputTokens: _usageNum(actual.outputTokens) + _usageNum(warmup.outputTokens),
@@ -30,12 +43,26 @@ export function _combineUsageWithWarmup(actual, warmup) {
30
43
  warmupInputTokens: _usageNum(warmup.inputTokens),
31
44
  warmupCachedTokens: _usageNum(warmup.cachedTokens),
32
45
  warmupOutputTokens: _usageNum(warmup.outputTokens),
46
+ warmupPromptTokens: _usageNum(warmup.promptTokens),
47
+ warmupCacheWriteTokens: _usageNum(warmup.cacheWriteTokens),
33
48
  raw: {
34
49
  ...actualRaw,
35
50
  warmup_usage: warmupRaw,
36
51
  ...(actualTicks || warmupTicks ? { cost_in_usd_ticks: actualTicks + warmupTicks } : {}),
37
52
  },
38
53
  };
54
+ if (separateMainContext) {
55
+ // OAuth startup prewarm is billable but is not part of the main
56
+ // request's context footprint. Keep these fields opt-in so shared xAI
57
+ // usage objects retain their prior shape and accounting behavior.
58
+ combined.mainInputTokens = _usageNum(actual.inputTokens);
59
+ combined.mainOutputTokens = _usageNum(actual.outputTokens);
60
+ combined.mainCachedTokens = _usageNum(actual.cachedTokens);
61
+ combined.mainPromptTokens = _usageNum(actual.promptTokens);
62
+ combined.mainCacheWriteTokens = _usageNum(actual.cacheWriteTokens);
63
+ combined.mainUsageAvailable = true;
64
+ }
65
+ return combined;
39
66
  }
40
67
 
41
68
  export function _parseEvent(raw) {
@@ -98,9 +98,29 @@ export const WS_PRE_RESPONSE_CREATED_MS = (() => {
98
98
  // Single inter-chunk idle timer. Resets on EVERY received frame — any frame,
99
99
  // including metadata/keepalive, proves the socket is live.
100
100
  export const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
101
+ // Bound the second allocation performed when ws RawData is decoded to UTF-8.
102
+ // The ws library has already assembled the Buffer by this point, so this check
103
+ // must stay before data.toString() below. xAI shares this stream implementation
104
+ // but retains its existing unbounded behavior.
105
+ export const OPENAI_WS_MAX_INCOMING_FRAME_BYTES = 16 * 1024 * 1024;
101
106
  const X_CODEX_TURN_STATE_HEADER = 'x-codex-turn-state';
102
107
  const WS_TRACE_ENABLED = process.env.MIXDOG_WS_TRACE === '1';
103
108
 
109
+ function _incomingFrameByteLength(data) {
110
+ if (typeof data === 'string') return Buffer.byteLength(data);
111
+ if (Array.isArray(data)) {
112
+ let total = 0;
113
+ for (const chunk of data) {
114
+ const size = Number(chunk?.byteLength ?? chunk?.length);
115
+ if (!Number.isFinite(size) || size < 0) return null;
116
+ total += size;
117
+ }
118
+ return total;
119
+ }
120
+ const size = Number(data?.byteLength ?? data?.length);
121
+ return Number.isFinite(size) && size >= 0 ? size : null;
122
+ }
123
+
104
124
  function _writeWsLifecycleTrace(lifecycle) {
105
125
  process.stderr.write(`[ws-trace] t=${new Date().toISOString()} lifecycle=${lifecycle}\n`);
106
126
  }
@@ -207,13 +227,6 @@ function _traceWsHeaderKeys(entry, event, midState, traceProvider, model) {
207
227
  else entry.wsHeaderKeysEmptyTraced = true;
208
228
  } catch {}
209
229
  }
210
- // _wsErrLabel moved to openai-ws-pool.mjs (imported above).
211
- // Delta/matching helpers (_sansInput, _stableStringify, _cloneJson,
212
- // _logicalResponseItemMatch, _stripResponseItemsFromHead, _computeDelta,
213
- // _estimateFrameTokens) → openai-ws-delta.mjs. Usage/event helpers
214
- // (_combineUsageWithWarmup, _parseEvent, _incompleteReasonFromEvent,
215
- // _isMaxOutputIncompleteReason, _httpStatusFromWsClose) → openai-ws-events.mjs.
216
- // All imported + re-exported at the top of this file.
217
230
  // tool_search_call.arguments parse. Module-scope (exported) for direct test
218
231
  // coverage. Same policy as the function_call_arguments.done path and
219
232
  // openai-oauth _parseJsonObject —
@@ -260,6 +273,9 @@ export async function _streamResponse({
260
273
  }
261
274
  const preResponseCreatedMs = _positiveInt(_timeouts?.preResponseCreatedMs, WS_PRE_RESPONSE_CREATED_MS);
262
275
  const interChunkMs = _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS);
276
+ const maxIncomingFrameBytes = traceProvider === 'xai'
277
+ ? 0
278
+ : _positiveInt(_timeouts?.maxIncomingFrameBytes, OPENAI_WS_MAX_INCOMING_FRAME_BYTES);
263
279
  // First-MEANINGFUL-frame deadline. Distinct from preResponseCreatedMs (a
264
280
  // short pre-created byte-silence window that resetIdle clears on the FIRST
265
281
  // frame of any kind): this timer is cleared only by a meaningful response
@@ -777,6 +793,20 @@ export async function _streamResponse({
777
793
  };
778
794
 
779
795
  messageHandler = (data) => {
796
+ const frameBytes = _incomingFrameByteLength(data);
797
+ if (maxIncomingFrameBytes > 0 && frameBytes != null && frameBytes > maxIncomingFrameBytes) {
798
+ const err = new Error(
799
+ `OpenAI WebSocket response frame is too large (${frameBytes} bytes; limit ${maxIncomingFrameBytes} bytes); request is retryable`,
800
+ );
801
+ err.code = 'EOPENAIWSFRAMETOOLARGE';
802
+ err.wsFrameTooLarge = true;
803
+ err.retryable = true;
804
+ terminalError = err;
805
+ midState.wsFrameTooLarge = true;
806
+ try { socket.close(1009, 'frame_too_large'); } catch {}
807
+ finish();
808
+ return;
809
+ }
780
810
  if (midState.sendSpan && midState.sendStartedAt != null
781
811
  && midState.sendSpanAttemptFirstEvent !== true) {
782
812
  midState.sendSpanAttemptFirstEvent = true;
@@ -1383,24 +1413,3 @@ export async function _streamResponse({
1383
1413
  });
1384
1414
  }
1385
1415
 
1386
- /**
1387
- * Classify a handshake error for retry eligibility.
1388
- *
1389
- * Default-deny: anything we don't recognize as transient returns null (treat
1390
- * as permanent). Permanent buckets (401/403/404/429) also return null — the
1391
- * server has made a deterministic decision that a retry can't change.
1392
- *
1393
- * Returns one of:
1394
- * 'timeout' — `ws` handshakeTimeout fired
1395
- * 'reset' — ECONNRESET / socket hang up
1396
- * 'dns' — EAI_AGAIN / ENOTFOUND / EAI_NODATA
1397
- * 'refused' — ECONNREFUSED
1398
- * 'network' — ENETUNREACH / EHOSTUNREACH / EPIPE
1399
- * 'acquire_timeout' — hard client-side open/acquire deadline fired
1400
- * 'http_5xx' (with specific status e.g. 'http_503') — server overload
1401
- * null — not retryable
1402
- */
1403
- // Thin re-export wrapper: handshake classification now lives in the shared
1404
- // retry-classifier (classifyHandshakeError). Kept here as a named export so
1405
- // internal call sites (_acquireWithRetry) and any external importer keep
1406
- // resolving the same symbol.
@@ -363,10 +363,7 @@ export function classifyMidstreamError(err, signals, policy = {}) {
363
363
  return _classifyMidstreamWs(err, signals, attemptIndex, policy)
364
364
  }
365
365
 
366
- // Verbatim relocation of openai-oauth-ws's _classifyMidstreamError. `signals`
367
- // carries sawResponseCreated / sawCompleted / emittedText / emittedToolCall /
368
- // wsCloseCode / firstByteTimeout / wsSendFailed /
369
- // userAbort / watchdogAbort / responseFailedPayload exactly as before.
366
+ // WebSocket classification consumes the provider's stream-state signals.
370
367
  function _classifyMidstreamWs(err, state, attemptIndex, policy) {
371
368
  if (state.sawCompleted) return null
372
369
  // Once a tool call has been dispatched, no transport outcome is replay-safe.
@@ -374,6 +371,9 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
374
371
  // already be executing, so retry/fallback could duplicate its side effect.
375
372
  if (state.emittedToolCall) return null
376
373
  if (state.emittedText || err?.liveTextEmitted) return null
374
+ if (err?.wsFrameTooLarge || state.wsFrameTooLarge) {
375
+ return _allowMidstream('ws_frame_too_large', attemptIndex, policy)
376
+ }
377
377
  if (state.firstByteTimeout || err?.firstByteTimeout) {
378
378
  return _allowMidstream('first_byte_timeout', attemptIndex, policy)
379
379
  }
@@ -450,9 +450,7 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
450
450
  return null
451
451
  }
452
452
 
453
- // Verbatim relocation of anthropic-oauth's _classifyMidstreamError. `signals`
454
- // carries sawMessageStart / sawCompleted / emittedToolCall / userAbort /
455
- // watchdogAbort exactly as before.
453
+ // SSE classification consumes the provider's stream-state signals.
456
454
  function _classifyMidstreamSse(err, state, attemptIndex, policy) {
457
455
  if (attemptIndex >= policy.defaultRetries) return null
458
456
  if (state.sawCompleted) return null
@@ -641,6 +641,14 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
641
641
  // additive — callers that ignore these fields keep working.
642
642
  deltaCachedRead: response.usage.cachedTokens || 0,
643
643
  deltaCacheWrite: response.usage.cacheWriteTokens || 0,
644
+ // Billing deltas include OAuth WS warmup. Context
645
+ // snapshots/baselines must describe only the main send.
646
+ contextInputTokens: response.usage.mainInputTokens ?? response.usage.inputTokens ?? 0,
647
+ contextOutputTokens: response.usage.mainOutputTokens ?? response.usage.outputTokens ?? 0,
648
+ contextPromptTokens: response.usage.mainPromptTokens ?? response.usage.promptTokens ?? 0,
649
+ contextCachedReadTokens: response.usage.mainCachedTokens ?? response.usage.cachedTokens ?? 0,
650
+ contextCacheWriteTokens: response.usage.mainCacheWriteTokens ?? response.usage.cacheWriteTokens ?? 0,
651
+ contextUsageAvailable: response.usage.mainUsageAvailable !== false,
644
652
  sendTools,
645
653
  ts: Date.now(),
646
654
  }));
@@ -0,0 +1,170 @@
1
+ import { isAgentOwner } from '../agent-owner.mjs';
2
+
3
+ export const DEFAULT_COMPACTION_BUFFER_TOKENS = 0;
4
+ export const DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
5
+ export const DEFAULT_MAIN_COMPACTION_BUFFER_RATIO = 0.05;
6
+ export const MAX_COMPACTION_BUFFER_RATIO = 0.25;
7
+ const MAX_BUFFER_INPUT_RATIO = 0.999_999;
8
+ export const DEFAULT_COMPACTION_KEEP_TOKENS = 8_000;
9
+ const LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
10
+
11
+ export function positiveTokenInt(value) {
12
+ const n = Number(value);
13
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
14
+ }
15
+ function envTokenInt(name) {
16
+ return positiveTokenInt(process.env[name]);
17
+ }
18
+ export function normalizeCompactionBufferRatio(value, fallback = DEFAULT_COMPACTION_BUFFER_RATIO) {
19
+ const n = Number(value);
20
+ if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
21
+ return fallback;
22
+ }
23
+ export function resolveBufferRatioCandidate(percentInputs = [], ratioInputs = []) {
24
+ for (const raw of percentInputs) {
25
+ const n = Number(raw);
26
+ if (Number.isFinite(n) && n > 0) return Math.min(MAX_BUFFER_INPUT_RATIO, n / 100);
27
+ }
28
+ for (const raw of ratioInputs) {
29
+ const n = Number(raw);
30
+ if (Number.isFinite(n) && n > 0) return Math.min(MAX_BUFFER_INPUT_RATIO, n > 1 ? n / 100 : n);
31
+ }
32
+ return null;
33
+ }
34
+ export function resolveCompactBufferRatio(cfg = {}) {
35
+ const resolved = resolveBufferRatioCandidate(
36
+ [cfg.bufferPercent, cfg.bufferPct, process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT],
37
+ [cfg.bufferRatio, cfg.bufferFraction, process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO],
38
+ );
39
+ return normalizeCompactionBufferRatio(resolved, DEFAULT_COMPACTION_BUFFER_RATIO);
40
+ }
41
+ function positiveTokenCandidate(values = []) {
42
+ for (const value of values) {
43
+ const tokens = positiveTokenInt(value);
44
+ if (tokens) return tokens;
45
+ }
46
+ return null;
47
+ }
48
+ function resolveMainBufferSetting(cfg = {}) {
49
+ const configTokens = positiveTokenCandidate([cfg.mainBufferTokens, cfg.mainBuffer]);
50
+ if (configTokens) return { tokens: configTokens };
51
+ const configRatio = resolveBufferRatioCandidate(
52
+ [cfg.mainBufferPercent, cfg.mainBufferPct],
53
+ [cfg.mainBufferRatio, cfg.mainBufferFraction],
54
+ );
55
+ if (configRatio !== null) return { ratio: configRatio };
56
+ const envTokens = positiveTokenCandidate([process.env.MIXDOG_MAIN_COMPACT_BUFFER_TOKENS]);
57
+ if (envTokens) return { tokens: envTokens };
58
+ const envRatio = resolveBufferRatioCandidate(
59
+ [process.env.MIXDOG_MAIN_COMPACT_BUFFER_PERCENT],
60
+ [process.env.MIXDOG_MAIN_COMPACT_BUFFER_RATIO],
61
+ );
62
+ return envRatio === null ? null : { ratio: envRatio };
63
+ }
64
+ export function resolveMainCompactBufferRatio(cfg = {}) {
65
+ const setting = resolveMainBufferSetting(cfg);
66
+ return setting?.ratio ?? DEFAULT_MAIN_COMPACTION_BUFFER_RATIO;
67
+ }
68
+ export function compactionBufferTokensForBoundary(boundaryTokens, opts = {}) {
69
+ const boundary = Math.max(0, Math.floor(Number(boundaryTokens) || 0));
70
+ const explicit = Math.max(0, Math.floor(Number(opts.explicitTokens) || 0));
71
+ if (!boundary) return explicit;
72
+ const maxRatio = normalizeCompactionBufferRatio(opts.maxRatio, MAX_COMPACTION_BUFFER_RATIO);
73
+ const cap = Math.max(0, Math.floor(boundary * maxRatio));
74
+ if (explicit > 0) return Math.max(0, Math.min(explicit, cap));
75
+ const ratio = normalizeCompactionBufferRatio(opts.ratio, DEFAULT_COMPACTION_BUFFER_RATIO);
76
+ return Math.max(0, Math.min(Math.floor(boundary * ratio), cap));
77
+ }
78
+ export function isPersistedZeroBufferTelemetry(cfg = {}, boundaryTokens = 0) {
79
+ const boundary = positiveTokenInt(boundaryTokens);
80
+ if (!boundary) return false;
81
+ if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
82
+ for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
83
+ const n = Number(process.env[envName]);
84
+ if (Number.isFinite(n) && n > 0) return false;
85
+ }
86
+ for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
87
+ const n = Number(cfg?.[key]);
88
+ if (Number.isFinite(n) && n > 0) return false;
89
+ }
90
+ const ratio = Number(cfg?.bufferRatio);
91
+ if (Number.isFinite(ratio) && ratio > 0) return false;
92
+ const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
93
+ return Number.isFinite(explicitTokens) && explicitTokens === 0;
94
+ }
95
+ export function isLegacyDefaultBufferTelemetry(cfg = {}, boundaryTokens = 0) {
96
+ const boundary = positiveTokenInt(boundaryTokens);
97
+ if (!boundary) return false;
98
+ if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
99
+ for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
100
+ const n = Number(process.env[envName]);
101
+ if (Number.isFinite(n) && n > 0) return false;
102
+ }
103
+ for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
104
+ const n = Number(cfg?.[key]);
105
+ if (Number.isFinite(n) && n > 0) return false;
106
+ }
107
+ const explicitTokens = positiveTokenInt(cfg?.bufferTokens ?? cfg?.buffer);
108
+ const ratio = Number(cfg?.bufferRatio);
109
+ if (!explicitTokens || !Number.isFinite(ratio) || Math.abs(ratio - LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO) > 1e-9) return false;
110
+ const expectedTokens = Math.floor(boundary * LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO);
111
+ const cfgBoundary = positiveTokenInt(cfg?.boundaryTokens);
112
+ const cfgTrigger = positiveTokenInt(cfg?.triggerTokens);
113
+ return explicitTokens === expectedTokens
114
+ || (cfgBoundary === boundary && cfgTrigger > 0 && explicitTokens === Math.max(0, boundary - cfgTrigger));
115
+ }
116
+ export function compactBufferConfigForBoundary(cfg = {}, boundaryTokens = 0) {
117
+ const base = cfg || {};
118
+ if (!isLegacyDefaultBufferTelemetry(base, boundaryTokens)
119
+ && !isPersistedZeroBufferTelemetry(base, boundaryTokens)) return base;
120
+ return { ...base, bufferTokens: null, buffer: null, bufferRatio: null };
121
+ }
122
+ export function resolveCompactBufferTokens(boundaryTokens, cfg = {}, opts = {}) {
123
+ const boundary = positiveTokenInt(boundaryTokens);
124
+ const effectiveCfg = compactBufferConfigForBoundary(cfg, boundary);
125
+ const configured = positiveTokenInt(effectiveCfg.bufferTokens ?? effectiveCfg.buffer)
126
+ || envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS') || 0;
127
+ if (!boundary) return configured || positiveTokenInt(opts.defaultTokens) || DEFAULT_COMPACTION_BUFFER_TOKENS;
128
+ return compactionBufferTokensForBoundary(boundary, {
129
+ explicitTokens: configured,
130
+ ratio: resolveCompactBufferRatio(effectiveCfg),
131
+ maxRatio: opts.maxRatio ?? MAX_COMPACTION_BUFFER_RATIO,
132
+ });
133
+ }
134
+ export function resolveMainCompactBufferTokens(boundaryTokens, cfg = {}, opts = {}) {
135
+ const boundary = positiveTokenInt(boundaryTokens);
136
+ const setting = resolveMainBufferSetting(cfg);
137
+ const configured = setting?.tokens || 0;
138
+ if (!boundary) return configured || positiveTokenInt(opts.defaultTokens) || DEFAULT_COMPACTION_BUFFER_TOKENS;
139
+ return compactionBufferTokensForBoundary(boundary, {
140
+ explicitTokens: configured,
141
+ ratio: setting?.ratio ?? DEFAULT_MAIN_COMPACTION_BUFFER_RATIO,
142
+ maxRatio: opts.maxRatio ?? MAX_COMPACTION_BUFFER_RATIO,
143
+ });
144
+ }
145
+ export function resolveCompactTriggerTokens(sessionOrConfig = {}, boundaryTokens = 0) {
146
+ return resolveSessionCompactPolicy(sessionOrConfig, boundaryTokens).triggerTokens;
147
+ }
148
+ export function resolveSessionCompactPolicy(sessionOrConfig = {}, boundaryTokens = 0) {
149
+ const cfg = sessionOrConfig?.compaction || sessionOrConfig || {};
150
+ const boundary = positiveTokenInt(boundaryTokens);
151
+ if (!boundary) {
152
+ return {
153
+ autoCompactTokenLimit: null,
154
+ triggerTokens: null,
155
+ bufferTokens: 0,
156
+ bufferRatio: isAgentOwner(sessionOrConfig)
157
+ ? resolveCompactBufferRatio(cfg)
158
+ : resolveMainCompactBufferRatio(cfg),
159
+ };
160
+ }
161
+ const rawLimit = positiveTokenInt(sessionOrConfig?.autoCompactTokenLimit ?? cfg?.autoCompactTokenLimit);
162
+ const explicitLimit = rawLimit && rawLimit < boundary ? rawLimit : null;
163
+ let triggerTokens;
164
+ if (explicitLimit) triggerTokens = explicitLimit;
165
+ else if (isAgentOwner(sessionOrConfig)) triggerTokens = Math.max(1, boundary - resolveCompactBufferTokens(boundary, cfg));
166
+ else triggerTokens = Math.max(1, boundary - resolveMainCompactBufferTokens(boundary, cfg));
167
+ const bufferTokens = Math.max(0, boundary - triggerTokens);
168
+ const bufferRatio = bufferTokens / boundary;
169
+ return { autoCompactTokenLimit: explicitLimit, triggerTokens, bufferTokens, bufferRatio };
170
+ }
@@ -1,8 +1,27 @@
1
1
  import { isOffloadedToolResultText } from './tool-result-offload.mjs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { providerNativeToolPrefixCount } from '../../../../session-runtime/provider-request-tools.mjs';
4
- import { isAgentOwner } from '../agent-owner.mjs';
5
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';
6
25
 
7
26
  // ---------------------------------------------------------------------------
8
27
  // Conservative, Unicode-aware token estimator.
@@ -377,228 +396,6 @@ function contextSummaryResult(state, count) {
377
396
  };
378
397
  }
379
398
 
380
- export const DEFAULT_COMPACTION_BUFFER_TOKENS = 0;
381
- export const DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
382
- // Main/user recall-fasttrack compacts at 95% by default. Lower triggers remain
383
- // opt-in through the mainBuffer* configuration and MIXDOG_MAIN_COMPACT_BUFFER_*.
384
- export const DEFAULT_MAIN_COMPACTION_BUFFER_RATIO = 0.05;
385
- export const MAX_COMPACTION_BUFFER_RATIO = 0.25;
386
- const MAX_BUFFER_INPUT_RATIO = 0.999_999;
387
- export const DEFAULT_COMPACTION_KEEP_TOKENS = 8_000;
388
- const LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
389
-
390
- export function positiveTokenInt(value) {
391
- const n = Number(value);
392
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
393
- }
394
-
395
- function envTokenInt(name) {
396
- return positiveTokenInt(process.env[name]);
397
- }
398
-
399
- export function normalizeCompactionBufferRatio(value, fallback = DEFAULT_COMPACTION_BUFFER_RATIO) {
400
- const n = Number(value);
401
- if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
402
- return fallback;
403
- }
404
-
405
- // Percent-named inputs (bufferPercent / bufferPct / *_BUFFER_PERCENT) carry a
406
- // PERCENT: 1 means 1% (0.01). Ratio-named inputs (bufferRatio / bufferFraction)
407
- // carry a fraction: 0.01 means 1%, and a legacy value > 1 is read as a percent.
408
- export function resolveBufferRatioCandidate(percentInputs = [], ratioInputs = []) {
409
- for (const raw of percentInputs) {
410
- const n = Number(raw);
411
- if (Number.isFinite(n) && n > 0) return Math.min(MAX_BUFFER_INPUT_RATIO, n / 100);
412
- }
413
- for (const raw of ratioInputs) {
414
- const n = Number(raw);
415
- if (Number.isFinite(n) && n > 0) return Math.min(MAX_BUFFER_INPUT_RATIO, n > 1 ? n / 100 : n);
416
- }
417
- return null;
418
- }
419
-
420
- export function resolveCompactBufferRatio(cfg = {}) {
421
- const resolved = resolveBufferRatioCandidate(
422
- [cfg.bufferPercent, cfg.bufferPct, process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT],
423
- [cfg.bufferRatio, cfg.bufferFraction, process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO],
424
- );
425
- return normalizeCompactionBufferRatio(resolved, DEFAULT_COMPACTION_BUFFER_RATIO);
426
- }
427
-
428
- // Main/user settings are deliberately separate from the semantic-agent buffer:
429
- // `buffer*` and MIXDOG_AGENT_COMPACT_BUFFER_* remain agent-only controls.
430
- // Source precedence is config before environment; within each source the fixed
431
- // unit precedence is tokens, percent, then ratio. Invalid/zero values are
432
- // ignored, and percent/ratio inputs are capped below 100%/1.0.
433
- function positiveTokenCandidate(values = []) {
434
- for (const value of values) {
435
- const tokens = positiveTokenInt(value);
436
- if (tokens) return tokens;
437
- }
438
- return null;
439
- }
440
-
441
- function resolveMainBufferSetting(cfg = {}) {
442
- const configTokens = positiveTokenCandidate([cfg.mainBufferTokens, cfg.mainBuffer]);
443
- if (configTokens) return { tokens: configTokens };
444
- const configRatio = resolveBufferRatioCandidate(
445
- [cfg.mainBufferPercent, cfg.mainBufferPct],
446
- [cfg.mainBufferRatio, cfg.mainBufferFraction],
447
- );
448
- if (configRatio !== null) return { ratio: configRatio };
449
-
450
- const envTokens = positiveTokenCandidate([process.env.MIXDOG_MAIN_COMPACT_BUFFER_TOKENS]);
451
- if (envTokens) return { tokens: envTokens };
452
- const envRatio = resolveBufferRatioCandidate(
453
- [process.env.MIXDOG_MAIN_COMPACT_BUFFER_PERCENT],
454
- [process.env.MIXDOG_MAIN_COMPACT_BUFFER_RATIO],
455
- );
456
- return envRatio === null ? null : { ratio: envRatio };
457
- }
458
-
459
- export function resolveMainCompactBufferRatio(cfg = {}) {
460
- const setting = resolveMainBufferSetting(cfg);
461
- return setting?.ratio ?? DEFAULT_MAIN_COMPACTION_BUFFER_RATIO;
462
- }
463
-
464
- export function compactionBufferTokensForBoundary(boundaryTokens, opts = {}) {
465
- const boundary = Math.max(0, Math.floor(Number(boundaryTokens) || 0));
466
- const explicit = Math.max(0, Math.floor(Number(opts.explicitTokens) || 0));
467
- if (!boundary) return explicit;
468
- const maxRatio = normalizeCompactionBufferRatio(opts.maxRatio, MAX_COMPACTION_BUFFER_RATIO);
469
- const cap = Math.max(0, Math.floor(boundary * maxRatio));
470
- if (explicit > 0) return Math.max(0, Math.min(explicit, cap));
471
- const ratio = normalizeCompactionBufferRatio(opts.ratio, DEFAULT_COMPACTION_BUFFER_RATIO);
472
- return Math.max(0, Math.min(Math.floor(boundary * ratio), cap));
473
- }
474
-
475
- export function isPersistedZeroBufferTelemetry(cfg = {}, boundaryTokens = 0) {
476
- const boundary = positiveTokenInt(boundaryTokens);
477
- if (!boundary) return false;
478
- if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
479
- for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
480
- const n = Number(process.env[envName]);
481
- if (Number.isFinite(n) && n > 0) return false;
482
- }
483
- for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
484
- const n = Number(cfg?.[key]);
485
- if (Number.isFinite(n) && n > 0) return false;
486
- }
487
- const ratio = Number(cfg?.bufferRatio);
488
- if (Number.isFinite(ratio) && ratio > 0) return false;
489
- const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
490
- if (!Number.isFinite(explicitTokens) || explicitTokens !== 0) return false;
491
- return true;
492
- }
493
-
494
- export function isLegacyDefaultBufferTelemetry(cfg = {}, boundaryTokens = 0) {
495
- const boundary = positiveTokenInt(boundaryTokens);
496
- if (!boundary) return false;
497
- if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
498
- for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
499
- const n = Number(process.env[envName]);
500
- if (Number.isFinite(n) && n > 0) return false;
501
- }
502
- // Percent/fraction-named fields are operator config. Legacy/default
503
- // telemetry persisted bufferTokens + bufferRatio after a check/compact pass.
504
- for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
505
- const n = Number(cfg?.[key]);
506
- if (Number.isFinite(n) && n > 0) return false;
507
- }
508
- const explicitTokens = positiveTokenInt(cfg?.bufferTokens ?? cfg?.buffer);
509
- const ratio = Number(cfg?.bufferRatio);
510
- if (!explicitTokens || !Number.isFinite(ratio) || Math.abs(ratio - LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO) > 1e-9) return false;
511
- const expectedTokens = Math.floor(boundary * LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO);
512
- const cfgBoundary = positiveTokenInt(cfg?.boundaryTokens);
513
- const cfgTrigger = positiveTokenInt(cfg?.triggerTokens);
514
- return explicitTokens === expectedTokens
515
- || (cfgBoundary === boundary && cfgTrigger > 0 && explicitTokens === Math.max(0, boundary - cfgTrigger));
516
- }
517
-
518
- export function compactBufferConfigForBoundary(cfg = {}, boundaryTokens = 0) {
519
- const base = cfg || {};
520
- if (!isLegacyDefaultBufferTelemetry(base, boundaryTokens)
521
- && !isPersistedZeroBufferTelemetry(base, boundaryTokens)) {
522
- return base;
523
- }
524
- return {
525
- ...base,
526
- bufferTokens: null,
527
- buffer: null,
528
- bufferRatio: null,
529
- };
530
- }
531
-
532
- export function resolveCompactBufferTokens(boundaryTokens, cfg = {}, opts = {}) {
533
- const boundary = positiveTokenInt(boundaryTokens);
534
- const effectiveCfg = compactBufferConfigForBoundary(cfg, boundary);
535
- const configured = positiveTokenInt(effectiveCfg.bufferTokens ?? effectiveCfg.buffer)
536
- || envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')
537
- || 0;
538
- if (!boundary) return configured || positiveTokenInt(opts.defaultTokens) || DEFAULT_COMPACTION_BUFFER_TOKENS;
539
- return compactionBufferTokensForBoundary(boundary, {
540
- explicitTokens: configured,
541
- ratio: resolveCompactBufferRatio(effectiveCfg),
542
- maxRatio: opts.maxRatio ?? MAX_COMPACTION_BUFFER_RATIO,
543
- });
544
- }
545
-
546
- export function resolveMainCompactBufferTokens(boundaryTokens, cfg = {}, opts = {}) {
547
- const boundary = positiveTokenInt(boundaryTokens);
548
- const setting = resolveMainBufferSetting(cfg);
549
- const configured = setting?.tokens || 0;
550
- if (!boundary) return configured || positiveTokenInt(opts.defaultTokens) || DEFAULT_COMPACTION_BUFFER_TOKENS;
551
- return compactionBufferTokensForBoundary(boundary, {
552
- explicitTokens: configured,
553
- ratio: setting?.ratio ?? DEFAULT_MAIN_COMPACTION_BUFFER_RATIO,
554
- maxRatio: opts.maxRatio ?? MAX_COMPACTION_BUFFER_RATIO,
555
- });
556
- }
557
-
558
- export function resolveCompactTriggerTokens(sessionOrConfig = {}, boundaryTokens = 0) {
559
- return resolveSessionCompactPolicy(sessionOrConfig, boundaryTokens).triggerTokens;
560
- }
561
-
562
- // Single source of truth for per-session compaction policy math. Manager
563
- // (compactTriggerForSession), the turn loop (resolveWorkerCompactPolicy), and
564
- // the /context gauge all derive their trigger/buffer from here so the numbers
565
- // never diverge. Rules:
566
- // - a truly-explicit sub-boundary auto-compact limit always wins
567
- // (trigger = limit) for every session type;
568
- // - agent-owned semantic sessions otherwise keep the default early-trigger
569
- // buffer (config-driven, default 10% -> compact at 90% of the boundary);
570
- // - main/user recall-fasttrack sessions use their independently configurable
571
- // buffer (default 25% -> compact at 75% of the effective boundary).
572
- // Returns the sanitized explicit limit (null when absent/legacy full-window)
573
- // plus triggerTokens / bufferTokens / bufferRatio for the given boundary.
574
- export function resolveSessionCompactPolicy(sessionOrConfig = {}, boundaryTokens = 0) {
575
- const cfg = sessionOrConfig?.compaction || sessionOrConfig || {};
576
- const boundary = positiveTokenInt(boundaryTokens);
577
- if (!boundary) {
578
- return {
579
- autoCompactTokenLimit: null,
580
- triggerTokens: null,
581
- bufferTokens: 0,
582
- bufferRatio: isAgentOwner(sessionOrConfig)
583
- ? resolveCompactBufferRatio(cfg)
584
- : resolveMainCompactBufferRatio(cfg),
585
- };
586
- }
587
- const rawLimit = positiveTokenInt(sessionOrConfig?.autoCompactTokenLimit ?? cfg?.autoCompactTokenLimit);
588
- const explicitLimit = rawLimit && rawLimit < boundary ? rawLimit : null;
589
- let triggerTokens;
590
- if (explicitLimit) {
591
- triggerTokens = explicitLimit;
592
- } else if (isAgentOwner(sessionOrConfig)) {
593
- triggerTokens = Math.max(1, boundary - resolveCompactBufferTokens(boundary, cfg));
594
- } else {
595
- triggerTokens = Math.max(1, boundary - resolveMainCompactBufferTokens(boundary, cfg));
596
- }
597
- const bufferTokens = Math.max(0, boundary - triggerTokens);
598
- const bufferRatio = bufferTokens / boundary;
599
- return { autoCompactTokenLimit: explicitLimit, triggerTokens, bufferTokens, bufferRatio };
600
- }
601
-
602
399
  function stripSystemReminder(text) {
603
400
  return String(text || '')
604
401
  .replace(/^\s*<system-reminder>\s*/i, '')