mixdog 0.9.47 → 0.9.49

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 (83) hide show
  1. package/package.json +14 -4
  2. package/scripts/agent-parallel-smoke.mjs +4 -1
  3. package/scripts/agent-route-batch-test.mjs +40 -0
  4. package/scripts/code-graph-aggregate-cwd-test.mjs +154 -0
  5. package/scripts/code-graph-description-contract.mjs +185 -0
  6. package/scripts/code-graph-disk-hit-test.mjs +55 -0
  7. package/scripts/code-graph-dispatch-test.mjs +96 -0
  8. package/scripts/compact-pressure-test.mjs +40 -0
  9. package/scripts/deferred-tool-loading-test.mjs +233 -0
  10. package/scripts/execution-completion-dedup-test.mjs +48 -0
  11. package/scripts/explore-prompt-policy-test.mjs +88 -3
  12. package/scripts/live-worker-smoke.mjs +68 -16
  13. package/scripts/memory-core-input-test.mjs +33 -13
  14. package/scripts/native-edit-wire-test.mjs +152 -0
  15. package/scripts/openai-oauth-ws-1006-retry-test.mjs +294 -16
  16. package/scripts/patch-binary-cache-test.mjs +181 -0
  17. package/scripts/prompt-immediate-render-test.mjs +89 -0
  18. package/scripts/provider-toolcall-test.mjs +280 -15
  19. package/scripts/shell-failure-diagnostics-test.mjs +211 -0
  20. package/scripts/statusline-quota-hysteresis-test.mjs +26 -1
  21. package/scripts/streaming-tail-window-test.mjs +29 -0
  22. package/scripts/tool-failures.mjs +21 -3
  23. package/scripts/tool-smoke.mjs +263 -38
  24. package/scripts/tool-tui-presentation-test.mjs +17 -1
  25. package/scripts/tui-perf-run.ps1 +26 -0
  26. package/scripts/tui-transcript-perf-test.mjs +7 -1
  27. package/scripts/verify-release-assets-test.mjs +647 -0
  28. package/scripts/verify-release-assets.mjs +293 -0
  29. package/scripts/windows-hide-spawn-options-test.mjs +19 -0
  30. package/src/cli.mjs +1 -0
  31. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +16 -5
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +35 -11
  33. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -11
  34. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +28 -0
  35. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +34 -34
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +45 -38
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -11
  39. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +4 -4
  40. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +10 -6
  41. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +4 -3
  42. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +4 -3
  43. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +16 -1
  44. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -2
  45. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +0 -1
  46. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +13 -8
  47. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +30 -16
  48. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +25 -8
  49. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +69 -4
  50. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +5 -4
  51. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +12 -12
  53. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +12 -7
  54. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +77 -18
  55. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  56. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +99 -24
  57. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
  58. package/src/runtime/memory/tool-defs.mjs +1 -2
  59. package/src/session-runtime/context-status.mjs +25 -16
  60. package/src/session-runtime/model-route-api.mjs +2 -0
  61. package/src/session-runtime/runtime-core.mjs +2 -2
  62. package/src/session-runtime/session-turn-api.mjs +4 -1
  63. package/src/session-runtime/tool-catalog.mjs +113 -19
  64. package/src/session-runtime/tool-defs.mjs +1 -0
  65. package/src/standalone/agent-tool/helpers.mjs +25 -6
  66. package/src/standalone/agent-tool.mjs +75 -41
  67. package/src/tui/App.jsx +4 -0
  68. package/src/tui/app/input-parsers.mjs +8 -9
  69. package/src/tui/components/Markdown.jsx +6 -1
  70. package/src/tui/components/Message.jsx +11 -2
  71. package/src/tui/components/PromptInput.jsx +19 -21
  72. package/src/tui/components/Spinner.jsx +4 -4
  73. package/src/tui/components/StatusLine.jsx +6 -6
  74. package/src/tui/components/TranscriptItem.jsx +2 -2
  75. package/src/tui/components/prompt-input/immediate-render.mjs +47 -0
  76. package/src/tui/components/tool-execution/surface-detail.mjs +14 -9
  77. package/src/tui/dist/index.mjs +130 -45
  78. package/src/tui/engine/agent-job-feed.mjs +21 -2
  79. package/src/tui/engine/turn.mjs +12 -1
  80. package/src/tui/markdown/measure-rendered-rows.mjs +1 -1
  81. package/src/tui/markdown/streaming-markdown.mjs +20 -0
  82. package/src/ui/statusline.mjs +5 -5
  83. package/src/vendor/statusline/src/gateway/session-routes.mjs +22 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.47",
3
+ "version": "0.9.49",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -34,7 +34,7 @@
34
34
  ],
