mixdog 0.9.46 → 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 (90) 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/defaults/cycle3-review-prompt.md +10 -4
  32. package/src/defaults/memory-promote-prompt.md +4 -3
  33. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +16 -5
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +35 -11
  35. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -11
  36. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +28 -0
  37. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +34 -34
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +45 -38
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -11
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +4 -4
  42. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +10 -6
  43. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +4 -3
  44. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +4 -3
  45. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +16 -1
  46. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -2
  47. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +0 -1
  48. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +13 -8
  49. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +30 -16
  50. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +25 -8
  51. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +69 -4
  52. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +5 -4
  53. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +12 -12
  55. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +12 -7
  56. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +77 -18
  57. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  58. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +99 -24
  59. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
  60. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
  61. package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
  62. package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
  63. package/src/runtime/memory/lib/query-handlers.mjs +49 -6
  64. package/src/runtime/memory/lib/recall-format.mjs +21 -6
  65. package/src/runtime/memory/tool-defs.mjs +2 -3
  66. package/src/session-runtime/context-status.mjs +25 -16
  67. package/src/session-runtime/model-route-api.mjs +2 -0
  68. package/src/session-runtime/runtime-core.mjs +2 -2
  69. package/src/session-runtime/session-turn-api.mjs +4 -1
  70. package/src/session-runtime/tool-catalog.mjs +113 -19
  71. package/src/session-runtime/tool-defs.mjs +1 -0
  72. package/src/standalone/agent-tool/helpers.mjs +25 -6
  73. package/src/standalone/agent-tool.mjs +75 -41
  74. package/src/tui/App.jsx +4 -0
  75. package/src/tui/app/input-parsers.mjs +8 -9
  76. package/src/tui/components/Markdown.jsx +6 -1
  77. package/src/tui/components/Message.jsx +11 -2
  78. package/src/tui/components/PromptInput.jsx +19 -21
  79. package/src/tui/components/Spinner.jsx +4 -4
  80. package/src/tui/components/StatusLine.jsx +6 -6
  81. package/src/tui/components/TranscriptItem.jsx +2 -2
  82. package/src/tui/components/prompt-input/immediate-render.mjs +47 -0
  83. package/src/tui/components/tool-execution/surface-detail.mjs +14 -9
  84. package/src/tui/dist/index.mjs +130 -45
  85. package/src/tui/engine/agent-job-feed.mjs +21 -2
  86. package/src/tui/engine/turn.mjs +12 -1
  87. package/src/tui/markdown/measure-rendered-rows.mjs +1 -1
  88. package/src/tui/markdown/streaming-markdown.mjs +20 -0
  89. package/src/ui/statusline.mjs +5 -5
  90. package/src/vendor/statusline/src/gateway/session-routes.mjs +22 -10
@@ -181,6 +181,32 @@ export function formatShellToolFailure(message) {
181
181
  return `Error: [shell-tool-failed] ${text}`;
182
182
  }
183
183
 
