mixdog 0.9.84 → 0.9.86

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 (91) hide show
  1. package/README.md +7 -1
  2. package/package.json +3 -2
  3. package/scripts/channel-daemon-stub.mjs +2 -1
  4. package/src/defaults/agents.json +12 -0
  5. package/src/defaults/skills/setup/SKILL.md +44 -12
  6. package/src/help.mjs +40 -9
  7. package/src/lib/keychain-cjs.cjs +11 -1
  8. package/src/rules/agent/43-title-agent.md +22 -0
  9. package/src/rules/shared/01-tool.md +3 -3
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
  11. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
  12. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
  13. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +30 -3
  14. package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
  15. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
  16. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +38 -4
  18. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
  19. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +183 -41
  20. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
  21. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +12 -2
  22. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
  23. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
  24. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
  25. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
  26. package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
  27. package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
  28. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
  29. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
  30. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +44 -73
  33. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  34. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
  35. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  36. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
  37. package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
  38. package/src/runtime/agent/orchestrator/tools/token-manifest.json +5 -0
  39. package/src/runtime/channels/lib/owned-runtime.mjs +6 -8
  40. package/src/runtime/channels/lib/parent-bridge.mjs +27 -15
  41. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
  42. package/src/runtime/channels/lib/worker-ipc.mjs +17 -4
  43. package/src/runtime/media/adapters/gemini-video.mjs +1 -1
  44. package/src/runtime/media/renditions.mjs +1 -1
  45. package/src/runtime/media/store.mjs +1 -1
  46. package/src/runtime/memory/index.mjs +14 -5
  47. package/src/runtime/memory/lib/agent-ipc.mjs +11 -12
  48. package/src/runtime/shared/atomic-file.mjs +76 -38
  49. package/src/runtime/shared/automation-attachments.mjs +3 -3
  50. package/src/runtime/shared/child-guardian.mjs +15 -5
  51. package/src/runtime/shared/child-spawn-gate.mjs +6 -12
  52. package/src/runtime/shared/err-text.mjs +1 -0
  53. package/src/runtime/shared/resource-admission.mjs +7 -3
  54. package/src/runtime/shared/safe-ipc-send.mjs +34 -0
  55. package/src/runtime/shared/turn-snapshot.mjs +0 -3
  56. package/src/session-runtime/lifecycle-api.mjs +43 -12
  57. package/src/session-runtime/prewarm.mjs +35 -0
  58. package/src/session-runtime/remote-control.mjs +8 -47
  59. package/src/session-runtime/remote-transcript.mjs +3 -7
  60. package/src/session-runtime/route-preparation.mjs +24 -0
  61. package/src/session-runtime/runtime-core.mjs +42 -86
  62. package/src/session-runtime/session-turn-api.mjs +9 -2
  63. package/src/session-runtime/workflow-agents-api.mjs +3 -0
  64. package/src/standalone/agent-shard/shard-child.mjs +300 -0
  65. package/src/standalone/agent-shard/shard-pool.mjs +442 -0
  66. package/src/standalone/agent-tool.mjs +222 -0
  67. package/src/standalone/channel-daemon-transport.mjs +126 -185
  68. package/src/standalone/channel-daemon.mjs +11 -11
  69. package/src/standalone/channel-worker.mjs +21 -19
  70. package/src/standalone/memory-runtime-proxy.mjs +98 -23
  71. package/src/tui/App.jsx +16 -9
  72. package/src/tui/app/channel-pickers.mjs +3 -8
  73. package/src/tui/app/route-pickers.mjs +117 -152
  74. package/src/tui/app/settings-picker.mjs +90 -89
  75. package/src/tui/app/slash-commands.mjs +7 -7
  76. package/src/tui/app/slash-dispatch.mjs +23 -58
  77. package/src/tui/app/theme-effort-pickers.mjs +3 -1
  78. package/src/tui/components/StatusLine.jsx +1 -1
  79. package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
  80. package/src/tui/dist/index.mjs +358 -380
  81. package/src/tui/engine/live-share.mjs +2 -2
  82. package/src/tui/engine/session-api-ext.mjs +24 -9
  83. package/src/tui/engine/session-flow.mjs +20 -10
  84. package/src/tui/engine/turn.mjs +28 -16
  85. package/src/tui/engine.mjs +24 -2
  86. package/src/tui/figures.mjs +0 -1
  87. package/src/ui/statusline-format.mjs +0 -1
  88. package/src/hooks/lib/permission-evaluator.cjs +0 -24
  89. package/src/lib/config-cjs.cjs +0 -61
  90. package/src/runtime/shared/launcher-control.mjs +0 -258
  91. package/src/runtime/shared/workspace-router.mjs +0 -259
