mixdog 0.9.99 → 0.9.101

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 +1 -1
  2. package/scripts/agent-shard-spread-perf.mjs +245 -0
  3. package/scripts/agent-turn-trace-probe.mjs +128 -0
  4. package/scripts/dependency-lock-cache-key.mjs +30 -0
  5. package/scripts/fixtures/session-shard-fixture-worker.mjs +13 -0
  6. package/scripts/fixtures/spawn-lease-child-harness.mjs +8 -0
  7. package/scripts/fixtures/spawn-lease-fixture-worker.mjs +33 -0
  8. package/scripts/prune-embedding-runtime.mjs +5 -1
  9. package/src/app.mjs +3 -0
  10. package/src/headless-command.mjs +4 -0
  11. package/src/headless-role.mjs +13 -0
  12. package/src/rules/agent/00-core.md +5 -8
  13. package/src/rules/agent/30-explorer.md +55 -70
  14. package/src/rules/shared/01-tool.md +27 -15
  15. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +39 -0
  16. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +22 -8
  17. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +13 -2
  19. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +13 -2
  20. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +21 -7
  21. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +18 -6
  22. package/src/runtime/agent/orchestrator/session/context-utils.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +30 -58
  24. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -0
  25. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +16 -35
  26. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +6 -5
  27. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -0
  28. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +6 -2
  29. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +6 -6
  30. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +5 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +136 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +17 -0
  33. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -0
  34. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +76 -49
  35. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  36. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +104 -5
  37. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +51 -68
  38. package/src/runtime/memory/lib/http-router.mjs +4 -2
  39. package/src/runtime/memory/lib/http-wire.mjs +9 -0
  40. package/src/runtime/memory/lib/memory-cycle3.mjs +6 -4
  41. package/src/runtime/memory/lib/pg/process.mjs +111 -0
  42. package/src/runtime/memory/lib/pg/supervisor.mjs +17 -0
  43. package/src/runtime/memory/lib/query-handlers.mjs +3 -4
  44. package/src/runtime/memory/lib/session-ingest.mjs +32 -0
  45. package/src/runtime/shared/child-spawn-gate.mjs +27 -2
  46. package/src/runtime/shared/child-spawn-remote.mjs +139 -0
  47. package/src/runtime/shared/tool-surface.mjs +1 -0
  48. package/src/runtime/shared/transcript-writer.mjs +56 -0
  49. package/src/runtime/shared/turn-snapshot.mjs +127 -3
  50. package/src/session-runtime/config-helpers.mjs +25 -0
  51. package/src/session-runtime/cwd-plugins.mjs +8 -1
  52. package/src/session-runtime/remote-transcript.mjs +27 -15
  53. package/src/session-runtime/runtime-core.mjs +61 -12
  54. package/src/session-runtime/session-lifecycle.mjs +33 -1
  55. package/src/session-runtime/session-turn-api.mjs +14 -14
  56. package/src/session-runtime/settings-api.mjs +31 -0
  57. package/src/session-runtime/tool-surface.mjs +19 -7
  58. package/src/session-runtime/workflow-agents-api.mjs +11 -29
  59. package/src/session-runtime/workflow.mjs +35 -22
  60. package/src/standalone/agent-host-runtime.mjs +315 -0
  61. package/src/standalone/agent-tool/helpers.mjs +17 -0
  62. package/src/standalone/agent-tool/shard-spread.mjs +673 -0
  63. package/src/standalone/agent-tool/spawn-flow.mjs +84 -25
  64. package/src/standalone/agent-tool.mjs +8 -1
  65. package/src/standalone/daemon.mjs +24 -1
  66. package/src/standalone/explore-tool.mjs +18 -26
  67. package/src/standalone/memory-runtime-proxy.mjs +15 -1
  68. package/src/standalone/session-protocol.mjs +5 -0
  69. package/src/standalone/session-runtime-pool.mjs +305 -10
  70. package/src/standalone/session-runtime-worker.mjs +26 -24
  71. package/src/standalone/session-service.mjs +159 -28
  72. package/src/standalone/session-state-patch.mjs +46 -0
  73. package/src/standalone/session-wire.mjs +1 -1
  74. package/src/tui/app/core-memory-picker.mjs +2 -34
  75. package/src/tui/app/settings-picker.mjs +70 -5
  76. package/src/tui/components/prompt-input/escape-policy.mjs +6 -4
  77. package/src/tui/dist/index.mjs +65 -30
  78. package/src/tui/session/session-api-ext.mjs +17 -0
  79. package/src/tui/session/session-api.mjs +35 -0
  80. package/src/tui/session-local.mjs +62 -0
  81. package/src/workflows/default/WORKFLOW.md +1 -2
  82. package/src/workflows/solo/WORKFLOW.md +1 -1
  83. package/src/workflows/solo-bench/WORKFLOW.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.99",
