mixdog 0.9.135 → 0.9.136

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 (121) hide show
  1. package/package.json +3 -1
  2. package/scripts/tool-stress.mjs +8 -1
  3. package/src/agents/maintainer/agent.json +1 -1
  4. package/src/defaults/skills/setup/SKILL.md +8 -43
  5. package/src/help.mjs +0 -2
  6. package/src/rules/lead/01-general.md +0 -4
  7. package/src/rules/shared/01-tool.md +10 -4
  8. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +30 -4
  9. package/src/runtime/agent/orchestrator/agent-trace-timing.test.mjs +57 -0
  10. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +3 -3
  11. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.test.mjs +40 -0
  12. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -0
  13. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -5
  14. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +17 -4
  15. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +40 -0
  16. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +150 -41
  17. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -0
  18. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +34 -0
  19. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +18 -1
  20. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +10 -9
  21. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +124 -18
  22. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-top-k.test.mjs +39 -0
  23. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +7 -1
  24. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +4 -1
  25. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +82 -5
  26. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +2 -2
  27. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  28. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +78 -12
  29. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +34 -3
  30. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -5
  31. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +87 -0
  32. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  33. package/src/runtime/channels/lib/config.mjs +14 -101
  34. package/src/runtime/channels/lib/index-drop-trace.mjs +2 -5
  35. package/src/runtime/channels/lib/owned-runtime.mjs +4 -80
  36. package/src/runtime/channels/lib/scheduler.mjs +75 -18
  37. package/src/runtime/channels/lib/status-snapshot.mjs +5 -68
  38. package/src/runtime/channels/lib/tool-dispatch.mjs +12 -55
  39. package/src/runtime/channels/lib/worker-ipc.mjs +1 -16
  40. package/src/runtime/channels/lib/worker-main.mjs +22 -307
  41. package/src/runtime/media/renditions.mjs +68 -5
  42. package/src/runtime/media/renditions.test.mjs +45 -0
  43. package/src/runtime/media/store.mjs +1 -0
  44. package/src/runtime/memory/index.mjs +4 -3
  45. package/src/runtime/memory/lib/cycle-scheduler.mjs +17 -6
  46. package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +7 -1
  47. package/src/runtime/memory/lib/memory-cycle-requests.mjs +13 -0
  48. package/src/runtime/shared/config.mjs +8 -49
  49. package/src/runtime/shared/schedule-time.mjs +25 -0
  50. package/src/runtime/shared/schedule-time.test.mjs +29 -0
  51. package/src/runtime/shared/schedules-db.mjs +73 -1
  52. package/src/runtime/shared/tool-card-model.mjs +1 -1
  53. package/src/runtime/shared/transcript-writer.mjs +10 -17
  54. package/src/runtime/shared/update-checker.mjs +7 -2
  55. package/src/runtime/shared/update-checker.test.mjs +33 -0
  56. package/src/session-runtime/channel-config-api.mjs +3 -20
  57. package/src/session-runtime/config-lifecycle.mjs +1 -42
  58. package/src/session-runtime/lifecycle-api.mjs +1 -19
  59. package/src/session-runtime/model-route-api.mjs +0 -2
  60. package/src/session-runtime/model-settings-persist.test.mjs +0 -1
  61. package/src/session-runtime/prewarm.mjs +2 -7
  62. package/src/session-runtime/prewarm.test.mjs +0 -1
  63. package/src/session-runtime/runtime-core.mjs +6 -185
  64. package/src/session-runtime/session-lifecycle.mjs +4 -17
  65. package/src/session-runtime/session-transcript.mjs +54 -0
  66. package/src/session-runtime/session-turn-api.mjs +4 -44
  67. package/src/session-runtime/settings-api.mjs +0 -45
  68. package/src/session-runtime/tool-catalog-schema.mjs +1 -1
  69. package/src/standalone/channel-admin.mjs +8 -150
  70. package/src/standalone/daemon.mjs +5 -48
  71. package/src/standalone/seeds.mjs +18 -1
  72. package/src/standalone/session-protocol.mjs +0 -8
  73. package/src/standalone/session-service.mjs +1 -3
  74. package/src/tui/App.jsx +0 -5
  75. package/src/tui/app/app-view.jsx +1 -2
  76. package/src/tui/app/create-app-pickers.mjs +0 -20
  77. package/src/tui/app/doctor.mjs +2 -6
  78. package/src/tui/app/prompt-submit.mjs +2 -49
  79. package/src/tui/app/settings-picker.mjs +22 -96
  80. package/src/tui/app/slash-commands.mjs +0 -2
  81. package/src/tui/app/slash-dispatch.mjs +0 -17
  82. package/src/tui/app/use-prompt-handlers.mjs +4 -1
  83. package/src/tui/components/StatusLine.jsx +4 -4
  84. package/src/tui/dist/index.mjs +1003 -1461
  85. package/src/tui/session/context-state.mjs +0 -1
  86. package/src/tui/session/queue-helpers.mjs +1 -1
  87. package/src/tui/session/queue-helpers.test.mjs +10 -1
  88. package/src/tui/session/session-api-ext.mjs +0 -89
  89. package/src/tui/session/session-api.mjs +0 -2
  90. package/src/tui/session/tool-card-results.mjs +1 -1
  91. package/src/tui/session/tool-result-status.mjs +3 -3
  92. package/src/tui/session/tool-result-status.test.mjs +3 -3
  93. package/src/tui/session-local.mjs +0 -26
  94. package/src/workflows/default/WORKFLOW.md +13 -15
  95. package/src/workflows/solo/WORKFLOW.md +6 -7
  96. package/src/runtime/channels/lib/config-lock.mjs +0 -13
  97. package/src/runtime/channels/lib/drop-trace.mjs +0 -71
  98. package/src/runtime/channels/lib/format.mjs +0 -190
  99. package/src/runtime/channels/lib/inbound-handler.mjs +0 -387
  100. package/src/runtime/channels/lib/inbound-routing.mjs +0 -81
  101. package/src/runtime/channels/lib/interaction-handlers.mjs +0 -70
  102. package/src/runtime/channels/lib/interaction-workflows.mjs +0 -76
  103. package/src/runtime/channels/lib/memory-client.mjs +0 -289
  104. package/src/runtime/channels/lib/output-forwarder.mjs +0 -934
  105. package/src/runtime/channels/lib/session-control.mjs +0 -36
  106. package/src/runtime/channels/lib/session-discovery.mjs +0 -217
  107. package/src/runtime/channels/lib/session-discovery.test.mjs +0 -32
  108. package/src/runtime/channels/lib/telegram-format.mjs +0 -280
  109. package/src/runtime/channels/lib/tool-format.mjs +0 -125
  110. package/src/runtime/channels/lib/transcript-binding.mjs +0 -336
  111. package/src/runtime/channels/lib/transcript-discovery.mjs +0 -208
  112. package/src/runtime/channels/providers/discord-access.mjs +0 -30
  113. package/src/runtime/channels/providers/discord-attachments.mjs +0 -69
  114. package/src/runtime/channels/providers/discord-gateway.mjs +0 -230
  115. package/src/runtime/channels/providers/discord.mjs +0 -730
  116. package/src/runtime/channels/providers/telegram.mjs +0 -598
  117. package/src/runtime/channels/providers/telegram.test.mjs +0 -75
  118. package/src/session-runtime/remote-control.mjs +0 -126
  119. package/src/session-runtime/remote-transcript.mjs +0 -134
  120. package/src/session-runtime/remote-transition-queue.mjs +0 -14
  121. package/src/tui/app/channel-pickers.mjs +0 -384