@@ -1,15 +1,14 @@
1
- import { spawnSync } from 'child_process';
2
1
  import { existsSync } from 'fs';
3
- import { basename, dirname, join } from 'path';
2
+ import { basename, delimiter, dirname, join } from 'path';
4
3
 
5
4
  let _resolvedShell = null;
6
5
  let _configuredShell = null;
7
6
  // Per-kind cache for resolveShellFor(). 'default' aliases resolveShell()'s
8
- // singleton; 'bash'/'powershell' get their own memoized slots. ONLY a
9
- // successful (non-null) resolution is cached — a resolution MISS is
10
- // deliberately not memoized so the next call re-probes (Git Bash / pwsh may
11
- // be installed mid-session, and PATH can change for a long-lived host).
7
+ // singleton; 'bash'/'powershell' get their own memoized slots. Misses receive
8
+ // a short TTL so a missing optional shell cannot rescan PATH for every tool
9
+ // call while a long-lived host can still discover a later installation.
12
10
  const _resolvedShellByKind = new Map();
11
+ const SHELL_RESOLUTION_MISS_TTL_MS = 30_000;
13
12
 
14
13
  export function setConfiguredShell(value = '') {
15
14
  const next = String(value || '').trim();
@@ -43,30 +42,39 @@ function shellSpec(shell, shellType = shellTypeFor(shell)) {
43
42
  return { shell, shellArg: '-c', shellArgs: ['-c'], shellType };
44
43
  }
45
44
 
46
- function firstExistingPathFromWhere(commandName) {
47
- try {
48
- const r = spawnSync('cmd.exe', ['/d', '/s', '/c', `where ${commandName}`], {
49
- stdio: ['ignore', 'pipe', 'ignore'],
50
- windowsHide: true,
51
- timeout: 1000,
52
- });
53
- if (r.status !== 0 || !r.stdout) return null;
54
- const lines = r.stdout.toString('utf8').split(/\r?\n/).map(s => s.trim()).filter(Boolean);
55
- return lines.find(p => existsSync(p)) || null;
56
- } catch {
57
- return null;
45
+ function allExistingPathsFromPath(commandName) {
46
+ const rawPath = String(process.env.PATH || '');
47
+ let cwd = '';
48
+ try { cwd = process.cwd(); } catch {}
49
+ const entries = [cwd, ...rawPath.split(delimiter)]
50
+ .map((entry) => entry.trim().replace(/^"(.*)"$/, '$1'))
51
+ .filter(Boolean);
52
+ const seen = new Set();
53
+ const matches = [];
54
+ for (const entry of entries) {
55
+ const candidate = join(entry, commandName);
56
+ const key = process.platform === 'win32' ? candidate.toLowerCase() : candidate;
57
+ if (seen.has(key)) continue;
58
+ seen.add(key);
59
+ if (existsSync(candidate)) matches.push(candidate);
58
60
  }
61
+ return matches;
62
+ }
63
+
64
+ function firstExistingPathFromPath(commandName, excludeRe = null) {
65
+ return allExistingPathsFromPath(commandName)
66
+ .find((candidate) => !excludeRe || !excludeRe.test(candidate)) || null;
59
67
  }
60
68
 
61
69
  function resolveWindowsPowerShell() {
62
- const pwsh = firstExistingPathFromWhere('pwsh.exe');
70
+ const pwsh = firstExistingPathFromPath('pwsh.exe');
63
71
  if (pwsh) return shellSpec(pwsh, 'powershell');
64
72
 
65
73
  const systemRoot = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
66
74
  const bundled = join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
67
75
  if (existsSync(bundled)) return shellSpec(bundled, 'powershell');
68
76
 
69
- const powershell = firstExistingPathFromWhere('powershell.exe');
77
+ const powershell = firstExistingPathFromPath('powershell.exe');
70
78
  if (powershell) return shellSpec(powershell, 'powershell');
71
79
 
72
80
  return shellSpec('powershell.exe', 'powershell');
@@ -109,12 +117,12 @@ function _isWindows() {
109
117
  // not Git-for-Windows bash).
110
118
  // Returns a posix shellSpec, or null when Git Bash is genuinely not installed.
111
119
  function resolveWindowsGitBash() {
112
- for (const git of allExistingPathsFromWhere('git.exe')) {
120
+ for (const git of allExistingPathsFromPath('git.exe')) {
113
121
  const bash = probeGitBashFromGitExe(git);
114
122
  if (bash) return shellSpec(bash, 'posix');
115
123
  }
116
124
  // Fallback: a bare `bash.exe` on PATH, but never the System32 WSL launcher.
117
- const bash = firstExistingPathFromWhereExcluding('bash.exe', /\\system32\\/i);
125
+ const bash = firstExistingPathFromPath('bash.exe', /\\system32\\/i);
118
126
  if (bash) return shellSpec(bash, 'posix');
119
127
  // Final fallback: probe well-known Git-for-Windows install roots on the
120
128
  // filesystem directly. PATH-independent and spawn-free, so it still resolves
@@ -164,36 +172,6 @@ function probeGitBashFromGitExe(gitExe) {
164
172
  return null;
165
173
  }
166
174
 
167
- function allExistingPathsFromWhere(commandName) {
168
- try {
169
- const r = spawnSync('cmd.exe', ['/d', '/s', '/c', `where ${commandName}`], {
170
- stdio: ['ignore', 'pipe', 'ignore'],
171
- windowsHide: true,
172
- timeout: 1000,
173
- });
174
- if (r.status !== 0 || !r.stdout) return [];
175
- return r.stdout.toString('utf8').split(/\r?\n/).map(s => s.trim())
176
- .filter(Boolean).filter(p => existsSync(p));
177
- } catch {
178
- return [];
179
- }
180
- }
181
-
182
- function firstExistingPathFromWhereExcluding(commandName, excludeRe) {
183
- try {
184
- const r = spawnSync('cmd.exe', ['/d', '/s', '/c', `where ${commandName}`], {
185
- stdio: ['ignore', 'pipe', 'ignore'],
186
- windowsHide: true,
187
- timeout: 1000,
188
- });
189
- if (r.status !== 0 || !r.stdout) return null;
190
- const lines = r.stdout.toString('utf8').split(/\r?\n/).map(s => s.trim()).filter(Boolean);
191
- return lines.find(p => existsSync(p) && !excludeRe.test(p)) || null;
192
- } catch {
193
- return null;
194
- }
195
- }
196
-
197
175
  // Kind-aware shell resolution. kind:
198
176
  //
199
177
  // Resolve a real bash on macOS/Linux. When 'bash' is explicitly requested we
@@ -204,13 +182,8 @@ function resolvePosixBash() {
204
182
  for (const p of ['/bin/bash', '/usr/bin/bash', '/usr/local/bin/bash', '/opt/homebrew/bin/bash']) {
205
183
  if (existsSync(p)) return shellSpec(p, 'posix');
206
184
  }
207
- try {
208
- const r = spawnSync('which', ['bash'], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 1000 });
209
- if (r.status === 0 && r.stdout) {
210
- const p = r.stdout.toString('utf8').split(/\r?\n/).map(s => s.trim()).find(Boolean);
211
- if (p && existsSync(p)) return shellSpec(p, 'posix');
212
- }
213
- } catch { /* fall through to /bin/sh */ }
185
+ const fromPath = firstExistingPathFromPath('bash');
186
+ if (fromPath) return shellSpec(fromPath, 'posix');
214
187
  return shellSpec('/bin/sh', 'posix');
215
188
  }
216
189
 
@@ -220,12 +193,15 @@ function resolvePosixBash() {
220
193
  // binary (/bin/bash, /usr/bin/bash, or `bash` on PATH), falling back to
221
194
  // /bin/sh only when no bash exists (dash/ash distros break on bash syntax).
222
195
  // 'powershell' → on Windows, resolveShell(); elsewhere pwsh if present, else null.
223
- // Each kind is memoized independently, but ONLY on success: a resolution miss
224
- // (null) is not cached, so the caller's clear-error path is re-probed on the
225
- // next call rather than pinned for the life of the process.
196
+ // Each kind is memoized independently. Successes remain stable for the process;
197
+ // misses expire quickly so installs/PATH changes remain discoverable.
226
198
  export function resolveShellFor(kind = 'default') {
227
199
  if (kind == null || kind === 'default') return resolveShell();
228
- if (_resolvedShellByKind.has(kind)) return _resolvedShellByKind.get(kind);
200
+ const cached = _resolvedShellByKind.get(kind);
201
+ if (cached) {
202
+ if (cached.spec || cached.expiresAt > Date.now()) return cached.spec;
203
+ _resolvedShellByKind.delete(kind);
204
+ }
229
205
 
230
206
  let spec = null;
231
207
  if (kind === 'bash') {
@@ -234,21 +210,16 @@ export function resolveShellFor(kind = 'default') {
234
210
  if (_isWindows()) {
235
211
  spec = resolveShell();
236
212
  } else {
237
- const pwsh = (() => {
238
- try {
239
- const r = spawnSync('which', ['pwsh'], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 1000 });
240
- if (r.status !== 0 || !r.stdout) return null;
241
- const p = r.stdout.toString('utf8').split(/\r?\n/).map(s => s.trim()).find(Boolean);
242
- return p && existsSync(p) ? p : null;
243
- } catch { return null; }
244
- })();
213
+ const pwsh = firstExistingPathFromPath('pwsh');
245
214
  spec = pwsh ? shellSpec(pwsh, 'powershell') : null;
246
215
  }
247
216
  } else {
248
217
  spec = resolveShell();
249
218
  }
250
219
 
251
- // Cache positively only — see the _resolvedShellByKind / doc comments above.
252
- if (spec) _resolvedShellByKind.set(kind, spec);
220
+ _resolvedShellByKind.set(kind, {
221
+ spec,
222
+ expiresAt: spec ? Number.POSITIVE_INFINITY : Date.now() + SHELL_RESOLUTION_MISS_TTL_MS,
223
+ });
253
224
  return spec;
254
225
  }
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.1.1",
2
+ "version": "0.1.2",
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.1/mixdog-graph-darwin-arm64",
7
- "sha256": "3fb76a5e3064547b68e7bcab6ce2a855ce615a732157d836cb561c5d7c8d86b6"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.2/mixdog-graph-darwin-arm64",
7
+ "sha256": "e54f09013af2ae057eae89c629237af79caeb248e59c335c64c88fc7b720d662"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.1/mixdog-graph-darwin-x64",
11
- "sha256": "64eeffc17984a76b0fbf7d85f9d84cb5e1bb949a251053feb260f70481235bb1"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.2/mixdog-graph-darwin-x64",
11
+ "sha256": "d4506109fab2a392157ee016d9dfd2524d74363925004f008795eab3fba3de23"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.1/mixdog-graph-linux-arm64",
15
- "sha256": "15d5cbb7575b3e0671c6f5fbef3460c5c9353f17968a80faba737fcc15b49c33"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.2/mixdog-graph-linux-arm64",
15
+ "sha256": "ba5f3e52f95c6e0c976115fea9e60c1a1d4534565a15c2da7284ad9c59c1eeb5"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.1/mixdog-graph-linux-x64",
19
- "sha256": "5079544c09ae2311295cef21978fda3c1c06f93beaaf666cfe1f68971d5b27ae"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.2/mixdog-graph-linux-x64",
19
+ "sha256": "3642d773317a468fe5ba5efc4544ae91a68117b78d577ee2aad37e2639fcc01a"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.1/mixdog-graph-win32-x64.exe",
23
- "sha256": "acfbcd02b96154f4fb342a44dec1a4808752944c5941bbb7983d84ed64464830"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.2/mixdog-graph-win32-x64.exe",
23
+ "sha256": "3d86870fc6d3ce1151dd97b91fc634fd2755e3f3c74f10809aaea34ae3e2aa02"
24
24
  }
25
25
  }
26
26
  }
@@ -0,0 +1,286 @@
1
+ 'use strict';
2
+ // Warm standby pwsh pool (Windows only). Every one-shot shell call pays the
3
+ // pwsh.exe startup cost (~300ms measured). This pool keeps a few pre-spawned
4
+ // pwsh processes blocked on stdin; execShellCommand adopts one and feeds the
5
+ // command via stdin, cutting call latency roughly in half (measured
6
+ // ~330ms → ~155ms end-to-end).
7
+ //
8
+ // Semantics parity (validated empirically against `pwsh -Command <script>`
9
+ // on exit codes, streams, unicode, multi-line scripts, explicit `exit N`,
10
+ // native failures, Write-Error, parse errors):
11
+ // - the bootstrap reads RAW stdin bytes and decodes UTF-8 explicitly
12
+ // ([Console]::In uses the ANSI codepage for redirected stdin — Korean/emoji
13
+ // would mojibake otherwise);
14
+ // - the 0/1 exit mapping (`if ($?) { exit 0 } else { exit 1 }`) is APPENDED
15
+ // INTO the script text so `$?` reflects the user script's LAST statement,
16
+ // exactly what pwsh -Command does implicitly; explicit `exit N` and
17
+ // terminating errors bypass it with identical codes;
18
+ // - cwd is applied in-script (Set-Location + [Environment]::CurrentDirectory)
19
+ // since the standby was spawned before the call's cwd was known.
20
+ //
21
+ // Adoption rules keep this pool NARROW: win32 + pwsh (never Windows
22
+ // PowerShell 5.1, whose `$?` native semantics differ) + the standard
23
+ // -NoLogo/-NoProfile/-NonInteractive/-Command argv + an env whose signature
24
+ // matches the standby's spawn env. Everything else falls back to the classic
25
+ // cold spawn path unchanged. Kill-switch: MIXDOG_PWSH_STANDBY_POOL=0.
26
+ import { spawn } from 'node:child_process';
27
+ import { basename } from 'node:path';
28
+ import { createHash, randomBytes } from 'node:crypto';
29
+ import { killProcessTree, windowsPidHasDescendants } from '../builtin/shell-job-process.mjs';
30
+
31
+ function _poolTarget() {
32
+ const n = Math.floor(Number(process.env.MIXDOG_PWSH_STANDBY_POOL));
33
+ if (Number.isFinite(n) && n >= 0) return Math.min(8, n);
34
+ return 2;
35
+ }
36
+ const POOL_TARGET = _poolTarget();
37
+ // Only standbys older than this are handed out: a younger one is still paying
38
+ // pwsh startup, so adopting it would just relocate the cold cost. Callers get
39
+ // null (classic spawn) while the pool warms.
40
+ const STANDBY_READY_AGE_MS = 400;
41
+ const EXPECTED_ARGS = Object.freeze(['-NoLogo', '-NoProfile', '-NonInteractive', '-Command']);
42
+
43
+ // Marker-loop bootstrap: the standby serves MANY commands over its lifetime.
44
+ // Protocol per command (all lines UTF-8 over stdin):
45
+ // <script lines...>
46
+ // <<<MIXDOG_RUN_<nonce>>>>
47
+ // The loop resets env + location to the spawn baseline, executes the script
48
+ // with the same `$?`→0/1 exit mapping pwsh -Command applies implicitly, then
49
+ // emits `<<<MIXDOG_DONE_<nonce>:<code>>>>` on stdout and
50
+ // `<<<MIXDOG_DONE_<nonce>>>>` on stderr (both streams provably drained).
51
+ // stdin EOF exits the process with the LAST command's code — that is exactly
52
+ // what background-promotion relies on for its exit-file. A user script that
53
+ // calls `exit N` terminates the whole process with N (identical code to the
54
+ // one-shot path); the caller settles via 'close' and the entry is discarded.
55
+ function _standbyBootScript(nonce) {
56
+ const runSentinel = `<<<MIXDOG_RUN_${nonce}>>>`;
57
+ const donePrefix = `<<<MIXDOG_DONE_${nonce}`;
58
+ return [
59
+ '$null = & {0}',
60
+ "[Console]::Out.Write('')",
61
+ "[Console]::Error.Write('')",
62
+ '$__sr = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), (New-Object System.Text.UTF8Encoding($false)))',
63
+ '$__baseEnv = @{}',
64
+ 'Get-ChildItem env: | ForEach-Object { $__baseEnv[$_.Name] = $_.Value }',
65
+ '$__baseLoc = (Get-Location).ProviderPath',
66
+ '$__code = 0',
67
+ 'while ($true) {',
68
+ ' $__buf = New-Object System.Text.StringBuilder',
69
+ ' while ($true) {',
70
+ ' $__line = $__sr.ReadLine()',
71
+ ' if ($null -eq $__line) { exit $__code }',
72
+ ` if ($__line -eq '${runSentinel}') { break }`,
73
+ ' [void]$__buf.AppendLine($__line)',
74
+ ' }',
75
+ ' foreach ($__n in @(Get-ChildItem env: | ForEach-Object Name)) { if (-not $__baseEnv.ContainsKey($__n)) { Remove-Item ("env:$__n") -ErrorAction SilentlyContinue } }',
76
+ ' foreach ($__k in $__baseEnv.Keys) { Set-Item ("env:$__k") $__baseEnv[$__k] }',
77
+ ' Set-Location -LiteralPath $__baseLoc',
78
+ ' $global:LASTEXITCODE = 0',
79
+ ' $global:__MIXDOG_RC = 1',
80
+ ' try {',
81
+ " $__sc = [ScriptBlock]::Create($__buf.ToString() + [Environment]::NewLine + 'if ($?) { $global:__MIXDOG_RC = 0 } else { $global:__MIXDOG_RC = 1 }')",
82
+ ' & $__sc',
83
+ ' } catch {',
84
+ ' $global:__MIXDOG_RC = 1',
85
+ " [Console]::Error.WriteLine('Exception: ' + $_.Exception.Message)",
86
+ ' }',
87
+ ' $__code = $global:__MIXDOG_RC',
88
+ ' [Console]::Out.Flush()',
89
+ ' [Console]::Error.Flush()',
90
+ ` [Console]::Out.WriteLine('${donePrefix}:' + $__code + '>>>')`,
91
+ ` [Console]::Error.WriteLine('${donePrefix}>>>')`,
92
+ '}',
93
+ ].join('\n');
94
+ }
95
+
96
+ const _idle = [];
97
+ let _envSig = null;
98
+ let _exitHookInstalled = false;
99
+
100
+ function _envSignature(env) {
101
+ const h = createHash('sha1');
102
+ for (const k of Object.keys(env).sort()) {
103
+ h.update(k); h.update('='); h.update(String(env[k] ?? '')); h.update('\0');
104
+ }
105
+ return h.digest('base64');
106
+ }
107
+
108
+ function _killEntry(entry) {
109
+ try { entry.child.kill(); } catch { /* already down */ }
110
+ }
111
+
112
+ function _discardIdle(entry) {
113
+ const i = _idle.indexOf(entry);
114
+ if (i !== -1) _idle.splice(i, 1);
115
+ }
116
+
117
+ function _installExitHook() {
118
+ if (_exitHookInstalled) return;
119
+ _exitHookInstalled = true;
120
+ // Idle standbys block on stdin forever; never leave them behind.
121
+ process.once('exit', () => {
122
+ for (const entry of _idle.splice(0)) _killEntry(entry);
123
+ });
124
+ }
125
+
126
+ function _spawnStandby(shell, env, sig) {
127
+ let child;
128
+ const nonce = randomBytes(8).toString('hex');
129
+ try {
130
+ child = spawn(shell, [...EXPECTED_ARGS, _standbyBootScript(nonce)], {
131
+ env,
132
+ cwd: process.env.SystemRoot || 'C:\\Windows',
133
+ windowsHide: true,
134
+ stdio: ['pipe', 'pipe', 'pipe'],
135
+ detached: false,
136
+ });
137
+ } catch { return; }
138
+ const entry = { child, sig, nonce, spawnedAt: Date.now(), taken: false, consumed: false };
139
+ child.on('error', () => { if (!entry.taken) { _discardIdle(entry); _killEntry(entry); } });
140
+ child.on('exit', () => { if (!entry.taken) _discardIdle(entry); });
141
+ // A dead standby's pending stdin write must never crash the daemon.
142
+ child.stdin?.on?.('error', () => { /* swallow — entry is discarded via exit */ });
143
+ // Idle standbys must not hold the event loop open (process handle AND
144
+ // pipe handles each ref it). Re-ref'd on take.
145
+ try {
146
+ child.unref();
147
+ child.stdin?.unref?.();
148
+ child.stdout?.unref?.();
149
+ child.stderr?.unref?.();
150
+ } catch { /* best-effort */ }
151
+ _idle.push(entry);
152
+ }
153
+
154
+ function _topUp(shell, env, sig) {
155
+ while (_idle.length < POOL_TARGET) _spawnStandby(shell, env, sig);
156
+ }
157
+
158
+ /**
159
+ * Adopt a warm standby for this call, or return null for the classic spawn
160
+ * path. On success returns { child, feed(spawnCommand, cwd) }; the caller
161
+ * wires its stdio/exit handlers FIRST and then calls feed() exactly once.
162
+ */
163
+ export function takePwshStandby({ shell, shellArgs, env }) {
164
+ if (process.platform !== 'win32' || POOL_TARGET === 0) return null;
165
+ const stem = basename(String(shell || '')).toLowerCase();
166
+ if (stem !== 'pwsh' && stem !== 'pwsh.exe') return null;
167
+ if (!Array.isArray(shellArgs)
168
+ || shellArgs.length !== EXPECTED_ARGS.length
169
+ || shellArgs.some((a, i) => a !== EXPECTED_ARGS[i])) return null;
170
+ if (!env || typeof env !== 'object') return null;
171
+ _installExitHook();
172
+ const sig = _envSignature(env);
173
+ if (sig !== _envSig) {
174
+ // Env changed (secrets scrub set, locale, config reload): stale
175
+ // standbys would run with the old env — drain and respawn.
176
+ for (const entry of _idle.splice(0)) _killEntry(entry);
177
+ _envSig = sig;
178
+ }
179
+ _topUp(shell, env, sig);
180
+ const now = Date.now();
181
+ let entry = null;
182
+ for (let i = 0; i < _idle.length; i += 1) {
183
+ const cand = _idle[i];
184
+ if (now - cand.spawnedAt < STANDBY_READY_AGE_MS) continue;
185
+ if (cand.child.exitCode !== null || cand.child.signalCode !== null) continue;
186
+ if (!cand.child.stdin || cand.child.stdin.destroyed) continue;
187
+ _idle.splice(i, 1);
188
+ entry = cand;
189
+ break;
190
+ }
191
+ if (!entry) return null;
192
+ entry.taken = true;
193
+ entry.consumed = false;
194
+ try {
195
+ entry.child.ref();
196
+ entry.child.stdin?.ref?.();
197
+ entry.child.stdout?.ref?.();
198
+ entry.child.stderr?.ref?.();
199
+ } catch { /* best-effort */ }
200
+ _topUp(shell, env, sig);
201
+ return {
202
+ child: entry.child,
203
+ nonce: entry.nonce,
204
+ runSentinel: `<<<MIXDOG_RUN_${entry.nonce}>>>`,
205
+ doneMarkerPrefix: `<<<MIXDOG_DONE_${entry.nonce}`,
206
+ run(spawnCommand, cwdForRun) {
207
+ const esc = String(cwdForRun || '').replace(/'/g, "''");
208
+ const prelude = esc
209
+ ? `Set-Location -LiteralPath '${esc}' -ErrorAction Stop\n[Environment]::CurrentDirectory = '${esc}'\n`
210
+ : '';
211
+ try {
212
+ const body = prelude + String(spawnCommand ?? '');
213
+ entry.child.stdin.write(body.endsWith('\n') ? body : `${body}\n`, 'utf8');
214
+ entry.child.stdin.write(`<<<MIXDOG_RUN_${entry.nonce}>>>\n`, 'utf8');
215
+ return true;
216
+ } catch {
217
+ // Feed failed (child died in the take→run window): kill so the
218
+ // caller's close handler settles with an error result.
219
+ _killEntry(entry);
220
+ return false;
221
+ }
222
+ },
223
+ // Background-promotion handoff: after the in-flight command completes,
224
+ // the loop reads EOF and exits with that command's code — the adopted
225
+ // job's close-wired exit file then records the REAL command exit.
226
+ endStdin() {
227
+ entry.consumed = true;
228
+ try { entry.child.stdin.end(); } catch { /* already gone */ }
229
+ },
230
+ // Post-command recycle: return the standby to the pool ONLY when the
231
+ // command left no descendant processes attached to its stdio (a live
232
+ // grandchild would pollute the NEXT command's capture). Dirty, dead,
233
+ // env-stale, or surplus standbys are tree-killed instead. Runs off the
234
+ // result path; the caller releases its admission lease afterwards.
235
+ async recycle() {
236
+ if (entry.consumed) return false;
237
+ entry.consumed = true;
238
+ const c = entry.child;
239
+ const alive = () => c.exitCode === null && c.signalCode === null;
240
+ let dirty = true;
241
+ if (alive() && c.stdin && !c.stdin.destroyed) {
242
+ try { dirty = await windowsPidHasDescendants(c.pid); } catch { dirty = true; }
243
+ }
244
+ if (dirty || !alive() || !c.stdin || c.stdin.destroyed
245
+ || _idle.length >= POOL_TARGET || _envSig !== entry.sig) {
246
+ try { killProcessTree(c.pid, 'SIGKILL'); } catch { /* already down */ }
247
+ _killEntry(entry);
248
+ return false;
249
+ }
250
+ entry.taken = false;
251
+ entry.consumed = false;
252
+ entry.spawnedAt = Date.now() - STANDBY_READY_AGE_MS;
253
+ try {
254
+ c.unref();
255
+ c.stdin?.unref?.();
256
+ c.stdout?.unref?.();
257
+ c.stderr?.unref?.();
258
+ } catch { /* best-effort */ }
259
+ _idle.push(entry);
260
+ return true;
261
+ },
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Boot-time warmup: spawn the standby processes NOW (same gates as take) so a
267
+ * session's first shell call finds a warm pool instead of paying the pwsh
268
+ * startup cost. Best-effort; returns true when standbys were ensured.
269
+ */
270
+ export function prewarmPwshStandbyPool({ shell, shellArgs, env }) {
271
+ if (process.platform !== 'win32' || POOL_TARGET === 0) return false;
272
+ const stem = basename(String(shell || '')).toLowerCase();
273
+ if (stem !== 'pwsh' && stem !== 'pwsh.exe') return false;
274
+ if (!Array.isArray(shellArgs)
275
+ || shellArgs.length !== EXPECTED_ARGS.length
276
+ || shellArgs.some((a, i) => a !== EXPECTED_ARGS[i])) return false;
277
+ if (!env || typeof env !== 'object') return false;
278
+ _installExitHook();
279
+ const sig = _envSignature(env);
280
+ if (sig !== _envSig) {
281
+ for (const entry of _idle.splice(0)) _killEntry(entry);
282
+ _envSig = sig;
283
+ }
284
+ _topUp(shell, env, sig);
285
+ return true;
286
+ }
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.7.20",
2
+ "version": "0.7.21",
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.20/mixdog-patch-darwin-arm64",
7
- "sha256": "12b9b3ad3365fcde21c83c1b42236f6bd70dbe411d79e5c0f7c3c5743f1a3185"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.21/mixdog-patch-darwin-arm64",
7
+ "sha256": "f52db5c65ffdc28ace93a6dd54d7d1c8e76f3daba45a74e63676513fcee4b51b"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.20/mixdog-patch-darwin-x64",
11
- "sha256": "ff5de6da5fec3abbf617c0c3b1ead8e724d4cb0e9948277253889c83bc27a570"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.21/mixdog-patch-darwin-x64",
11
+ "sha256": "024bacaaead16a368fa4a2a148f66f3892ee03be78803c40dda9acd59f3dd2c1"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.20/mixdog-patch-linux-arm64",
15
- "sha256": "76d7cf54df7face32fb20862425ce25ad44e9800440132dea3f0ad387c92f07e"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.21/mixdog-patch-linux-arm64",
15
+ "sha256": "c197931e005fb89211b60b66008bc0c3b42200393483e0ae4a251e2111a06d44"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.20/mixdog-patch-linux-x64",
19
- "sha256": "43f6610beadf25eca752a09775fcb8858818ad2d9f7b8cf6a00deaa47571f3f0"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.21/mixdog-patch-linux-x64",
19
+ "sha256": "8d958f9315ec82a17fa2e21560630f66a4e55a8a56ad92d69ba9f3f5ff61c15c"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.20/mixdog-patch-win32-x64.exe",
23
- "sha256": "4d5d2242f9662aeb968c24df571a498008d9c504fd31214fe9dac31aac669fdd"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.21/mixdog-patch-win32-x64.exe",
23
+ "sha256": "cc927c7bc6f09ecae591c058d0053cd5bc65ee17c56c9bce6871487be1d2baa2"
24
24
  }
25
25
  }
26
26
  }