mixdog 0.9.38 → 0.9.40

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 (124) hide show
  1. package/package.json +9 -4
  2. package/scripts/abort-recovery-test.mjs +43 -2
  3. package/scripts/agent-tag-reuse-smoke.mjs +146 -6
  4. package/scripts/agent-terminal-reap-test.mjs +127 -0
  5. package/scripts/agent-trace-io-test.mjs +69 -0
  6. package/scripts/code-graph-disk-hit-test.mjs +224 -0
  7. package/scripts/execution-completion-dedup-test.mjs +157 -0
  8. package/scripts/execution-pending-resume-kick-test.mjs +57 -2
  9. package/scripts/execution-resume-esc-integration-test.mjs +176 -0
  10. package/scripts/explore-bench.mjs +38 -2
  11. package/scripts/explore-prompt-policy-test.mjs +152 -11
  12. package/scripts/find-fuzzy-hidden-test.mjs +122 -0
  13. package/scripts/internal-comms-bench-test.mjs +226 -0
  14. package/scripts/internal-comms-bench.mjs +185 -58
  15. package/scripts/internal-comms-smoke.mjs +171 -23
  16. package/scripts/live-worker-smoke.mjs +38 -2
  17. package/scripts/memory-cycle-routing-test.mjs +111 -0
  18. package/scripts/memory-rule-contract-test.mjs +93 -0
  19. package/scripts/output-style-smoke.mjs +2 -2
  20. package/scripts/rg-runner-test.mjs +240 -0
  21. package/scripts/routing-corpus-test.mjs +349 -0
  22. package/scripts/routing-corpus.mjs +211 -32
  23. package/scripts/session-orphan-sweep-test.mjs +83 -0
  24. package/scripts/steering-drain-buckets-test.mjs +316 -2
  25. package/scripts/tool-smoke.mjs +21 -13
  26. package/scripts/tool-tui-presentation-test.mjs +202 -0
  27. package/scripts/tui-transcript-perf-test.mjs +279 -0
  28. package/src/agents/heavy-worker/AGENT.md +10 -7
  29. package/src/agents/reviewer/AGENT.md +6 -4
  30. package/src/agents/worker/AGENT.md +7 -5
  31. package/src/rules/agent/00-common.md +4 -4
  32. package/src/rules/agent/00-core.md +11 -14
  33. package/src/rules/agent/20-skip-protocol.md +3 -3
  34. package/src/rules/agent/30-explorer.md +50 -60
  35. package/src/rules/agent/40-cycle1-agent.md +15 -24
  36. package/src/rules/agent/41-cycle2-agent.md +33 -57
  37. package/src/rules/agent/42-cycle3-agent.md +28 -42
  38. package/src/rules/lead/01-general.md +7 -10
  39. package/src/rules/lead/lead-brief.md +11 -14
  40. package/src/rules/lead/lead-tool.md +6 -5
  41. package/src/rules/shared/01-tool.md +44 -45
  42. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -1
  43. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
  44. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
  46. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
  47. package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
  48. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
  49. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -0
  50. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
  51. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
  52. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +67 -2
  53. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/store.mjs +232 -43
  56. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
  58. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +27 -13
  59. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
  60. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
  62. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
  63. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
  65. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
  66. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
  67. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
  68. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
  69. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
  70. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
  72. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
  73. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
  74. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
  75. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
  76. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
  77. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
  78. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  80. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
  81. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
  82. package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
  83. package/src/runtime/shared/buffered-appender.mjs +13 -2
  84. package/src/runtime/shared/tool-primitives.mjs +4 -1
  85. package/src/runtime/shared/tool-status.mjs +27 -0
  86. package/src/runtime/shared/tool-surface.mjs +6 -3
  87. package/src/session-runtime/config-helpers.mjs +14 -0
  88. package/src/session-runtime/context-status.mjs +1 -0
  89. package/src/session-runtime/effort.mjs +6 -2
  90. package/src/session-runtime/lifecycle-api.mjs +4 -0
  91. package/src/session-runtime/model-recency.mjs +5 -2
  92. package/src/session-runtime/runtime-core.mjs +36 -2
  93. package/src/session-runtime/session-turn-api.mjs +12 -0
  94. package/src/session-runtime/tool-catalog.mjs +34 -0
  95. package/src/standalone/agent-tool/notify.mjs +13 -0
  96. package/src/standalone/agent-tool.mjs +53 -70
  97. package/src/standalone/explore-tool.mjs +6 -7
  98. package/src/tui/App.jsx +33 -1
  99. package/src/tui/app/model-options.mjs +5 -3
  100. package/src/tui/app/model-picker.mjs +12 -24
  101. package/src/tui/app/transcript-window.mjs +10 -10
  102. package/src/tui/app/use-transcript-window.mjs +38 -56
  103. package/src/tui/components/ToolExecution.jsx +11 -6
  104. package/src/tui/components/TranscriptItem.jsx +1 -1
  105. package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
  106. package/src/tui/components/tool-execution/text-format.mjs +10 -19
  107. package/src/tui/dist/index.mjs +866 -300
  108. package/src/tui/engine/agent-job-feed.mjs +216 -19
  109. package/src/tui/engine/agent-response-tail.mjs +68 -0
  110. package/src/tui/engine/notification-plan.mjs +16 -0
  111. package/src/tui/engine/session-api.mjs +14 -2
  112. package/src/tui/engine/session-flow.mjs +22 -2
  113. package/src/tui/engine/tool-card-results.mjs +64 -37
  114. package/src/tui/engine/tool-result-status.mjs +75 -21
  115. package/src/tui/engine/turn.mjs +172 -77
  116. package/src/tui/engine.mjs +199 -39
  117. package/src/tui/index.jsx +2 -2
  118. package/src/workflows/bench/WORKFLOW.md +25 -35
  119. package/src/workflows/default/WORKFLOW.md +38 -32
  120. package/src/workflows/solo/WORKFLOW.md +19 -22
  121. package/scripts/_jitter-fuzz.mjs +0 -44410
  122. package/scripts/_jitter-fuzz2.mjs +0 -44400
  123. package/scripts/_jitter-probe.mjs +0 -44397
  124. package/scripts/_jp2.mjs +0 -45614