35
35
  "scripts": {
36
36
  "prepack": "npm run build:tui",
37
- "prepublishOnly": "node -e \"if(!process.env.CI){console.error('local npm publish is disabled — run the Release workflow: gh workflow run release -f bump=patch');process.exit(1)}\"",
37
+ "prepublishOnly": "node -e \"if(!process.env.CI){console.error('local npm publish is disabled — run npm run release:patch');process.exit(1)}\"",
38
38
  "start": "node src/cli.mjs",
39
39
  "smoke": "node scripts/smoke.mjs && node --test scripts/ship-mode-test.mjs && npm run test:memory-routing && node --test scripts/explore-prompt-policy-test.mjs && node --test scripts/routing-corpus-test.mjs",
40
40
  "smoke:all": "npm run smoke && npm run smoke:boot && npm run smoke:tools && npm run smoke:patch && npm run smoke:output && npm run smoke:tui && npm run smoke:freevars && npm run smoke:logguard && npm run smoke:live-worker && npm run test:session",
@@ -46,22 +46,32 @@
46
46
  "smoke:tools": "node scripts/tool-smoke.mjs",
47
47
  "smoke:patch": "node scripts/apply-patch-edit-smoke.mjs",
48
48
  "smoke:output": "node scripts/output-style-smoke.mjs",
49
- "smoke:tui": "node scripts/tui-render-smoke.mjs && npm run test:tui-queue",
49
+ "smoke:tui": "node scripts/tui-render-smoke.mjs && npm run test:tui-input-render && npm run test:tui-streaming-window && npm run test:tui-queue",
50
50
  "smoke:freevars": "node scripts/freevar-smoke.mjs",
51
51
  "smoke:logguard": "node scripts/log-writer-guard-smoke.mjs",
52
52
  "smoke:live-worker": "node scripts/live-worker-smoke.mjs",
53
53
  "smoke:agent-tag-reuse": "node scripts/agent-tag-reuse-smoke.mjs",
54
54
  "test:agent-terminal-reap": "node scripts/agent-terminal-reap-test.mjs",
55
+ "test:agent-fanout": "node scripts/agent-parallel-smoke.mjs && node --test scripts/agent-route-batch-test.mjs scripts/execution-completion-dedup-test.mjs",
55
56
  "test:toolcall": "node --test scripts/toolcall-args-test.mjs",
56
57
  "test:shipmode": "node --test scripts/ship-mode-test.mjs",
57
- "test:shellhardening": "node --test scripts/shell-hardening-test.mjs",
58
+ "test:shellhardening": "node --test scripts/shell-hardening-test.mjs scripts/shell-failure-diagnostics-test.mjs",
58
59
  "test:placeholder": "node --test scripts/compacted-placeholder-scrub-test.mjs",
59
60
  "test:providers": "node --test scripts/provider-toolcall-test.mjs",
61
+ "test:deferred-tools": "node --test scripts/deferred-tool-loading-test.mjs",
60
62
  "test:anthropic-oauth-race": "node --test scripts/anthropic-oauth-refresh-race-test.mjs",
61
63
  "test:grok-oauth-race": "node --test scripts/grok-oauth-refresh-race-test.mjs",
62
64
  "test:atomiclock": "node --test scripts/atomic-lock-tryonce-test.mjs",
63
65
  "test:memory-routing": "node --test scripts/memory-cycle-routing-test.mjs scripts/memory-rule-contract-test.mjs scripts/maintenance-default-routes-test.mjs",
66
+ "test:code-graph-dispatch": "node --test scripts/code-graph-dispatch-test.mjs",
67
+ "test:code-graph-clean-cache": "node --test scripts/code-graph-dispatch-test.mjs",
64
68
  "test:tui-queue": "node --test scripts/submit-commandbusy-race-test.mjs scripts/steering-drain-buckets-test.mjs scripts/abort-recovery-test.mjs scripts/execution-pending-resume-kick-test.mjs scripts/execution-resume-esc-integration-test.mjs",
69
+ "test:tui-input-render": "node --test scripts/prompt-immediate-render-test.mjs",
70
+ "test:tui-streaming-window": "node --test scripts/streaming-tail-window-test.mjs",
71
+ "test:release-assets": "node --check scripts/verify-release-assets.mjs && node --check scripts/verify-release-assets-test.mjs && node --test scripts/verify-release-assets-test.mjs",
72
+ "test:release-focused": "npm run test:release-assets && npm run test:patch-binary-cache && npm run test:providers && npm run test:deferred-tools && npm run smoke:compact && npm run smoke:tools && node --test scripts/code-graph-aggregate-cwd-test.mjs && npm run test:code-graph-dispatch && node --test scripts/code-graph-disk-hit-test.mjs && npm run test:shellhardening && node --test scripts/windows-hide-spawn-options-test.mjs && node --test scripts/tui-transcript-perf-test.mjs",
73
+ "test:native-edit-wire": "node --test scripts/native-edit-wire-test.mjs",
74
+ "test:patch-binary-cache": "node --test scripts/patch-binary-cache-test.mjs",
65
75
  "test:session": "node --test scripts/session-orphan-sweep-test.mjs",
66
76
  "test:rebindtail": "node --test scripts/forwarder-rebind-tail-test.mjs",
67
77
  "failures": "node scripts/tool-failures.mjs",
@@ -142,7 +142,10 @@ async function main() {
142
142
  await realInitProviders({ 'openai-oauth': { enabled: true } });
143
143
 
144
144
  // --- 1+2: parallel spawns return task_ids fast and non-blocking ---
145
- const batchSize = 6;
145
+ const requestedBatchSize = Number.parseInt(process.env.MIXDOG_AGENT_PARALLEL_SMOKE_BATCH || '6', 10);
146
+ const batchSize = Number.isFinite(requestedBatchSize) && requestedBatchSize > 0
147
+ ? requestedBatchSize
148
+ : 6;
146
149
  const t0 = Date.now();
147
150
  const outs = await Promise.all(
148
151
  Array.from({ length: batchSize }, (_, i) => agent.execute({
@@ -0,0 +1,40 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ test('parallel agent routes flush in one atomic batch', async () => {
8
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-agent-route-batch-'));
9
+ const previousDataDir = process.env.MIXDOG_DATA_DIR;
10
+ process.env.MIXDOG_DATA_DIR = root;
11
+ try {
12
+ const {
13
+ flushAgentStatuslineRoutes,
14
+ writeAgentStatuslineRoute,
15
+ } = await import(`../src/standalone/agent-tool/helpers.mjs?batch=${Date.now()}`);
16
+ const {
17
+ readGatewaySessionRoute,
18
+ } = await import('../src/vendor/statusline/src/gateway/session-routes.mjs');
19
+
20
+ const count = 48;
21
+ for (let i = 0; i < count; i += 1) {
22
+ assert.equal(writeAgentStatuslineRoute(`sess_route_batch_${i}`, {
23
+ id: 'worker',
24
+ name: 'Worker',
25
+ provider: 'openai-oauth',
26
+ model: 'gpt-5.5',
27
+ }), true);
28
+ }
29
+ assert.equal(flushAgentStatuslineRoutes(), true);
30
+
31
+ const routes = Array.from({ length: count }, (_, i) =>
32
+ readGatewaySessionRoute(`sess_route_batch_${i}`));
33
+ assert.equal(routes.every((route) => route?.defaultProvider === 'openai-oauth'), true);
34
+ assert.equal(new Set(routes.map((route) => route?.updatedAt)).size, 1);
35
+ } finally {
36
+ if (previousDataDir == null) delete process.env.MIXDOG_DATA_DIR;
37
+ else process.env.MIXDOG_DATA_DIR = previousDataDir;
38
+ rmSync(root, { recursive: true, force: true });
39
+ }
40
+ });
@@ -0,0 +1,154 @@
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, relative } 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 previousDataDir = process.env.MIXDOG_DATA_DIR;
9
+ const previousGraphBin = process.env.MIXDOG_GRAPH_BIN;
10
+ const ambientDataDir = previousDataDir || join(process.env.MIXDOG_HOME || join(homedir(), '.mixdog'), 'data');
11
+ const ambientGraphBin = findCachedGraphBinary(ambientDataDir);
12
+ const isolatedDataDir = await mkdtemp(join(tmpdir(), 'mixdog-code-graph-test-data-'));
13
+ process.env.MIXDOG_DATA_DIR = isolatedDataDir;
14
+ if (!previousGraphBin && ambientGraphBin) process.env.MIXDOG_GRAPH_BIN = ambientGraphBin;
15
+ const { executeCodeGraphTool } = await import('../src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs');
16
+ const { drainCodeGraphCache } = await import('../src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs');
17
+ let testTail = Promise.resolve();
18
+
19
+ function serialTest(name, fn) {
20
+ test(name, async (t) => {
21
+ const previous = testTail;
22
+ let release;
23
+ testTail = new Promise((resolve) => { release = resolve; });
24
+ await previous;
25
+ try {
26
+ await fn(t);
27
+ } finally {
28
+ release();
29
+ }
30
+ });
31
+ }
32
+
33
+ async function makeProject() {
34
+ const root = await mkdtemp(join(tmpdir(), 'mixdog-code-graph-project-'));
35
+ await mkdir(join(root, 'src'));
36
+ await writeFile(join(root, 'package.json'), '{}');
37
+ await writeFile(join(root, 'src', 'one.mjs'), 'export const one = 1;\n');
38
+ await writeFile(join(root, 'src', 'two.mjs'), 'export const two = 2;\n');
39
+ return root;
40
+ }
41
+
42
+ const primaryProject = await makeProject();
43
+
44
+ test.after(async () => {
45
+ drainCodeGraphCache();
46
+ await Promise.all([
47
+ rm(primaryProject, { recursive: true, force: true }),
48
+ rm(isolatedDataDir, { recursive: true, force: true }),
49
+ ]);
50
+ if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
51
+ else process.env.MIXDOG_DATA_DIR = previousDataDir;
52
+ if (previousGraphBin === undefined) delete process.env.MIXDOG_GRAPH_BIN;
53
+ else process.env.MIXDOG_GRAPH_BIN = previousGraphBin;
54
+ });
55
+
56
+ serialTest('aggregate file anchors recover an invalid cwd only for one project root', async () => {
57
+ const project = primaryProject;
58
+ const invalidCwd = parse(project).root;
59
+ const files = [join(project, 'src', 'one.mjs'), join(project, 'src', 'two.mjs')];
60
+ const relativeFiles = files.map((file) => relative(invalidCwd, file));
61
+ const result = await executeCodeGraphTool('code_graph', { mode: 'overview', files }, invalidCwd);
62
+ assert.match(result, new RegExp(`# overview ${files[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
63
+ assert.match(result, new RegExp(`# overview ${files[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
64
+
65
+ 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, '\\$&')}`));
68
+ drainCodeGraphCache();
69
+ });
70
+
71
+ serialTest('singleton JSON files retain aggregate cwd provenance', async () => {
72
+ const project = primaryProject;
73
+ const invalidCwd = parse(project).root;
74
+ const file = join(project, 'src', 'one.mjs');
75
+ const rejected = /aggregate file anchors do not all exist under exactly one detectable project root/;
76
+ const result = await executeCodeGraphTool(
77
+ 'code_graph',
78
+ { mode: 'overview', files: JSON.stringify([file]), cwd: invalidCwd },
79
+ project,
80
+ );
81
+ 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,
89
+ );
90
+ const scalarResult = await executeCodeGraphTool(
91
+ 'code_graph',
92
+ { mode: 'overview', file: join(project, 'missing.mjs'), cwd: invalidCwd },
93
+ project,
94
+ );
95
+ assert.match(scalarResult, /^Error: code_graph: file not found:/);
96
+ });
97
+
98
+ serialTest('aggregate cwd recovery rejects missing and multi-root anchors', async () => {
99
+ const first = await makeProject();
100
+ const second = await makeProject();
101
+ const invalidCwd = parse(first).root;
102
+ const rejected = /aggregate file anchors do not all exist under exactly one detectable project root/;
103
+ const relativeFirst = relative(invalidCwd, join(first, 'src', 'one.mjs'));
104
+ const relativeSecond = relative(invalidCwd, join(second, 'src', 'one.mjs'));
105
+ 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
+ );
122
+ } finally {
123
+ await Promise.all([rm(first, { recursive: true, force: true }), rm(second, { recursive: true, force: true })]);
124
+ }
125
+ });
126
+
127
+ serialTest('aggregate cwd recovery rejects capped, comma-delimited, and wildcard anchors', async () => {
128
+ const project = await makeProject();
129
+ const invalidCwd = parse(project).root;
130
+ const rejected = /aggregate file anchors do not all exist under exactly one detectable project root/;
131
+ const cappedFiles = await Promise.all(Array.from({ length: 21 }, async (_, index) => {
132
+ const file = join(project, 'src', `anchor-${index}.mjs`);
133
+ await writeFile(file, `export const anchor${index} = ${index};\n`);
134
+ return file;
135
+ }));
136
+ const literalWildcard = join(project, 'src', 'literal[glob].mjs');
137
+ await writeFile(literalWildcard, 'export const wildcard = true;\n');
138
+ try {
139
+ await assert.rejects(
140
+ executeCodeGraphTool('code_graph', { mode: 'overview', files: cappedFiles }, invalidCwd),
141
+ rejected,
142
+ );
143
+ await assert.rejects(
144
+ executeCodeGraphTool('code_graph', { mode: 'overview', files: `${join(project, 'src', 'one.mjs')},${join(project, 'src', 'two.mjs')}` }, invalidCwd),
145
+ rejected,
146
+ );
147
+ await assert.rejects(
148
+ executeCodeGraphTool('code_graph', { mode: 'overview', files: [literalWildcard] }, invalidCwd),
149
+ rejected,
150
+ );
151
+ } finally {
152
+ await rm(project, { recursive: true, force: true });
153
+ }
154
+ });
@@ -0,0 +1,185 @@
1
+ const FILE_MODES = ['overview', 'imports', 'dependents', 'related', 'impact'];
2
+ const SYMBOL_MODES = ['find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'];
3
+ const EXACT_MODES = ['find_symbol', 'references', 'callers', 'callees'];
4
+ const KEYWORD_MODES = ['symbol_search', 'search'];
5
+ const NEGATION = /\b(?:no|not|never|without|cannot|won['’]t|ain['’]t|(?:is|are|was|were|do|does|did|has|have|had|can|could|would|should|must)(?:\s+not|n['’]t)|will\s+not|fail(?:s|ed|ing)?\s+to)\b/i;
6
+
7
+ const clauses = (text) => String(text || '').split(/[.;]\s*/).map((part) => part.trim()).filter(Boolean);
8
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9
+ const includesTerm = (text, term) => {
10
+ if (/^[a-z_][a-z0-9_]*$/i.test(term)) {
11
+ return new RegExp(`(?<![a-z0-9_])${escapeRegExp(term)}(?![a-z0-9_])`, 'i').test(text);
12
+ }
13
+ return text.toLowerCase().includes(term.toLowerCase());
14
+ };
15
+ const includesAll = (text, terms) => terms.every((term) => includesTerm(text, term));
16
+ const hasPositiveClause = (text, terms) => clauses(text).some((clause) => !NEGATION.test(clause) && includesAll(clause, terms));
17
+ const hasModeClause = (text, label, modes, target = null) => (
18
+ hasPositiveClause(text, [label, ...modes, ...(target ? [target] : [])])
19
+ );
20
+ const hasContradictoryTargetAssignment = (text) => (
21
+ hasPositiveClause(text, [...FILE_MODES, 'symbols[]'])
22
+ || hasPositiveClause(text, [...SYMBOL_MODES, 'files[]'])
23
+ );
24
+ const removeIdentifier = (text, identifier) => (
25
+ text.replace(new RegExp(`(?<![a-z0-9_])${escapeRegExp(identifier)}(?![a-z0-9_])`, 'gi'), '')
26
+ );
27
+
28
+ export const CODE_GRAPH_EQUIVALENT_DESCRIPTION_PROBES = [
29
+ {
30
+ description: [
31
+ 'Keywords use search/symbol_search while callers/callees/references/find_symbol use exact identifiers.',
32
+ 'symbols[] targets callees, search, references, symbol_search, callers, and find_symbol symbol modes.',
33
+ 'files[] targets impact, related, dependents, imports, and overview file modes.',
34
+ ].join(' '),
35
+ modeDescription: [
36
+ 'A file outline uses files[] for symbols with files.',
37
+ 'Impact, related, dependents, imports, and overview are file modes.',
38
+ 'Callees, callers, references, search, symbol_search, and find_symbol are symbol modes.',
39
+ 'Keywords from fileless symbols route to symbol_search.',
40
+ ].join(' '),
41
+ symbolsDescription: [
42
+ 'Keywords select search/symbol_search while callers/callees/references/find_symbol select exact identifiers.',
43
+ 'Pass multiple exact symbols in one symbols[] call.',
44
+ ].join(' '),
45
+ },
46
+ {
47
+ description: [
48
+ 'Exact identifiers route through callees, references, callers, or find_symbol.',
49
+ 'Keywords route through search or symbol_search.',
50
+ 'The overview, impact, imports, related, and dependents file modes accept files[].',
51
+ 'The search, callees, find_symbol, references, symbol_search, and callers symbol modes accept symbols[].',
52
+ ].join(' '),
53
+ modeDescription: [
54
+ 'Related, overview, impact, dependents, and imports are file modes.',
55
+ 'Symbols with files produce a file outline from files[].',
56
+ 'References, callers, callees, find_symbol, search, and symbol_search are symbol modes.',
57
+ 'Fileless symbols treat keywords as symbol_search input.',
58
+ ].join(' '),
59
+ symbolsDescription: [
60
+ 'Exact identifiers select references, callees, callers, or find_symbol.',
61
+ 'Keywords select symbol_search or search.',
62
+ 'Multiple exact symbols belong in one symbols[] call.',
63
+ ].join(' '),
64
+ },
65
+ ];
66
+
67
+ export const CODE_GRAPH_DESCRIPTION_MUTATION_CORPUS = [
68
+ {
69
+ name: 'contracted negated file assignment',
70
+ mutate: (parts) => ({
71
+ ...parts,
72
+ description: parts.description.replace(/file modes take files\[\]/i, "file modes aren't assigned files[]"),
73
+ }),
74
+ },
75
+ {
76
+ name: 'contracted outline polarity inversion',
77
+ allPositiveProbes: true,
78
+ mutate: (parts) => ({
79
+ ...parts,
80
+ modeDescription: parts.modeDescription.replace(/file outline/i, "isn't a file outline"),
81
+ }),
82
+ },
83
+ {
84
+ name: 'keyword routing polarity inversion',
85
+ allPositiveProbes: true,
86
+ mutate: (parts) => ({
87
+ ...parts,
88
+ description: parts.description.replace(/keywords\s+(?:use|route through|select)/i, "keywords won't use"),
89
+ }),
90
+ },
91
+ {
92
+ name: 'symbols-with-files outline inversion',
93
+ allPositiveProbes: true,
94
+ mutate: (parts) => ({
95
+ ...parts,
96
+ modeDescription: parts.modeDescription.replace(/symbols with files/i, 'symbols without files'),
97
+ }),
98
+ },
99
+ {
100
+ name: 'exact-symbol one-call inversion',
101
+ allPositiveProbes: true,
102
+ mutate: (parts) => ({
103
+ ...parts,
104
+ symbolsDescription: parts.symbolsDescription.replace(/one symbols\[\] call/i, 'one files[] call'),
105
+ }),
106
+ },
107
+ {
108
+ name: 'contradictory file-mode assignment retained beside correct clause',
109
+ allPositiveProbes: true,
110
+ mutate: (parts) => ({
111
+ ...parts,
112
+ description: `${parts.description} Overview, imports, dependents, related, and impact take symbols[].`,
113
+ }),
114
+ },
115
+ {
116
+ name: 'contradictory symbol-mode assignment retained beside correct clause',
117
+ allPositiveProbes: true,
118
+ mutate: (parts) => ({
119
+ ...parts,
120
+ description: `${parts.description} Find_symbol, symbol_search, search, references, callers, and callees take files[].`,
121
+ }),
122
+ },
123
+ {
124
+ name: 'swapped file-mode/symbol-mode array assignments',
125
+ allPositiveProbes: true,
126
+ mutate: (parts) => ({
127
+ ...parts,
128
+ description: `${parts.description} Overview, imports, dependents, related, and impact take symbols[]. Find_symbol, symbol_search, search, references, callers, and callees take files[].`,
129
+ }),
130
+ },
131
+ {
132
+ name: 'standalone search removed from symbol modes and keyword routing',
133
+ allPositiveProbes: true,
134
+ mutate: (parts) => ({
135
+ ...parts,
136
+ description: removeIdentifier(parts.description, 'search'),
137
+ modeDescription: removeIdentifier(parts.modeDescription, 'search'),
138
+ symbolsDescription: removeIdentifier(parts.symbolsDescription, 'search'),
139
+ }),
140
+ },
141
+ ];
142
+
143
+ export function hasCodeGraphDescriptionContract({ description, modeDescription, symbolsDescription }) {
144
+ return (
145
+ hasPositiveClause(description, ['file modes', 'files[]'])
146
+ && hasModeClause(description, 'symbol modes', SYMBOL_MODES, 'symbols[]')
147
+ && hasPositiveClause(description, ['exact identifiers', ...EXACT_MODES])
148
+ && hasPositiveClause(description, ['keywords', ...KEYWORD_MODES])
149
+ && !hasContradictoryTargetAssignment(description)
150
+ && hasModeClause(modeDescription, 'file modes', FILE_MODES)
151
+ && hasPositiveClause(modeDescription, ['symbols with files', 'files[]', 'file outline'])
152
+ && hasModeClause(modeDescription, 'symbol modes', SYMBOL_MODES)
153
+ && hasPositiveClause(modeDescription, ['fileless symbols', 'symbol_search', 'keywords'])
154
+ && !hasContradictoryTargetAssignment(modeDescription)
155
+ && hasPositiveClause(symbolsDescription, ['exact identifiers', ...EXACT_MODES])
156
+ && hasPositiveClause(symbolsDescription, ['keywords', ...KEYWORD_MODES])
157
+ && hasPositiveClause(symbolsDescription, ['multiple exact symbols', 'one symbols[] call'])
158
+ && !hasContradictoryTargetAssignment(symbolsDescription)
159
+ );
160
+ }
161
+
162
+ export function assertCodeGraphDescriptionContract(parts) {
163
+ if (!hasCodeGraphDescriptionContract(parts)) {
164
+ throw new Error('code_graph descriptions must preserve per-mode files[]/symbols[] batching and exact-vs-keyword routing');
165
+ }
166
+ for (const probe of CODE_GRAPH_EQUIVALENT_DESCRIPTION_PROBES) {
167
+ if (!hasCodeGraphDescriptionContract(probe)) {
168
+ throw new Error('code_graph description contract must accept equivalent reordered compact prose');
169
+ }
170
+ for (const mutation of CODE_GRAPH_DESCRIPTION_MUTATION_CORPUS.filter((entry) => entry.allPositiveProbes)) {
171
+ const mutated = mutation.mutate(probe);
172
+ if (JSON.stringify(mutated) === JSON.stringify(probe)) {
173
+ throw new Error(`code_graph description mutation did not apply to positive probe: ${mutation.name}`);
174
+ }
175
+ if (hasCodeGraphDescriptionContract(mutated)) {
176
+ throw new Error(`code_graph description contract accepted positive-probe mutation: ${mutation.name}`);
177
+ }
178
+ }
179
+ }
180
+ for (const mutation of CODE_GRAPH_DESCRIPTION_MUTATION_CORPUS) {
181
+ if (hasCodeGraphDescriptionContract(mutation.mutate(parts))) {
182
+ throw new Error(`code_graph description contract accepted mutation: ${mutation.name}`);
183
+ }
184
+ }
185
+ }
@@ -1,16 +1,71 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import {
4
+ _codeGraphWorkerFailure,
4
5
  _isCompatibleDiskCodeGraphEntry,
5
6
  _postCodeGraphWorkerSuccess,
7
+ _prewarmCodeGraph,
6
8
  _prepareDiskCodeGraphFastPath,
7
9
  _runDiskCodeGraphFastPath,
10
+ _spawnCodeGraphWorker,
8
11
  _validateDiskCodeGraphHit,
9
12
  } from '../src/runtime/agent/orchestrator/tools/code-graph/build.mjs';
10
13
  import { CODE_GRAPH_MAX_FILES } from '../src/runtime/agent/orchestrator/tools/code-graph/constants.mjs';
11
14
  import { _serializeGraph } from '../src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs';
12
15
  import { _persistDiskCodeGraphCacheNow } from '../src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs';
13
16
 
17
+ function workerMessage(message, hooks = {}) {
18
+ return _spawnCodeGraphWorker('/worker', '/worker', 0, null, () => {}, null, null, {
19
+ createWorker: () => ({
20
+ once(event, listener) {
21
+ if (event === 'message') queueMicrotask(() => listener(message));
22
+ },
23
+ terminate() {},
24
+ }),
25
+ getGeneration: () => 0,
26
+ setMemoryCache: () => {},
27
+ setDiskCache: () => {},
28
+ ...hooks,
29
+ });
30
+ }
31
+
32
+ test('Worker message path preserves success and validates failure text', async () => {
33
+ const graph = { signature: 'success', nodes: new Map() };
34
+ const memory = [];
35
+ const disk = [];
36
+ assert.equal(await workerMessage({ ok: true, signature: 'success', graph }, {
37
+ setMemoryCache: (cwd, entry) => memory.push({ cwd, entry }),
38
+ setDiskCache: (cwd, entry) => disk.push({ cwd, entry }),
39
+ }), graph);
40
+ assert.deepEqual(memory, [{ cwd: '/worker', entry: { ts: memory[0].entry.ts, signature: 'success', graph } }]);
41
+ assert.deepEqual(disk, [{ cwd: '/worker', entry: graph }]);
42
+ await assert.rejects(workerMessage({ ok: false, error: ' worker build failed ' }), /worker build failed/);
43
+ for (const message of [{ ok: false }, { ok: false, error: ' \t ' }, null]) {
44
+ await assert.rejects(workerMessage(message), /code-graph prewarm worker failed/);
45
+ }
46
+ });
47
+
48
+ test('worker failure protocol trims its error text', () => {
49
+ assert.equal(
50
+ _codeGraphWorkerFailure({ ok: false, error: ' disk cache flush failed ' }).message,
51
+ 'disk cache flush failed',
52
+ );
53
+ for (const message of [{ ok: false }, { ok: false, error: '' }, { ok: false, error: ' \t ' }]) {
54
+ assert.equal(_codeGraphWorkerFailure(message).message, 'code-graph prewarm worker failed');
55
+ }
56
+ });
57
+
58
+ test('best-effort prewarm swallows build failures', () => {
59
+ let swallowed = false;
60
+ _prewarmCodeGraph('/worker', () => ({
61
+ catch(handler) {
62
+ swallowed = true;
63
+ handler(new Error('worker build failed'));
64
+ },
65
+ }));
66
+ assert.equal(swallowed, true);
67
+ });
68
+
14
69
  test('signature-validated disk hit restores memory without a Worker build', async () => {
15
70
  const graph = { nodes: new Map() };
16
71
  let restored = null;
@@ -0,0 +1,96 @@
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 } 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 previousDataDir = process.env.MIXDOG_DATA_DIR;
9
+ const previousGraphBin = process.env.MIXDOG_GRAPH_BIN;
10
+ const ambientDataDir = previousDataDir || join(process.env.MIXDOG_HOME || join(homedir(), '.mixdog'), 'data');
11
+ const ambientGraphBin = findCachedGraphBinary(ambientDataDir);
12
+ const isolatedDataDir = await mkdtemp(join(tmpdir(), 'mixdog-code-graph-dispatch-data-'));
13
+ process.env.MIXDOG_DATA_DIR = isolatedDataDir;
14
+ if (!previousGraphBin && ambientGraphBin) process.env.MIXDOG_GRAPH_BIN = ambientGraphBin;
15
+
16
+ const { executeCodeGraphTool } = await import('../src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs');
17
+ const { drainCodeGraphCache } = await import('../src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs');
18
+ const project = await mkdtemp(join(tmpdir(), 'mixdog-code-graph-dispatch-project-'));
19
+ const sourceDir = join(project, 'src');
20
+ await mkdir(sourceDir);
21
+ await writeFile(join(project, 'package.json'), '{}');
22
+ await writeFile(join(sourceDir, 'one.mjs'), [
23
+ 'export function alpha() { return beta(); }',
24
+ 'export function beta() { return 2; }',
25
+ '',
26
+ ].join('\n'));
27
+ await writeFile(join(sourceDir, 'two.mjs'), [
28
+ "import { alpha } from './one.mjs';",
29
+ 'export function gamma() { return alpha(); }',
30
+ '',
31
+ ].join('\n'));
32
+
33
+ test.after(async () => {
34
+ drainCodeGraphCache();
35
+ await Promise.all([
36
+ rm(project, { recursive: true, force: true }),
37
+ rm(isolatedDataDir, { recursive: true, force: true }),
38
+ ]);
39
+ if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
40
+ else process.env.MIXDOG_DATA_DIR = previousDataDir;
41
+ if (previousGraphBin === undefined) delete process.env.MIXDOG_GRAPH_BIN;
42
+ else process.env.MIXDOG_GRAPH_BIN = previousGraphBin;
43
+ });
44
+
45
+ test('dispatch partitions every file and symbol mode by its supported target array', async () => {
46
+ const files = ['src/one.mjs', 'src/two.mjs'];
47
+ const assertNoErrorBody = (mode, result) => {
48
+ assert.doesNotMatch(result, /(?:^|\n)Error:/i, `${mode} returned a caught/error body`);
49
+ };
50
+ const fileModeEvidence = {
51
+ overview: [/file: src\/one\.mjs/, /file: src\/two\.mjs/, /symbols:.*alpha/, /symbols:.*gamma/],
52
+ imports: [/# imports src\/two\.mjs[\s\S]*src\/one\.mjs/],
53
+ dependents: [/# dependents src\/one\.mjs[\s\S]*src\/two\.mjs/],
54
+ related: [/file\tsrc\/one\.mjs/, /file\tsrc\/two\.mjs/, /# related/],
55
+ impact: [/file\tsrc\/one\.mjs/, /file\tsrc\/two\.mjs/, /external_callers\t\d+/],
56
+ symbols: [/function alpha \(L1\)/, /function beta \(L2\)/, /function gamma \(L2\)/],
57
+ };
58
+ for (const mode of ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols']) {
59
+ const result = await executeCodeGraphTool('code_graph', { mode, files }, project);
60
+ assertNoErrorBody(mode, result);
61
+ for (const file of files) assert.match(result, new RegExp(`# ${mode} ${file.replace('.', '\\.')}`));
62
+ for (const evidence of fileModeEvidence[mode]) assert.match(result, evidence);
63
+ }
64
+
65
+ const exactModeEvidence = {
66
+ find_symbol: [/src\/one\.mjs:1/, /src\/one\.mjs:2/, /# candidates/],
67
+ references: [/src\/two\.mjs:2:\d+\s+export function gamma\(\) \{ return alpha\(\); \}/, /src\/one\.mjs:1:\d+\s+export function alpha\(\) \{ return beta\(\); \}/],
68
+ callers: [/caller=gamma/, /caller=alpha/],
69
+ callees: [/# callees/, /\bbeta\b/, /\(no callees\)/],
70
+ };
71
+ for (const mode of ['find_symbol', 'references', 'callers', 'callees']) {
72
+ const result = await executeCodeGraphTool('code_graph', {
73
+ mode,
74
+ symbols: ['alpha', 'beta'],
75
+ body: false,
76
+ }, project);
77
+ assertNoErrorBody(mode, result);
78
+ assert.match(result, new RegExp(`# ${mode} alpha\\b`));
79
+ assert.match(result, new RegExp(`# ${mode} beta\\b`));
80
+ for (const evidence of exactModeEvidence[mode]) assert.match(result, evidence);
81
+ }
82
+
83
+ for (const mode of ['symbol_search', 'search']) {
84
+ const result = await executeCodeGraphTool('code_graph', {
85
+ mode,
86
+ symbols: ['alp', 'bet'],
87
+ body: false,
88
+ }, project);
89
+ const routedMode = mode === 'search' ? 'symbol_search' : mode;
90
+ assertNoErrorBody(mode, result);
91
+ assert.match(result, new RegExp(`# ${routedMode} alp\\b`));
92
+ assert.match(result, new RegExp(`# ${routedMode} bet\\b`));
93
+ assert.match(result, /# search keyword=alp matches=\d+ shown=\d+[\s\S]*\balpha\b/);
94
+ assert.match(result, /# search keyword=bet matches=\d+ shown=\d+[\s\S]*\bbeta\b/);
95
+ }
96
+ });
@@ -396,6 +396,46 @@ test('provider baseline pressure includes only the configured extra reserve', ()
396
396
  assert.equal(resolveCompactionPressureTokens(1, policy, { messages, sessionRef: session }), 87_000);
397
397
  });
398
398
 
399
+ test('context status uses the automatic compaction pressure numerator and trigger', () => {
400
+ const session = {
401
+ id: 'status-pressure',
402
+ provider: 'openai',
403
+ model: 'm',
404
+ tools: [],
405
+ contextWindow: 100_000,
406
+ messages: [{ role: 'user', content: 'small local estimate' }],
407
+ compaction: {},
408
+ };
409
+ recordProviderContextBaseline(session, session.messages, {
410
+ inputTokens: 80_000,
411
+ outputTokens: 2_000,
412
+ });
413
+ const policy = policyFor(session);
414
+ const expectedPressure = resolveCompactionPressureTokens(
415
+ estimateMessagesTokens(session.messages),
416
+ policy,
417
+ { messages: session.messages, sessionRef: session },
418
+ );
419
+ const { contextStatus } = createContextStatus({
420
+ getSession: () => session,
421
+ getRoute: () => ({ provider: 'openai', model: 'm' }),
422
+ getCurrentCwd: () => '',
423
+ getMode: () => 'default',
424
+ });
425
+
426
+ const status = contextStatus();
427
+ assert.equal(status.usedTokens, expectedPressure);
428
+ assert.equal(status.currentEstimatedTokens, expectedPressure);
429
+ assert.equal(status.compaction.triggerTokens, policy.triggerTokens);
430
+ assert.equal(
431
+ shouldCompactForSession(estimateMessagesTokens(session.messages), policy, {
432
+ messages: session.messages,
433
+ sessionRef: session,
434
+ }),
435
+ status.usedTokens >= status.compaction.triggerTokens,
436
+ );
437
+ });
438
+
399
439
  test('provider baselines fingerprint actual sendTools and reject provider, model, tool-schema, and prefix changes', () => {
400
440
  const make = () => ({
401
441
  provider: 'openai',