184
+ export function _shellFailureStatus(result, timeout) {
185
+ // Prefer the signal reported by the process. `killed` is only a fallback
186
+ // for platforms (notably taskkill on Windows) that close without one.
187
+ const signal = result.signal || (result.killed ? 'SIGKILL' : null);
188
+ const exitCode = signal ? null : result.exitCode;
189
+ const shellToolFailed = result.failurePhase === 'tool' || !!result.outputCaptureError;
190
+ const killCause = result.killCause || null;
191
+ const causeDetail = killCause ? ` cause: ${killCause}` : '';
192
+ const signalDetail = signal ? ` signal: ${signal}` : '';
193
+ const timeoutHint = result.timedOut
194
+ ? ` — command killed after ${timeout} ms; if it legitimately needs longer, retry with a larger timeout`
195
+ : '';
196
+ const statusDetail = shellToolFailed
197
+ ? `[${result.outputCaptureError ? 'output capture failed' : (result.failureReason || 'tool failed')}${causeDetail}${signalDetail}]`
198
+ : (result.timedOut
199
+ ? `[timeout: ${timeout}ms${signalDetail || ' signal: unknown'}${causeDetail}]${timeoutHint}`
200
+ : (signal
201
+ ? `[signal: ${signal}${causeDetail}]`
202
+ : (exitCode !== 0 && exitCode !== null ? `[exit code: ${exitCode}]` : '')));
203
+ return { signal, exitCode, shellToolFailed, statusDetail };
204
+ }
205
+
206
+ export function _composeShellFailure(statusMarker, errorPrefix, warningBlock, payload) {
207
+ return `${errorPrefix}${statusMarker}${warningBlock ? `\n${warningBlock}` : ''}\n\n${payload}`;
208
+ }
209
+
184
210
  export async function executeBashTool(args, workDir, options = {}) {
185
211
  const requestedCwd = args.cwd ?? args.workdir;
186
212
  const cwdResult = resolveOptionalCwd(requestedCwd, workDir);
@@ -539,12 +565,9 @@ export async function executeBashTool(args, workDir, options = {}) {
539
565
  }
540
566
  const stdout = stripAnsi(result.stdout || '');
541
567
  const stderr = stripAnsi(result.stderr || '');
542
- const signal = result.timedOut
543
- ? 'SIGTERM'
544
- : (result.killed ? 'SIGKILL' : (result.signal || null));
545
- const exitCode = signal ? null : result.exitCode;
568
+ const failureStatus = _shellFailureStatus(result, timeout);
569
+ const { signal, exitCode, shellToolFailed } = failureStatus;
546
570
  const benignExitOne = _isBenignSearchExitOne(command, exitCode, signal, stderr);
547
- const shellToolFailed = result.failurePhase === 'tool' || !!result.outputCaptureError;
548
571
  const shellRunFailed = !shellToolFailed && (!!signal || (exitCode !== 0 && exitCode !== null && !benignExitOne));
549
572
  const isReallyErrored = shellToolFailed || shellRunFailed;
550
573
  const _driftNote = '';
@@ -556,16 +579,7 @@ export async function executeBashTool(args, workDir, options = {}) {
556
579
  // Timeout marker carries an inline recovery hint so the caller can
557
580
  // act in one round (increase ceiling or detach) instead of repeating
558
581
  // the same command and hitting the same wall.
559
- const timeoutHint = result.timedOut
560
- ? ` — command killed after ${timeout} ms; if it legitimately needs longer, retry with a larger timeout`
561
- : '';
562
- const statusDetail = shellToolFailed
563
- ? `[${result.outputCaptureError ? 'output capture failed' : (result.failureReason || 'tool failed')}]`
564
- : (result.timedOut
565
- ? `[timeout: ${timeout}ms signal: ${signal || 'SIGTERM'}]${timeoutHint}`
566
- : (signal
567
- ? `[signal: ${signal}]`
568
- : (shellRunFailed ? `[exit code: ${exitCode}]` : '')));
582
+ const statusDetail = failureStatus.statusDetail;
569
583
  const statusMarker = shellToolFailed
570
584
  ? `[shell-tool-failed] ${statusDetail}`
571
585
  : (shellRunFailed ? `[shell-run-failed] ${statusDetail}` : '');
@@ -602,7 +616,7 @@ export async function executeBashTool(args, workDir, options = {}) {
602
616
  wmicRewrite?.note || '',
603
617
  ].filter(Boolean).join('\n');
604
618
  const payload = `${body}${stderrBlock}${spillBlock}${_driftNote}`;
605
- if (statusMarker) return _prependDestructiveWarning(command, `${errorPrefix}${warningBlock ? `${warningBlock}\n` : ''}${statusMarker}\n\n${payload}`);
619
+ if (statusMarker) return _prependDestructiveWarning(command, _composeShellFailure(statusMarker, errorPrefix, warningBlock, payload));
606
620
  return _prependDestructiveWarning(command, warningBlock ? `${warningBlock}\n${payload}` : payload);
607
621
  }
608
622
  finally {
@@ -155,12 +155,23 @@ export function _postCodeGraphWorkerSuccess(graph, postMessage, drainCache = dra
155
155
  postMessage({ ok: true, signature: graph.signature, graph });
156
156
  }
157
157
 
158
- export function prewarmCodeGraph(cwd) {
158
+ // Exported for the focused worker-protocol regression test.
159
+ export function _codeGraphWorkerFailure(message) {
160
+ const error = typeof message?.error === 'string' ? message.error.trim() : '';
161
+ return new Error(error || 'code-graph prewarm worker failed');
162
+ }
163
+
164
+ // Exported for the focused best-effort prewarm regression test.
165
+ export function _prewarmCodeGraph(cwd, build = buildCodeGraphAsync) {
159
166
  if (!cwd) return;
160
167
  // Reuse the buildCodeGraphAsync single-flight path. Fire-and-forget, and
161
168
  // best-effort: skip a fresh worker spawn when the child-spawn gate is busy
162
169
  // so this warm never queues ahead of real code_graph/find queries.
163
- buildCodeGraphAsync(cwd, null, { bestEffort: true }).catch(() => { /* best-effort */ });
170
+ build(cwd, null, { bestEffort: true }).catch(() => { /* best-effort */ });
171
+ }
172
+
173
+ export function prewarmCodeGraph(cwd) {
174
+ _prewarmCodeGraph(cwd);
164
175
  }
165
176
 
166
177
  export function prewarmCodeGraphSymbols(cwd, symbols, { language = null } = {}) {
@@ -248,7 +259,7 @@ export async function buildCodeGraphAsync(cwd, signal = null, { bestEffort = fal
248
259
  }
249
260
  }
250
261
 
251
- function _spawnCodeGraphWorker(
262
+ export function _spawnCodeGraphWorker(
252
263
  cwd,
253
264
  graphCwd,
254
265
  genAtStart,
@@ -256,6 +267,12 @@ function _spawnCodeGraphWorker(
256
267
  preAcquiredRelease = null,
257
268
  manifest = null,
258
269
  signature = null,
270
+ {
271
+ createWorker = (url, options) => new Worker(url, options),
272
+ getGeneration = _getCodeGraphGen,
273
+ setMemoryCache = _setCodeGraphCache,
274
+ setDiskCache = _setDiskCodeGraphEntry,
275
+ } = {},
259
276
  ) {
260
277
  let _worker = null;
261
278
  return new Promise((resolve, reject) => {
@@ -281,7 +298,7 @@ function _spawnCodeGraphWorker(
281
298
  if (signal?.aborted) { settle(new Error('aborted')); return; }
282
299
  const workerUrl = new URL('../code-graph-prewarm-worker.mjs', import.meta.url);
283
300
  try {
284
- _worker = new Worker(workerUrl, {
301
+ _worker = createWorker(workerUrl, {
285
302
  workerData: { cwd, manifest, signature },
286
303
  execArgv: [],
287
304
  });
@@ -305,14 +322,14 @@ function _spawnCodeGraphWorker(
305
322
  w.once('message', (msg) => {
306
323
  try {
307
324
  if (msg && msg.ok && msg.graph && typeof msg.signature === 'string') {
308
- const genStillCurrent = _getCodeGraphGen(graphCwd) === genAtStart;
325
+ const genStillCurrent = getGeneration(graphCwd) === genAtStart;
309
326
  if (genStillCurrent) {
310
- _setCodeGraphCache(graphCwd, { ts: Date.now(), signature: msg.signature, graph: msg.graph });
311
- _setDiskCodeGraphEntry(graphCwd, msg.graph);
327
+ setMemoryCache(graphCwd, { ts: Date.now(), signature: msg.signature, graph: msg.graph });
328
+ setDiskCache(graphCwd, msg.graph);
312
329
  }
313
330
  settle(genStillCurrent ? msg.graph : new Error('code-graph build invalidated during prewarm'));
314
331
  } else {
315
- settle(new Error('code-graph prewarm worker failed'));
332
+ settle(_codeGraphWorkerFailure(msg));
316
333
  }
317
334
  } catch (e) { settle(e instanceof Error ? e : new Error(String(e))); }
318
335
  });
@@ -50,6 +50,7 @@ function _collectGraphSymbolList(args) {
50
50
  }
51
51
 
52
52
  const CODE_GRAPH_FILE_BATCH_CAP = 20;
53
+ const _AGGREGATE_FILE_WILDCARD_RE = /[*?[\]{}]/;
53
54
 
54
55
  // Absorb: file/files arriving as a JSON-stringified array
55
56
  // (file:"[\"a.mjs\",\"b.mjs\"]") — parse to a real array so the graph lookup
@@ -74,21 +75,21 @@ function _normalizeGraphFileArgs(args) {
74
75
  if (fileArr) { out.files = Array.isArray(out.files) ? [...fileArr, ...out.files] : fileArr; delete out.file; }
75
76
  if (filesArr) out.files = filesArr;
76
77
  // Collapse a lone entry back to the single-file field for the fast path.
77
- if (Array.isArray(out.files) && out.files.length === 1 && !out.file) {
78
+ if (Array.isArray(out.files) && out.files.length === 1 && !out.file && !filesArr) {
78
79
  out.file = out.files[0];
79
80
  delete out.files;
80
81
  }
81
82
  return out;
82
83
  }
83
84
 
84
- function _collectGraphFileList(args) {
85
+ function _collectGraphFileList(args, { cap = true } = {}) {
85
86
  const split = (s) => String(s || '').split(/,+/).map((t) => t.trim()).filter(Boolean);
86
87
  const list = [...new Set([
87
88
  ...(Array.isArray(args?.files) ? args.files.map((f) => String(f || '').trim()).filter(Boolean) : []),
88
89
  ...(typeof args?.files === 'string' ? split(args.files) : []),
89
90
  ...(typeof args?.file === 'string' && args.file.trim() ? [args.file.trim()] : []),
90
91
  ])];
91
- if (list.length > CODE_GRAPH_FILE_BATCH_CAP) {
92
+ if (cap && list.length > CODE_GRAPH_FILE_BATCH_CAP) {
92
93
  const capped = list.slice(0, CODE_GRAPH_FILE_BATCH_CAP);
93
94
  capped._capped = true;
94
95
  return capped;
@@ -96,6 +97,57 @@ function _collectGraphFileList(args) {
96
97
  return list;
97
98
  }
98
99
 
100
+ function _hasAggregateFileArgs(args) {
101
+ return (Array.isArray(args?.files) && args.files.some((f) => String(f || '').trim()))
102
+ || (typeof args?.files === 'string' && args.files.trim());
103
+ }
104
+
105
+ // An invalid caller cwd may be recovered for an explicit files aggregate only
106
+ // when every supplied anchor points at the same detectable project. Do not use
107
+ // the batch cap here: an omitted anchor could belong to another project.
108
+ function _resolveAggregateFileProjectRoot(args, baseCwd) {
109
+ if (!_hasAggregateFileArgs(args)) return null;
110
+ // Comma-delimited strings are parsed for normal batch dispatch, but are not
111
+ // unambiguous enough to select a project root. JSON array strings have
112
+ // already been normalized to an actual array above.
113
+ if (typeof args?.files === 'string' && args.files.includes(',')) return null;
114
+ const files = _collectGraphFileList(args, { cap: false });
115
+ // Recovery cannot silently discard anchors that normal batch dispatch caps.
116
+ if (files.length > CODE_GRAPH_FILE_BATCH_CAP) return null;
117
+ const roots = new Set();
118
+ for (const file of files) {
119
+ // Never infer a root from a glob-shaped anchor, including a literal file
120
+ // whose name contains a glob metacharacter.
121
+ if (_AGGREGATE_FILE_WILDCARD_RE.test(file)) return null;
122
+ const abs = isAbsolute(file) ? pathResolve(file) : pathResolve(baseCwd, file);
123
+ if (!existsSync(abs)) return null;
124
+ let isDirectory = false;
125
+ try { isDirectory = statSync(abs).isDirectory(); } catch { return null; }
126
+ const root = isDirectory ? _findDirProjectRoot(abs) : _resolveFileProjectRoot(abs);
127
+ if (!root) return null;
128
+ roots.add(pathResolve(root));
129
+ if (roots.size > 1) return null;
130
+ }
131
+ return roots.size === 1 ? [...roots][0] : null;
132
+ }
133
+
134
+ // Aggregate recovery resolves relative anchors against the caller's original
135
+ // cwd. Keep those resolved paths when dispatching under the recovered root;
136
+ // otherwise codeGraph resolves them a second time below that root.
137
+ function _absolutizeAggregateFileArgs(args, baseCwd) {
138
+ const absolutize = (file) => {
139
+ const trimmed = String(file || '').trim();
140
+ return trimmed && !isAbsolute(trimmed) ? pathResolve(baseCwd, trimmed) : file;
141
+ };
142
+ return {
143
+ ...args,
144
+ file: typeof args?.file === 'string' ? absolutize(args.file) : args?.file,
145
+ files: Array.isArray(args?.files)
146
+ ? args.files.map(absolutize)
147
+ : (typeof args?.files === 'string' ? absolutize(args.files) : args?.files),
148
+ };
149
+ }
150
+
99
151
  async function codeGraph(args, cwd, signal = null, options = {}) {
100
152
  let mode = String(args?.mode || '').trim();
101
153
  if (!mode) throw new Error('code_graph: "mode" is required');
@@ -539,8 +591,21 @@ export async function executeCodeGraphTool(name, args, cwd, signal = null, optio
539
591
  delete args.file;
540
592
  }
541
593
  const fileArg = (args && typeof args.file === 'string' && args.file.trim()) ? args.file.trim() : '';
594
+ const hasAggregateFileArgs = _hasAggregateFileArgs(args);
542
595
  let effectiveCwd = baseCwd;
543
- if (fileArg) {
596
+ const baseProjectRoot = _findDirProjectRoot(baseCwd);
597
+ if (hasAggregateFileArgs && !baseProjectRoot) {
598
+ const aggregateRoot = _resolveAggregateFileProjectRoot(args, baseCwd);
599
+ if (!aggregateRoot) {
600
+ throw new Error(
601
+ `${name}: cwd '${baseCwd}' is not inside a project and aggregate file anchors do not all `
602
+ + `exist under exactly one detectable project root. Refusing to index an arbitrary tree.`,
603
+ );
604
+ }
605
+ effectiveCwd = aggregateRoot;
606
+ args = _absolutizeAggregateFileArgs(args, baseCwd);
607
+ }
608
+ if (fileArg && !hasAggregateFileArgs) {
544
609
  const abs = isAbsolute(fileArg) ? pathResolve(fileArg) : pathResolve(baseCwd, fileArg);
545
610
  if (!existsSync(abs)) {
546
611
  const elsewhere = findFileByBasename(pathResolve(baseCwd), abs);
@@ -9,8 +9,8 @@
9
9
  // seeds _codeGraphCache directly. Graph is structuredClone-transferred —
10
10
  // Maps/Sets of plain objects only, runtime caches are empty Maps that
11
11
  // survive the clone.
12
- // - On any failure: postMessage({ ok: false }). Main thread propagates
13
- // an error — find_symbol / code_graph tools throw. No sync fallback.
12
+ // - On any failure: postMessage({ ok: false, error }). Main thread propagates
13
+ // the worker error — find_symbol / code_graph tools throw. No sync fallback.
14
14
  import { parentPort, workerData } from 'node:worker_threads';
15
15
  import { _buildCodeGraph, _postCodeGraphWorkerSuccess } from './code-graph.mjs';
16
16
 
@@ -37,6 +37,7 @@ try {
37
37
  parentPort.postMessage({ ok: false });
38
38
  }
39
39
  }
40
- } catch {
41
- parentPort.postMessage({ ok: false });
40
+ } catch (error) {
41
+ const message = (error instanceof Error ? error.message : String(error)).trim();
42
+ parentPort.postMessage(message ? { ok: false, error: message } : { ok: false });
42
43
  }
@@ -3,7 +3,7 @@ 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 code structure/flow over verified source files only. IDs→graph; literal/zero→grep. Exact identifiers: find_symbol/references/callers/callees; keywords: symbol_search/search. Unsupported target arrays are omitted, never silently mixed. Batch symbols[]/files[] by mode.',
6
+ description: 'Repo code structure/flow over verified source files only. IDs→graph; literal/zero→grep. File modes take files[]; symbol modes find_symbol/symbol_search/search/references/callers/callees take symbols[]. Exact identifiers use find_symbol/references/callers/callees and keywords use symbol_search/search. Unsupported target arrays are omitted, never silently mixed. Batch targets per mode.',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: {
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.7.18",
3
- "_comment": "Rewritten by .github/workflows/graph-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-graph binary on the GitHub release. A local cargo build under native/mixdog-graph/target/release always takes precedence at runtime. (v0.5.236 entries were filled manually after CI's commit step hit detached HEAD; the workflow now checks out ref: main so future releases self-update.)",
2
+ "version": "0.1.1",
3
+ "_comment": "Synced from immutable graph-v release assets.",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/tribgames/mixdog/releases/download/v0.7.18/mixdog-graph-darwin-arm64",
7
- "sha256": "34653a7cdf457295c4835fa9819627e9690c323b6012e6f571259478406bbe04"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.1/mixdog-graph-darwin-arm64",
7
+ "sha256": "3fb76a5e3064547b68e7bcab6ce2a855ce615a732157d836cb561c5d7c8d86b6"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/v0.7.18/mixdog-graph-darwin-x64",
11
- "sha256": "1c0648ed8a1b9fd2d8bc44d85b91262cb11e0773d2280d6ec351ee1ff1c579f0"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.1/mixdog-graph-darwin-x64",
11
+ "sha256": "64eeffc17984a76b0fbf7d85f9d84cb5e1bb949a251053feb260f70481235bb1"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/v0.7.18/mixdog-graph-linux-arm64",
15
- "sha256": "71c9d69125b97e93466e5a5613c00db2d797c1f25cc4237e776225574c5f6970"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.1/mixdog-graph-linux-arm64",
15
+ "sha256": "15d5cbb7575b3e0671c6f5fbef3460c5c9353f17968a80faba737fcc15b49c33"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/v0.7.18/mixdog-graph-linux-x64",
19
- "sha256": "4800a7ac77b777ddb5862f252cdc00b55230455921e0c58416d7a77b120604a7"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.1/mixdog-graph-linux-x64",
19
+ "sha256": "5079544c09ae2311295cef21978fda3c1c06f93beaaf666cfe1f68971d5b27ae"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/v0.7.18/mixdog-graph-win32-x64.exe",
23
- "sha256": "1d0d62f5b78965c7d5b05ebc7cf7bdef1a3e14cc2600aa205ca762e56c446934"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.1/mixdog-graph-win32-x64.exe",
23
+ "sha256": "acfbcd02b96154f4fb342a44dec1a4808752944c5941bbb7983d84ed64464830"
24
24
  }
25
25
  }
26
26
  }
@@ -70,30 +70,35 @@ function nativePatchPersistent() {
70
70
  return /^(1|true|yes|server|persistent)$/i.test(nativePatchMode());
71
71
  }
72
72
 
73
- export function nativePatchBinPath() {
73
+ export function nativePatchBinPath(options = {}) {
74
74
  if (process.env.MIXDOG_PATCH_NATIVE_BIN) return process.env.MIXDOG_PATCH_NATIVE_BIN;
75
75
  // Local cargo build first, then a fetched/cached prebuilt; absence is
76
76
  // a hard error at dispatch (no JS fallback in native-only mode).
77
- if (existsSync(NATIVE_PATCH_DEFAULT_BIN)) return NATIVE_PATCH_DEFAULT_BIN;
78
- return findCachedPatchBinary(getPluginData()) || NATIVE_PATCH_DEFAULT_BIN;
77
+ const defaultBin = options.defaultBin || NATIVE_PATCH_DEFAULT_BIN;
78
+ if (existsSync(defaultBin)) return defaultBin;
79
+ const dataDir = options.dataDir || getPluginData();
80
+ return findCachedPatchBinary(dataDir, options.fetcherOptions) || defaultBin;
79
81
  }
80
82
 
81
- export async function ensureNativePatchBinaryAvailable() {
83
+ export async function ensureNativePatchBinaryAvailable(options = {}) {
82
84
  if (!nativePatchEnabled()) {
83
85
  throw new Error('apply_patch: native engine disabled via MIXDOG_PATCH_NATIVE; set it to "auto" or "1" to apply patches.');
84
86
  }
85
- const current = nativePatchBinPath();
87
+ const current = nativePatchBinPath(options);
86
88
  if (existsSync(current)) return current;
87
89
  if (process.env.MIXDOG_PATCH_NATIVE_BIN) {
88
90
  throw new Error(`apply_patch: native patch binary not found at MIXDOG_PATCH_NATIVE_BIN=${current}.`);
89
91
  }
90
92
  try {
91
- const fetched = await ensurePatchBinary(getPluginData());
93
+ const fetched = await ensurePatchBinary(
94
+ options.dataDir || getPluginData(),
95
+ options.fetcherOptions,
96
+ );
92
97
  if (fetched && existsSync(fetched)) return fetched;
93
98
  } catch (err) {
94
99
  throw new Error(`apply_patch: native patch binary unavailable — ${err?.message || String(err)}`);
95
100
  }
96
- const resolved = nativePatchBinPath();
101
+ const resolved = nativePatchBinPath(options);
97
102
  if (existsSync(resolved)) return resolved;
98
103
  throw new Error(`apply_patch: native patch binary not found at ${resolved}.`);
99
104
  }
@@ -7,6 +7,8 @@
7
7
  // ensurePatchBinary(dataDir) -> absolute path to the verified binary.
8
8
  // Throws on no-asset / download / verify failure.
9
9
  // findCachedPatchBinary(dataDir) -> path | null (sync, no network).
10
+ // Both accept an optional dependency object used by deterministic tests:
11
+ // { bundledManifest, download }.
10
12
 
11
13
  import { createHash } from 'node:crypto';
12
14
  import {
@@ -34,16 +36,68 @@ function patchBinDir(dataDir) {
34
36
  return join(dataDir, 'patch-bin');
35
37
  }
36
38
 
37
- async function loadManifest(dataDir) {
39
+ function readJson(path) {
40
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
41
+ }
42
+
43
+ function manifestVersion(manifest) {
44
+ const value = String(manifest?.version || '');
45
+ if (!/^\d+\.\d+\.\d+$/.test(value)) return null;
46
+ return value.split('.').map(Number);
47
+ }
48
+
49
+ function compareManifestVersions(a, b) {
50
+ const av = manifestVersion(a);
51
+ const bv = manifestVersion(b);
52
+ if (!av || !bv) return null;
53
+ for (let i = 0; i < 3; i++) {
54
+ if (av[i] !== bv[i]) return av[i] > bv[i] ? 1 : -1;
55
+ }
56
+ return 0;
57
+ }
58
+
59
+ function validSha256(value) {
60
+ return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value);
61
+ }
62
+
63
+ function validCachedUpgrade(manifest, pkey) {
64
+ if (!manifestVersion(manifest)) return false;
65
+ const asset = manifest.assets?.[pkey];
66
+ if (!asset || !validSha256(asset.sha256) || typeof asset.url !== 'string') return false;
67
+ const expectedUrl = `https://github.com/tribgames/mixdog/releases/download/patch-v${manifest.version}`
68
+ + `/mixdog-patch-${pkey}${binSuffix()}`;
69
+ return asset.url === expectedUrl;
70
+ }
71
+
72
+ function bundledManifest(options) {
73
+ if (options.bundledManifest) return options.bundledManifest;
74
+ return existsSync(BUNDLED_MANIFEST_PATH) ? readJson(BUNDLED_MANIFEST_PATH) : null;
75
+ }
76
+
77
+ function selectLocalManifest(dataDir, options = {}) {
78
+ const bundled = bundledManifest(options);
38
79
  const cached = join(patchBinDir(dataDir), 'manifest.json');
39
- if (existsSync(cached)) {
40
- try { return JSON.parse(readFileSync(cached, 'utf8')); } catch { /* fall through */ }
80
+ const cachedManifest = existsSync(cached) ? readJson(cached) : null;
81
+ if (bundled) {
82
+ // The installed manifest is the minimum policy. A cache may advance it,
83
+ // but only with a strict newer semver and a trusted, fully hashed asset.
84
+ if (compareManifestVersions(cachedManifest, bundled) === 1
85
+ && validCachedUpgrade(cachedManifest, platformKey())) {
86
+ return cachedManifest;
87
+ }
88
+ return bundled;
41
89
  }
42
- if (existsSync(BUNDLED_MANIFEST_PATH)) {
43
- return JSON.parse(readFileSync(BUNDLED_MANIFEST_PATH, 'utf8'));
90
+ return validCachedUpgrade(cachedManifest, platformKey()) ? cachedManifest : null;
91
+ }
92
+
93
+ async function loadManifest(dataDir, options = {}) {
94
+ const local = selectLocalManifest(dataDir, options);
95
+ if (local) return local;
96
+ const fetchFn = options.fetch || fetch;
97
+ const res = await fetchFn(MANIFEST_URL, { signal: AbortSignal.timeout(30_000) });
98
+ if (!res.ok) {
99
+ throw new Error(`[patch-fetcher] manifest fetch failed: ${res.status} ${res.statusText}`);
44
100
  }
45
- const res = await fetch(MANIFEST_URL, { signal: AbortSignal.timeout(30_000) });
46
- if (!res.ok) throw new Error(`[patch-fetcher] manifest fetch failed: ${res.status} ${res.statusText}`);
47
101
  return res.json();
48
102
  }
49
103
 
@@ -86,13 +140,18 @@ function gcPatchBin(dir, keepFile) {
86
140
  } catch { /* dir may not exist yet */ }
87
141
  }
88
142
 
89
- export function findCachedPatchBinary(dataDir) {
143
+ export function findCachedPatchBinary(dataDir, options = {}) {
90
144
  try {
91
145
  const dir = patchBinDir(dataDir);
92
- const hit = readdirSync(dir).find(
93
- (n) => n.startsWith('mixdog-patch') && !n.endsWith('.json') && !n.includes('.tmp-'),
94
- );
95
- return hit ? join(dir, hit) : null;
146
+ const manifest = selectLocalManifest(dataDir, options);
147
+ const asset = manifest?.assets?.[platformKey()];
148
+ const version = manifestVersion(manifest);
149
+ if (!version || !validSha256(asset?.sha256)) return null;
150
+ const fileName = `mixdog-patch-${manifest.version}${binSuffix()}`;
151
+ const hit = join(dir, fileName);
152
+ if (!existsSync(hit)) return null;
153
+ const actual = createHash('sha256').update(readFileSync(hit)).digest('hex');
154
+ return actual === asset.sha256.toLowerCase() ? hit : null;
96
155
  } catch {
97
156
  return null;
98
157
  }
@@ -100,13 +159,13 @@ export function findCachedPatchBinary(dataDir) {
100
159
 
101
160
  let _inflight = null;
102
161
 
103
- export function ensurePatchBinary(dataDir) {
162
+ export function ensurePatchBinary(dataDir, options = {}) {
104
163
  if (_inflight) return _inflight;
105
164
  _inflight = (async () => {
106
- const manifest = await loadManifest(dataDir);
165
+ const manifest = await loadManifest(dataDir, options);
107
166
  const pkey = platformKey();
108
167
  const asset = manifest.assets?.[pkey];
109
- if (!asset || !asset.url || !asset.sha256) {
168
+ if (!asset || !asset.url || !validSha256(asset.sha256) || !manifestVersion(manifest)) {
110
169
  // Unsupported platform/arch (e.g. win32-arm64): the manifest has no
111
170
  // downloadable asset for this {os}-{arch}. apply_patch is native-only
112
171
  // (no JS apply fallback), so this is terminal — surface a single clear,
@@ -125,12 +184,12 @@ export function ensurePatchBinary(dataDir) {
125
184
  const fileName = `mixdog-patch-${version}${binSuffix()}`;
126
185
  const destPath = join(dir, fileName);
127
186
  if (existsSync(destPath)) {
128
- try { if (await sha256File(destPath) === asset.sha256) return destPath; } catch { /* re-download */ }
187
+ try { if (await sha256File(destPath) === asset.sha256.toLowerCase()) return destPath; } catch { /* re-download */ }
129
188
  }
130
189
  const tmpPath = `${destPath}.tmp-${process.pid}-${Date.now()}`;
131
- await downloadWithRetry(asset.url, tmpPath);
190
+ await (options.download || downloadWithRetry)(asset.url, tmpPath);
132
191
  const actual = await sha256File(tmpPath);
133
- if (actual !== asset.sha256) {
192
+ if (actual !== asset.sha256.toLowerCase()) {
134
193
  try { rmSync(tmpPath, { force: true }); } catch { /* best-effort */ }
135
194
  throw new Error(`[patch-fetcher] sha256 mismatch for ${pkey}: expected ${asset.sha256}, got ${actual}`);
136
195
  }
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.7.19",
2
+ "version": "0.7.20",
3
3
  "_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-darwin-arm64",
7
- "sha256": "c808fcfb69d2817fd7678dd364d2e1dd68f6ef2d9807f618bdf110c03ca00ccb"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.20/mixdog-patch-darwin-arm64",
7
+ "sha256": "12b9b3ad3365fcde21c83c1b42236f6bd70dbe411d79e5c0f7c3c5743f1a3185"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-darwin-x64",
11
- "sha256": "7ed0eeff84796559ca865fc93e7a2a94f2c3fa00126f919064bb8273656fcd85"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.20/mixdog-patch-darwin-x64",
11
+ "sha256": "ff5de6da5fec3abbf617c0c3b1ead8e724d4cb0e9948277253889c83bc27a570"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-linux-arm64",
15
- "sha256": "3cad9c9542f2d97732953a102ef72daf4c79be5361560698d5fd2c5f0c18eda7"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.20/mixdog-patch-linux-arm64",
15
+ "sha256": "76d7cf54df7face32fb20862425ce25ad44e9800440132dea3f0ad387c92f07e"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-linux-x64",
19
- "sha256": "cb0e30346f437811dff19238e1854bd37d97a78f35b13e0b704ebeaa9499ae87"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.20/mixdog-patch-linux-x64",
19
+ "sha256": "43f6610beadf25eca752a09775fcb8858818ad2d9f7b8cf6a00deaa47571f3f0"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-win32-x64.exe",
23
- "sha256": "4bf61a99f63e446570d9d2a9c9ef38475d0d70c0a2b632a5f44d4fe9b684b968"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.20/mixdog-patch-win32-x64.exe",
23
+ "sha256": "4d5d2242f9662aeb968c24df571a498008d9c504fd31214fe9dac31aac669fdd"
24
24
  }
25
25
  }
26
26
  }