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
@@ -6,11 +6,13 @@ import { homedir, tmpdir } from 'node:os';
6
6
  import { findCachedGraphBinary } from '../src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs';
7
7
 
8
8
  const previousDataDir = process.env.MIXDOG_DATA_DIR;
9
+ const previousHome = process.env.MIXDOG_HOME;
9
10
  const previousGraphBin = process.env.MIXDOG_GRAPH_BIN;
10
11
  const ambientDataDir = previousDataDir || join(process.env.MIXDOG_HOME || join(homedir(), '.mixdog'), 'data');
11
12
  const ambientGraphBin = findCachedGraphBinary(ambientDataDir);
12
13
  const isolatedDataDir = await mkdtemp(join(tmpdir(), 'mixdog-code-graph-test-data-'));
13
14
  process.env.MIXDOG_DATA_DIR = isolatedDataDir;
15
+ process.env.MIXDOG_HOME = isolatedDataDir;
14
16
  if (!previousGraphBin && ambientGraphBin) process.env.MIXDOG_GRAPH_BIN = ambientGraphBin;
15
17
  const { executeCodeGraphTool } = await import('../src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs');
16
18
  const { drainCodeGraphCache } = await import('../src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs');
@@ -40,6 +42,9 @@ async function makeProject() {
40
42
  }
41
43
 
42
44
  const primaryProject = await makeProject();
45
+ await writeFile(join(isolatedDataDir, 'projects.json'), JSON.stringify({
46
+ projects: [{ name: 'primary', path: primaryProject, addedAt: 1 }],
47
+ }));
43
48
 
44
49
  test.after(async () => {
45
50
  drainCodeGraphCache();
@@ -49,6 +54,8 @@ test.after(async () => {
49
54
  ]);
50
55
  if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
51
56
  else process.env.MIXDOG_DATA_DIR = previousDataDir;
57
+ if (previousHome === undefined) delete process.env.MIXDOG_HOME;
58
+ else process.env.MIXDOG_HOME = previousHome;
52
59
  if (previousGraphBin === undefined) delete process.env.MIXDOG_GRAPH_BIN;
53
60
  else process.env.MIXDOG_GRAPH_BIN = previousGraphBin;
54
61
  });