3
+ "version": "0.9.101",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ // Agent shard spread PERF harness (manual, real provider turns).
3
+ //
4
+ // Measures the Lead-process event-loop impact of an 8-agent fanout in both
5
+ // modes against the SAME real provider route:
6
+ // OFF — workers run in-process (this process plays the Lead shard).
7
+ // ON — workers run as daemon-hosted sessions on other shards
8
+ // (MIXDOG_AGENT_SHARD_SPREAD=1), daemon isolated via MIXDOG_RUNTIME_ROOT.
9
+ //
10
+ // Run: node scripts/agent-shard-spread-perf.mjs [provider model effort]
11
+ // Defaults to grok-oauth/grok-4.3 low. Uses the REAL user config/keychain for
12
+ // provider auth; worker-index/statusline writes go to a temp dataDir.
13
+ import { copyFileSync, existsSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs';
14
+ import { execFileSync } from 'node:child_process';
15
+ import { tmpdir } from 'node:os';
16
+ import { homedir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import { performance } from 'node:perf_hooks';
19
+
20
+ const ROOT = mkdtempSync(join(tmpdir(), 'mixdog-spread-perf-'));
21
+ process.env.MIXDOG_RUNTIME_ROOT = ROOT; // isolated perf daemon
22
+ process.env.MIXDOG_DAEMON_SKIP_MEMORY = '1'; // no memory runtime for perf
23
+ // Shard prewarm/creates must not boot a per-root Postgres either: an isolated
24
+ // perf root's postmaster outlives the daemon and leaks (observed: six temp
25
+ // roots holding ~60-100MB PG trees each after a perf session).
26
+ process.env.MIXDOG_BOOT_CORE_MEMORY = '0';
27
+ process.env.MIXDOG_AGENT_TRACE_DISABLE = '1';
28
+ process.env.MIXDOG_AGENT_SHARD_SPREAD = '0'; // phase-controlled below
29
+
30
+ // Isolated DATA_DIR: the daemon owner lock lives in the data dir, so sharing
31
+ // the user's data dir would collide with the LIVE daemon (and revision-2
32
+ // clients must never trigger a replacement drain on it). Copy only the config
33
+ // and the provider credential files the perf route needs.
34
+ const DATA_DIR = join(ROOT, 'data');
35
+ mkdirSync(DATA_DIR, { recursive: true });
36
+ const REAL_DATA_DIR = join(homedir(), '.mixdog', 'data');
37
+ for (const file of [
38
+ 'mixdog-config.json',
39
+ 'grok-oauth.json',
40
+ 'grok-oauth-models.json',
41
+ 'openai-oauth.json',
42
+ 'openai-oauth-models.json',
43
+ 'anthropic-oauth-credentials.json',
44
+ 'anthropic-oauth-models.json',
45
+ ]) {
46
+ const from = join(REAL_DATA_DIR, file);
47
+ if (existsSync(from)) copyFileSync(from, join(DATA_DIR, file));
48
+ }
49
+ process.env.MIXDOG_DATA_DIR = DATA_DIR;
50
+
51
+ const PROVIDER = process.argv[2] || 'grok-oauth';
52
+ const MODEL = process.argv[3] || 'grok-4.3';
53
+ const EFFORT = process.argv[4] || 'low';
54
+ const FANOUT = Math.max(1, Number(process.env.SPREAD_PERF_FANOUT) || 8);
55
+ const PHASES = (process.env.SPREAD_PERF_PHASE || 'both').toLowerCase();
56
+ const KEEP = process.env.SPREAD_PERF_KEEP === '1';
57
+ const JOB_TIMEOUT_MS = Math.max(30_000, Number(process.env.SPREAD_PERF_TIMEOUT_MS) || 240_000);
58
+ const WARM_WAIT_MS = Math.max(0, Number(process.env.SPREAD_PERF_WARM_WAIT_MS) || 0);
59
+ const EVICT_WAIT_MS = Math.max(0, Number(process.env.SPREAD_PERF_EVICT_WAIT_MS) || 0);
60
+ const REPO = 'C:/Project/mixdog';
61
+ const PROBE_INTERVAL_MS = 25;
62
+
63
+ const cfgMod = await import('../src/runtime/agent/orchestrator/config.mjs');
64
+ const reg = await import('../src/runtime/agent/orchestrator/providers/registry.mjs');
65
+ const mgr = await import('../src/runtime/agent/orchestrator/session/manager.mjs');
66
+ const { createStandaloneAgent } = await import('../src/standalone/agent-tool.mjs');
67
+ const {
68
+ attachSession,
69
+ ensureDaemon,
70
+ probeSessionHealth,
71
+ readSessionDiscovery,
72
+ shutdownDaemon,
73
+ } = await import('../src/standalone/session-client.mjs');
74
+
75
+ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
76
+ function taskId(text) { return String(text).match(/agent task: (\S+)/)?.[1] || null; }
77
+
78
+ function startLagProbe() {
79
+ const samples = [];
80
+ let last = performance.now();
81
+ const timer = setInterval(() => {
82
+ const now = performance.now();
83
+ samples.push(Math.max(0, now - last - PROBE_INTERVAL_MS));
84
+ last = now;
85
+ }, PROBE_INTERVAL_MS);
86
+ return {
87
+ stop() {
88
+ clearInterval(timer);
89
+ const sorted = [...samples].sort((a, b) => a - b);
90
+ const at = (q) => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * q))] ?? 0;
91
+ const mean = sorted.length ? sorted.reduce((s, v) => s + v, 0) / sorted.length : 0;
92
+ return {
93
+ samples: sorted.length,
94
+ meanMs: mean,
95
+ p95Ms: at(0.95),
96
+ p99Ms: at(0.99),
97
+ maxMs: sorted[sorted.length - 1] ?? 0,
98
+ over50ms: sorted.filter((v) => v > 50).length,
99
+ over250ms: sorted.filter((v) => v > 250).length,
100
+ };
101
+ },
102
+ };
103
+ }
104
+
105
+ async function waitJob(agent, out, label, timeoutMs = JOB_TIMEOUT_MS) {
106
+ const id = taskId(out);
107
+ if (!id) throw new Error(`missing task id for ${label}: ${out}`);
108
+ const startedAt = Date.now();
109
+ let last = '';
110
+ while (Date.now() - startedAt < timeoutMs) {
111
+ last = await agent.execute({ type: 'read', task_id: id }, { invocationSource: 'model-tool', cwd: REPO });
112
+ if (/status: (completed|failed|error|cancelled)/.test(last)) return last;
113
+ await sleep(500);
114
+ }
115
+ return `status: timeout\n${last}`;
116
+ }
117
+
118
+ async function runPhase(name) {
119
+ const dataDir = join(ROOT, `data-${name}`);
120
+ mkdirSync(dataDir, { recursive: true });
121
+ const agent = createStandaloneAgent({
122
+ cfgMod, reg, mgr, dataDir, cwd: REPO,
123
+ });
124
+ const probe = startLagProbe();
125
+ const t0 = Date.now();
126
+ const outs = await Promise.all(Array.from({ length: FANOUT }, (_, index) => agent.execute({
127
+ type: 'spawn',
128
+ agent: 'worker',
129
+ provider: PROVIDER,
130
+ model: MODEL,
131
+ effort: EFFORT,
132
+ tag: `${name}-w${index}`,
133
+ cwd: REPO,
134
+ prompt: `Read the file package.json in the current repo and reply with exactly the value of its "name" field plus the token #${index}. One short line. Do not edit anything.`,
135
+ }, { invocationSource: 'model-tool', cwd: REPO })));
136
+ const results = await Promise.all(outs.map((out, index) => waitJob(agent, out, `${name}-w${index}`)));
137
+ const wallMs = Date.now() - t0;
138
+ const lag = probe.stop();
139
+ const ok = results.filter((r) => /status: completed/.test(r) && /mixdog/i.test(r)).length;
140
+ try { agent.closeAll(`spread-perf-${name}-end`); } catch { /* teardown */ }
141
+ return { name, wallMs, lag, ok, results };
142
+ }
143
+
144
+ function report(phase) {
145
+ const { lag } = phase;
146
+ process.stdout.write(
147
+ `[${phase.name}] ok=${phase.ok}/${FANOUT} wall=${(phase.wallMs / 1000).toFixed(1)}s `
148
+ + `loopLag mean=${lag.meanMs.toFixed(2)}ms p95=${lag.p95Ms.toFixed(1)}ms `
149
+ + `p99=${lag.p99Ms.toFixed(1)}ms max=${lag.maxMs.toFixed(0)}ms `
150
+ + `>50ms=${lag.over50ms} >250ms=${lag.over250ms} (n=${lag.samples})\n`,
151
+ );
152
+ }
153
+
154
+ function processRssMb(pids) {
155
+ const rss = new Map();
156
+ const wanted = pids.filter((pid) => Number.isInteger(pid) && pid > 0);
157
+ if (!wanted.length) return rss;
158
+ try {
159
+ const out = execFileSync('powershell', [
160
+ '-NoProfile', '-Command',
161
+ `Get-Process -Id ${wanted.join(',')} -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Id) $($_.WorkingSet64)" }`,
162
+ ], { encoding: 'utf8' });
163
+ for (const line of out.split(/\r?\n/)) {
164
+ const [pid, bytes] = line.trim().split(/\s+/);
165
+ if (pid && bytes) rss.set(Number(pid), Number(bytes) / (1024 * 1024));
166
+ }
167
+ } catch { /* best-effort */ }
168
+ return rss;
169
+ }
170
+
171
+ async function printShardLoad(discovery) {
172
+ const health = await probeSessionHealth({ port: discovery.port, token: discovery.token, timeoutMs: 3_000 });
173
+ const shards = health?.sessionShards?.shards || [];
174
+ const rss = processRssMb([health?.pid, ...shards.map((shard) => shard.pid)]);
175
+ const daemonRss = rss.get(Number(health?.pid));
176
+ process.stdout.write(`[load] daemon pid=${health?.pid} sessions=${health?.sessions} busy=${health?.busy}`
177
+ + `${daemonRss ? ` rss=${daemonRss.toFixed(0)}MB` : ''}\n`);
178
+ for (const shard of shards) {
179
+ if (!shard.pid) continue;
180
+ const mb = rss.get(Number(shard.pid));
181
+ process.stdout.write(`[load] shard ${shard.index}: pid=${shard.pid} runtimes=${shard.runtimes}`
182
+ + ` pending=${shard.pending}${mb ? ` rss=${mb.toFixed(0)}MB` : ''}\n`);
183
+ }
184
+ }
185
+
186
+ try {
187
+ // Phase 1 — spread OFF: workers share this process's event loop.
188
+ const off = PHASES !== 'on' ? await runPhase('off') : null;
189
+ if (off) report(off);
190
+
191
+ // Phase 2 — spread ON: isolated daemon + shard pool host the workers.
192
+ let on = null;
193
+ if (PHASES !== 'off') {
194
+ process.env.MIXDOG_AGENT_SHARD_SPREAD = '1';
195
+ const discovery = await ensureDaemon({ cwd: REPO, log: (line) => process.stdout.write(`[daemon-spawn] ${line}\n`) });
196
+ const health = await probeSessionHealth({ port: discovery.port, token: discovery.token });
197
+ process.stdout.write(`[on] perf daemon pid=${discovery.pid} rev=${health?.revision}\n`);
198
+ if (WARM_WAIT_MS > 0) {
199
+ // Warm-daemon scenario: the spread-prewarm starts on client REGISTRATION,
200
+ // so attach once, then give it time to boot the peer shards before the
201
+ // fanout — mirroring a daemon that has been serving a client for a while.
202
+ await attachSession({
203
+ discovery,
204
+ lifecycle: false,
205
+ cwd: REPO,
206
+ onFrame: () => {},
207
+ onFatal: () => {},
208
+ }).catch(() => {});
209
+ await sleep(WARM_WAIT_MS);
210
+ await printShardLoad(discovery).catch(() => {});
211
+ }
212
+ on = await runPhase('on');
213
+ report(on);
214
+ await printShardLoad(discovery).catch(() => {});
215
+ if (EVICT_WAIT_MS > 0) {
216
+ // Reclamation probe: idle+unwatched worker runtimes should be evicted
217
+ // (service sweep) and their shard RSS returned after this window.
218
+ await sleep(EVICT_WAIT_MS);
219
+ process.stdout.write(`[load] after ${(EVICT_WAIT_MS / 1000).toFixed(0)}s idle:\n`);
220
+ await printShardLoad(discovery).catch(() => {});
221
+ }
222
+ }
223
+
224
+ for (const phase of [off, on].filter(Boolean)) {
225
+ for (const [index, result] of phase.results.entries()) {
226
+ if (!/status: completed/.test(result) || !/mixdog/i.test(result)) {
227
+ process.stdout.write(`--- ${phase.name}-w${index} ---\n${result.slice(0, 600)}\n`);
228
+ }
229
+ }
230
+ }
231
+ const gate = on || off;
232
+ const verdict = gate.ok === FANOUT ? 'PASS' : 'FAIL';
233
+ process.stdout.write(`verdict: ${verdict} (${gate.name}.ok=${gate.ok}/${FANOUT})\n`);
234
+ process.exitCode = gate.ok === FANOUT ? 0 : 1;
235
+ } finally {
236
+ if (KEEP) {
237
+ process.stdout.write(`kept root: ${ROOT}\n`);
238
+ } else {
239
+ try { await shutdownDaemon(readSessionDiscovery()); } catch { /* teardown */ }
240
+ await sleep(300);
241
+ try { rmSync(ROOT, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); } catch { /* temp */ }
242
+ }
243
+ }
244
+ process.exit(process.exitCode || 0);
245
+
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ // Turn-loop stage probe (manual, real provider turns).
3
+ //
4
+ // Runs ONE spread worker through a deliberately sequential multi-round task
5
+ // with the agent trace enabled, then prints the per-iteration stage rows:
6
+ // loop — send_ms / pre_send_ms / tool_resume_ms / message_count
7
+ // sse — ttft_ms / stream_total_ms per provider request
8
+ // turn_timing — queue/route/preflight/provider attribution
9
+ // usage — input/cached tokens per request (prefix-cache hit signal)
10
+ //
11
+ // Run: node scripts/agent-turn-trace-probe.mjs [provider model effort]
12
+ import { copyFileSync, existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
13
+ import { tmpdir, homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+
16
+ const ROOT = mkdtempSync(join(tmpdir(), 'mixdog-turn-trace-'));
17
+ process.env.MIXDOG_RUNTIME_ROOT = ROOT;
18
+ process.env.MIXDOG_DAEMON_SKIP_MEMORY = '1';
19
+ process.env.MIXDOG_BOOT_CORE_MEMORY = '0';
20
+ process.env.MIXDOG_AGENT_SHARD_SPREAD = '1';
21
+ // The probe's whole point is the trace: explicit path (shared by daemon and
22
+ // shard children via env inheritance) + timing rows.
23
+ const TRACE_PATH = join(ROOT, 'agent-trace.jsonl');
24
+ delete process.env.MIXDOG_AGENT_TRACE_DISABLE;
25
+ process.env.MIXDOG_AGENT_TRACE_PATH = TRACE_PATH;
26
+ process.env.MIXDOG_AGENT_TRACE_TIMING = '1';
27
+
28
+ const DATA_DIR = join(ROOT, 'data');
29
+ mkdirSync(DATA_DIR, { recursive: true });
30
+ const REAL_DATA_DIR = join(homedir(), '.mixdog', 'data');
31
+ for (const file of [
32
+ 'mixdog-config.json',
33
+ 'grok-oauth.json',
34
+ 'grok-oauth-models.json',
35
+ 'openai-oauth.json',
36
+ 'openai-oauth-models.json',
37
+ 'anthropic-oauth-credentials.json',
38
+ 'anthropic-oauth-models.json',
39
+ ]) {
40
+ const from = join(REAL_DATA_DIR, file);
41
+ if (existsSync(from)) copyFileSync(from, join(DATA_DIR, file));
42
+ }
43
+ process.env.MIXDOG_DATA_DIR = DATA_DIR;
44
+
45
+ const PROVIDER = process.argv[2] || 'grok-oauth';
46
+ const MODEL = process.argv[3] || 'grok-4.3';
47
+ const EFFORT = process.argv[4] || 'low';
48
+ const REPO = 'C:/Project/mixdog';
49
+ const JOB_TIMEOUT_MS = 240_000;
50
+
51
+ const cfgMod = await import('../src/runtime/agent/orchestrator/config.mjs');
52
+ const reg = await import('../src/runtime/agent/orchestrator/providers/registry.mjs');
53
+ const mgr = await import('../src/runtime/agent/orchestrator/session/manager.mjs');
54
+ const { createStandaloneAgent } = await import('../src/standalone/agent-tool.mjs');
55
+ const { ensureDaemon, readSessionDiscovery, shutdownDaemon } = await import('../src/standalone/session-client.mjs');
56
+
57
+ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
58
+
59
+ try {
60
+ await ensureDaemon({ cwd: REPO, log: () => {} });
61
+ const agent = createStandaloneAgent({ cfgMod, reg, mgr, dataDir: join(ROOT, 'data-probe'), cwd: REPO });
62
+ const t0 = Date.now();
63
+ const out = await agent.execute({
64
+ type: 'spawn',
65
+ agent: 'worker',
66
+ provider: PROVIDER,
67
+ model: MODEL,
68
+ effort: EFFORT,
69
+ tag: 'turn-trace',
70
+ cwd: REPO,
71
+ prompt: [
72
+ 'Execute these steps strictly IN ORDER, exactly ONE tool call per assistant message',
73
+ '(never batch two calls in one message — this measures sequential rounds):',
74
+ '1) read package.json',
75
+ '2) read README.md (first 40 lines)',
76
+ '3) run the shell command: node -v',
77
+ '4) read apps/desktop/package.json',
78
+ '5) grep the string "createSessionRuntimePool" under src/standalone (files list only)',
79
+ 'Then reply with one line: DONE <package name> <node version>. Do not edit anything.',
80
+ ].join('\n'),
81
+ }, { invocationSource: 'model-tool', cwd: REPO });
82
+ const id = String(out).match(/agent task: (\S+)/)?.[1];
83
+ if (!id) throw new Error(`no task id: ${out}`);
84
+ let last = '';
85
+ while (Date.now() - t0 < JOB_TIMEOUT_MS) {
86
+ last = await agent.execute({ type: 'read', task_id: id }, { invocationSource: 'model-tool', cwd: REPO });
87
+ if (/status: (completed|failed|error|cancelled)/.test(last)) break;
88
+ await sleep(500);
89
+ }
90
+ process.stdout.write(`wall=${((Date.now() - t0) / 1000).toFixed(1)}s\n--- result ---\n${last.slice(0, 400)}\n`);
91
+ try { agent.closeAll('turn-trace probe end'); } catch { /* teardown */ }
92
+
93
+ // Shard children flush their local trace buffers on a short timer.
94
+ await sleep(9_000);
95
+ const rows = existsSync(TRACE_PATH)
96
+ ? readFileSync(TRACE_PATH, 'utf8').split('\n').filter(Boolean).flatMap((line) => {
97
+ try { return [JSON.parse(line)]; } catch { return []; }
98
+ })
99
+ : [];
100
+ process.stdout.write(`--- trace (${rows.length} rows) ---\n`);
101
+ for (const row of rows) {
102
+ if (row.kind === 'loop') {
103
+ process.stdout.write(`[loop] iter=${row.iteration ?? row.payload?.iteration} send=${row.send_ms ?? row.payload?.send_ms}ms`
104
+ + ` preSend=${row.pre_send_ms ?? row.payload?.pre_send_ms}ms toolResume=${row.tool_resume_ms ?? row.payload?.tool_resume_ms}ms`
105
+ + ` msgs=${row.message_count ?? row.payload?.message_count}\n`);
106
+ } else if (row.kind === 'sse') {
107
+ process.stdout.write(`[sse] ttft=${row.ttft_ms}ms streamTotal=${row.stream_total_ms}ms\n`);
108
+ } else if (row.kind === 'turn_timing') {
109
+ process.stdout.write(`[turn] status=${row.status} ttft=${row.ttft_ms}ms e2eTtft=${row.end_to_end_ttft_ms}ms`
110
+ + ` queue=${row.queue_ms}ms route=${row.route_ms}ms preflight=${row.preflight_ms}ms provider=${row.provider_ms}ms\n`);
111
+ } else if (row.kind === 'usage_raw') {
112
+ process.stdout.write(`[usage] iter=${row.iteration} input=${row.input_tokens} cached=${row.cached_tokens}`
113
+ + ` cacheWrite=${row.cache_write_tokens} uncached=${row.uncached_input_tokens} output=${row.output_tokens}`
114
+ + ` chain=${row.chain_continuous ?? '-'}\n`);
115
+ } else if (row.kind === 'tool') {
116
+ const p = row.payload || row;
117
+ process.stdout.write(`[tool] ${row.tool || row.tool_name || p.tool || p.tool_name || ''}`
118
+ + ` ${row.duration_ms ?? row.tool_ms ?? p.duration_ms ?? p.tool_ms ?? '?'}ms\n`);
119
+ } else {
120
+ process.stdout.write(`[${row.kind}] ${JSON.stringify(row).slice(0, 220)}\n`);
121
+ }
122
+ }
123
+ } finally {
124
+ try { await shutdownDaemon(readSessionDiscovery()); } catch { /* teardown */ }
125
+ await sleep(300);
126
+ try { rmSync(ROOT, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); } catch { /* temp */ }
127
+ }
128
+ process.exit(0);
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from 'node:crypto'
4
+ import { readFile } from 'node:fs/promises'
5
+ import { resolve } from 'node:path'
6
+ import { pathToFileURL } from 'node:url'
7
+
8
+ import { normalizeRuntimeLockfile } from './runtime-dependency-cache-key.mjs'
9
+
10
+ export const DEPENDENCY_LOCK_CACHE_SCHEMA = 1
11
+
12
+ export function dependencyLockCacheKey(lockfile) {
13
+ const normalized = normalizeRuntimeLockfile(lockfile)
14
+ const fingerprint = createHash('sha256')
15
+ .update(JSON.stringify(normalized))
16
+ .digest('hex')
17
+ return `dependency-lock-v${DEPENDENCY_LOCK_CACHE_SCHEMA}-${fingerprint}`
18
+ }
19
+
20
+ const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ''
21
+ if (invokedPath === import.meta.url) {
22
+ const lockfilePath = process.argv[2]
23
+ if (!lockfilePath) throw new Error('Usage: dependency-lock-cache-key.mjs <package-lock.json>')
24
+ readFile(resolve(lockfilePath))
25
+ .then((lockfile) => process.stdout.write(`${dependencyLockCacheKey(lockfile)}\n`))
26
+ .catch((error) => {
27
+ process.stderr.write(`Dependency lock cache key failed: ${error?.message || error}\n`)
28
+ process.exitCode = 1
29
+ })
30
+ }
@@ -0,0 +1,13 @@
1
+ // Minimal session-shard stand-in for pool placement tests: acknowledges the
2
+ // shard IPC surface (create/call/snapshot/prewarm/workload/shutdown) without
3
+ // booting the real session runtime graph, so placement is observable fast.
4
+ process.on('message', (message) => {
5
+ if (!message || typeof message !== 'object' || !message.requestId) return;
6
+ process.send({
7
+ type: 'response',
8
+ requestId: message.requestId,
9
+ ok: true,
10
+ value: message.type === 'create' ? { created: true } : { ready: true },
11
+ });
12
+ if (message.type === 'shutdown') setImmediate(() => process.exit(0));
13
+ });
@@ -0,0 +1,8 @@
1
+ // Child-side gate harness: runs child-spawn-gate acquire() in shard mode so a
2
+ // forked test parent can play the pool's role in the lease protocol.
3
+ const { acquire } = await import(new URL('../../src/runtime/shared/child-spawn-gate.mjs', import.meta.url));
4
+ const release = await acquire(null, 'search', { ownerKey: 'harness' });
5
+ process.send({ type: 'harness-event', event: 'granted' });
6
+ release();
7
+ process.send({ type: 'harness-event', event: 'released' });
8
+ setTimeout(() => process.exit(0), 200);
@@ -0,0 +1,33 @@
1
+ // Minimal shard stand-in for the machine spawn-budget protocol test. Replies
2
+ // ok to any requestId message (prewarm/shutdown) and drives the lease
3
+ // protocol on a fixed clock; grants are recorded as marker files so the test
4
+ // can observe ordering without extra IPC surface.
5
+ import { writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ const outDir = process.env.SPAWN_LEASE_FIXTURE_DIR || process.cwd();
9
+ const record = (name) => {
10
+ try { writeFileSync(join(outDir, name), String(Date.now())); } catch { /* test observes absence */ }
11
+ };
12
+
13
+ process.on('message', (message) => {
14
+ if (!message || typeof message !== 'object') return;
15
+ if (message.type === 'spawn-lease-result') {
16
+ record(`${message.leaseId}-${message.ok === true ? 'granted' : 'rejected'}`);
17
+ return;
18
+ }
19
+ if (message.requestId) {
20
+ process.send({ type: 'response', requestId: message.requestId, ok: true, value: { ready: true } });
21
+ if (message.type === 'shutdown') setImmediate(() => process.exit(0));
22
+ }
23
+ });
24
+
25
+ process.send({ type: 'spawn-lease', leaseId: 'lease-a', lane: 'search', ownerKey: 'fixture-a' });
26
+ setTimeout(() => {
27
+ process.send({ type: 'spawn-lease', leaseId: 'lease-b', lane: 'search', ownerKey: 'fixture-b' });
28
+ }, 150);
29
+ // Release A well after the test has observed that B stays queued behind the
30
+ // machine cap of 1.
31
+ setTimeout(() => {
32
+ process.send({ type: 'spawn-release', leaseId: 'lease-a' });
33
+ }, 1_800);
@@ -29,7 +29,11 @@ async function removeChildrenExcept(directory, keep) {
29
29
  }
30
30
  await Promise.all(entries
31
31
  .filter((entry) => !keep.has(entry.name))
32
- .map((entry) => rm(join(directory, entry.name), { recursive: true, force: true })))
32
+ // Windows: pruning a freshly-installed npm tree races AV scans; bounded
33
+ // retries absorb the transient ENOTEMPTY/EPERM rmdir failures.
34
+ .map((entry) => rm(join(directory, entry.name), {
35
+ recursive: true, force: true, maxRetries: 10, retryDelay: 250,
36
+ })))
33
37
  }
34
38
 
35
39
  async function packageName(directory) {
package/src/app.mjs CHANGED
@@ -87,6 +87,9 @@ export async function run(argv = [], classifiedInvocation = null) {
87
87
  model: opts.model,
88
88
  effort: opts.effort,
89
89
  fast: opts.fast,
90
+ explore: opts.explore,
91
+ webSearch: opts.webSearch,
92
+ memory: opts.memory,
90
93
  cwd: process.cwd(),
91
94
  });
92
95
  }
