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.
- package/README.md +7 -1
- package/package.json +3 -2
- package/scripts/channel-daemon-stub.mjs +2 -1
- package/src/defaults/agents.json +12 -0
- package/src/defaults/skills/setup/SKILL.md +44 -12
- package/src/help.mjs +40 -9
- package/src/lib/keychain-cjs.cjs +11 -1
- package/src/rules/agent/43-title-agent.md +22 -0
- package/src/rules/shared/01-tool.md +3 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +30 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +38 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +183 -41
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +44 -73
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
- package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +5 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +6 -8
- package/src/runtime/channels/lib/parent-bridge.mjs +27 -15
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
- package/src/runtime/channels/lib/worker-ipc.mjs +17 -4
- package/src/runtime/media/adapters/gemini-video.mjs +1 -1
- package/src/runtime/media/renditions.mjs +1 -1
- package/src/runtime/media/store.mjs +1 -1
- package/src/runtime/memory/index.mjs +14 -5
- package/src/runtime/memory/lib/agent-ipc.mjs +11 -12
- package/src/runtime/shared/atomic-file.mjs +76 -38
- package/src/runtime/shared/automation-attachments.mjs +3 -3
- package/src/runtime/shared/child-guardian.mjs +15 -5
- package/src/runtime/shared/child-spawn-gate.mjs +6 -12
- package/src/runtime/shared/err-text.mjs +1 -0
- package/src/runtime/shared/resource-admission.mjs +7 -3
- package/src/runtime/shared/safe-ipc-send.mjs +34 -0
- package/src/runtime/shared/turn-snapshot.mjs +0 -3
- package/src/session-runtime/lifecycle-api.mjs +43 -12
- package/src/session-runtime/prewarm.mjs +35 -0
- package/src/session-runtime/remote-control.mjs +8 -47
- package/src/session-runtime/remote-transcript.mjs +3 -7
- package/src/session-runtime/route-preparation.mjs +24 -0
- package/src/session-runtime/runtime-core.mjs +42 -86
- package/src/session-runtime/session-turn-api.mjs +9 -2
- package/src/session-runtime/workflow-agents-api.mjs +3 -0
- package/src/standalone/agent-shard/shard-child.mjs +300 -0
- package/src/standalone/agent-shard/shard-pool.mjs +442 -0
- package/src/standalone/agent-tool.mjs +222 -0
- package/src/standalone/channel-daemon-transport.mjs +126 -185
- package/src/standalone/channel-daemon.mjs +11 -11
- package/src/standalone/channel-worker.mjs +21 -19
- package/src/standalone/memory-runtime-proxy.mjs +98 -23
- package/src/tui/App.jsx +16 -9
- package/src/tui/app/channel-pickers.mjs +3 -8
- package/src/tui/app/route-pickers.mjs +117 -152
- package/src/tui/app/settings-picker.mjs +90 -89
- package/src/tui/app/slash-commands.mjs +7 -7
- package/src/tui/app/slash-dispatch.mjs +23 -58
- package/src/tui/app/theme-effort-pickers.mjs +3 -1
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
- package/src/tui/dist/index.mjs +358 -380
- package/src/tui/engine/live-share.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +24 -9
- package/src/tui/engine/session-flow.mjs +20 -10
- package/src/tui/engine/turn.mjs +28 -16
- package/src/tui/engine.mjs +24 -2
- package/src/tui/figures.mjs +0 -1
- package/src/ui/statusline-format.mjs +0 -1
- package/src/hooks/lib/permission-evaluator.cjs +0 -24
- package/src/lib/config-cjs.cjs +0 -61
- package/src/runtime/shared/launcher-control.mjs +0 -258
- 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.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
224
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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.
|
|
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.
|
|
7
|
-
"sha256": "
|
|
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.
|
|
11
|
-
"sha256": "
|
|
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.
|
|
15
|
-
"sha256": "
|
|
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.
|
|
19
|
-
"sha256": "
|
|
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.
|
|
23
|
-
"sha256": "
|
|
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.
|
|
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.
|
|
7
|
-
"sha256": "
|
|
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.
|
|
11
|
-
"sha256": "
|
|
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.
|
|
15
|
-
"sha256": "
|
|
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.
|
|
19
|
-
"sha256": "
|
|
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.
|
|
23
|
-
"sha256": "
|
|
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
|
}
|