@@ -63,8 +70,8 @@ serialTest('aggregate file anchors recover an invalid cwd only for one project r
63
70
  assert.match(result, new RegExp(`# overview ${files[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
64
71
 
65
72
  const relativeResult = await executeCodeGraphTool('code_graph', { mode: 'overview', files: relativeFiles }, invalidCwd);
66
- assert.match(relativeResult, new RegExp(`# overview ${files[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
67
- assert.match(relativeResult, new RegExp(`# overview ${files[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
73
+ assert.match(relativeResult, /file: src\/one\.mjs/);
74
+ assert.match(relativeResult, /file: src\/two\.mjs/);
68
75
  drainCodeGraphCache();
69
76
  });
70
77
 
@@ -72,21 +79,18 @@ serialTest('singleton JSON files retain aggregate cwd provenance', async () => {
72
79
  const project = primaryProject;
73
80
  const invalidCwd = parse(project).root;
74
81
  const file = join(project, 'src', 'one.mjs');
75
- const rejected = /aggregate file anchors do not all exist under exactly one detectable project root/;
76
82
  const result = await executeCodeGraphTool(
77
83
  'code_graph',
78
84
  { mode: 'overview', files: JSON.stringify([file]), cwd: invalidCwd },
79
85
  project,
80
86
  );
81
87
  assert.doesNotMatch(result, /^Error:/);
82
- await assert.rejects(
83
- executeCodeGraphTool(
84
- 'code_graph',
85
- { mode: 'overview', files: JSON.stringify([join(project, 'missing.mjs')]), cwd: invalidCwd },
86
- project,
87
- ),
88
- rejected,
88
+ const missingAggregate = await executeCodeGraphTool(
89
+ 'code_graph',
90
+ { mode: 'overview', files: JSON.stringify([join(project, 'missing.mjs')]), cwd: invalidCwd },
91
+ project,
89
92
  );
93
+ assert.match(missingAggregate, /^Error: code_graph: file not found:/);
90
94
  const scalarResult = await executeCodeGraphTool(
91
95
  'code_graph',
92
96
  { mode: 'overview', file: join(project, 'missing.mjs'), cwd: invalidCwd },
@@ -99,26 +103,27 @@ serialTest('aggregate cwd recovery rejects missing and multi-root anchors', asyn
99
103
  const first = await makeProject();
100
104
  const second = await makeProject();
101
105
  const invalidCwd = parse(first).root;
102
- const rejected = /aggregate file anchors do not all exist under exactly one detectable project root/;
103
106
  const relativeFirst = relative(invalidCwd, join(first, 'src', 'one.mjs'));
104
107
  const relativeSecond = relative(invalidCwd, join(second, 'src', 'one.mjs'));
105
108
  try {
106
- await assert.rejects(
107
- executeCodeGraphTool('code_graph', { mode: 'overview', files: [join(first, 'src', 'one.mjs'), join(first, 'missing.mjs')] }, invalidCwd),
108
- rejected,
109
- );
110
- await assert.rejects(
111
- executeCodeGraphTool('code_graph', { mode: 'overview', files: [relativeFirst, relative(invalidCwd, join(first, 'missing.mjs'))] }, invalidCwd),
112
- rejected,
113
- );
114
- await assert.rejects(
115
- executeCodeGraphTool('code_graph', { mode: 'overview', files: [join(first, 'src', 'one.mjs'), join(second, 'src', 'one.mjs')] }, invalidCwd),
116
- rejected,
117
- );
118
- await assert.rejects(
119
- executeCodeGraphTool('code_graph', { mode: 'overview', files: [relativeFirst, relativeSecond] }, invalidCwd),
120
- rejected,
121
- );
109
+ for (const filesArg of [
110
+ [join(first, 'src', 'one.mjs'), join(first, 'missing.mjs')],
111
+ [relativeFirst, relative(invalidCwd, join(first, 'missing.mjs'))],
112
+ ]) {
113
+ assert.match(
114
+ await executeCodeGraphTool('code_graph', { mode: 'overview', files: filesArg }, invalidCwd),
115
+ /^Error: code_graph: file not found:/,
116
+ );
117
+ }
118
+ for (const filesArg of [
119
+ [join(first, 'src', 'one.mjs'), join(second, 'src', 'one.mjs')],
120
+ [relativeFirst, relativeSecond],
121
+ ]) {
122
+ assert.match(
123
+ await executeCodeGraphTool('code_graph', { mode: 'overview', files: filesArg }, invalidCwd),
124
+ /^Error: code_graph: file anchor is not owned by a trusted project:/,
125
+ );
126
+ }
122
127
  } finally {
123
128
  await Promise.all([rm(first, { recursive: true, force: true }), rm(second, { recursive: true, force: true })]);
124
129
  }
@@ -127,7 +132,6 @@ serialTest('aggregate cwd recovery rejects missing and multi-root anchors', asyn
127
132
  serialTest('aggregate cwd recovery rejects capped, comma-delimited, and wildcard anchors', async () => {
128
133
  const project = await makeProject();
129
134
  const invalidCwd = parse(project).root;
130
- const rejected = /aggregate file anchors do not all exist under exactly one detectable project root/;
131
135
  const cappedFiles = await Promise.all(Array.from({ length: 21 }, async (_, index) => {
132
136
  const file = join(project, 'src', `anchor-${index}.mjs`);
133
137
  await writeFile(file, `export const anchor${index} = ${index};\n`);
@@ -136,17 +140,17 @@ serialTest('aggregate cwd recovery rejects capped, comma-delimited, and wildcard
136
140
  const literalWildcard = join(project, 'src', 'literal[glob].mjs');
137
141
  await writeFile(literalWildcard, 'export const wildcard = true;\n');
138
142
  try {
139
- await assert.rejects(
140
- executeCodeGraphTool('code_graph', { mode: 'overview', files: cappedFiles }, invalidCwd),
141
- rejected,
143
+ assert.match(
144
+ await executeCodeGraphTool('code_graph', { mode: 'overview', files: cappedFiles }, invalidCwd),
145
+ /file list exceeds cap/,
142
146
  );
143
- await assert.rejects(
144
- executeCodeGraphTool('code_graph', { mode: 'overview', files: `${join(project, 'src', 'one.mjs')},${join(project, 'src', 'two.mjs')}` }, invalidCwd),
145
- rejected,
147
+ assert.match(
148
+ await executeCodeGraphTool('code_graph', { mode: 'overview', files: `${join(project, 'src', 'one.mjs')},${join(project, 'src', 'two.mjs')}` }, invalidCwd),
149
+ /^Error: code_graph: file anchor is not owned by a trusted project:/,
146
150
  );
147
- await assert.rejects(
148
- executeCodeGraphTool('code_graph', { mode: 'overview', files: [literalWildcard] }, invalidCwd),
149
- rejected,
151
+ assert.match(
152
+ await executeCodeGraphTool('code_graph', { mode: 'overview', files: [literalWildcard] }, invalidCwd),
153
+ /wildcard-shaped file anchors/,
150
154
  );
151
155
  } finally {
152
156
  await rm(project, { recursive: true, force: true });
@@ -262,6 +262,17 @@ test('Worker drains all migrated cache entries before posting success', () => {
262
262
  assert.deepEqual(events, ['drain', 'post']);
263
263
  });
264
264
 
265
+ test('cache-disabled scoped Worker posts success without touching disk-cache state', () => {
266
+ const events = [];
267
+ _postCodeGraphWorkerSuccess(
268
+ { signature: 'scoped', nodes: new Map() },
269
+ (message) => events.push(message.ok ? 'post' : 'bad-post'),
270
+ () => events.push('drain'),
271
+ { cache: false },
272
+ );
273
+ assert.deepEqual(events, ['post']);
274
+ });
275
+
265
276
  test('strict persistence failure prevents Worker success post', () => {
266
277
  let posted = false;
267
278
  assert.throws(
@@ -0,0 +1,273 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
4
+ import { join, parse } from 'node:path';
5
+ import { homedir, tmpdir } from 'node:os';
6
+ import { findCachedGraphBinary } from '../src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs';
7
+
8
+ const home = await mkdtemp(join(tmpdir(), 'mixdog-federation-home-'));
9
+ const data = join(home, 'data');
10
+ const ambientGraphBin = findCachedGraphBinary(join(process.env.MIXDOG_HOME || join(homedir(), '.mixdog'), 'data'));
11
+ process.env.MIXDOG_HOME = home;
12
+ process.env.MIXDOG_DATA_DIR = data;
13
+ delete process.env.MIXDOG_SESSION_CWD;
14
+ if (!process.env.MIXDOG_GRAPH_BIN && ambientGraphBin) process.env.MIXDOG_GRAPH_BIN = ambientGraphBin;
15
+
16
+ const {
17
+ _isFilesystemRootPath,
18
+ collectTrustedCodeGraphRoots,
19
+ } = await import('../src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs');
20
+ const {
21
+ _codeGraphInflightKey,
22
+ _runGraphFilesChunked,
23
+ _scopeCodeGraphManifest,
24
+ } = await import('../src/runtime/agent/orchestrator/tools/code-graph/build.mjs');
25
+
26
+ test.after(() => rm(home, { recursive: true, force: true }));
27
+
28
+ test('filesystem-root semantics recognize Unix and Windows roots only', () => {
29
+ assert.equal(_isFilesystemRootPath('/'), true);
30
+ assert.equal(_isFilesystemRootPath('C:\\'), true);
31
+ assert.equal(_isFilesystemRootPath('d:/'), true);
32
+ assert.equal(_isFilesystemRootPath('C:'), false);
33
+ assert.equal(_isFilesystemRootPath('/tmp'), false);
34
+ assert.equal(_isFilesystemRootPath('C:\\repo'), false);
35
+ });
36
+
37
+ test('trusted sources are unioned, normalized, deduped, and filtered', async () => {
38
+ const project = await mkdtemp(join(tmpdir(), 'mixdog-trusted-project-'));
39
+ const nested = join(project, 'nested');
40
+ await mkdir(nested);
41
+ await writeFile(join(nested, 'package.json'), '{}');
42
+ const missing = join(project, 'missing');
43
+ try {
44
+ const roots = collectTrustedCodeGraphRoots(parse(project).root, {
45
+ registered: () => [project, `${project}/`],
46
+ selected: () => [nested, parse(project).root],
47
+ cached: () => [project, missing],
48
+ });
49
+ assert.deepEqual(new Set(roots), new Set([project, nested]));
50
+ } finally {
51
+ await rm(project, { recursive: true, force: true });
52
+ }
53
+ });
54
+
55
+ test('nested project exclusion precedes cap selection and scoped truncation metadata', () => {
56
+ const parent = join(parse(process.cwd()).root, 'parent-scope');
57
+ const child = join(parent, 'child');
58
+ const manifest = [
59
+ { rel: 'child/a.mjs', fp: 'a' },
60
+ { rel: 'child/b.mjs', fp: 'b' },
61
+ { rel: 'child/c.mjs', fp: 'c' },
62
+ { rel: 'parent-one.mjs', fp: '1' },
63
+ { rel: 'parent-two.mjs', fp: '2' },
64
+ ];
65
+ const scoped = _scopeCodeGraphManifest(manifest, parent, {
66
+ excludedProjectRoots: [child],
67
+ maxFiles: 2,
68
+ });
69
+ assert.deepEqual(scoped.indexed.map((row) => row.rel), ['parent-one.mjs', 'parent-two.mjs']);
70
+ assert.equal(scoped.manifest.length, 2);
71
+ assert.equal(scoped.truncated, false);
72
+
73
+ const truncated = _scopeCodeGraphManifest(
74
+ [...manifest, { rel: 'parent-three.mjs', fp: '3' }],
75
+ parent,
76
+ { excludedProjectRoots: [child], maxFiles: 2 },
77
+ );
78
+ assert.equal(truncated.manifest.length, 3);
79
+ assert.equal(truncated.indexed.length, 2);
80
+ assert.equal(truncated.truncated, true);
81
+ });
82
+
83
+ test('nested Windows project exclusion is case-insensitive and containment-aware', () => {
84
+ const scoped = _scopeCodeGraphManifest([
85
+ { rel: 'Child/inside.mjs', fp: 'inside' },
86
+ { rel: 'childish/outside.mjs', fp: 'outside' },
87
+ { rel: 'parent.mjs', fp: 'parent' },
88
+ ], 'C:\\Repo', {
89
+ excludedProjectRoots: ['c:\\repo\\CHILD'],
90
+ });
91
+ assert.deepEqual(scoped.manifest.map((row) => row.rel), [
92
+ 'childish/outside.mjs',
93
+ 'parent.mjs',
94
+ ]);
95
+ assert.deepEqual(scoped.prefixes, ['child']);
96
+ });
97
+
98
+ test('scoped in-flight keys cannot collide through delimiters', () => {
99
+ const onePrefix = _codeGraphInflightKey('/repo', {
100
+ scoped: true,
101
+ maxFiles: 100,
102
+ prefixes: ['child|vendor'],
103
+ });
104
+ const twoPrefixes = _codeGraphInflightKey('/repo', {
105
+ scoped: true,
106
+ maxFiles: 100,
107
+ prefixes: ['child', 'vendor'],
108
+ });
109
+ assert.notEqual(onePrefix, twoPrefixes);
110
+ assert.notEqual(onePrefix, _codeGraphInflightKey('/repo'));
111
+ });
112
+
113
+ test('scoped --files builds use bounded chunks and merge relationship records', async () => {
114
+ const rels = Array.from({ length: 10_000 }, (_, index) => `src/file-${index}.mjs`);
115
+ const calls = [];
116
+ const records = await _runGraphFilesChunked('/repo', rels, [{ rel: 'reused.mjs' }], {
117
+ maxArgChars: 1_024,
118
+ runGraphFiles: async (_root, rels) => {
119
+ calls.push(rels);
120
+ return [
121
+ ...rels.map((rel) => ({ rel, fp: rel, resolvedImports: ['reused.mjs'] })),
122
+ { rel: 'reused.mjs', importedBy: [rels[0]] },
123
+ ];
124
+ },
125
+ });
126
+ assert.ok(calls.length > 1);
127
+ for (const rels of calls) {
128
+ assert.ok(rels.reduce((sum, rel) => sum + rel.length + 3, 0) <= 1_024);
129
+ }
130
+ assert.equal(records.filter((row) => row.rel !== 'reused.mjs').length, 10_000);
131
+ assert.deepEqual(new Set(records.map((row) => row.rel)), new Set([...rels, 'reused.mjs']));
132
+ assert.deepEqual(
133
+ new Set(records.find((row) => row.rel === 'reused.mjs').importedBy),
134
+ new Set(calls.map((rels) => rels[0])),
135
+ );
136
+ });
137
+
138
+ test('default trusted sources include actual registration, selected nested project, and cache manifest', async () => {
139
+ const parent = await mkdtemp(join(tmpdir(), 'mixdog-source-parent-'));
140
+ const child = join(parent, 'child');
141
+ const cached = await mkdtemp(join(tmpdir(), 'mixdog-source-cache-'));
142
+ await mkdir(child);
143
+ await writeFile(join(parent, 'package.json'), '{}');
144
+ await writeFile(join(child, 'package.json'), '{}');
145
+ await mkdir(join(data, 'code-graph-cache'), { recursive: true });
146
+ await writeFile(join(home, 'projects.json'), JSON.stringify({
147
+ projects: [{ name: 'parent', path: parent, addedAt: 1 }],
148
+ }));
149
+ await writeFile(join(data, 'code-graph-cache', 'manifest.json'), JSON.stringify({
150
+ [cached]: { hash: '12345678', builtAt: 1 },
151
+ }));
152
+ process.env.MIXDOG_SESSION_CWD = child;
153
+ try {
154
+ const roots = collectTrustedCodeGraphRoots(parse(parent).root);
155
+ assert.deepEqual(new Set(roots), new Set([parent, child, cached]));
156
+ } finally {
157
+ delete process.env.MIXDOG_SESSION_CWD;
158
+ await Promise.all([
159
+ rm(parent, { recursive: true, force: true }),
160
+ rm(cached, { recursive: true, force: true }),
161
+ ]);
162
+ }
163
+ });
164
+
165
+ test('root federation labels repositories, includes registered non-Git projects, and isolates failures', async () => {
166
+ const good = await mkdtemp(join(tmpdir(), 'mixdog-federated-good-'));
167
+ const bad = await mkdtemp(join(tmpdir(), 'mixdog-federated-bad-'));
168
+ await mkdir(join(good, 'src'));
169
+ await writeFile(join(good, 'package.json'), '{}');
170
+ await writeFile(join(good, 'src', 'good.mjs'), 'export function federatedNeedle() { return 1; }\n');
171
+ await writeFile(join(home, 'projects.json'), JSON.stringify({
172
+ projects: [
173
+ { name: 'good', path: good, addedAt: 2 },
174
+ { name: 'bad', path: bad, addedAt: 1 },
175
+ ],
176
+ }));
177
+ await writeFile(join(bad, 'package.json'), '{}');
178
+ const { executeCodeGraphTool } = await import('../src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs');
179
+ const result = await executeCodeGraphTool(
180
+ 'code_graph',
181
+ { mode: 'symbol_search', symbol: 'federatedNeedle' },
182
+ parse(good).root,
183
+ );
184
+ assert.match(result, new RegExp(`# project .+ \\[${good.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`));
185
+ assert.match(result, /federatedNeedle/);
186
+ assert.match(result, new RegExp(`# project .+ \\[${bad.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`));
187
+ assert.match(result, /contains zero eligible files/);
188
+
189
+ await Promise.all([
190
+ rm(good, { recursive: true, force: true }),
191
+ rm(bad, { recursive: true, force: true }),
192
+ ]);
193
+ });
194
+
195
+ test('trusted file and relationship anchors route to owners; missing and untrusted anchors stop at root', async () => {
196
+ const project = await mkdtemp(join(tmpdir(), 'mixdog-file-route-'));
197
+ const untrusted = await mkdtemp(join(tmpdir(), 'mixdog-untrusted-route-'));
198
+ await writeFile(join(project, 'package.json'), '{}');
199
+ await writeFile(join(project, 'one.mjs'), 'export function routedNeedle() { return 1; }\n');
200
+ await writeFile(join(project, 'two.mjs'), "import { routedNeedle } from './one.mjs';\nexport const routedUse = routedNeedle();\n");
201
+ await writeFile(join(untrusted, 'package.json'), '{}');
202
+ await writeFile(join(untrusted, 'hidden.mjs'), 'export const hidden = 1;\n');
203
+ await writeFile(join(home, 'projects.json'), JSON.stringify({
204
+ projects: [{ name: 'route', path: project, addedAt: 1 }],
205
+ }));
206
+ const { executeCodeGraphTool } = await import('../src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs');
207
+ const root = parse(project).root;
208
+ const overview = await executeCodeGraphTool('code_graph', { mode: 'overview' }, root);
209
+ assert.match(overview, /# project .+\[/);
210
+ assert.match(overview, /files\t2/);
211
+ const imports = await executeCodeGraphTool('code_graph', { mode: 'imports', file: join(project, 'two.mjs') }, root);
212
+ assert.match(imports, /# project .+\[/);
213
+ assert.match(imports, /one\.mjs/);
214
+ const dependents = await executeCodeGraphTool('code_graph', {
215
+ mode: 'dependents',
216
+ file: join(project, 'one.mjs'),
217
+ }, root);
218
+ assert.match(dependents, /# project .+\[/);
219
+ assert.match(dependents, /two\.mjs/);
220
+ const missing = await executeCodeGraphTool('code_graph', {
221
+ mode: 'overview',
222
+ file: join(project, 'definitely-missing.mjs'),
223
+ }, root);
224
+ assert.match(missing, /^Error: code_graph: file not found:/);
225
+ assert.doesNotMatch(missing, /Same filename exists/);
226
+ const rejected = await executeCodeGraphTool('code_graph', {
227
+ mode: 'overview',
228
+ file: join(untrusted, 'hidden.mjs'),
229
+ }, root);
230
+ assert.match(rejected, /^Error: code_graph: file anchor is not owned by a trusted project:/);
231
+ await Promise.all([
232
+ rm(project, { recursive: true, force: true }),
233
+ rm(untrusted, { recursive: true, force: true }),
234
+ ]);
235
+ });
236
+
237
+ test('overlapping trusted roots keep nested symbols and edges out of the parent graph', async () => {
238
+ const parent = await mkdtemp(join(tmpdir(), 'mixdog-overlap-parent-'));
239
+ const child = join(parent, 'child');
240
+ await mkdir(child);
241
+ await writeFile(join(parent, 'package.json'), '{}');
242
+ await writeFile(join(parent, 'parent.mjs'), "import { childNeedle } from './child/child.mjs';\nexport const parentUse = childNeedle();\n");
243
+ await writeFile(join(child, 'package.json'), '{}');
244
+ await writeFile(join(child, 'child.mjs'), 'export function childNeedle() { return 1; }\n');
245
+ await writeFile(join(home, 'projects.json'), JSON.stringify({
246
+ projects: [
247
+ { name: 'parent', path: parent, addedAt: 2 },
248
+ { name: 'child', path: child, addedAt: 1 },
249
+ ],
250
+ }));
251
+ const { executeCodeGraphTool } = await import('../src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs');
252
+ const result = await executeCodeGraphTool('code_graph', {
253
+ mode: 'symbol_search',
254
+ symbol: 'childNeedle',
255
+ }, parse(parent).root);
256
+ const parentLabel = `# project ${parent.split(/[\\/]/).pop().toLowerCase()} [${parent.toLowerCase()}]`;
257
+ const childLabel = `# project ${child.split(/[\\/]/).pop().toLowerCase()} [${child.toLowerCase()}]`;
258
+ const normalized = result.toLowerCase();
259
+ const parentSection = normalized.slice(
260
+ normalized.indexOf(parentLabel),
261
+ normalized.indexOf(childLabel) > normalized.indexOf(parentLabel)
262
+ ? normalized.indexOf(childLabel)
263
+ : normalized.length,
264
+ );
265
+ assert.doesNotMatch(parentSection, /child\/child\.mjs|childneedle\s+\(/);
266
+ assert.match(normalized.slice(normalized.indexOf(childLabel)), /childneedle/);
267
+ const imports = await executeCodeGraphTool('code_graph', {
268
+ mode: 'imports',
269
+ file: join(parent, 'parent.mjs'),
270
+ }, parse(parent).root);
271
+ assert.doesNotMatch(imports.split('# raw')[0], /child\/child\.mjs/);
272
+ await rm(parent, { recursive: true, force: true });
273
+ });
@@ -21,6 +21,9 @@ import {
21
21
  import { initialCompactionConfig } from '../src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs';
22
22
  import { resolveSessionCompactionPolicy } from '../src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs';
23
23
  import { sendWithRecovery } from '../src/runtime/agent/orchestrator/session/send-with-recovery.mjs';
24
+ import { recallFastTrackCompactMessages } from '../src/runtime/agent/orchestrator/session/compact/engine.mjs';
25
+ import { _combineUsageWithWarmup } from '../src/runtime/agent/orchestrator/providers/openai-ws-events.mjs';
26
+ import { applyAskTerminalUsageTotals } from '../src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs';
24
27
 
25
28
  function policyFor(session) {
26
29
  return resolveWorkerCompactPolicy(session, []);
@@ -371,12 +374,13 @@ test('tool reserve and status cache detect nested schema mutation', () => {
371
374
  assert.ok(statusAfter.request.toolSchemaTokens > statusBefore.request.toolSchemaTokens);
372
375
  });
373
376
 
374
- test('native replay fingerprint detects encrypted reasoning and assistant metadata mutation', () => {
377
+ test('native replay fingerprint detects encrypted reasoning and provider metadata mutation', () => {
375
378
  const message = {
376
379
  role: 'assistant',
377
380
  content: '',
378
381
  reasoningItems: [{ type: 'reasoning', encrypted_content: 'short' }],
379
382
  assistantBlocks: [{ type: 'tool_use', id: 'c1', name: 'read', input: { path: 'a' }, cache_control: { type: 'ephemeral' } }],
383
+ providerMetadata: { gemini: { thoughtParts: [{ text: 'short', thoughtSignature: 'sig' }] } },
380
384
  };
381
385
  const messages = [message];
382
386
  const first = summarizeContextMessages(messages).estimatedTokens;
@@ -386,6 +390,58 @@ test('native replay fingerprint detects encrypted reasoning and assistant metada
386
390
  message.assistantBlocks[0].cache_control.mode = 'metadata_'.repeat(1_000);
387
391
  const third = summarizeContextMessages(messages).estimatedTokens;
388
392
  assert.ok(third > second, 'in-place assistant-block metadata mutation must invalidate the message memo');
393
+ message.providerMetadata.gemini.thoughtParts[0].text = 'gemini_private_thought_'.repeat(1_000);
394
+ const fourth = summarizeContextMessages(messages).estimatedTokens;
395
+ assert.ok(fourth > third, 'in-place Gemini replay metadata mutation must invalidate the message memo');
396
+ });
397
+
398
+ test('recall compaction preserves provider-scoped Gemini replay metadata on the live tail', () => {
399
+ const providerMetadata = {
400
+ gemini: {
401
+ textParts: [{ text: 'signed answer', thoughtSignature: 'sig-tail' }],
402
+ },
403
+ };
404
+ const result = recallFastTrackCompactMessages([
405
+ { role: 'user', content: `old request ${'x'.repeat(20_000)}` },
406
+ { role: 'assistant', content: `old answer ${'y'.repeat(20_000)}` },
407
+ { role: 'user', content: 'recent request' },
408
+ { role: 'assistant', content: 'signed answer', providerMetadata },
409
+ ], 8_000, {
410
+ force: true,
411
+ recallText: 'Older history summary.',
412
+ recallTailMaxUsers: 1,
413
+ recallTailTokenCap: 4_000,
414
+ });
415
+ const tail = result.messages.find((message) => message?.content === 'signed answer');
416
+ assert.deepEqual(tail?.providerMetadata, providerMetadata);
417
+ });
418
+
419
+ test('recall tail cap drops oversized replay metadata instead of raising the budget', () => {
420
+ const result = recallFastTrackCompactMessages([
421
+ { role: 'user', content: `old request ${'x'.repeat(20_000)}` },
422
+ { role: 'assistant', content: `old answer ${'y'.repeat(20_000)}` },
423
+ { role: 'user', content: 'recent request' },
424
+ {
425
+ role: 'assistant',
426
+ content: 'recent answer',
427
+ providerMetadata: {
428
+ gemini: {
429
+ thoughtParts: [{
430
+ text: 'private_'.repeat(20_000),
431
+ thoughtSignature: 'sig',
432
+ }],
433
+ },
434
+ },
435
+ },
436
+ ], 8_000, {
437
+ force: true,
438
+ recallText: 'Older history summary.',
439
+ recallTailMaxUsers: 1,
440
+ recallTailTokenCap: 200,
441
+ });
442
+ assert.ok(result.diagnostics.tailTokens <= 200, `tail tokens ${result.diagnostics.tailTokens}`);
443
+ const recent = result.messages.find((message) => message?.content === 'recent answer');
444
+ assert.equal(recent?.providerMetadata, undefined);
389
445
  });
390
446
 
391
447
  test('provider baseline pressure includes only the configured extra reserve', () => {
@@ -572,6 +628,108 @@ test('thinking-only continuation without assistant replay excludes provider outp
572
628
  }), false, 'unreplayed thinking output must not cause an early compact');
573
629
  });
574
630
 
631
+ test('OpenAI OAuth WS warmup remains billed but does not double the main context footprint', async () => {
632
+ const main = {
633
+ inputTokens: 167_635,
634
+ outputTokens: 611,
635
+ cachedTokens: 160_000,
636
+ promptTokens: 167_635,
637
+ raw: {},
638
+ };
639
+ const usage = _combineUsageWithWarmup(main, main, { separateMainContext: true });
640
+ assert.equal(usage.inputTokens, 335_270, 'billing usage must retain warmup plus main input');
641
+ const { initProviders, getProvider } = await import('../src/runtime/agent/orchestrator/providers/registry.mjs');
642
+ const { createSession, askSession, getSession } = await import('../src/runtime/agent/orchestrator/session/manager.mjs');
643
+ await initProviders({ 'openai-oauth': { enabled: true } });
644
+ const provider = getProvider('openai-oauth');
645
+ const originalSend = provider.send;
646
+ const compactEvents = [];
647
+ provider.send = async () => ({ content: 'done', usage });
648
+ try {
649
+ const session = createSession({
650
+ provider: 'openai-oauth',
651
+ model: 'warmup-context-regression',
652
+ tools: [],
653
+ cwd: process.cwd(),
654
+ skipAgentRules: true,
655
+ skipSkills: true,
656
+ compaction: { auto: true },
657
+ });
658
+ session.contextWindow = 272_000;
659
+ session.rawContextWindow = 272_000;
660
+ await askSession(session.id, 'large OAuth WebSocket request', null, null, process.cwd(), null, {
661
+ onCompactEvent: event => compactEvents.push(event),
662
+ });
663
+
664
+ const persisted = getSession(session.id);
665
+ const policy = { ...policyFor(persisted), reserveTokens: 0 };
666
+ const messageTokens = estimateMessagesTokens(persisted.messages);
667
+ const pressure = resolveCompactionPressureTokens(messageTokens, policy, {
668
+ messages: persisted.messages,
669
+ sessionRef: persisted,
670
+ });
671
+ assert.equal(persisted.totalInputTokens, 335_270, 'incremental lifetime totals must retain warmup billing');
672
+ assert.equal(persisted.lastContextTokens, 167_635, 'incremental context snapshot must exclude warmup');
673
+ assert.equal(persisted.contextPressureBaselineTokens, 168_246, 'stored baseline must use main request usage');
674
+ assert.equal(pressure, 168_246, 'resolved pressure must use the stored baseline without transcript growth');
675
+ assert.equal(policy.triggerTokens, 258_400, 'fixture trigger must match the reported premature-compaction threshold');
676
+ assert.equal(shouldCompactForSession(messageTokens, policy, {
677
+ messages: persisted.messages,
678
+ sessionRef: persisted,
679
+ pressureTokens: pressure,
680
+ }), false, 'main-request pressure must not trigger compaction');
681
+ assert.equal(compactEvents.length, 0, 'warmup must not cause an auto-compaction');
682
+ } finally {
683
+ provider.send = originalSend;
684
+ }
685
+ });
686
+
687
+ test('warmup-only OpenAI OAuth WS usage stays billable but invalidates context usage', () => {
688
+ const warmup = { inputTokens: 167_635, outputTokens: 611, cachedTokens: 160_000, promptTokens: 167_635 };
689
+ const usage = _combineUsageWithWarmup(null, warmup, { separateMainContext: true });
690
+ const session = { provider: 'openai-oauth', contextPressureBaselineTokens: 99_000 };
691
+ assert.equal(usage.mainUsageAvailable, false);
692
+ assert.equal(recordProviderContextBaseline(session, [], usage), false);
693
+ assert.equal(session.contextPressureBaselineTokens, null, 'warmup-only usage must not remain a provider baseline');
694
+ applyAskTerminalUsageTotals(session, { usage, lastTurnUsage: usage });
695
+ assert.equal(session.totalInputTokens, 167_635, 'warmup-only usage remains billable');
696
+ assert.equal(session.lastContextTokens, null, 'warmup-only usage has no main-request context snapshot');
697
+ });
698
+
699
+ test('xAI warmup remains billable while main usage alone drives context pressure', () => {
700
+ const actual = { inputTokens: 20, outputTokens: 2, cachedTokens: 5, promptTokens: 20, raw: { provider: 'xai', cost_in_usd_ticks: 200 } };
701
+ const warmup = { inputTokens: 10, outputTokens: 1, cachedTokens: 3, promptTokens: 10, raw: { phase: 'warmup', cost_in_usd_ticks: 100 } };
702
+ const usage = _combineUsageWithWarmup(actual, warmup, { separateMainContext: true });
703
+ assert.deepEqual(usage, {
704
+ ...actual,
705
+ inputTokens: 30,
706
+ outputTokens: 3,
707
+ cachedTokens: 8,
708
+ promptTokens: 30,
709
+ warmupInputTokens: 10,
710
+ warmupCachedTokens: 3,
711
+ warmupOutputTokens: 1,
712
+ warmupPromptTokens: 10,
713
+ warmupCacheWriteTokens: 0,
714
+ raw: {
715
+ provider: 'xai',
716
+ cost_in_usd_ticks: 300,
717
+ warmup_usage: { phase: 'warmup', cost_in_usd_ticks: 100 },
718
+ },
719
+ mainInputTokens: 20,
720
+ mainOutputTokens: 2,
721
+ mainCachedTokens: 5,
722
+ mainPromptTokens: 20,
723
+ mainCacheWriteTokens: 0,
724
+ mainUsageAvailable: true,
725
+ });
726
+ const session = { provider: 'xai', contextPressureBaselineTokens: null };
727
+ assert.equal(recordProviderContextBaseline(session, [], usage), true);
728
+ applyAskTerminalUsageTotals(session, { usage, lastTurnUsage: usage });
729
+ assert.equal(session.totalInputTokens, 30, 'lifetime total includes xAI warmup');
730
+ assert.equal(session.lastContextTokens, 20, 'context snapshot excludes xAI warmup');
731
+ });
732
+
575
733
  test('successful compact invalidates stale usage and cannot immediately compact again', () => {
576
734
  const session = { provider: 'openai', contextWindow: 100_000, compaction: {} };
577
735
  const policy = { ...policyFor(session), reserveTokens: 0 };