mixdog 0.9.51 → 0.9.52
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/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 +55 -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/gemini-provider-test.mjs +1053 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +324 -3
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +320 -1
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- 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/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- 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 +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- 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 +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +130 -47
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- 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 +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +122 -63
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -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/pre-send-compact.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- 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/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- 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/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- 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 -13
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +6 -3
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- 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/dist/index.mjs +73 -278
- package/src/tui/engine/session-api-ext.mjs +13 -2
- 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 +4 -1
- package/src/tui/lib/voice-setup.mjs +16 -0
|
@@ -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,7 @@ 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';
|
|
24
25
|
|
|
25
26
|
function policyFor(session) {
|
|
26
27
|
return resolveWorkerCompactPolicy(session, []);
|
|
@@ -371,12 +372,13 @@ test('tool reserve and status cache detect nested schema mutation', () => {
|
|
|
371
372
|
assert.ok(statusAfter.request.toolSchemaTokens > statusBefore.request.toolSchemaTokens);
|
|
372
373
|
});
|
|
373
374
|
|
|
374
|
-
test('native replay fingerprint detects encrypted reasoning and
|
|
375
|
+
test('native replay fingerprint detects encrypted reasoning and provider metadata mutation', () => {
|
|
375
376
|
const message = {
|
|
376
377
|
role: 'assistant',
|
|
377
378
|
content: '',
|
|
378
379
|
reasoningItems: [{ type: 'reasoning', encrypted_content: 'short' }],
|
|
379
380
|
assistantBlocks: [{ type: 'tool_use', id: 'c1', name: 'read', input: { path: 'a' }, cache_control: { type: 'ephemeral' } }],
|
|
381
|
+
providerMetadata: { gemini: { thoughtParts: [{ text: 'short', thoughtSignature: 'sig' }] } },
|
|
380
382
|
};
|
|
381
383
|
const messages = [message];
|
|
382
384
|
const first = summarizeContextMessages(messages).estimatedTokens;
|
|
@@ -386,6 +388,58 @@ test('native replay fingerprint detects encrypted reasoning and assistant metada
|
|
|
386
388
|
message.assistantBlocks[0].cache_control.mode = 'metadata_'.repeat(1_000);
|
|
387
389
|
const third = summarizeContextMessages(messages).estimatedTokens;
|
|
388
390
|
assert.ok(third > second, 'in-place assistant-block metadata mutation must invalidate the message memo');
|
|
391
|
+
message.providerMetadata.gemini.thoughtParts[0].text = 'gemini_private_thought_'.repeat(1_000);
|
|
392
|
+
const fourth = summarizeContextMessages(messages).estimatedTokens;
|
|
393
|
+
assert.ok(fourth > third, 'in-place Gemini replay metadata mutation must invalidate the message memo');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('recall compaction preserves provider-scoped Gemini replay metadata on the live tail', () => {
|
|
397
|
+
const providerMetadata = {
|
|
398
|
+
gemini: {
|
|
399
|
+
textParts: [{ text: 'signed answer', thoughtSignature: 'sig-tail' }],
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
const result = recallFastTrackCompactMessages([
|
|
403
|
+
{ role: 'user', content: `old request ${'x'.repeat(20_000)}` },
|
|
404
|
+
{ role: 'assistant', content: `old answer ${'y'.repeat(20_000)}` },
|
|
405
|
+
{ role: 'user', content: 'recent request' },
|
|
406
|
+
{ role: 'assistant', content: 'signed answer', providerMetadata },
|
|
407
|
+
], 8_000, {
|
|
408
|
+
force: true,
|
|
409
|
+
recallText: 'Older history summary.',
|
|
410
|
+
recallTailMaxUsers: 1,
|
|
411
|
+
recallTailTokenCap: 4_000,
|
|
412
|
+
});
|
|
413
|
+
const tail = result.messages.find((message) => message?.content === 'signed answer');
|
|
414
|
+
assert.deepEqual(tail?.providerMetadata, providerMetadata);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('recall tail cap drops oversized replay metadata instead of raising the budget', () => {
|
|
418
|
+
const result = recallFastTrackCompactMessages([
|
|
419
|
+
{ role: 'user', content: `old request ${'x'.repeat(20_000)}` },
|
|
420
|
+
{ role: 'assistant', content: `old answer ${'y'.repeat(20_000)}` },
|
|
421
|
+
{ role: 'user', content: 'recent request' },
|
|
422
|
+
{
|
|
423
|
+
role: 'assistant',
|
|
424
|
+
content: 'recent answer',
|
|
425
|
+
providerMetadata: {
|
|
426
|
+
gemini: {
|
|
427
|
+
thoughtParts: [{
|
|
428
|
+
text: 'private_'.repeat(20_000),
|
|
429
|
+
thoughtSignature: 'sig',
|
|
430
|
+
}],
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
], 8_000, {
|
|
435
|
+
force: true,
|
|
436
|
+
recallText: 'Older history summary.',
|
|
437
|
+
recallTailMaxUsers: 1,
|
|
438
|
+
recallTailTokenCap: 200,
|
|
439
|
+
});
|
|
440
|
+
assert.ok(result.diagnostics.tailTokens <= 200, `tail tokens ${result.diagnostics.tailTokens}`);
|
|
441
|
+
const recent = result.messages.find((message) => message?.content === 'recent answer');
|
|
442
|
+
assert.equal(recent?.providerMetadata, undefined);
|
|
389
443
|
});
|
|
390
444
|
|
|
391
445
|
test('provider baseline pressure includes only the configured extra reserve', () => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { semanticCompactMessages, recallFastTrackCompactMessages, SUMMARY_PREFIX, COMPACT_TYPE_SEMANTIC, COMPACT_TYPE_RECALL_FASTTRACK, normalizeCompactType } from '../src/runtime/agent/orchestrator/session/compact.mjs';
|
|
3
3
|
import { agentLoop } from '../src/runtime/agent/orchestrator/session/loop.mjs';
|
|
4
4
|
import { estimateMessagesTokens, estimateToolSchemaTokens } from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
|
|
5
|
+
import { runSessionCompaction } from '../src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs';
|
|
5
6
|
import { autoCompactWindowForRoute, summarizeGatewayUsage } from '../src/vendor/statusline/src/gateway/route-meta.mjs';
|
|
6
7
|
|
|
7
8
|
function assert(condition, message) {
|
|
@@ -104,6 +105,85 @@ const wholeTranscriptForced = await semanticCompactMessages(wholeTranscriptProvi
|
|
|
104
105
|
assert(wholeTranscriptForced.semantic === true && wholeTranscriptForcedCalls === 1, 'forced semantic compact must summarize when tail budget would preserve the whole live transcript (default tailTurns)');
|
|
105
106
|
assert(findSummary(wholeTranscriptForced.messages), 'whole-transcript forced semantic compact should insert an anchored summary');
|
|
106
107
|
|
|
108
|
+
// Manual /compact must reduce the active session transcript directly through
|
|
109
|
+
// semantic compaction even when main-session policy is recall-fasttrack. If the
|
|
110
|
+
// override regresses, this test enters Memory first (unregistered in this smoke)
|
|
111
|
+
// and reports recall-fasttrack metadata before falling back.
|
|
112
|
+
let manualDirectCalls = 0;
|
|
113
|
+
let manualDirectPrompt = '';
|
|
114
|
+
const manualDirectMessages = [
|
|
115
|
+
{ role: 'system', content: 'MANUAL_SYSTEM_RULE_STAYS_EXACT' },
|
|
116
|
+
{
|
|
117
|
+
role: 'user',
|
|
118
|
+
content: '# Session\nCwd: C:\\Injected\\Noise\nModel: fake-model\nWorkflow: Solo\n\n# Additional context\nOLD_ADDITIONAL_CONTEXT_INJECTION\n\n# Prefetch\nOLD_PREFETCH_INJECTION\n\n# Task\nREAL_OLD_USER_REQUEST\n<system-reminder>OLD_SYSTEM_REMINDER_INJECTION</system-reminder>\n<memory-context>OLD_MEMORY_CONTEXT_INJECTION</memory-context>',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
role: 'assistant',
|
|
122
|
+
content: 'REAL_OLD_ASSISTANT_REPLY\n```text\nOLD_CODE_FENCE_TRACE\n```',
|
|
123
|
+
toolCalls: [{ id: 'OLD_TOOL_CALL_ID', name: 'OLD_TOOL_CALL_TRACE', arguments: '{"password":"OLD_TOOL_SECRET"}' }],
|
|
124
|
+
},
|
|
125
|
+
{ role: 'tool', toolCallId: 'OLD_TOOL_CALL_ID', content: 'OLD_TOOL_RESULT_TRACE' },
|
|
126
|
+
{ role: 'developer', content: 'OLD_DEVELOPER_INJECTION' },
|
|
127
|
+
{ role: 'user', content: 'Reference files:\nOLD_REFERENCE_FILE_INJECTION' },
|
|
128
|
+
{ role: 'user', content: '[mixdog-runtime] OLD_RUNTIME_INJECTION' },
|
|
129
|
+
{ role: 'user', content: 'CURRENT_REQUEST_STAYS_VERBATIM' },
|
|
130
|
+
{ role: 'assistant', content: 'CURRENT_ASSISTANT_STAYS_VERBATIM' },
|
|
131
|
+
];
|
|
132
|
+
const manualDirectSession = {
|
|
133
|
+
id: `compact-smoke-manual-direct-${process.pid}`,
|
|
134
|
+
provider: 'manual-direct-smoke',
|
|
135
|
+
model: 'fake-model',
|
|
136
|
+
contextWindow: 100_000,
|
|
137
|
+
compactBoundaryTokens: 100_000,
|
|
138
|
+
messages: manualDirectMessages.map((m) => ({ ...m })),
|
|
139
|
+
tools: [],
|
|
140
|
+
compaction: {
|
|
141
|
+
type: COMPACT_TYPE_RECALL_FASTTRACK,
|
|
142
|
+
compactType: COMPACT_TYPE_RECALL_FASTTRACK,
|
|
143
|
+
tailTurns: 1,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
const manualDirect = await runSessionCompaction(manualDirectSession, {
|
|
147
|
+
mode: 'manual',
|
|
148
|
+
force: true,
|
|
149
|
+
compactType: COMPACT_TYPE_SEMANTIC,
|
|
150
|
+
filterOldHistoryForIngest: true,
|
|
151
|
+
provider: {
|
|
152
|
+
name: 'manual-direct-smoke',
|
|
153
|
+
async send(sentMessages) {
|
|
154
|
+
manualDirectCalls += 1;
|
|
155
|
+
manualDirectPrompt = sentMessages.map((m) => String(m?.content || '')).join('\n');
|
|
156
|
+
return { content: '## Goal\n- compact only the active session\n\n## Constraints & Preferences\n- preserve the recent tail\n\n## Progress\n### Done\n- reduced old transcript content\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- use direct semantic compaction\n\n## Next Steps\n- continue from the preserved tail\n\n## Critical Context\n- active session only\n\n## Relevant Files\n- session-crud.mjs' };
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
model: 'fake-model',
|
|
160
|
+
});
|
|
161
|
+
assert(manualDirectCalls === 1, 'manual direct compact should make exactly one semantic provider call');
|
|
162
|
+
assert(manualDirect.compactType === COMPACT_TYPE_SEMANTIC, 'manual direct compact must bypass recall-fasttrack policy');
|
|
163
|
+
assert(manualDirect.semanticCompact === true, 'manual direct compact should produce a semantic summary');
|
|
164
|
+
assert(manualDirect.recallFastTrackError == null, 'manual direct compact must not touch Memory recall');
|
|
165
|
+
assert(findSummary(manualDirectSession.messages), 'manual direct compact should replace old active-session history with a handoff summary');
|
|
166
|
+
assert(manualDirectPrompt.includes('REAL_OLD_USER_REQUEST'), 'manual compact prompt should retain genuine old user prose');
|
|
167
|
+
assert(manualDirectPrompt.includes('REAL_OLD_ASSISTANT_REPLY'), 'manual compact prompt should retain genuine old assistant prose');
|
|
168
|
+
for (const excluded of [
|
|
169
|
+
'OLD_ADDITIONAL_CONTEXT_INJECTION',
|
|
170
|
+
'OLD_PREFETCH_INJECTION',
|
|
171
|
+
'OLD_SYSTEM_REMINDER_INJECTION',
|
|
172
|
+
'OLD_MEMORY_CONTEXT_INJECTION',
|
|
173
|
+
'OLD_CODE_FENCE_TRACE',
|
|
174
|
+
'OLD_TOOL_CALL_TRACE',
|
|
175
|
+
'OLD_TOOL_SECRET',
|
|
176
|
+
'OLD_TOOL_RESULT_TRACE',
|
|
177
|
+
'OLD_DEVELOPER_INJECTION',
|
|
178
|
+
'OLD_REFERENCE_FILE_INJECTION',
|
|
179
|
+
'OLD_RUNTIME_INJECTION',
|
|
180
|
+
]) {
|
|
181
|
+
assert(!manualDirectPrompt.includes(excluded), `manual compact old-history filter must exclude ${excluded}`);
|
|
182
|
+
}
|
|
183
|
+
assert(manualDirectSession.messages.some((m) => m.role === 'system' && m.content === 'MANUAL_SYSTEM_RULE_STAYS_EXACT'), 'manual compact must preserve system rules separately');
|
|
184
|
+
assert(manualDirectSession.messages.some((m) => m.role === 'user' && m.content === 'CURRENT_REQUEST_STAYS_VERBATIM'), 'manual compact must preserve the recent user turn verbatim');
|
|
185
|
+
assert(manualDirectSession.messages.some((m) => m.role === 'assistant' && m.content === 'CURRENT_ASSISTANT_STAYS_VERBATIM'), 'manual compact must preserve the recent assistant turn verbatim');
|
|
186
|
+
|
|
107
187
|
assert(normalizeCompactType('type1') === COMPACT_TYPE_SEMANTIC, 'type1 should resolve to semantic compact');
|
|
108
188
|
assert(normalizeCompactType('recall-fasttrack') === COMPACT_TYPE_RECALL_FASTTRACK, 'type2 should resolve to recall fast-track compact');
|
|
109
189
|
// Alias parity: snake_case + dash + no-dash spellings of fast-track must all
|