@@ -5,7 +5,7 @@
5
5
  import { createHash } from 'node:crypto';
6
6
  import { join } from 'node:path';
7
7
  import {
8
- readdirSync, readFileSync, statSync, existsSync, mkdirSync, renameSync, unlinkSync,
8
+ readdirSync, readFileSync, statSync, existsSync, mkdirSync, renameSync, unlinkSync, openSync, readSync, closeSync,
9
9
  } from 'node:fs';
10
10
  import { getPluginData } from '../../config.mjs';
11
11
  import { writeJsonAtomicSync } from '../../../../shared/atomic-file.mjs';
@@ -19,6 +19,7 @@ import {
19
19
  CODE_GRAPH_DISK_DIR,
20
20
  CODE_GRAPH_DISK_MAX_ENTRIES,
21
21
  CODE_GRAPH_DISK_MAX_BYTES,
22
+ CODE_GRAPH_FAST_PATH_MAX_BYTES,
22
23
  ORPHAN_TMP_MIN_AGE_MS,
23
24
  RE_CACHE_TMP,
24
25
  RE_MANIFEST_TMP,
@@ -83,6 +84,13 @@ function _migrateLegacyDiskCache() {
83
84
  }
84
85
  }
85
86
 
87
+ // This intentionally does not call _loadDiskCodeGraphCache: the parent-side
88
+ // fast path uses it to leave a legacy blob's synchronous read/JSON.parse to
89
+ // the Worker, where normal one-shot migration still happens.
90
+ export function hasLegacyDiskCodeGraphCache() {
91
+ return existsSync(join(getPluginData(), CODE_GRAPH_DISK_FILE));
92
+ }
93
+
86
94
  function _pruneDiskCodeGraphEntries(_now = Date.now()) {
87
95
  for (const [cwd, entry] of _diskCodeGraphCache) {
88
96
  if (!entry || typeof entry !== 'object') {
@@ -278,7 +286,10 @@ function _ensureCwdLoaded(cwd) {
278
286
  } catch { /* skip corrupt per-cwd file */ }
279
287
  }
280
288
 
281
- function _persistDiskCodeGraphCacheNow() {
289
+ export function _persistDiskCodeGraphCacheNow({
290
+ strict = false,
291
+ writeJson = writeJsonAtomicSync,
292
+ } = {}) {
282
293
  try {
283
294
  _loadDiskCodeGraphCache();
284
295
  _pruneDiskCodeGraphEntries();
@@ -307,8 +318,15 @@ function _persistDiskCodeGraphCacheNow() {
307
318
  // rebuildable — a busy lock just skips this flush; _scheduleDiskCodeGraphCacheFlush
308
319
  // re-schedules on the next build. Exit drain still runs (unref'd timer
309
320
  // cancelled + direct call).
310
- writeJsonAtomicSync(file, entry, { compact: true, lock: true, timeoutMs: 0 });
311
- manifest[cwd] = { hash, builtAt: entry.builtAt || Date.now() };
321
+ writeJson(file, entry, { compact: true, lock: true, timeoutMs: 0 });
322
+ let bytes = null;
323
+ try { bytes = statSync(file).size; } catch { /* written entry may be unavailable */ }
324
+ manifest[cwd] = {
325
+ hash,
326
+ builtAt: entry.builtAt || Date.now(),
327
+ bytes: Number.isFinite(bytes) ? bytes : undefined,
328
+ maxFiles: Number.isFinite(entry.maxFiles) ? entry.maxFiles : undefined,
329
+ };
312
330
  }
313
331
  const pruned = _pruneCodeGraphManifestForBudget(manifest, dir);
314
332
  manifest = pruned.manifest;
@@ -320,7 +338,7 @@ function _persistDiskCodeGraphCacheNow() {
320
338
  }
321
339
 
322
340
  const manifestFile = join(dir, 'manifest.json');
323
- writeJsonAtomicSync(manifestFile, manifest, { compact: true, lock: true, timeoutMs: 0 });
341
+ writeJson(manifestFile, manifest, { compact: true, lock: true, timeoutMs: 0 });
324
342
  _diskManifest = manifest;
325
343
 
326
344
  // Sweep orphan per-cwd files. validHashes now includes every hash in
@@ -329,6 +347,7 @@ function _persistDiskCodeGraphCacheNow() {
329
347
  _sweepCodeGraphCacheDir(dir, validHashes, { sweepJson: true });
330
348
  } catch (err) {
331
349
  process.stderr.write(`[code-graph] disk cache persist failed (target: ${_codeGraphDiskDir()}): ${err?.message || err}\n`);
350
+ if (strict) throw err;
332
351
  }
333
352
  }
334
353
 
@@ -355,6 +374,16 @@ function drainCodeGraphCacheNow() {
355
374
  }
356
375
  registerCodeGraphDrain(drainCodeGraphCacheNow);
357
376
 
377
+ // Worker-only success fencing: unlike the exit/parent drain, a failed
378
+ // persistence must reject the Worker result instead of being log-only.
379
+ export function drainCodeGraphCacheStrict() {
380
+ if (_diskCodeGraphCacheFlushTimer) {
381
+ clearTimeout(_diskCodeGraphCacheFlushTimer);
382
+ _diskCodeGraphCacheFlushTimer = null;
383
+ }
384
+ _persistDiskCodeGraphCacheNow({ strict: true });
385
+ }
386
+
358
387
  // Public: delegate to the state module's drain hook (which invokes the
359
388
  // registered drainCodeGraphCacheNow). Preserves the original facade behavior.
360
389
  export function drainCodeGraphCache() {
@@ -369,6 +398,40 @@ export function getDiskCodeGraphEntry(cwd) {
369
398
  return _diskCodeGraphCache.get(key);
370
399
  }
371
400
 
401
+ // Inspect only manifest metadata (or a stat fallback), never parse the entry.
402
+ // The main-thread fast path uses this to leave large/legacy entries to Worker
403
+ // isolation while still allowing small compatible entries to avoid Worker boot.
404
+ export function probeDiskCodeGraphEntry(cwd, maxBytes = CODE_GRAPH_FAST_PATH_MAX_BYTES) {
405
+ const key = _canonicalGraphCwd(cwd);
406
+ const meta = _diskManifest?.[key];
407
+ if (!meta || typeof meta !== 'object' || !_isCodeGraphCacheHash(meta.hash)) return null;
408
+ const file = join(_codeGraphDiskDir(), `${meta.hash}.json`);
409
+ let bytes = Number(meta.bytes);
410
+ if (!Number.isFinite(bytes) || bytes < 0) {
411
+ try { bytes = statSync(file).size; } catch { return null; }
412
+ }
413
+ let maxFiles = Number.isFinite(meta.maxFiles) ? meta.maxFiles : null;
414
+ // Legacy manifests have no per-entry metadata. Read only the compact JSON
415
+ // header (maxFiles precedes nodes) rather than parsing a potentially huge
416
+ // payload on the main thread.
417
+ if (maxFiles === null) {
418
+ let fd = null;
419
+ try {
420
+ fd = openSync(file, 'r');
421
+ const header = Buffer.allocUnsafe(4096);
422
+ const read = readSync(fd, header, 0, header.length, 0);
423
+ const match = /"maxFiles":(\d+)/.exec(header.toString('utf8', 0, read));
424
+ if (match) maxFiles = Number(match[1]);
425
+ } catch { /* leave legacy/corrupt metadata in the Worker path */ }
426
+ finally { if (fd !== null) try { closeSync(fd); } catch {} }
427
+ }
428
+ return {
429
+ bytes,
430
+ maxFiles,
431
+ isFastPathEligible: bytes <= maxBytes,
432
+ };
433
+ }
434
+
372
435
  // Ensure the on-disk manifest/sweep boot pass has run (idempotent).
373
436
  export function ensureDiskCodeGraphLoaded(now = Date.now()) {
374
437
  _loadDiskCodeGraphCache(now);
@@ -103,8 +103,16 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
103
103
  // Name-only "symbols" calls (symbols[]/symbol without a file) are symbol
104
104
  // lookups, not a file outline — absorb into symbol_search instead of
105
105
  // erroring "file not found in graph: (missing file)".
106
- if (mode === 'symbols' && !String(args?.file || '').trim()
107
- && ((Array.isArray(args?.symbols) && args.symbols.length) || String(args?.symbol || '').trim())) {
106
+ if (mode === 'symbols'
107
+ && !String(args?.file || '').trim()
108
+ && !String(args?.files || '').trim()
109
+ && ((Array.isArray(args?.symbols) && args.symbols.length)
110
+ || (typeof args?.symbols === 'string' && args.symbols.trim())
111
+ || String(args?.symbol || '').trim())) {
112
+ if (!args.symbol && typeof args.symbols === 'string' && args.symbols.trim()) {
113
+ args = { ...args, symbol: args.symbols };
114
+ delete args.symbols;
115
+ }
108
116
  mode = 'symbol_search';
109
117
  }
110
118
 
@@ -523,8 +531,14 @@ export async function resolveSymbolReadSpan(cwd, { symbol, path = null, language
523
531
  export async function executeCodeGraphTool(name, args, cwd, signal = null, options = {}) {
524
532
  if (!cwd) throw new Error('find_symbol/code_graph requires cwd — caller did not provide a working directory');
525
533
  args = _normalizeGraphFileArgs(args);
526
- const fileArg = (args && typeof args.file === 'string' && args.file.trim()) ? args.file.trim() : '';
527
534
  const baseCwd = (args && typeof args.cwd === 'string' && args.cwd.trim()) ? args.cwd.trim() : cwd;
535
+ const symbolMode = ['find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees', 'symbols'].includes(args?.mode);
536
+ if (symbolMode && typeof args?.file === 'string' && args.file.trim()
537
+ && pathResolve(baseCwd, args.file.trim()) === pathResolve(baseCwd)) {
538
+ args = { ...args };
539
+ delete args.file;
540
+ }
541
+ const fileArg = (args && typeof args.file === 'string' && args.file.trim()) ? args.file.trim() : '';
528
542
  let effectiveCwd = baseCwd;
529
543
  if (fileArg) {
530
544
  const abs = isAbsolute(fileArg) ? pathResolve(fileArg) : pathResolve(baseCwd, fileArg);
@@ -33,7 +33,7 @@ function _graphBinaryPath() {
33
33
  try { return findCachedGraphBinary(getPluginData()); } catch { return null; }
34
34
  }
35
35
 
36
- async function _runGraphBinaryJsonl(absRoot, extraArgs, stdinLines = null) {
36
+ async function _runGraphBinaryJsonl(absRoot, extraArgs, stdinLines = null, signal = null) {
37
37
  let binPath = _graphBinaryPath();
38
38
  if (!binPath) {
39
39
  // No local build or cached binary — fetch the prebuilt from the release
@@ -54,14 +54,10 @@ async function _runGraphBinaryJsonl(absRoot, extraArgs, stdinLines = null) {
54
54
 
55
55
  // Inner spawn + promise — extracted so we can retry once on EAGAIN.
56
56
  //
57
- // child-spawn-gate is NOT acquired here. This function runs inside the
58
- // code-graph prewarm WORKER THREAD (via _buildCodeGraph), and worker_threads
59
- // do not share module-level state with the main thread acquiring here would
60
- // create a SECOND, independent semaphore that never coordinates with the
61
- // main-thread rg gate. Instead the gate is held on the MAIN THREAD across the
62
- // whole graph-build worker's lifetime (see buildCodeGraphAsync). The binary
63
- // child is spawned exclusively from this worker path, so one main-side slot
64
- // per worker correctly bounds native graph spawns against rg.
57
+ // child-spawn-gate is NOT acquired here. Worker builds and main-thread
58
+ // signature validation both hold their slot in buildCodeGraphAsync; worker
59
+ // threads do not share module-level state with the main thread, so acquiring
60
+ // here would create an independent semaphore that cannot coordinate with rg.
65
61
  const _spawnOnce = () => new Promise((resolve, reject) => {
66
62
  // When stdinLines is supplied (--files mode), stream one JSON object per
67
63
  // line to the child's STDIN — the reused nodes' metadata — so Rust can
@@ -79,6 +75,7 @@ async function _runGraphBinaryJsonl(absRoot, extraArgs, stdinLines = null) {
79
75
  const STDERR_CAP = 8 * 1024;
80
76
  let settled = false;
81
77
  let timedOut = false;
78
+ let aborted = false;
82
79
 
83
80
  // ── timeout + kill helpers (mirrors rg-runner's _killRgProc/_escalateRgKill) ──
84
81
  let timeoutTimer = null;
@@ -130,7 +127,12 @@ async function _runGraphBinaryJsonl(absRoot, extraArgs, stdinLines = null) {
130
127
  clearTimeout(forceSettleTimer);
131
128
  forceSettleTimer = null;
132
129
  }
130
+ if (onAbort && signal) {
131
+ try { signal.removeEventListener('abort', onAbort); } catch { /* ignore */ }
132
+ onAbort = null;
133
+ }
133
134
  };
135
+ let onAbort = null;
134
136
 
135
137
  // Arm timeout — unref so it doesn't keep the process alive. On timeout we
136
138
  // start SIGTERM→grace→force-kill but do NOT settle yet: the promise stays
@@ -157,6 +159,14 @@ async function _runGraphBinaryJsonl(absRoot, extraArgs, stdinLines = null) {
157
159
  if (forceSettleTimer.unref) forceSettleTimer.unref();
158
160
  }, timeoutMs);
159
161
  if (timeoutTimer.unref) timeoutTimer.unref();
162
+ if (signal) {
163
+ onAbort = () => {
164
+ aborted = true;
165
+ _killProc();
166
+ };
167
+ if (signal.aborted) onAbort();
168
+ else signal.addEventListener('abort', onAbort, { once: true });
169
+ }
160
170
 
161
171
  proc.stdout.on('data', (c) => chunks.push(c));
162
172
  proc.stderr.on('data', (c) => {
@@ -186,6 +196,10 @@ async function _runGraphBinaryJsonl(absRoot, extraArgs, stdinLines = null) {
186
196
  reject(new Error(`[code-graph] mixdog-graph timed out after ${timeoutMs}ms`));
187
197
  return;
188
198
  }
199
+ if (aborted) {
200
+ reject(new Error('aborted'));
201
+ return;
202
+ }
189
203
  if (code !== 0) {
190
204
  reject(new Error(`[code-graph] mixdog-graph exited ${code}: ${stderrText.trim().slice(0, 200)}`));
191
205
  return;
@@ -215,7 +229,7 @@ async function _runGraphBinaryJsonl(absRoot, extraArgs, stdinLines = null) {
215
229
  throw err;
216
230
  }
217
231
  }
218
- export function _runGraphManifest(absRoot) { return _runGraphBinaryJsonl(absRoot, ['--manifest']); }
232
+ export function _runGraphManifest(absRoot, signal = null) { return _runGraphBinaryJsonl(absRoot, ['--manifest'], null, signal); }
219
233
  export function _runGraphWalk(absRoot) { return _runGraphBinaryJsonl(absRoot, []); }
220
234
  // --files (design A: full-graph resolution) full-parses only `rels` (argv) but
221
235
  // resolves imports across the WHOLE tree. The reused nodes' metas are streamed
@@ -12,7 +12,7 @@
12
12
  // - On any failure: postMessage({ ok: false }). Main thread propagates
13
13
  // an error — find_symbol / code_graph tools throw. No sync fallback.
14
14
  import { parentPort, workerData } from 'node:worker_threads';
15
- import { _buildCodeGraph } from './code-graph.mjs';
15
+ import { _buildCodeGraph, _postCodeGraphWorkerSuccess } from './code-graph.mjs';
16
16
 
17
17
  const cwd = workerData && workerData.cwd ? workerData.cwd : null;
18
18
 
@@ -27,9 +27,12 @@ try {
27
27
  if (!cwd) {
28
28
  parentPort.postMessage({ ok: false });
29
29
  } else {
30
- const graph = await _buildCodeGraph(cwd);
30
+ const graph = await _buildCodeGraph(cwd, {
31
+ manifest: Array.isArray(workerData.manifest) ? workerData.manifest : null,
32
+ signature: typeof workerData.signature === 'string' ? workerData.signature : null,
33
+ });
31
34
  if (graph && typeof graph.signature === 'string') {
32
- parentPort.postMessage({ ok: true, signature: graph.signature, graph });
35
+ _postCodeGraphWorkerSuccess(graph, (message) => parentPort.postMessage(message));
33
36
  } else {
34
37
  parentPort.postMessage({ ok: false });
35
38
  }
@@ -3,17 +3,14 @@ export const CODE_GRAPH_TOOL_DEFS = [
3
3
  name: 'code_graph',
4
4
  title: 'Code Graph',
5
5
  annotations: { title: 'Code Graph', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false, compressibleLossless: true },
6
- description: 'Repo-local code structure/flow lookup (not web): symbols/references/calls/deps. Symbols by name need no pre-verification; files[] must be verified paths. Batch symbols[]/files[].',
6
+ description: 'Repo-local code structure/flow (not web): symbols/references/calls/deps Public files[] must be verified paths; verified source files only. IDs→graph; literal/zero→grep. Exact identifiers: find_symbol/references/callers/callees; keywords: symbol_search/search Unsupported target arrays omitted, never silently mixed. Legacy file/symbol/language compatible, hidden. Batch symbols[]/files[] by mode',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: {
10
- mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers'], description: 'Repo-local graph operation.' },
11
- files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Verified source file(s); array fans out per file. `file` alias too.' },
12
- symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Identifier(s)/keyword(s); array batches in one call. `symbol` alias too. Required for callers/callees/references/find_symbol/symbol_search.' },
13
- symbol: { type: 'string', description: 'Singular alias for symbols.' },
14
- file: { type: 'string', description: 'Singular alias for files.' },
10
+ mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'], description: 'Repo-local: file modes={overview,imports,dependents,related,impact}; symbols with files→files[] file outline; symbol modes={find_symbol,symbol_search,search,references,callers,callees}; fileless symbols→symbol_search keywords.' },
11
+ files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Verified source files; supported targets only.' },
12
+ symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Exact identifiers: find_symbol/references/callers/callees; keywords: symbol_search/search; multiple exact symbols use one symbols[] call; one call.' },
15
13
  body: { type: 'boolean', description: 'Include body.' },
16
- language: { type: 'string', description: 'Language hint.' },
17
14
  limit: { type: 'number', minimum: 1, description: 'Max results.' },
18
15
  depth: { type: 'number', minimum: 1, maximum: 5, description: 'Caller depth.' },
19
16
  page: { type: 'number', minimum: 1, description: 'Caller page.' },
@@ -31,6 +31,7 @@ export {
31
31
  prewarmCodeGraphIfProject,
32
32
  buildCodeGraphAsync,
33
33
  _buildCodeGraph,
34
+ _postCodeGraphWorkerSuccess,
34
35
  } from './code-graph/build.mjs';
35
36
  export {
36
37
  resolveSymbolReadSpan,
@@ -26,7 +26,7 @@ export const PATCH_TOOL_DEFS = [
26
26
  name: 'apply_patch',
27
27
  title: 'Mixdog Apply Patch',
28
28
  annotations: { title: 'Mixdog Apply Patch', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, compressible: false, compressibleLossless: true },
29
- description: 'Apply file patches. Put known edits in one patch, not multiple turns; sections run in listed order.',
29
+ description: 'Apply known file edits in one patch; sections run in listed order. Do not split a dependent edit across turns.',
30
30
  freeformDescription: APPLY_PATCH_FREEFORM_DESCRIPTION,
31
31
  freeform: {
32
32
  type: 'grammar',
@@ -496,10 +496,10 @@ export async function sonnetCascade(candidates, rulesDigest, options = {}) {
496
496
  `NO prose, NO preamble, NO meta-commentary. First character must be a digit.`,
497
497
  ].join('\n')
498
498
 
499
- // Hardcoded resolveMaintenancePreset falls back to first preset (HAIKU)
500
- // when no binding exists, which would defeat the cascade. SONNET HIGH
501
- // matches the worker pool's default preset id from agent-config.
502
- const preset = options.cascadePreset || 'SONNET HIGH'
499
+ // Keep the cascade on the same maintenance route as every other memory
500
+ // cycle call. An explicit override remains available for focused tests and
501
+ // controlled callers.
502
+ const preset = options.cascadePreset || resolveMaintenancePreset('memory')
503
503
  const llmCall = typeof options?.callLlm === 'function' ? options.callLlm : callAgentDispatch
504
504
  let raw
505
505
  try {
@@ -2,6 +2,7 @@
2
2
  // DB status/update/merge writers plus the cosine-similarity dedup pass.
3
3
  // Facade (memory-cycle2.mjs) re-exports the public members unchanged.
4
4
  import { deleteRootEmbedding, syncRootEmbedding } from './memory-embed.mjs'
5
+ import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
5
6
  import { callAgentDispatch } from './agent-ipc.mjs'
6
7
  import { __mixdogMemoryLog, throwIfAborted } from './memory-cycle2-shared.mjs'
7
8
 
@@ -318,7 +319,7 @@ async function _llmJudgePair(summaryA, summaryB, siblingContext = [], options =
318
319
  agent: 'cycle2-agent',
319
320
  taskType: 'maintenance',
320
321
  mode: 'cycle2-phase_merge_judge',
321
- preset: 'HAIKU',
322
+ preset: options.preset || resolveMaintenancePreset('memory'),
322
323
  timeout: 30000,
323
324
  cwd: null,
324
325
  }, prompt)
@@ -413,7 +414,7 @@ export async function runPhaseMerge(db, options = {}) {
413
414
  // callAgentDispatch (IPC), which is unavailable in the standalone
414
415
  // memory service and failed every judge call with
415
416
  // `agent-ipc: IPC channel unavailable`.
416
- { signal, callLlm: options?.callLlm },
417
+ { signal, preset: options?.preset, callLlm: options?.callLlm },
417
418
  )
418
419
  throwIfAborted(signal)
419
420
  if (shouldMerge) await doMerge(pair.a, pair.b, pair.sim)
@@ -459,7 +460,7 @@ export async function runPhaseMerge(db, options = {}) {
459
460
  String(row.entry_summary ?? ''),
460
461
  String(row.core_summary ?? ''),
461
462
  [],
462
- { signal, callLlm: options?.callLlm },
463
+ { signal, preset: options?.preset, callLlm: options?.callLlm },
463
464
  )
464
465
  throwIfAborted(signal)
465
466
  if (!verdictMerge) continue
@@ -51,6 +51,17 @@ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispa
51
51
  }, prompt)
52
52
  }
53
53
 
54
+ export async function invokeCycle3Maintenance(prompt, options = {}) {
55
+ const preset = options.preset || resolveMaintenancePreset('memory')
56
+ return await invokeLlm(
57
+ prompt,
58
+ 'cycle3-review',
59
+ preset,
60
+ Number(options.timeout ?? 600000),
61
+ options.callLlm,
62
+ )
63
+ }
64
+
54
65
  function throwIfAborted(signal) {
55
66
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
56
67
  }
@@ -507,7 +518,6 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
507
518
  .replace('{{CORE_REVIEW}}', coreReview)
508
519
  .replace('{{CURRENT_RULES}}', rulesDigest)
509
520
 
510
- const preset = resolveMaintenancePreset('memory')
511
521
  const timeout = Number(config?.cycle3?.timeout ?? 600000)
512
522
  const mode = 'cycle3-review'
513
523
 
@@ -516,7 +526,7 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
516
526
  let raw
517
527
  try {
518
528
  throwIfAborted(signal)
519
- raw = await invokeLlm(prompt, mode, preset, timeout, options.callLlm)
529
+ raw = await invokeCycle3Maintenance(prompt, { ...options, timeout, mode })
520
530
  } catch (err) {
521
531
  if (signal?.aborted) throw signal.reason ?? err
522
532
  __mixdogMemoryLog(`[cycle3] LLM error: ${err.message}\n`)
@@ -38,6 +38,13 @@ function scheduleFlush(path, q) {
38
38
  q.timer.unref?.();
39
39
  }
40
40
 
41
+ function deleteIfIdle(path, q) {
42
+ if (!q.timer && !q.flushing && !q.inFlight && q.chunks.length === 0
43
+ && queues.get(path) === q) {
44
+ queues.delete(path);
45
+ }
46
+ }
47
+
41
48
  function _flush(path, q) {
42
49
  if (q.flushing) return;
43
50
  if (q.chunks.length === 0) return;
@@ -70,7 +77,7 @@ function _flush(path, q) {
70
77
  if (q.chunks.length > 0) {
71
78
  if (q.bytes >= BUFFER_FLUSH_BYTES) _flush(path, q);
72
79
  else scheduleFlush(path, q);
73
- }
80
+ } else deleteIfIdle(path, q);
74
81
  });
75
82
  }
76
83
 
@@ -120,7 +127,10 @@ export function drainPathSync(path) {
120
127
  }
121
128
  q.inFlight = null;
122
129
  }
123
- if (q.chunks.length === 0) return;
130
+ if (q.chunks.length === 0) {
131
+ deleteIfIdle(path, q);
132
+ return;
133
+ }
124
134
  const data = q.chunks.join('');
125
135
  q.chunks = [];
126
136
  q.bytes = 0;
@@ -129,6 +139,7 @@ export function drainPathSync(path) {
129
139
  } catch {
130
140
  // Best-effort exit drain; nothing to recover from here.
131
141
  }
142
+ deleteIfIdle(path, q);
132
143
  }
133
144
 
134
145
  /**
@@ -250,10 +250,13 @@ export function bridgeAgentModelSummary(args) {
250
250
  }
251
251
 
252
252
  export function summarizeLineWindow(a) {
253
+ const hasOffset = a.offset != null;
253
254
  const offset = a.offset ?? a.start_line ?? a.startLine ?? a.line;
254
255
  const limit = a.limit ?? a.line_count ?? a.lineCount ?? a.lines;
255
256
  if (offset == null && limit == null) return '';
256
- const start = Number(offset);
257
+ // `read` uses a zero-based offset, while legacy start_line/line inputs are
258
+ // already one-based. Surface the human line number, never a raw array index.
259
+ const start = Number(offset) + (hasOffset ? 1 : 0);
257
260
  const count = Number(limit);
258
261
  if (Number.isFinite(start) && Number.isFinite(count) && count > 0) {
259
262
  return `lines ${start}-${Math.max(start, start + count - 1)}`;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared terminal-status parsing for tool result surfaces.
3
+ *
4
+ * A terminal result status describes the tool's reported outcome; it is not
5
+ * evidence that the tool invocation itself failed. Call-failure accounting is
6
+ * owned by the TUI engine's isError/toolKind envelope fields.
7
+ */
8
+ export function normalizeToolTerminalStatus(value) {
9
+ const raw = String(value || '').trim().toLowerCase();
10
+ if (!raw) return '';
11
+ if (/^(running|pending|queued|in_progress|in-progress)$/.test(raw)) return 'running';
12
+ if (/^(completed|complete|done|success|succeeded|ok)$/.test(raw)) return 'completed';
13
+ if (/^(failed|fail|error|errored|timeout|timed_out|killed)$/.test(raw)) return 'failed';
14
+ if (/^(cancelled|canceled|cancel)$/.test(raw)) return 'cancelled';
15
+ if (/^(denied|deny|refused|rejected)$/.test(raw)) return 'denied';
16
+ return '';
17
+ }
18
+
19
+ export function toolResultTerminalStatus(text) {
20
+ const body = String(text || '');
21
+ const tagged = body.match(/<status[^>]*>([\s\S]*?)<\/status>/i)?.[1]?.trim();
22
+ if (tagged) return normalizeToolTerminalStatus(tagged);
23
+ const bracketed = body.match(/^\[status:\s*([^\]]*)\]/mi)?.[1]?.trim();
24
+ if (bracketed) return normalizeToolTerminalStatus(bracketed);
25
+ const inline = body.match(/^(?:status|state):\s*([^\s·,;]+)/mi)?.[1]?.trim();
26
+ return normalizeToolTerminalStatus(inline);
27
+ }
@@ -452,7 +452,10 @@ export function toolWorkUnit(name, args = {}, category = '') {
452
452
  case 'read_mcp_resource':
453
453
  return unitDescriptor('Read', { count: queryCount(a, 'uri', 'uris') || 1, noun: 'resource' });
454
454
  case 'apply_patch': {
455
- const creating = a.old_string === '';
455
+ const patchText = String(a.patch ?? '');
456
+ const creating = a.old_string === '' || /^\*\*\*\s+Add File:/mi.test(patchText);
457
+ const deleting = (!creating && a.new_string === '' && a.old_string != null)
458
+ || /^\*\*\*\s+Delete File:/mi.test(patchText);
456
459
  // A dry_run patch validates the diff WITHOUT writing any file, so the
457
460
  // header must not claim "Editing/Edited" (which made a pure validation
458
461
  // look like a real edit). Surface it as "Checking/Checked" instead.
@@ -466,8 +469,8 @@ export function toolWorkUnit(name, args = {}, category = '') {
466
469
  }
467
470
  return unitDescriptor('Patch', {
468
471
  count: patchFileCount(a) || 1,
469
- active: creating ? 'Creating' : 'Editing',
470
- done: creating ? 'Created' : 'Edited',
472
+ active: creating ? 'Creating' : deleting ? 'Deleting' : 'Editing',
473
+ done: creating ? 'Created' : deleting ? 'Deleted' : 'Edited',
471
474
  noun: 'file',
472
475
  });
473
476
  }
@@ -175,6 +175,20 @@ export function autoClearProviderDefaults(providerIdleMs = null) {
175
175
  });
176
176
  }
177
177
 
178
+ // Agent terminal retention is deliberately narrower than Auto Clear's general
179
+ // resolution: listed providers use their provider cache lifetime. Unlisted
180
+ // providers use the Advanced "default" row, or the built-in default when no
181
+ // override is configured, so completed agents are still eventually reclaimed.
182
+ export function resolveAgentTerminalReapMs(config, provider) {
183
+ const key = clean(provider).toLowerCase();
184
+ const raw = config?.autoClear && typeof config.autoClear === 'object' ? config.autoClear : {};
185
+ const overrides = normalizeAutoClearProviderIdleMs(raw.providerIdleMs);
186
+ if (!key || key === 'default' || !Object.hasOwn(AUTO_CLEAR_PROVIDER_IDLE_MS, key)) {
187
+ return overrides.default ?? AUTO_CLEAR_PROVIDER_IDLE_MS.default;
188
+ }
189
+ return overrides[key] ?? AUTO_CLEAR_PROVIDER_IDLE_MS[key];
190
+ }
191
+
178
192
  // Resolve the effective auto-clear idle window for a config + provider:
179
193
  // an explicit user-set idleMs (config.autoClear.idleMs) always wins; else
180
194
  // fall back to the provider's default; else the global 1h default.
@@ -100,6 +100,7 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
100
100
  const lastContextTokens = Number(session?.lastContextTokens || 0);
101
101
  const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
102
102
  messageCount: messageSummary.count,
103
+ estimatedMessageTokens: messageSummary.estimatedTokens,
103
104
  });
104
105
  const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
105
106
  const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
@@ -2,7 +2,7 @@
2
2
  import { clean } from './session-text.mjs';
3
3
 
4
4
  export const TOOL_MODES = new Set(['full', 'readonly', 'lead']);
5
- export const ALL_EFFORT_LEVELS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max']);
5
+ export const ALL_EFFORT_LEVELS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
6
6
  export const EFFORT_LABELS = {
7
7
  none: 'None',
8
8
  low: 'Low',
@@ -10,11 +10,14 @@ export const EFFORT_LABELS = {
10
10
  high: 'High',
11
11
  xhigh: 'Extra High',
12
12
  max: 'Max',
13
+ ultra: 'Ultra',
13
14
  };
14
15
 
15
16
  export const EFFORT_OPTIONS_BY_PROVIDER = {
16
17
  openai: ['none', 'low', 'medium', 'high', 'xhigh'],
17
- 'openai-oauth': ['none', 'low', 'medium', 'high', 'xhigh'],
18
+ // gpt-5.6+ catalogs declare max/ultra; the openai-oauth transport folds
19
+ // ultra -> max on the wire (openai-oauth.mjs _normalizeReasoningEffort).
20
+ 'openai-oauth': ['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
18
21
  anthropic: ['low', 'medium', 'high', 'xhigh', 'max'],
19
22
  'anthropic-oauth': ['low', 'medium', 'high', 'xhigh', 'max'],
20
23
  xai: ['none', 'low', 'medium', 'high'],
@@ -36,6 +39,7 @@ export const EFFORT_BY_FAMILY = {
36
39
  grok: ['none', 'low', 'medium', 'high'],
37
40
  };
38
41
  export const EFFORT_FALLBACKS = {
42
+ ultra: ['ultra', 'max', 'xhigh', 'high', 'medium', 'low'],
39
43
  max: ['max', 'xhigh', 'high', 'medium', 'low'],
40
44
  xhigh: ['xhigh', 'high', 'medium', 'low'],
41
45
  high: ['high', 'medium', 'low'],
@@ -30,6 +30,7 @@ export function createLifecycleApi(deps) {
30
30
  invalidateContextStatusCache, invalidatePreSessionToolSurface,
31
31
  applyResolvedCwd, resolveRoute, applyDeferredToolSurface, standaloneTools,
32
32
  pushTranscriptRebind,
33
+ notificationListeners, remoteStateListeners,
33
34
  } = deps;
34
35
  return {
35
36
  async close(reason = 'cli-exit', options = {}) {
@@ -132,6 +133,9 @@ export function createLifecycleApi(deps) {
132
133
  ok = mgr.closeSession(session.id, reason, { tombstone });
133
134
  setSession(null);
134
135
  }
136
+ invalidateContextStatusCache();
137
+ notificationListeners?.clear?.();
138
+ remoteStateListeners?.clear?.();
135
139
  const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
136
140
  ? import('../runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
137
141
  .then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
@@ -56,10 +56,13 @@ export function compareProviderModelRecency(a, b) {
56
56
  }
57
57
  const ta = providerModelReleaseTime(a);
58
58
  const tb = providerModelReleaseTime(b);
59
- if (ta !== tb) return tb - ta;
60
- if (a.latest !== b.latest) return a.latest ? -1 : 1;
61
59
  const versionDelta = compareProviderModelVersion(a, b);
60
+ // Release dates win only when both sides have one; sparse OAuth catalogs
61
+ // must not sink undated (often newest) models below dated/latest ones.
62
+ if (ta > 0 && tb > 0 && ta !== tb) return tb - ta;
62
63
  if (versionDelta) return versionDelta;
64
+ if (a.latest !== b.latest) return a.latest ? -1 : 1;
65
+ if (ta !== tb) return tb - ta;
63
66
  return clean(a.display || a.id).localeCompare(clean(b.display || b.id));
64
67
  }
65
68