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.
- package/package.json +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/bench-run.mjs +2 -2
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +159 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +137 -0
- package/scripts/max-output-recovery-test.mjs +86 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +387 -6
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +447 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +525 -0
- package/scripts/provider-toolcall-test.mjs +492 -11
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +5 -7
- package/src/cli.mjs +15 -2
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +27 -13
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +114 -308
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +31 -21
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -286
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +42 -5
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +84 -150
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -119
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +28 -74
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +136 -20
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +94 -33
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +201 -123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +50 -45
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +229 -111
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +130 -63
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +37 -224
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +81 -42
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +30 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +353 -108
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +317 -250
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +125 -85
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +46 -202
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -17
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +436 -0
- package/src/runtime/shared/process-shutdown.mjs +33 -4
- package/src/runtime/shared/resource-admission.mjs +364 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +60 -13
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +49 -105
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +390 -104
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +29 -200
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +275 -343
- package/src/tui/engine/session-api-ext.mjs +50 -6
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +5 -1
- package/src/tui/lib/voice-setup.mjs +21 -5
- package/scripts/_devtools-stub.mjs +0 -1
- 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,
|
|
67
|
-
assert.match(relativeResult,
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
140
|
-
executeCodeGraphTool('code_graph', { mode: 'overview', files: cappedFiles }, invalidCwd),
|
|
141
|
-
|
|
143
|
+
assert.match(
|
|
144
|
+
await executeCodeGraphTool('code_graph', { mode: 'overview', files: cappedFiles }, invalidCwd),
|
|
145
|
+
/file list exceeds cap/,
|
|
142
146
|
);
|
|
143
|
-
|
|
144
|
-
executeCodeGraphTool('code_graph', { mode: 'overview', files: `${join(project, 'src', 'one.mjs')},${join(project, 'src', 'two.mjs')}` }, invalidCwd),
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
executeCodeGraphTool('code_graph', { mode: 'overview', files: [literalWildcard] }, invalidCwd),
|
|
149
|
-
|
|
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
|
|
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 };
|