@@ -1,6 +1,7 @@
1
1
  const VALUE_OPTIONS = new Set(['--provider', '--model', '--effort', '--workflow']);
2
2
  const FLAG_OPTIONS = new Set([
3
3
  '--readonly', '--help', '-h', '--plain', '--react', '--remote', '--onboarding', '--fast',
4
+ '--explore', '--web-search', '--memory',
4
5
  ]);
5
6
  const HEADLESS_ROLE_ALIASES = new Map([
6
7
  ['explorer', 'explore'], ['explore', 'explore'],
@@ -108,6 +109,9 @@ export function classifyCliInvocation(argv = []) {
108
109
  model: parsed.values['--model'],
109
110
  effort: parsed.values['--effort'],
110
111
  fast: argv.includes('--fast'),
112
+ explore: argv.includes('--explore'),
113
+ webSearch: argv.includes('--web-search'),
114
+ memory: argv.includes('--memory'),
111
115
  toolMode: argv.includes('--readonly') ? 'readonly' : 'full',
112
116
  remote: argv.includes('--remote'),
113
117
  forceOnboarding: argv.includes('--onboarding'),
@@ -83,6 +83,9 @@ export async function runHeadlessRole({
83
83
  model,
84
84
  effort,
85
85
  fast,
86
+ explore = false,
87
+ webSearch = false,
88
+ memory = false,
86
89
  cwd = process.cwd(),
87
90
  write = (text) => stdout.write(text),
88
91
  writeErr = (text) => stderr.write(text),
@@ -90,6 +93,16 @@ export async function runHeadlessRole({
90
93
  } = {}) {
91
94
  const cleanAgent = clean(agent);
92
95
  const cleanMessage = clean(message);
96
+ // Classic headless surface: explorer, web search, and memory tools start OFF
97
+ // and opt back in per run (--explore / --web-search / --memory). An explicit
98
+ // MIXDOG_FEATURE_* value from the caller environment always wins.
99
+ for (const [key, enabled] of [
100
+ ['MIXDOG_FEATURE_EXPLORE', explore],
101
+ ['MIXDOG_FEATURE_WEB_SEARCH', webSearch],
102
+ ['MIXDOG_FEATURE_MEMORY', memory],
103
+ ]) {
104
+ if (!clean(process.env[key])) process.env[key] = enabled === true ? '1' : '0';
105
+ }
93
106
  if (!cleanAgent) {
94
107
  writeErr('mixdog: agent is required\n');
95
108
  return 1;
@@ -2,13 +2,10 @@
2
2
 
3
3
  - Agent communication is English.
4
4
  - Call tools immediately: no preamble/progress; text only in final handoff.
5
- - Final handoff is fragments: outcome, key `file:line`, verification
6
- command+result, material risk/blocker. Never repeat the brief, process,
7
- search path, or facts; never retrieve only to report.
8
- - Limit the handoff to 30 lines unless `Deliver:` sets another limit. Never use
9
- unrequested headings/tables, prose narration, raw logs/tool traces,
10
- speculative next checks, restated briefs, articles, or politeness.
11
- - Follow stricter role contracts and runtime wrap-up requirements.
12
- - Your final message ends the task: emit the handoff text only when the work is
5
+ - Final handoff: outcome, key `file:line`, material risk/blocker, and one
6
+ batched verification result when files were edited fragments, max 30
7
+ lines unless `Deliver:` overrides; never raw logs, process narration, or
8
+ brief restatement.
9
+ - Your final message ends the task: emit the handoff only when the work is
13
10
  done. If a tool failed and stays unresolved, fix and re-run it, or say so
14
11
  explicitly in the handoff.