@@ -520,7 +520,13 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
520
520
  // Skipped for pattern arrays and when `-i` is already set.
521
521
  const caseHintSuffix = async () => {
522
522
  const caseInsensitive = args['-i'] === true;
523
- if (caseInsensitive || patterns.length !== 1 || !/[A-Za-z]/.test(patterns[0])) return '';
523
+ // A broad no-match must not trigger a second whole-tree scan just to
524
+ // produce an advisory casing hint. The requested case-sensitive result
525
+ // is already complete; retain the hint only for explicit file scopes.
526
+ if (grepStat.isDirectory()
527
+ || caseInsensitive
528
+ || patterns.length !== 1
529
+ || !/[A-Za-z]/.test(patterns[0])) return '';
524
530
  try {
525
531
  const probeArgs = buildGrepRgArgs({
526
532
  patterns,
@@ -568,7 +568,10 @@ export function preflightPowerShellHygiene(command, { shellType, shellName } = {
568
568
  // regex/search args are never mistaken for real syntax.
569
569
  const masked = maskQuotedRegions(rewritten);
570
570
 
571
- for (const segment of shellSplitSegments(rewritten)) {
571
+ // Only command heads matter here. Scan the quote-masked copy so PowerShell
572
+ // literals use PowerShell escaping rules (`\` is literal, backtick escapes)
573
+ // and nested bash snippets remain opaque.
574
+ for (const segment of shellSplitSegments(masked)) {
572
575
  for (const stage of shellSplitPipelineSegments(segment)) {
573
576
  const tokens = stripShellProbeWrappers(shellTokenize(stage) || []);
574
577
  if (tokens.length === 0) continue;
@@ -3,6 +3,7 @@
3
3
  // race), isCodeGraphTool. Extracted verbatim from code-graph.mjs.
4
4
  import { resolve as pathResolve, isAbsolute, relative as pathRelative, basename as pathBasename, extname } from 'node:path';
5
5
  import { homedir as osHomedir } from 'node:os';
6
+ import { createHash } from 'node:crypto';
6
7
  import { existsSync, statSync } from 'node:fs';
7
8
  import { readFile } from 'node:fs/promises';
8
9
  import { normalizeInputPath, toDisplayPath } from '../builtin.mjs';
@@ -29,6 +30,8 @@ import {
29
30
  _stripEmptyArgs,
30
31
  } from './project-root.mjs';
31
32
  import { buildCodeGraphAsync, prewarmCodeGraph, prewarmCodeGraphSymbols } from './build.mjs';
33
+ import { _fileInfoFromRustRecord, _runGraphFiles } from './graph-binary.mjs';
34
+ import { _attachGraphRuntimeCaches } from './graph-model.mjs';
32
35
  import {
33
36
  _isFilesystemRootPath,
34
37
  collectTrustedCodeGraphRoots,
@@ -87,6 +90,74 @@ function _outlineLanguageForPath(file) {
87
90
  return null;
88
91
  }
89
92
 
93
+ const _exactFileGraphInflight = new Map();
94
+ const _exactFileGraphCache = new Map();
95
+ const EXACT_FILE_GRAPH_CACHE_MAX = 64;
96
+
97
+ async function _buildExactFileGraph(cwd, abs, signal = null) {
98
+ const root = pathResolve(cwd);
99
+ const file = pathResolve(abs);
100
+ const rel = _graphRel(file, root);
101
+ const key = `${root}\0${rel}`;
102
+ const sourceText = await readFile(file, { encoding: 'utf8', signal: signal || undefined });
103
+ const sourceHash = createHash('sha256').update(sourceText).digest('hex');
104
+ const cached = _exactFileGraphCache.get(key);
105
+ if (cached?.sourceHash === sourceHash) {
106
+ _exactFileGraphCache.delete(key);
107
+ _exactFileGraphCache.set(key, cached);
108
+ return cached.graph;
109
+ }
110
+ const existing = _exactFileGraphInflight.get(key);
111
+ if (existing?.sourceHash === sourceHash) return existing.promise;
112
+ const pending = (async () => {
113
+ const records = await _runGraphFiles(root, [rel], [], signal);
114
+ const record = records.find((item) => _graphRel(pathResolve(root, item.rel), root) === rel);
115
+ if (!record) return null;
116
+ const info = _fileInfoFromRustRecord(record, root);
117
+ const node = {
118
+ abs: info.abs,
119
+ rel: info.rel,
120
+ lang: info.lang,
121
+ fingerprint: info.fingerprint,
122
+ rawImports: info.rawImports,
123
+ resolvedImportsRel: [],
124
+ resolvedImports: [],
125
+ importedBy: [],
126
+ packageName: info.packageName,
127
+ namespaceName: info.namespaceName,
128
+ goPackageName: info.goPackageName,
129
+ topLevelTypes: info.topLevelTypes,
130
+ tokenSymbols: info.tokenSymbols,
131
+ symbols: Array.isArray(info.symbols) ? info.symbols : [],
132
+ };
133
+ const graph = _attachGraphRuntimeCaches({
134
+ cwd: root,
135
+ nodes: new Map([[node.rel, node]]),
136
+ reverse: new Map(),
137
+ builtAt: Date.now(),
138
+ signature: info.fingerprint || '',
139
+ truncated: false,
140
+ });
141
+ graph._sourceTextCache.set(node.rel, {
142
+ fingerprint: node.fingerprint || '',
143
+ text: sourceText,
144
+ });
145
+ _exactFileGraphCache.delete(key);
146
+ _exactFileGraphCache.set(key, { sourceHash, graph });
147
+ while (_exactFileGraphCache.size > EXACT_FILE_GRAPH_CACHE_MAX) {
148
+ _exactFileGraphCache.delete(_exactFileGraphCache.keys().next().value);
149
+ }
150
+ return graph;
151
+ })();
152
+ const inflight = { sourceHash, promise: pending };
153
+ _exactFileGraphInflight.set(key, inflight);
154
+ try {
155
+ return await pending;
156
+ } finally {
157
+ if (_exactFileGraphInflight.get(key) === inflight) _exactFileGraphInflight.delete(key);
158
+ }
159
+ }
160
+
90
161
  async function _mapWithConcurrency(values, mapper) {
91
162
  const out = new Array(values.length);
92
163
  let cursor = 0;
@@ -693,9 +764,17 @@ async function findSymbolTool(args, cwd, signal = null, options = {}) {
693
764
  else prewarmCodeGraph(cwd);
694
765
  return `prewarm scheduled: cwd=${cwd} symbols=${symbols.length}${symbols.length ? ` (${symbols.slice(0, 5).join(',')}${symbols.length > 5 ? `,+${symbols.length - 5}` : ''})` : ''}`;
695
766
  }
696
- const graph = await buildCodeGraphAsync(cwd, signal, {
697
- excludedProjectRoots: options?.excludedProjectRoots,
698
- });
767
+ const normFile = normalizeInputPath(args?.file);
768
+ const abs = normFile ? (isAbsolute(normFile) ? pathResolve(normFile) : pathResolve(cwd, normFile)) : null;
769
+ let exactFile = false;
770
+ if (abs && _outlineLanguageForPath(abs)) {
771
+ try { exactFile = statSync(abs).isFile(); } catch { exactFile = false; }
772
+ }
773
+ const graph = exactFile
774
+ ? await _buildExactFileGraph(cwd, abs, signal)
775
+ : await buildCodeGraphAsync(cwd, signal, {
776
+ excludedProjectRoots: options?.excludedProjectRoots,
777
+ });
699
778
  if (!graph) throw new Error(`find_symbol: cwd '${cwd}' is not an indexed/known project root or contains zero eligible files`);
700
779
  if (options?.scopedCacheOutcome && graph.truncated) {
701
780
  markScopedCacheIncomplete(options.scopedCacheOutcome);
@@ -703,8 +782,6 @@ async function findSymbolTool(args, cwd, signal = null, options = {}) {
703
782
  const symbol = String(args?.symbol || '').trim();
704
783
  const language = String(args?.language || '').trim() || null;
705
784
  const limit = Math.max(1, Math.min(50, Number(args?.limit || 20)));
706
- const normFile = normalizeInputPath(args?.file);
707
- const abs = normFile ? (isAbsolute(normFile) ? pathResolve(normFile) : pathResolve(cwd, normFile)) : null;
708
785
  const fileRel = abs ? _graphRel(abs, cwd) : null;
709
786
  if (fileRel && !graph.nodes.get(fileRel)) {
710
787
  return _appendSameBasenameHint(`Error: find_symbol: file not found in graph: ${normFile}`, normFile, graph);
@@ -248,7 +248,7 @@ export function _runGraphWalk(absRoot) { return _runGraphBinaryJsonl(absRoot, []
248
248
  // topLevelTypes}. Rust builds the index + resolves over ALL nodes (fresh +
249
249
  // reused) and emits fresh rels as full records, reused rels as lightweight
250
250
  // {rel, resolvedImports, importedBy}.
251
- export function _runGraphFiles(absRoot, rels, reusedMetas) {
251
+ export function _runGraphFiles(absRoot, rels, reusedMetas, signal = null) {
252
252
  const lines = Array.isArray(reusedMetas)
253
253
  ? reusedMetas.map((m) => JSON.stringify({
254
254
  rel: m.rel,
@@ -260,7 +260,7 @@ export function _runGraphFiles(absRoot, rels, reusedMetas) {
260
260
  topLevelTypes: Array.isArray(m.topLevelTypes) ? m.topLevelTypes : [],
261
261
  }))
262
262
  : [];
263
- return _runGraphBinaryJsonl(absRoot, ['--files', ...rels], lines);
263
+ return _runGraphBinaryJsonl(absRoot, ['--files', ...rels], lines, signal);
264
264
  }
265
265
 
266
266
  // Map a Rust FileRecord (rel/lang/fp/tokens/rawImports/resolvedImports/
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.1.17",
2
+ "version": "0.1.18",
3
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/graph-v0.1.17/mixdog-graph-darwin-arm64",
7
- "sha256": "3b8850422ad267b45572ceef7993fb57c2b7b9c71cc4c026056335828a28a65a"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.18/mixdog-graph-darwin-arm64",
7
+ "sha256": "c89403d4a4cd08aa27427b65e9f0efe4885fb35cbe788527b81279e03edc82eb"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.17/mixdog-graph-darwin-x64",
11
- "sha256": "0d793977ddd1d84851d1a9e3244415e7dc5b35f2f636a43f17af4745ea4485a8"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.18/mixdog-graph-darwin-x64",
11
+ "sha256": "43765bb6598f0b7db3ba5df8348bb7221fc0e4fec5f8ad9d4d53887d4951e42f"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.17/mixdog-graph-linux-arm64",
15
- "sha256": "036dcc5f16a5cfc72e95de198de8b5d2ad56d62e88677bb90222206624268a6a"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.18/mixdog-graph-linux-arm64",
15
+ "sha256": "ada18e0d9ca3ab8fe65098b81c8856d01992eb2f33a2f2923e886280e2b635a4"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.17/mixdog-graph-linux-x64",
19
- "sha256": "7bd4325dc1761f3fcf77cc8926a45e007f85d1b6954bfea819fafb6625635904"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.18/mixdog-graph-linux-x64",
19
+ "sha256": "724d219ec72828662f34ba7b0edf00938e8f9f00df76cb6c41675551e2c0137e"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.17/mixdog-graph-win32-x64.exe",
23
- "sha256": "c0441052d0a09ddb3a3c291c7e7bffcf767dac9b6353e2f2e3e6954be2994712"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.18/mixdog-graph-win32-x64.exe",
23
+ "sha256": "c24f4ffb0beec3f355bc4e33c02adafce2ba966df34922ec0ff2ce2cde23fe65"
24
24
  }
25
25
  }
26
26
  }
@@ -1,13 +1,16 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
+ import { dirname, resolve as pathResolve } from 'node:path';
3
4
  import { createInterface } from 'node:readline';
4
5
  import { EventEmitter } from 'node:events';
6
+ import { fileURLToPath } from 'node:url';
5
7
  import { hiddenSpawnOpts } from '../../../../shared/spawn-flags.mjs';
6
8
  import { packageNativeToolPath } from '../../../../shared/native-tool-paths.mjs';
7
9
  import { getPluginData } from '../../config.mjs';
8
10
  import { ensureSpawnBinary, findCachedSpawnBinary } from '../spawn-binary-fetcher.mjs';
9
11
 
10
12
  const RESTART_BACKOFF_MS = 30_000;
13
+ const SERVER_READY_TIMEOUT_MS = 5_000;
11
14
 
12
15
  let _server = null;
13
16
  let _binaryPath = undefined;
@@ -57,22 +60,24 @@ function acceptTask(message) {
57
60
  class LineStream extends EventEmitter {
58
61
  constructor() {
59
62
  super();
60
- this._pending = '';
63
+ this._pending = [];
61
64
  }
62
65
  setEncoding() { return this; }
63
66
  emit(event, ...args) {
64
67
  if (event === 'data' && this.listenerCount('data') === 0) {
65
- this._pending += args[0] == null ? '' : String(args[0]);
68
+ if (args[0] != null) this._pending.push(args[0]);
66
69
  return false;
67
70
  }
68
71
  return super.emit(event, ...args);
69
72
  }
70
73
  on(event, listener) {
71
74
  const ret = super.on(event, listener);
72
- if (event === 'data' && this._pending) {
75
+ if (event === 'data' && this._pending.length > 0) {
73
76
  const pending = this._pending;
74
- this._pending = '';
75
- queueMicrotask(() => listener(pending));
77
+ this._pending = [];
78
+ queueMicrotask(() => {
79
+ for (const chunk of pending) listener(chunk);
80
+ });
76
81
  }
77
82
  return ret;
78
83
  }
@@ -142,6 +147,17 @@ function _resolveBinary() {
142
147
  _binaryPath = explicit;
143
148
  return _binaryPath;
144
149
  }
150
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
151
+ const binName = process.platform === 'win32' ? 'mixdog-spawn.exe' : 'mixdog-spawn';
152
+ const localBuild = pathResolve(
153
+ moduleDir,
154
+ '../../../../../../native/mixdog-spawn/target/release',
155
+ binName,
156
+ );
157
+ if (existsSync(localBuild)) {
158
+ _binaryPath = localBuild;
159
+ return _binaryPath;
160
+ }
145
161
  const installed = packageNativeToolPath('spawn');
146
162
  if (existsSync(installed)) {
147
163
  _binaryPath = installed;
@@ -243,13 +259,23 @@ function _ensureServer() {
243
259
  _lastFailureAt = Date.now();
244
260
  return null;
245
261
  }
246
- const server = { child, pending: new Map(), sequence: 0 };
262
+ const server = { child, pending: new Map(), sequence: 0, ready: false, caps: {} };
263
+ let resolveReady;
264
+ server.readyPromise = new Promise((resolve) => { resolveReady = resolve; });
265
+ const settleReady = (value) => {
266
+ if (!resolveReady) return;
267
+ const resolve = resolveReady;
268
+ resolveReady = null;
269
+ resolve(value);
270
+ };
247
271
  const lines = createInterface({ input: child.stdout });
248
272
  lines.on('line', (line) => {
249
273
  let message;
250
274
  try { message = JSON.parse(line); } catch { return; }
251
275
  if (message?.ready === true) {
252
276
  server.caps = (message.caps && typeof message.caps === 'object') ? message.caps : {};
277
+ server.ready = true;
278
+ settleReady(true);
253
279
  return;
254
280
  }
255
281
  if (String(message?.event || '').startsWith('task_')) acceptTask(message);
@@ -257,8 +283,14 @@ function _ensureServer() {
257
283
  if (!pending) return;
258
284
  pending.onMessage(message);
259
285
  });
260
- child.on('error', (error) => { if (_server === server) _teardown(error); });
261
- child.on('exit', () => { if (_server === server) _teardown(); });
286
+ child.on('error', (error) => {
287
+ settleReady(false);
288
+ if (_server === server) _teardown(error);
289
+ });
290
+ child.on('exit', () => {
291
+ settleReady(false);
292
+ if (_server === server) _teardown();
293
+ });
262
294
  child.stdin.on('error', (error) => { if (_server === server) _teardown(error); });
263
295
  _setServerReferenced(server, false);
264
296
  _server = server;
@@ -274,17 +306,40 @@ export async function warmNativeSpawnServer() {
274
306
  }
275
307
 
276
308
  export async function ensureNativeSpawnServer() {
277
- if (_server) return true;
309
+ if (_server?.ready) return true;
278
310
  let binary = _resolveBinary();
279
311
  if (!binary) {
280
312
  binary = await ensureSpawnBinary(getPluginData());
281
313
  _binaryPath = binary;
282
314
  }
283
- if (!_ensureServer()) {
315
+ const server = _ensureServer();
316
+ if (!server) {
284
317
  throw Object.assign(new Error('verified native spawn server could not be started'), {
285
318
  code: 'NATIVE_SPAWN_UNAVAILABLE',
286
319
  });
287
320
  }
321
+ if (!server.ready) {
322
+ let timer;
323
+ let ready;
324
+ _setServerReferenced(server, true);
325
+ try {
326
+ ready = await Promise.race([
327
+ server.readyPromise,
328
+ new Promise((resolve) => {
329
+ timer = setTimeout(() => resolve(false), SERVER_READY_TIMEOUT_MS);
330
+ }),
331
+ ]);
332
+ } finally {
333
+ if (timer) clearTimeout(timer);
334
+ if (_server === server) _recomputeServerRef(server);
335
+ }
336
+ if (!ready) {
337
+ if (_server === server) _teardown(new Error('native spawn server ready timeout'));
338
+ throw Object.assign(new Error('verified native spawn server did not become ready'), {
339
+ code: 'NATIVE_SPAWN_UNAVAILABLE',
340
+ });
341
+ }
342
+ }
288
343
  return true;
289
344
  }
290
345
 
@@ -311,6 +366,7 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
311
366
  timeoutMs: Math.max(0, Number(spawnOptions.timeoutMs) || 0),
312
367
  outputLimit: Math.max(0, Number(spawnOptions.outputLimit) || 0),
313
368
  mergeStderr: spawnOptions.mergeStderr === true,
369
+ rawOutput: spawnOptions.rawOutput === true,
314
370
  ...(spawnOptions.stdinPipe === true ? { stdinPipe: true } : {}),
315
371
  command: spawnOptions.command || undefined,
316
372
  shellType: spawnOptions.shellType || undefined,
@@ -338,8 +394,18 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
338
394
  fake.emit('spawn');
339
395
  return;
340
396
  }
341
- if (event === 'stdout' && message.text) fake.stdout.emit('data', String(message.text));
342
- else if (event === 'stderr' && message.text) fake.stderr.emit('data', String(message.text));
397
+ if (event === 'stdout' && (message.dataBase64 || message.text)) {
398
+ const chunk = message.dataBase64
399
+ ? Buffer.from(String(message.dataBase64), 'base64')
400
+ : String(message.text);
401
+ fake.stdout.emit('data', chunk);
402
+ }
403
+ else if (event === 'stderr' && (message.dataBase64 || message.text)) {
404
+ const chunk = message.dataBase64
405
+ ? Buffer.from(String(message.dataBase64), 'base64')
406
+ : String(message.text);
407
+ fake.stderr.emit('data', chunk);
408
+ }
343
409
  else if (event === 'root_exit') {
344
410
  fake.exitCode = message.code == null ? null : Number(message.code);
345
411
  fake.signalCode = message.signal || null;
@@ -14,9 +14,30 @@ import { SHELL_OUTPUT_DISK_CAP } from '../shell-exec-output.mjs';
14
14
  // exactly like `-Command <script>`.
15
15
  const STANDBY_BOOTSTRAP = "$null = . ([scriptblock]::Create('$null')); [Console]::InputEncoding=[System.Text.UTF8Encoding]::new($false); . ([scriptblock]::Create([Console]::In.ReadToEnd()))";
16
16
  const STANDBY_ARGS = ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', STANDBY_BOOTSTRAP];
17
- const STANDBY_TTL_MS = 10 * 60_000;
17
+ const _configuredIdleMs = Number(process.env.MIXDOG_SHELL_WARM_STANDBY_IDLE_MS);
18
+ const STANDBY_TTL_MS = Number.isFinite(_configuredIdleMs) && _configuredIdleMs >= 100
19
+ ? Math.floor(_configuredIdleMs)
20
+ : 2 * 60_000;
18
21
 
19
22
  let _slot = null; // { native, shell, envSig, createdAt }
23
+ let _slotIdleTimer = null;
24
+
25
+ function _clearSlotIdleTimer() {
26
+ if (!_slotIdleTimer) return;
27
+ clearTimeout(_slotIdleTimer);
28
+ _slotIdleTimer = null;
29
+ }
30
+
31
+ function _armSlotIdleTimer(slot) {
32
+ _clearSlotIdleTimer();
33
+ _slotIdleTimer = setTimeout(() => {
34
+ _slotIdleTimer = null;
35
+ if (_slot !== slot) return;
36
+ _slot = null;
37
+ try { slot.native.child.kill(); } catch { /* best-effort */ }
38
+ }, STANDBY_TTL_MS);
39
+ _slotIdleTimer.unref?.();
40
+ }
20
41
 
21
42
  function _disabled() {
22
43
  return process.env.MIXDOG_SHELL_WARM_STANDBY === '0';
@@ -57,6 +78,7 @@ export function ensureWarmShellStandby({ shell, env }) {
57
78
  env,
58
79
  cwd: process.cwd(),
59
80
  outputLimit: SHELL_OUTPUT_DISK_CAP,
81
+ rawOutput: true,
60
82
  stdinPipe: true,
61
83
  shellType: 'powershell',
62
84
  command: '(warm shell standby)',
@@ -73,9 +95,15 @@ export function ensureWarmShellStandby({ shell, env }) {
73
95
  envSig: envSignature(env),
74
96
  createdAt: Date.now(),
75
97
  };
76
- native.child.once('close', () => { if (_slot === slot) _slot = null; });
77
- native.child.once('error', () => { if (_slot === slot) _slot = null; });
98
+ const release = () => {
99
+ if (_slot !== slot) return;
100
+ _slot = null;
101
+ _clearSlotIdleTimer();
102
+ };
103
+ native.child.once('close', release);
104
+ native.child.once('error', release);
78
105
  _slot = slot;
106
+ _armSlotIdleTimer(slot);
79
107
  }
80
108
 
81
109
  /** Take the parked standby for immediate use, or null on any mismatch.
@@ -98,12 +126,14 @@ export function takeWarmShellStandby({ shell, env, cwd }) {
98
126
  || Date.now() - slot.createdAt > STANDBY_TTL_MS
99
127
  || slot.envSig !== envSignature(env))) {
100
128
  _slot = null;
129
+ _clearSlotIdleTimer();
101
130
  try { slot.native.child.kill(); } catch { /* best-effort */ }
102
131
  }
103
132
  refill();
104
133
  return null;
105
134
  }
106
135
  _slot = null;
136
+ _clearSlotIdleTimer();
107
137
  // Back to active: the caller is about to run a real command on it and the
108
138
  // host must stay alive until that command settles.
109
139
  setNativeSpawnRequestIdle(slot.native.child, false);
@@ -125,6 +155,7 @@ export function takeWarmShellStandby({ shell, env, cwd }) {
125
155
  export function _resetWarmShellStandbyForTest() {
126
156
  const slot = _slot;
127
157
  _slot = null;
158
+ _clearSlotIdleTimer();
128
159
  if (slot?.native?.child) {
129
160
  try { slot.native.child.kill(); } catch { /* best-effort */ }
130
161
  }
@@ -35,7 +35,7 @@ export {
35
35
  // renders a path marker instead of pasting the tail. Matches the
36
36
  // SHELL_OUTPUT_MAX_CHARS used by the smart-truncate renderer in
37
37
  // builtin.mjs so spilled output and inline output share the same boundary.
38
- import { SHELL_OUTPUT_INLINE_CAP, SHELL_OUTPUT_DISK_CAP, stripAnsi, treeKill, TaskOutput, ExecResult } from './shell-exec-output.mjs';
38
+ import { SHELL_OUTPUT_INLINE_CAP, SHELL_OUTPUT_DISK_CAP, stripAnsi, treeKill, TaskOutput, ExecResult, ShellTextDecoder } from './shell-exec-output.mjs';
39
39
  export { stripAnsi, ExecResult } from './shell-exec-output.mjs';
40
40
 
41
41
  async function _execPolicyBlockMessage(command) {
@@ -221,6 +221,8 @@ export function execShellCommand({
221
221
  };
222
222
  const taskId = `shell_${randomUUID().slice(0, 8)}`;
223
223
  const taskOutput = new TaskOutput(taskId);
224
+ const stdoutDecoder = new ShellTextDecoder();
225
+ const stderrDecoder = new ShellTextDecoder();
224
226
  let timedOut = false;
225
227
  let killed = false;
226
228
  let killCause = null;
@@ -379,6 +381,7 @@ export function execShellCommand({
379
381
  env,
380
382
  cwd,
381
383
  outputLimit: SHELL_OUTPUT_DISK_CAP,
384
+ rawOutput: true,
382
385
  // NOTE (child-spawn-gate): the full command lifetime is intentionally
383
386
  // NOT gated — bash/pwsh commands can run for minutes and would starve
384
387
  // rg/code_graph. Only the spawn window above holds a slot.
@@ -429,21 +432,21 @@ export function execShellCommand({
429
432
  }
430
433
 
431
434
  const _stdoutData = (chunk) => {
432
- taskOutput.writeStdout(chunk);
435
+ const text = stdoutDecoder.write(chunk);
436
+ if (text) taskOutput.writeStdout(text);
433
437
  if (taskOutput.binaryOutput && !settled && !autoBackgrounded) _treeKillForceSettle('binary-output');
434
438
  if (taskOutput.writeError && !settled && !autoBackgrounded) _treeKillForceSettle('output-capture-error');
435
439
  };
436
440
  const _stderrData = (chunk) => {
437
- taskOutput.writeStderr(chunk);
441
+ const text = stderrDecoder.write(chunk);
442
+ if (text) taskOutput.writeStderr(text);
438
443
  if (taskOutput.binaryOutput && !settled && !autoBackgrounded) _treeKillForceSettle('binary-output');
439
444
  if (taskOutput.writeError && !settled && !autoBackgrounded) _treeKillForceSettle('output-capture-error');
440
445
  };
441
446
  if (child.stdout) {
442
- child.stdout.setEncoding('utf-8');
443
447
  child.stdout.on('data', _stdoutData);
444
448
  }
445
449
  if (child.stderr) {
446
- child.stderr.setEncoding('utf-8');
447
450
  child.stderr.on('data', _stderrData);
448
451
  }
449
452
 
@@ -460,6 +463,10 @@ export function execShellCommand({
460
463
  autoBgTimer = null;
461
464
  }
462
465
  detachAbortHandler();
466
+ const stdoutTail = stdoutDecoder.end();
467
+ const stderrTail = stderrDecoder.end();
468
+ if (stdoutTail) taskOutput.writeStdout(stdoutTail);
469
+ if (stderrTail) taskOutput.writeStderr(stderrTail);
463
470
  // getStdout/getStderr can throw on a spilled-file read failure (EBADF
464
471
  // after unlink race, EACCES). Without this catch the rejection bubbles
465
472
  // up and leaves the outer settle promise unresolved, hanging the call.
@@ -15,6 +15,7 @@ import {
15
15
  statSync,
16
16
  } from 'node:fs';
17
17
  import { join } from 'node:path';
18
+ import { StringDecoder } from 'node:string_decoder';
18
19
  import * as nodeUtil from 'node:util';
19
20
  import { getPluginData } from '../config.mjs';
20
21
 
@@ -86,6 +87,92 @@ export function inspectShellTextChunk(value, channel = 'stdout') {
86
87
  };
87
88
  }
88
89
 
90
+ function detectShellTextEncoding(buffer, final = false) {
91
+ if (buffer.length >= 2) {
92
+ if (buffer[0] === 0xff && buffer[1] === 0xfe) return { encoding: 'utf16le', bom: 2 };
93
+ if (buffer[0] === 0xfe && buffer[1] === 0xff) return { encoding: 'utf16be', bom: 2 };
94
+ }
95
+ const pairs = Math.min(Math.floor(buffer.length / 2), 128);
96
+ if (pairs >= 4) {
97
+ let evenNuls = 0;
98
+ let oddNuls = 0;
99
+ let printablePartners = 0;
100
+ for (let i = 0; i < pairs * 2; i += 2) {
101
+ if (buffer[i] === 0) evenNuls++;
102
+ if (buffer[i + 1] === 0) oddNuls++;
103
+ const partner = buffer[i] === 0 ? buffer[i + 1] : buffer[i];
104
+ if (partner === 9 || partner === 10 || partner === 13 || (partner >= 0x20 && partner <= 0x7e)) {
105
+ printablePartners++;
106
+ }
107
+ }
108
+ const dominantNuls = Math.max(evenNuls, oddNuls);
109
+ const minorityNuls = Math.min(evenNuls, oddNuls);
110
+ if (
111
+ dominantNuls / pairs >= 0.6
112
+ && minorityNuls / pairs <= 0.1
113
+ && printablePartners / pairs >= 0.75
114
+ ) {
115
+ return { encoding: oddNuls > evenNuls ? 'utf16le' : 'utf16be', bom: 0 };
116
+ }
117
+ }
118
+ if (!final && buffer.length < 4) return null;
119
+ return { encoding: 'utf8', bom: 0 };
120
+ }
121
+
122
+ export class ShellTextDecoder {
123
+ constructor() {
124
+ this.encoding = null;
125
+ this.decoder = null;
126
+ this.pending = Buffer.alloc(0);
127
+ this.byteRemainder = Buffer.alloc(0);
128
+ }
129
+
130
+ _decode(buffer) {
131
+ if (this.encoding !== 'utf16be') return this.decoder.write(buffer);
132
+ const joined = this.byteRemainder.length > 0
133
+ ? Buffer.concat([this.byteRemainder, buffer])
134
+ : buffer;
135
+ const evenLength = joined.length - (joined.length % 2);
136
+ this.byteRemainder = evenLength < joined.length ? joined.subarray(evenLength) : Buffer.alloc(0);
137
+ if (evenLength === 0) return '';
138
+ return this.decoder.write(Buffer.from(joined.subarray(0, evenLength)).swap16());
139
+ }
140
+
141
+ _start(final = false) {
142
+ const detected = detectShellTextEncoding(this.pending, final);
143
+ if (!detected) return '';
144
+ this.encoding = detected.encoding;
145
+ this.decoder = new StringDecoder(this.encoding === 'utf16be' ? 'utf16le' : this.encoding);
146
+ const buffered = this.pending.subarray(detected.bom);
147
+ this.pending = Buffer.alloc(0);
148
+ return this._decode(buffered);
149
+ }
150
+
151
+ write(value) {
152
+ if (value == null || value.length === 0) return '';
153
+ if (typeof value === 'string') {
154
+ const prefix = this.encoding ? '' : this._start(true);
155
+ return prefix + value;
156
+ }
157
+ const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
158
+ if (!this.encoding) {
159
+ this.pending = this.pending.length > 0 ? Buffer.concat([this.pending, buffer]) : buffer;
160
+ return this._start(false);
161
+ }
162
+ return this._decode(buffer);
163
+ }
164
+
165
+ end() {
166
+ let text = this.encoding ? '' : this._start(true);
167
+ if (this.encoding === 'utf16be' && this.byteRemainder.length > 0) {
168
+ text += '\uFFFD';
169
+ this.byteRemainder = Buffer.alloc(0);
170
+ }
171
+ if (this.decoder) text += this.decoder.end();
172
+ return text;
173
+ }
174
+ }
175
+
89
176
  // Delegate process-tree termination to the native manager.
90
177
  export function treeKill(child) {
91
178
  if (!child?.__nativeSpawn || typeof child.kill !== 'function') return false;
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.1.2",
2
+ "version": "0.1.3",
3
3
  "_comment": "Synced from immutable spawn-v release assets.",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.2/mixdog-spawn-darwin-arm64",
7
- "sha256": "3c2eb5ecf47dccb4aeea113f422ede24f6dde22dceab7c9c77f6963ef9aded7d"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.3/mixdog-spawn-darwin-arm64",
7
+ "sha256": "44b26d0a7bf2a1a32f3eaac9faff370540d1fa34bd1154e2dcbba283e6ee80da"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.2/mixdog-spawn-darwin-x64",
11
- "sha256": "7288b073b940d82b64e9e888e723ae95204a28472053676fb6650d5c68362836"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.3/mixdog-spawn-darwin-x64",
11
+ "sha256": "bb20f01607aef01c62b7a7cac172b8f2493d7320aab1381ce1c18a366c9ace37"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.2/mixdog-spawn-linux-arm64",
15
- "sha256": "308a53b7c9dbae5ce8c36dc98ab35ac869f0251671829cbdcb09162261b063bf"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.3/mixdog-spawn-linux-arm64",
15
+ "sha256": "ba638ce7ada9f9602bc81ec2bc47095d978e97068fd659fbe33b281eab8c1993"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.2/mixdog-spawn-linux-x64",
19
- "sha256": "9f619c17d13e2786e38a2a6017a5e9b1e2f88370132d2c0ff8ee80245b64d0c3"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.3/mixdog-spawn-linux-x64",
19
+ "sha256": "4ca31eeba80af2073776ffbf62f9fb9e59d48d55e120cd25deab02023a586558"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.2/mixdog-spawn-win32-x64.exe",
23
- "sha256": "ee229f2ee2066b2c1a7c984b2ac4cd5bcfed5758173115de366defb1f97c7b51"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.3/mixdog-spawn-win32-x64.exe",
23
+ "sha256": "dd8f47b94d877958e5522619d561f6286af02cc7ee7e60a3e7d4102a6c516f40"
24
24
  }
25
25
  }
26
26
  }