mixdog 0.9.50 → 0.9.52
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/package.json +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +55 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/hook-bus-test.mjs +23 -0
- package/scripts/internal-tools-normalization-test.mjs +10 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +520 -3
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-bench-cache-break-test.mjs +102 -0
- package/scripts/session-bench.mjs +101 -42
- package/scripts/shell-failure-diagnostics-test.mjs +73 -4
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
- package/scripts/smoke-loop-failure-summary.mjs +16 -0
- package/scripts/smoke-loop.mjs +2 -7
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/tool-failures.mjs +15 -1
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +43 -7
- package/scripts/web-fetch-routing-test.mjs +158 -0
- package/src/cli.mjs +15 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
- package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
- package/src/runtime/search/index.mjs +41 -0
- package/src/runtime/search/lib/http-fetch.mjs +154 -0
- package/src/runtime/search/tool-defs.mjs +23 -0
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +43 -18
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/standalone/hook-bus/handlers.mjs +4 -0
- package/src/standalone/hook-bus/rules.mjs +7 -1
- package/src/standalone/hook-bus.mjs +10 -2
- package/src/tui/App.jsx +17 -11
- package/src/tui/app/live-spinner-visibility.mjs +20 -0
- package/src/tui/dist/index.mjs +101 -281
- package/src/tui/engine/session-api-ext.mjs +13 -2
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +4 -1
- package/src/tui/lib/voice-setup.mjs +16 -0
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
endShellJobWait,
|
|
18
18
|
clearShellJobNotifyCtx,
|
|
19
19
|
shellJobPublicTaskResult,
|
|
20
|
+
attachShellJobResourceLease,
|
|
20
21
|
} from './shell-jobs.mjs';
|
|
21
22
|
import {
|
|
22
23
|
analyzeShellCommandEffects,
|
|
@@ -45,6 +46,7 @@ import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
|
45
46
|
import { resolveOptionalCwd } from './cwd-utils.mjs';
|
|
46
47
|
import { scrubLoaderVars, scrubProviderSecrets } from '../env-scrub.mjs';
|
|
47
48
|
import { resolveSessionCwd, stateFilePath, wrapPowerShellWithCwdProbe, wrapBashWithCwdProbe } from '../shell-state.mjs';
|
|
49
|
+
import { resourceAdmission } from '../../../../shared/resource-admission.mjs';
|
|
48
50
|
|
|
49
51
|
// Post-exec drift detection. After a foreground shell command, compare the
|
|
50
52
|
// live mtime+size of files mixdog has already read this session against their
|
|
@@ -263,7 +265,11 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
263
265
|
const shouldCreate = args.create === true || !userProvidedSession;
|
|
264
266
|
effectiveArgs = { ...effectiveArgs, create: shouldCreate };
|
|
265
267
|
try {
|
|
266
|
-
return await executeBashSessionTool('bash_session', effectiveArgs, bashWorkDir, {
|
|
268
|
+
return await executeBashSessionTool('bash_session', effectiveArgs, bashWorkDir, {
|
|
269
|
+
abortSignal: combinedPersistAbort.signal,
|
|
270
|
+
sessionId: options?.sessionId,
|
|
271
|
+
resourceAdmission: options?.resourceAdmission || resourceAdmission,
|
|
272
|
+
});
|
|
267
273
|
} finally {
|
|
268
274
|
combinedPersistAbort.cleanup();
|
|
269
275
|
}
|
|
@@ -406,24 +412,50 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
406
412
|
wrappedCommand = command;
|
|
407
413
|
}
|
|
408
414
|
if (runInBackground) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
shell,
|
|
416
|
-
shellArg,
|
|
417
|
-
shellArgs,
|
|
418
|
-
shellType,
|
|
419
|
-
// Per-terminal session stamp: the dispatching terminal's
|
|
420
|
-
// claude.exe pid (server-main threads callerSession.clientHostPid).
|
|
421
|
-
clientHostPid: options?.clientHostPid,
|
|
422
|
-
});
|
|
423
|
-
if (job && job.error) return formatShellToolFailure(job.error);
|
|
424
|
-
let task;
|
|
415
|
+
let asyncAbortSignal = null;
|
|
416
|
+
try { asyncAbortSignal = (await getAbortSignalForSession(options?.sessionId)) || null; }
|
|
417
|
+
catch { asyncAbortSignal = null; }
|
|
418
|
+
const combinedAsyncAbort = _combineAbortSignals(asyncAbortSignal, options?.abortSignal || null);
|
|
419
|
+
let asyncLease = null;
|
|
420
|
+
let job;
|
|
425
421
|
try {
|
|
426
|
-
|
|
422
|
+
asyncLease = await (options?.resourceAdmission || resourceAdmission).acquire('shell', {
|
|
423
|
+
signal: combinedAsyncAbort.signal,
|
|
424
|
+
label: String(command).replace(/\s+/g, ' ').slice(0, 120),
|
|
425
|
+
dependency: 'detached',
|
|
426
|
+
});
|
|
427
|
+
if (combinedAsyncAbort.signal?.aborted) {
|
|
428
|
+
throw combinedAsyncAbort.signal.reason || new Error('shell background task cancelled before spawn');
|
|
429
|
+
}
|
|
430
|
+
job = await startBackgroundShellJob({
|
|
431
|
+
command: wrappedCommand,
|
|
432
|
+
timeoutMs: timeout,
|
|
433
|
+
workDir: bashWorkDir,
|
|
434
|
+
mergeStderr,
|
|
435
|
+
spawnEnv,
|
|
436
|
+
shell,
|
|
437
|
+
shellArg,
|
|
438
|
+
shellArgs,
|
|
439
|
+
shellType,
|
|
440
|
+
// Per-terminal session stamp: the dispatching terminal's
|
|
441
|
+
// claude.exe pid (server-main threads callerSession.clientHostPid).
|
|
442
|
+
clientHostPid: options?.clientHostPid,
|
|
443
|
+
...(options?.shellJobRuntime || {}),
|
|
444
|
+
});
|
|
445
|
+
if (job && job.error) {
|
|
446
|
+
if (job.rollbackPending && attachShellJobResourceLease(job.jobId, asyncLease, { allowUnpersisted: true })) {
|
|
447
|
+
asyncLease = null;
|
|
448
|
+
}
|
|
449
|
+
return formatShellToolFailure(job.error);
|
|
450
|
+
}
|
|
451
|
+
if (combinedAsyncAbort.signal?.aborted) {
|
|
452
|
+
try { killShellJob(job.jobId); } catch {}
|
|
453
|
+
throw combinedAsyncAbort.signal.reason || new Error('shell background task cancelled before registration');
|
|
454
|
+
}
|
|
455
|
+
if (job && !job.error && attachShellJobResourceLease(job.jobId, asyncLease)) {
|
|
456
|
+
asyncLease = null;
|
|
457
|
+
}
|
|
458
|
+
const task = registerBackgroundTask({
|
|
427
459
|
taskId: job.jobId,
|
|
428
460
|
surface: 'shell',
|
|
429
461
|
operation: 'shell',
|
|
@@ -446,26 +478,31 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
446
478
|
resultType: 'shell_task_result',
|
|
447
479
|
cancel: () => killShellJob(job.jobId),
|
|
448
480
|
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
481
|
+
if (combinedAsyncAbort.signal?.aborted) {
|
|
482
|
+
try { killShellJob(job.jobId); } catch {}
|
|
483
|
+
cancelBackgroundTask(job.jobId, 'cancelled before background registration completed');
|
|
484
|
+
throw combinedAsyncAbort.signal.reason || new Error('shell background task cancelled during registration');
|
|
485
|
+
}
|
|
486
|
+
// Wire a one-shot completion push so the dispatching session learns
|
|
487
|
+
// the background task finished (no polling tool is auto-driven).
|
|
488
|
+
try {
|
|
489
|
+
watchBackgroundShellJob(job.jobId, {
|
|
490
|
+
notifyFn: typeof options?.notifyFn === 'function' ? options.notifyFn : null,
|
|
491
|
+
callerSessionId: options?.callerSessionId || options?.sessionId,
|
|
492
|
+
routingSessionId: options?.routingSessionId,
|
|
493
|
+
clientHostPid: options?.clientHostPid,
|
|
494
|
+
});
|
|
495
|
+
} catch { /* watcher arm is best-effort; never blocks the spawn */ }
|
|
496
|
+
return _prependDestructiveWarning(command, renderBackgroundTask(task));
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (job?.jobId && !job.error) {
|
|
499
|
+
try { killShellJob(job.jobId); } catch {}
|
|
500
|
+
}
|
|
501
|
+
return formatShellToolFailure(normalizeErrorMessage(error instanceof Error ? error.message : String(error)));
|
|
502
|
+
} finally {
|
|
503
|
+
combinedAsyncAbort.cleanup();
|
|
504
|
+
try { await asyncLease?.release(); } catch {}
|
|
452
505
|
}
|
|
453
|
-
// Wire a one-shot completion push so the dispatching session learns
|
|
454
|
-
// the background task finished (no polling tool is auto-driven). The
|
|
455
|
-
// notify ctx is threaded down from the MCP dispatch frame
|
|
456
|
-
// (server-main agentContext / _dispatchByModule) the same way the
|
|
457
|
-
// agent/explore-style tools receive notifyFn/routingSessionId/clientHostPid.
|
|
458
|
-
// Missing notifyFn (e.g. a non-MCP caller) degrades to a stderr
|
|
459
|
-
// diagnostic inside watchBackgroundShellJob — never fails the spawn.
|
|
460
|
-
try {
|
|
461
|
-
watchBackgroundShellJob(job.jobId, {
|
|
462
|
-
notifyFn: typeof options?.notifyFn === 'function' ? options.notifyFn : null,
|
|
463
|
-
callerSessionId: options?.callerSessionId || options?.sessionId,
|
|
464
|
-
routingSessionId: options?.routingSessionId,
|
|
465
|
-
clientHostPid: options?.clientHostPid,
|
|
466
|
-
});
|
|
467
|
-
} catch { /* watcher arm is best-effort; never blocks the spawn */ }
|
|
468
|
-
return _prependDestructiveWarning(command, renderBackgroundTask(task));
|
|
469
506
|
}
|
|
470
507
|
|
|
471
508
|
let bashAbortSignal = null;
|
|
@@ -10,9 +10,230 @@ export function isPidAlive(pid) {
|
|
|
10
10
|
try {
|
|
11
11
|
process.kill(pid, 0);
|
|
12
12
|
return true;
|
|
13
|
-
} catch {
|
|
14
|
-
return
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return error?.code === 'EPERM';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Admission follows the owned process group on POSIX, not just the spawned
|
|
19
|
+
// shell. A shell can exit after launching a descendant which remains in its
|
|
20
|
+
// detached group. Windows has no process-group probe in Node; the root PID is
|
|
21
|
+
// the strongest non-invasive lifecycle signal available here (tree-kill still
|
|
22
|
+
// uses taskkill /T).
|
|
23
|
+
export function isProcessTreeAlive(pid) {
|
|
24
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
25
|
+
if (process.platform === 'win32') return isPidAlive(pid);
|
|
26
|
+
try {
|
|
27
|
+
process.kill(-pid, 0);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return error?.code === 'EPERM';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const windowsSnapshotCaches = new WeakMap();
|
|
35
|
+
function windowsProcessSnapshot({ fresh = false, spawnFn = spawn } = {}) {
|
|
36
|
+
let windowsSnapshotCache = windowsSnapshotCaches.get(spawnFn);
|
|
37
|
+
if (!windowsSnapshotCache) {
|
|
38
|
+
windowsSnapshotCache = { at: 0, rows: null, inFlight: null };
|
|
39
|
+
windowsSnapshotCaches.set(spawnFn, windowsSnapshotCache);
|
|
40
|
+
}
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
if (!fresh && windowsSnapshotCache.rows && now - windowsSnapshotCache.at < 750) {
|
|
43
|
+
return Promise.resolve(windowsSnapshotCache.rows);
|
|
44
|
+
}
|
|
45
|
+
if (windowsSnapshotCache.inFlight) return windowsSnapshotCache.inFlight;
|
|
46
|
+
const command = [
|
|
47
|
+
"$ErrorActionPreference='Stop'",
|
|
48
|
+
'Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | ForEach-Object {',
|
|
49
|
+
' [Console]::Out.WriteLine(("{0}`t{1}`t{2}" -f $_.ProcessId,$_.ParentProcessId,$_.CreationDate))',
|
|
50
|
+
'}',
|
|
51
|
+
].join('; ');
|
|
52
|
+
windowsSnapshotCache.inFlight = new Promise((resolve) => {
|
|
53
|
+
let child;
|
|
54
|
+
let stdout = '';
|
|
55
|
+
let settled = false;
|
|
56
|
+
let timeout = null;
|
|
57
|
+
const finish = (rows) => {
|
|
58
|
+
if (settled) return;
|
|
59
|
+
settled = true;
|
|
60
|
+
if (timeout) clearTimeout(timeout);
|
|
61
|
+
windowsSnapshotCache.inFlight = null;
|
|
62
|
+
if (rows) {
|
|
63
|
+
windowsSnapshotCache.at = Date.now();
|
|
64
|
+
windowsSnapshotCache.rows = rows;
|
|
65
|
+
}
|
|
66
|
+
resolve(rows);
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
child = spawnFn('powershell.exe', [
|
|
70
|
+
'-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command,
|
|
71
|
+
], {
|
|
72
|
+
windowsHide: true,
|
|
73
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
74
|
+
});
|
|
75
|
+
child.stdout.setEncoding('utf8');
|
|
76
|
+
child.stdout.on('data', (chunk) => {
|
|
77
|
+
if (stdout.length <= 4 * 1024 * 1024) stdout += chunk;
|
|
78
|
+
});
|
|
79
|
+
child.once('error', () => finish(null));
|
|
80
|
+
child.once('close', (code) => {
|
|
81
|
+
if (code !== 0 || !stdout) {
|
|
82
|
+
finish(null);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const rows = new Map();
|
|
86
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
87
|
+
const [pidText, parentText, identity = ''] = line.split('\t');
|
|
88
|
+
const rowPid = Number(pidText);
|
|
89
|
+
const parentPid = Number(parentText);
|
|
90
|
+
if (!Number.isFinite(rowPid) || rowPid <= 0 || !Number.isFinite(parentPid)) continue;
|
|
91
|
+
rows.set(rowPid, { pid: rowPid, parentPid, identity });
|
|
92
|
+
}
|
|
93
|
+
finish(rows);
|
|
94
|
+
});
|
|
95
|
+
timeout = setTimeout(() => {
|
|
96
|
+
try { child.kill(); } catch {}
|
|
97
|
+
finish(null);
|
|
98
|
+
}, 2000);
|
|
99
|
+
if (typeof timeout.unref === 'function') timeout.unref();
|
|
100
|
+
} catch {
|
|
101
|
+
finish(null);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return windowsSnapshotCache.inFlight;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeWindowsSnapshot(snapshot) {
|
|
108
|
+
if (snapshot instanceof Map) return snapshot;
|
|
109
|
+
if (!Array.isArray(snapshot)) return null;
|
|
110
|
+
const rows = new Map();
|
|
111
|
+
for (const row of snapshot) {
|
|
112
|
+
const rowPid = Number(row?.pid);
|
|
113
|
+
const parentPid = Number(row?.parentPid);
|
|
114
|
+
if (!Number.isFinite(rowPid) || rowPid <= 0 || !Number.isFinite(parentPid)) continue;
|
|
115
|
+
rows.set(rowPid, { pid: rowPid, parentPid, identity: String(row?.identity ?? '') });
|
|
116
|
+
}
|
|
117
|
+
return rows;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Invoke onQuiescent exactly once, synchronously when already quiescent or
|
|
121
|
+
// after positive liveness probes stop observing the owned tree/group.
|
|
122
|
+
export function trackProcessTreeQuiescence(
|
|
123
|
+
pid,
|
|
124
|
+
onQuiescent,
|
|
125
|
+
{
|
|
126
|
+
pollMs = process.platform === 'win32' ? 250 : 25,
|
|
127
|
+
probe = null,
|
|
128
|
+
platform = process.platform,
|
|
129
|
+
windowsSnapshot = windowsProcessSnapshot,
|
|
130
|
+
windowsSnapshotSpawn = spawn,
|
|
131
|
+
waitForRootExit = false,
|
|
132
|
+
} = {},
|
|
133
|
+
) {
|
|
134
|
+
let settled = false;
|
|
135
|
+
let timer = null;
|
|
136
|
+
let rootExitConfirmed = !waitForRootExit;
|
|
137
|
+
const rootExitCheck = Promise.withResolvers();
|
|
138
|
+
let rootExitCheckComplete = false;
|
|
139
|
+
const ownedWindowsProcesses = new Map();
|
|
140
|
+
if (platform === 'win32') {
|
|
141
|
+
// The PID returned by spawn is durable pre-exit ownership evidence.
|
|
142
|
+
// Seed it before the first asynchronous snapshot so a child whose
|
|
143
|
+
// parent is this PID remains attributable even if the root is already
|
|
144
|
+
// absent when CIM returns.
|
|
145
|
+
ownedWindowsProcesses.set(pid, null);
|
|
146
|
+
}
|
|
147
|
+
const finish = () => {
|
|
148
|
+
if (settled) return;
|
|
149
|
+
settled = true;
|
|
150
|
+
if (timer) clearInterval(timer);
|
|
151
|
+
try {
|
|
152
|
+
Promise.resolve(onQuiescent?.()).catch(() => {});
|
|
153
|
+
} catch { /* lifecycle cleanup must not throw */ }
|
|
154
|
+
};
|
|
155
|
+
const markRootExitChecked = () => {
|
|
156
|
+
if (!rootExitConfirmed || rootExitCheckComplete) return;
|
|
157
|
+
rootExitCheckComplete = true;
|
|
158
|
+
rootExitCheck.resolve();
|
|
159
|
+
};
|
|
160
|
+
const applyWindowsSnapshot = (snapshot) => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
const rows = normalizeWindowsSnapshot(snapshot);
|
|
163
|
+
let alive = true;
|
|
164
|
+
if (!rows) {
|
|
165
|
+
const candidates = [...ownedWindowsProcesses.keys()];
|
|
166
|
+
alive = candidates.some((candidatePid) => isPidAlive(candidatePid));
|
|
167
|
+
} else {
|
|
168
|
+
const root = rows.get(pid);
|
|
169
|
+
if (root && ownedWindowsProcesses.get(pid) == null) {
|
|
170
|
+
ownedWindowsProcesses.set(pid, root.identity);
|
|
171
|
+
}
|
|
172
|
+
let discovered = true;
|
|
173
|
+
while (discovered) {
|
|
174
|
+
discovered = false;
|
|
175
|
+
for (const row of rows.values()) {
|
|
176
|
+
if (
|
|
177
|
+
!ownedWindowsProcesses.has(row.pid)
|
|
178
|
+
&& ownedWindowsProcesses.has(row.parentPid)
|
|
179
|
+
) {
|
|
180
|
+
ownedWindowsProcesses.set(row.pid, row.identity);
|
|
181
|
+
discovered = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const [ownedPid, identity] of ownedWindowsProcesses) {
|
|
186
|
+
const current = rows.get(ownedPid);
|
|
187
|
+
if (!current || (identity != null && current.identity !== identity)) {
|
|
188
|
+
ownedWindowsProcesses.delete(ownedPid);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
alive = ownedWindowsProcesses.size > 0;
|
|
192
|
+
}
|
|
193
|
+
markRootExitChecked();
|
|
194
|
+
if (rootExitConfirmed && !alive) finish();
|
|
195
|
+
};
|
|
196
|
+
const check = (fresh = false) => {
|
|
197
|
+
let alive = true;
|
|
198
|
+
try {
|
|
199
|
+
if (typeof probe === 'function') {
|
|
200
|
+
alive = probe(pid) === true;
|
|
201
|
+
} else if (platform !== 'win32') {
|
|
202
|
+
// The negative PID is the ownership boundary: never substitute
|
|
203
|
+
// a root-only probe on POSIX.
|
|
204
|
+
alive = isProcessTreeAlive(pid);
|
|
205
|
+
} else {
|
|
206
|
+
Promise.resolve(windowsSnapshot({ fresh, spawnFn: windowsSnapshotSpawn })).then(applyWindowsSnapshot, () => {});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
markRootExitChecked();
|
|
213
|
+
if (rootExitConfirmed && !alive) finish();
|
|
214
|
+
};
|
|
215
|
+
check();
|
|
216
|
+
if (!settled) {
|
|
217
|
+
timer = setInterval(check, Math.max(1, Number(pollMs) || 25));
|
|
218
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
15
219
|
}
|
|
220
|
+
return {
|
|
221
|
+
get pending() { return !settled; },
|
|
222
|
+
afterRootExitCheck() { return rootExitCheck.promise; },
|
|
223
|
+
rootExited() {
|
|
224
|
+
if (settled || rootExitConfirmed) return false;
|
|
225
|
+
rootExitConfirmed = true;
|
|
226
|
+
check(true);
|
|
227
|
+
return true;
|
|
228
|
+
},
|
|
229
|
+
cancel() {
|
|
230
|
+
if (settled) return false;
|
|
231
|
+
settled = true;
|
|
232
|
+
if (timer) clearInterval(timer);
|
|
233
|
+
markRootExitChecked();
|
|
234
|
+
return true;
|
|
235
|
+
},
|
|
236
|
+
};
|
|
16
237
|
}
|
|
17
238
|
|
|
18
239
|
export function killProcessTree(pid, signal = 'SIGTERM') {
|