mixdog 0.9.51 → 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/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 +324 -3
- 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 +320 -1
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +5 -7
- 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/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 +130 -47
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
- 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 +122 -63
- 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/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/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -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/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 +6 -3
- 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/tui/dist/index.mjs +73 -278
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, watch as fsWatch, writeFileSync } from 'fs';
|
|
2
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, unlinkSync, watch as fsWatch, writeFileSync } from 'fs';
|
|
3
3
|
import { basename } from 'path';
|
|
4
4
|
import { stripAnsi } from '../shell-command.mjs';
|
|
5
5
|
import { scrubLoaderVars, scrubProviderSecrets } from '../env-scrub.mjs';
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from './shell-job-paths.mjs';
|
|
35
35
|
import {
|
|
36
36
|
isPidAlive,
|
|
37
|
+
trackProcessTreeQuiescence,
|
|
37
38
|
killProcessTree,
|
|
38
39
|
_unregisterLiveJobPid,
|
|
39
40
|
_installShellJobsExitHook,
|
|
@@ -53,6 +54,88 @@ function sleep(ms) {
|
|
|
53
54
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
const SPAWN_ERROR_GUARD = Symbol('mixdog.spawnErrorGuard');
|
|
58
|
+
function awaitSpawnReady(child, label) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
if (!child || typeof child.once !== 'function') {
|
|
61
|
+
reject(new Error(`${label} spawn returned no child process`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let spawned = false;
|
|
65
|
+
let bufferedError = null;
|
|
66
|
+
const cleanup = () => child.removeListener('spawn', onSpawn);
|
|
67
|
+
const onError = (error) => {
|
|
68
|
+
if (spawned) {
|
|
69
|
+
bufferedError = bufferedError || error;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
cleanup();
|
|
73
|
+
child.removeListener('error', onError);
|
|
74
|
+
reject(error);
|
|
75
|
+
};
|
|
76
|
+
const onSpawn = () => {
|
|
77
|
+
cleanup();
|
|
78
|
+
spawned = true;
|
|
79
|
+
const pid = Number(child.pid);
|
|
80
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
81
|
+
child.removeListener('error', onError);
|
|
82
|
+
reject(new Error(`${label} spawn returned no pid`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
child[SPAWN_ERROR_GUARD] = {
|
|
86
|
+
adopt(handler) {
|
|
87
|
+
child.on('error', handler);
|
|
88
|
+
child.removeListener('error', onError);
|
|
89
|
+
if (bufferedError) handler(bufferedError);
|
|
90
|
+
delete child[SPAWN_ERROR_GUARD];
|
|
91
|
+
},
|
|
92
|
+
discard() {
|
|
93
|
+
child.removeListener('error', onError);
|
|
94
|
+
delete child[SPAWN_ERROR_GUARD];
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
resolve(child);
|
|
98
|
+
};
|
|
99
|
+
child.once('error', onError);
|
|
100
|
+
child.once('spawn', onSpawn);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function adoptSpawnErrorHandler(child, handler) {
|
|
105
|
+
const guard = child?.[SPAWN_ERROR_GUARD];
|
|
106
|
+
if (guard) guard.adopt(handler);
|
|
107
|
+
else child?.on?.('error', handler);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function rollbackSpawnedChild(child, { timeoutMs = 5000 } = {}) {
|
|
111
|
+
if (!child || child.exitCode != null || child.signalCode != null) {
|
|
112
|
+
return { confirmed: true, errors: [] };
|
|
113
|
+
}
|
|
114
|
+
const errors = [];
|
|
115
|
+
let timer = null;
|
|
116
|
+
let settled = false;
|
|
117
|
+
const outcome = await new Promise((resolve) => {
|
|
118
|
+
const finish = (confirmed) => {
|
|
119
|
+
if (settled) return;
|
|
120
|
+
settled = true;
|
|
121
|
+
if (timer) clearTimeout(timer);
|
|
122
|
+
child.removeListener('exit', onExit);
|
|
123
|
+
child.removeListener('close', onExit);
|
|
124
|
+
child.removeListener('error', onError);
|
|
125
|
+
resolve({ confirmed, errors });
|
|
126
|
+
};
|
|
127
|
+
const onExit = () => finish(true);
|
|
128
|
+
const onError = (error) => { errors.push(error); };
|
|
129
|
+
child.on('error', onError);
|
|
130
|
+
child.once('exit', onExit);
|
|
131
|
+
child.once('close', onExit);
|
|
132
|
+
timer = setTimeout(() => finish(false), Math.max(1, Number(timeoutMs) || 5000));
|
|
133
|
+
try { killProcessTree(child.pid, 'SIGKILL'); } catch (error) { errors.push(error); }
|
|
134
|
+
try { child.kill?.('SIGKILL'); } catch (error) { errors.push(error); }
|
|
135
|
+
});
|
|
136
|
+
return outcome;
|
|
137
|
+
}
|
|
138
|
+
|
|
56
139
|
const JOB_STATUS_PREVIEW_MAX_BYTES = 4096;
|
|
57
140
|
const JOB_STATUS_PREVIEW_MAX_LINES = 20;
|
|
58
141
|
const JOB_STATUS_PREVIEW_MAX_CHARS = 1200;
|
|
@@ -300,6 +383,11 @@ export function killShellJob(jobId) {
|
|
|
300
383
|
const detail = readShellJobDetail(jobId);
|
|
301
384
|
if (!detail) return null;
|
|
302
385
|
if (detail.status !== 'running') {
|
|
386
|
+
if (detail.terminationPending === true) {
|
|
387
|
+
killProcessTree(detail.pid, 'SIGKILL');
|
|
388
|
+
return { ...detail, killed: true, note: 'termination still awaiting confirmed process exit' };
|
|
389
|
+
}
|
|
390
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, detail.pid);
|
|
303
391
|
return { ...detail, killed: false, note: `task already ${detail.status}` };
|
|
304
392
|
}
|
|
305
393
|
killProcessTree(detail.pid, 'SIGTERM');
|
|
@@ -308,8 +396,14 @@ export function killShellJob(jobId) {
|
|
|
308
396
|
detail.error = 'killed by user (KillShell)';
|
|
309
397
|
detail.finishedAt = new Date().toISOString();
|
|
310
398
|
trimShellJobSpill(detail);
|
|
399
|
+
detail.terminationPending = true;
|
|
311
400
|
writeShellJobDetail(detail);
|
|
312
|
-
|
|
401
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, detail.pid, {
|
|
402
|
+
onConfirmed: () => {
|
|
403
|
+
detail.terminationPending = false;
|
|
404
|
+
try { writeShellJobDetail(detail); } catch {}
|
|
405
|
+
},
|
|
406
|
+
});
|
|
313
407
|
return { ...attachJobInsights(detail), killed: true };
|
|
314
408
|
}
|
|
315
409
|
|
|
@@ -343,7 +437,7 @@ function refreshShellJob(jobId) {
|
|
|
343
437
|
// producer can't leave a 100MB stdout/stderr log behind.
|
|
344
438
|
trimShellJobSpill(detail);
|
|
345
439
|
writeShellJobDetail(detail);
|
|
346
|
-
|
|
440
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, detail.pid);
|
|
347
441
|
return detail;
|
|
348
442
|
}
|
|
349
443
|
const timeoutMs = Number(detail.timeoutMs || 0);
|
|
@@ -355,8 +449,14 @@ function refreshShellJob(jobId) {
|
|
|
355
449
|
detail.finishedAt = new Date().toISOString();
|
|
356
450
|
detail.error = `timed out after ${timeoutMs} ms`;
|
|
357
451
|
trimShellJobSpill(detail);
|
|
452
|
+
detail.terminationPending = true;
|
|
358
453
|
writeShellJobDetail(detail);
|
|
359
|
-
|
|
454
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, detail.pid, {
|
|
455
|
+
onConfirmed: () => {
|
|
456
|
+
detail.terminationPending = false;
|
|
457
|
+
try { writeShellJobDetail(detail); } catch {}
|
|
458
|
+
},
|
|
459
|
+
});
|
|
360
460
|
return detail;
|
|
361
461
|
}
|
|
362
462
|
// Output size watchdog: cap the job's spilled stdout/stderr at the same
|
|
@@ -369,8 +469,14 @@ function refreshShellJob(jobId) {
|
|
|
369
469
|
detail.finishedAt = new Date().toISOString();
|
|
370
470
|
detail.error = `output exceeded ${SHELL_JOB_OUTPUT_DISK_CAP} byte cap`;
|
|
371
471
|
trimShellJobSpill(detail);
|
|
472
|
+
detail.terminationPending = true;
|
|
372
473
|
writeShellJobDetail(detail);
|
|
373
|
-
|
|
474
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, detail.pid, {
|
|
475
|
+
onConfirmed: () => {
|
|
476
|
+
detail.terminationPending = false;
|
|
477
|
+
try { writeShellJobDetail(detail); } catch {}
|
|
478
|
+
},
|
|
479
|
+
});
|
|
374
480
|
return detail;
|
|
375
481
|
}
|
|
376
482
|
if (detail.pid && !isPidAlive(detail.pid)) {
|
|
@@ -378,13 +484,13 @@ function refreshShellJob(jobId) {
|
|
|
378
484
|
detail.finishedAt = new Date().toISOString();
|
|
379
485
|
detail.error = 'process exited without completion marker';
|
|
380
486
|
writeShellJobDetail(detail);
|
|
381
|
-
|
|
487
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, detail.pid);
|
|
382
488
|
}
|
|
383
489
|
return detail;
|
|
384
490
|
}
|
|
385
491
|
|
|
386
|
-
export function startBackgroundShellJob(
|
|
387
|
-
return _startBackgroundShellJobImpl(
|
|
492
|
+
export async function startBackgroundShellJob(options) {
|
|
493
|
+
return _startBackgroundShellJobImpl(options || {});
|
|
388
494
|
}
|
|
389
495
|
|
|
390
496
|
// In-process completion watcher. After a background shell task is spawned the
|
|
@@ -417,6 +523,86 @@ const backgroundShellJobWatchers = new Map();
|
|
|
417
523
|
// cannot be armed because the direct notifyFn is unavailable, this poller still
|
|
418
524
|
// reconciles the shell job into the shared background-task row.
|
|
419
525
|
const shellTaskSafetyPollers = new Map();
|
|
526
|
+
const shellJobResourceLeases = new Map();
|
|
527
|
+
const unpersistedTerminalJobs = new Set();
|
|
528
|
+
const shellJobQuiescenceTrackers = new Map();
|
|
529
|
+
function releaseShellJobResourceLease(jobId) {
|
|
530
|
+
const lease = shellJobResourceLeases.get(jobId);
|
|
531
|
+
if (!lease) return false;
|
|
532
|
+
shellJobResourceLeases.delete(jobId);
|
|
533
|
+
try { Promise.resolve(lease.release()).catch(() => {}); } catch { /* admission cleanup must not mask job state */ }
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
export function attachShellJobResourceLease(jobId, lease, { allowUnpersisted = false } = {}) {
|
|
537
|
+
if (!jobId || !lease || typeof lease.release !== 'function') return false;
|
|
538
|
+
if (allowUnpersisted && unpersistedTerminalJobs.delete(jobId)) {
|
|
539
|
+
try { Promise.resolve(lease.release()).catch(() => {}); } catch {}
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
const detail = readShellJobDetail(jobId);
|
|
543
|
+
if ((!detail && !allowUnpersisted) || (detail && detail.status !== 'running')) {
|
|
544
|
+
try { Promise.resolve(lease.release()).catch(() => {}); } catch {}
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
releaseShellJobResourceLease(jobId);
|
|
548
|
+
shellJobResourceLeases.set(jobId, lease);
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
function releaseShellJobOwnershipWhenQuiescent(jobId, pid, {
|
|
552
|
+
onConfirmed = null,
|
|
553
|
+
allowLateLease = false,
|
|
554
|
+
deferUntilRootExit = false,
|
|
555
|
+
} = {}) {
|
|
556
|
+
const numericPid = Number(pid);
|
|
557
|
+
if (!jobId || !Number.isFinite(numericPid) || numericPid <= 0) return false;
|
|
558
|
+
const existing = shellJobQuiescenceTrackers.get(jobId);
|
|
559
|
+
if (existing) {
|
|
560
|
+
if (onConfirmed) existing.callbacks.add(onConfirmed);
|
|
561
|
+
if (allowLateLease) existing.allowLateLease = true;
|
|
562
|
+
if (!deferUntilRootExit) existing.tracker?.rootExited?.();
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
const entry = {
|
|
566
|
+
callbacks: new Set(onConfirmed ? [onConfirmed] : []),
|
|
567
|
+
allowLateLease,
|
|
568
|
+
tracker: null,
|
|
569
|
+
};
|
|
570
|
+
shellJobQuiescenceTrackers.set(jobId, entry);
|
|
571
|
+
entry.tracker = trackProcessTreeQuiescence(numericPid, () => {
|
|
572
|
+
shellJobQuiescenceTrackers.delete(jobId);
|
|
573
|
+
_unregisterLiveJobPid(numericPid);
|
|
574
|
+
for (const callback of entry.callbacks) {
|
|
575
|
+
try { callback(); } catch {}
|
|
576
|
+
}
|
|
577
|
+
if (!releaseShellJobResourceLease(jobId) && entry.allowLateLease) {
|
|
578
|
+
unpersistedTerminalJobs.add(jobId);
|
|
579
|
+
}
|
|
580
|
+
}, { waitForRootExit: deferUntilRootExit });
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
function trackChildUntilConfirmedExit(child, jobId, onConfirmed = null) {
|
|
584
|
+
const pid = Number(child?.pid);
|
|
585
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
586
|
+
_installShellJobsExitHook();
|
|
587
|
+
_registerLiveJobPid(pid, jobId);
|
|
588
|
+
let settled = false;
|
|
589
|
+
const onError = () => {};
|
|
590
|
+
const finish = () => {
|
|
591
|
+
if (settled) return;
|
|
592
|
+
settled = true;
|
|
593
|
+
child.removeListener('exit', finish);
|
|
594
|
+
child.removeListener('close', finish);
|
|
595
|
+
child.removeListener('error', onError);
|
|
596
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, pid, {
|
|
597
|
+
onConfirmed,
|
|
598
|
+
allowLateLease: true,
|
|
599
|
+
});
|
|
600
|
+
};
|
|
601
|
+
child.on('error', onError);
|
|
602
|
+
child.once('exit', finish);
|
|
603
|
+
child.once('close', finish);
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
420
606
|
// Persistent notify ctx per jobId, set at FIRST arm and surviving cancel — so a
|
|
421
607
|
// re-arm after a task wait timeout can reconstruct the notify wiring without the
|
|
422
608
|
// caller threading the ctx back in. Deleted only in the watcher's cleanup() on
|
|
@@ -524,8 +710,14 @@ function markShellJobCancelledByShutdown(jobId, reason = 'shutdown') {
|
|
|
524
710
|
detail.finishedAt = new Date().toISOString();
|
|
525
711
|
try { writeFileSync(detail.exitPath || shellJobExitPath(jobId), '137'); } catch { /* ignore */ }
|
|
526
712
|
try { writeFileSync(detail.donePath || shellJobDonePath(jobId), ''); } catch { /* ignore */ }
|
|
713
|
+
detail.terminationPending = true;
|
|
527
714
|
writeShellJobDetail(detail);
|
|
528
|
-
|
|
715
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, detail.pid, {
|
|
716
|
+
onConfirmed: () => {
|
|
717
|
+
detail.terminationPending = false;
|
|
718
|
+
try { writeShellJobDetail(detail); } catch {}
|
|
719
|
+
},
|
|
720
|
+
});
|
|
529
721
|
return true;
|
|
530
722
|
}
|
|
531
723
|
// Register a synchronous task waiter. Paired with endShellJobWait in a
|
|
@@ -555,12 +747,11 @@ export function shutdownShellJobs(reason = 'runtime-close', { sync = false } = {
|
|
|
555
747
|
try { cancelBackgroundShellJobWatch(jobId); } catch { /* ignore */ }
|
|
556
748
|
}
|
|
557
749
|
for (const pid of livePids) _killLiveJobPid(pid, { sync });
|
|
558
|
-
_liveJobPids.clear();
|
|
559
|
-
_liveJobIdsByPid.clear();
|
|
560
750
|
let marked = 0;
|
|
561
751
|
for (const jobId of jobIds) {
|
|
562
752
|
try { if (markShellJobCancelledByShutdown(jobId, reason)) marked += 1; } catch { /* ignore */ }
|
|
563
753
|
}
|
|
754
|
+
unpersistedTerminalJobs.clear();
|
|
564
755
|
backgroundShellJobWatchers.clear();
|
|
565
756
|
for (const jobId of [...shellTaskSafetyPollers.keys()]) clearShellTaskSafetyNet(jobId);
|
|
566
757
|
jobNotifyCtxByJobId.clear();
|
|
@@ -818,13 +1009,20 @@ function _armAdoptedJobCapPoll(jobId) {
|
|
|
818
1009
|
return tick;
|
|
819
1010
|
}
|
|
820
1011
|
|
|
821
|
-
function _startBackgroundShellJobImpl({
|
|
1012
|
+
async function _startBackgroundShellJobImpl({
|
|
1013
|
+
command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellArgs,
|
|
1014
|
+
shellType, clientHostPid, spawnFn = spawn, writeDetailFn = writeShellJobDetail,
|
|
1015
|
+
rollbackTimeoutMs = 5000,
|
|
1016
|
+
}) {
|
|
822
1017
|
// Route ANY PowerShell shell to the PS wrapper, regardless of platform.
|
|
823
1018
|
// Gating on win32 sent shell:'powershell' on macOS/Linux down the POSIX
|
|
824
1019
|
// path below, which spawns `pwsh <wrapper>.sh` — pwsh then tries to run a
|
|
825
1020
|
// bash script. isPowerShellShell already matches pwsh/powershell by stem.
|
|
826
1021
|
if (isPowerShellShell(shell, shellType)) {
|
|
827
|
-
return startBackgroundPowerShellJob({
|
|
1022
|
+
return startBackgroundPowerShellJob({
|
|
1023
|
+
command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, clientHostPid,
|
|
1024
|
+
spawnFn, writeDetailFn,
|
|
1025
|
+
});
|
|
828
1026
|
}
|
|
829
1027
|
|
|
830
1028
|
// POSIX-shell wrapper path. On Windows this runs for shell:'bash' (Git
|
|
@@ -903,19 +1101,19 @@ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr
|
|
|
903
1101
|
// R11: scrub loader/execution vars even though bash-tool.mjs already
|
|
904
1102
|
// scrubs upstream — defense-in-depth at the spawn site catches future
|
|
905
1103
|
// callers that build their own spawnEnv.
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1104
|
+
let child;
|
|
1105
|
+
try {
|
|
1106
|
+
child = spawnFn(shell, [wrappedTempPath], {
|
|
1107
|
+
cwd: workDir,
|
|
1108
|
+
env: scrubLoaderVars(scrubProviderSecrets({ ...spawnEnv })),
|
|
1109
|
+
stdio: 'ignore',
|
|
1110
|
+
...detachedSpawnOpts,
|
|
1111
|
+
});
|
|
1112
|
+
await awaitSpawnReady(child, 'POSIX background task');
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
try { unlinkSync(wrappedTempPath); } catch {}
|
|
1115
|
+
return { jobId, kind: 'bash', status: 'failed', error: `failed to spawn shell background task: ${e?.message || e}` };
|
|
1116
|
+
}
|
|
919
1117
|
const detail = {
|
|
920
1118
|
jobId,
|
|
921
1119
|
kind: 'bash',
|
|
@@ -934,7 +1132,79 @@ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr
|
|
|
934
1132
|
ownerHostPid: resolveJobOwnerHostPid(clientHostPid),
|
|
935
1133
|
startedAt: new Date().toISOString(),
|
|
936
1134
|
};
|
|
937
|
-
|
|
1135
|
+
let terminal = false;
|
|
1136
|
+
let runtimeOwned = false;
|
|
1137
|
+
const onRuntimeError = (error) => {
|
|
1138
|
+
if (terminal) return;
|
|
1139
|
+
terminal = true;
|
|
1140
|
+
detail.status = 'failed';
|
|
1141
|
+
detail.terminationPending = true;
|
|
1142
|
+
detail.error = `shell background task process error: ${error?.message || error}`;
|
|
1143
|
+
detail.finishedAt = new Date().toISOString();
|
|
1144
|
+
try { writeDetailFn(detail); } catch {}
|
|
1145
|
+
if (runtimeOwned) void (async () => {
|
|
1146
|
+
const rollback = await rollbackSpawnedChild(child, { timeoutMs: rollbackTimeoutMs });
|
|
1147
|
+
if (rollback.confirmed) {
|
|
1148
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, child.pid, {
|
|
1149
|
+
onConfirmed: () => {
|
|
1150
|
+
detail.terminationPending = false;
|
|
1151
|
+
try { writeDetailFn(detail); } catch {}
|
|
1152
|
+
},
|
|
1153
|
+
});
|
|
1154
|
+
} else {
|
|
1155
|
+
detail.error += `; termination unconfirmed after ${rollbackTimeoutMs}ms (process remains tracked)`;
|
|
1156
|
+
try { writeDetailFn(detail); } catch {}
|
|
1157
|
+
trackChildUntilConfirmedExit(child, jobId, () => {
|
|
1158
|
+
detail.terminationPending = false;
|
|
1159
|
+
try { writeDetailFn(detail); } catch {}
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
})();
|
|
1163
|
+
};
|
|
1164
|
+
adoptSpawnErrorHandler(child, onRuntimeError);
|
|
1165
|
+
if (terminal) {
|
|
1166
|
+
const rollback = await rollbackSpawnedChild(child, { timeoutMs: rollbackTimeoutMs });
|
|
1167
|
+
detail.terminationPending = !rollback.confirmed;
|
|
1168
|
+
try { writeDetailFn(detail); } catch {}
|
|
1169
|
+
try { unlinkSync(wrappedTempPath); } catch {}
|
|
1170
|
+
return rollback.confirmed
|
|
1171
|
+
? detail
|
|
1172
|
+
: {
|
|
1173
|
+
...detail,
|
|
1174
|
+
rollbackPending: trackChildUntilConfirmedExit(child, jobId, () => {
|
|
1175
|
+
detail.terminationPending = false;
|
|
1176
|
+
try { writeDetailFn(detail); } catch {}
|
|
1177
|
+
}),
|
|
1178
|
+
error: `${detail.error}; termination unconfirmed after ${rollbackTimeoutMs}ms (process remains tracked)`,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
try {
|
|
1182
|
+
writeDetailFn(detail);
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
terminal = true;
|
|
1185
|
+
const rollback = await rollbackSpawnedChild(child, { timeoutMs: rollbackTimeoutMs });
|
|
1186
|
+
child.removeListener('error', onRuntimeError);
|
|
1187
|
+
child[SPAWN_ERROR_GUARD]?.discard?.();
|
|
1188
|
+
try { unlinkSync(wrappedTempPath); } catch {}
|
|
1189
|
+
const failure = { jobId, kind: 'bash', status: 'failed', error: `failed to persist shell background task: ${e?.message || e}` };
|
|
1190
|
+
if (!rollback.confirmed) {
|
|
1191
|
+
failure.rollbackPending = trackChildUntilConfirmedExit(child, jobId);
|
|
1192
|
+
failure.error += `; termination unconfirmed after ${rollbackTimeoutMs}ms (process remains tracked)`;
|
|
1193
|
+
}
|
|
1194
|
+
return failure;
|
|
1195
|
+
}
|
|
1196
|
+
startChildGuardian({
|
|
1197
|
+
childPid: child.pid,
|
|
1198
|
+
childGroupPid: child.pid,
|
|
1199
|
+
label: 'shell-job',
|
|
1200
|
+
});
|
|
1201
|
+
_installShellJobsExitHook();
|
|
1202
|
+
_registerLiveJobPid(child.pid, jobId);
|
|
1203
|
+
runtimeOwned = true;
|
|
1204
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, child.pid, {
|
|
1205
|
+
allowLateLease: true,
|
|
1206
|
+
deferUntilRootExit: true,
|
|
1207
|
+
});
|
|
938
1208
|
// Deadline cleanup poke only when a timeout is enforced; an unlimited
|
|
939
1209
|
// (timeoutMs<=0) job has no deadline — completion is observed via the
|
|
940
1210
|
// fs.watch/poll watcher and refreshShellJob on task queries.
|
|
@@ -945,7 +1215,10 @@ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr
|
|
|
945
1215
|
return detail;
|
|
946
1216
|
}
|
|
947
1217
|
|
|
948
|
-
function startBackgroundPowerShellJob({
|
|
1218
|
+
async function startBackgroundPowerShellJob({
|
|
1219
|
+
command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, clientHostPid,
|
|
1220
|
+
spawnFn = spawn, writeDetailFn = writeShellJobDetail, rollbackTimeoutMs = 5000,
|
|
1221
|
+
}) {
|
|
949
1222
|
const jobId = `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
950
1223
|
const stdoutPath = shellJobStdoutPath(jobId);
|
|
951
1224
|
const rawStderrPath = shellJobStderrPath(jobId);
|
|
@@ -1059,27 +1332,23 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
1059
1332
|
// process; runtime.close()/exit hooks reap it instead of daemonizing it.
|
|
1060
1333
|
let child;
|
|
1061
1334
|
try {
|
|
1062
|
-
child =
|
|
1335
|
+
child = spawnFn(shell, wrapperArgs, {
|
|
1063
1336
|
cwd: workDir,
|
|
1064
1337
|
env: scrubLoaderVars(scrubProviderSecrets({ ...spawnEnv })),
|
|
1065
1338
|
detached: false,
|
|
1066
1339
|
stdio: 'ignore',
|
|
1067
1340
|
windowsHide: true,
|
|
1068
1341
|
});
|
|
1069
|
-
|
|
1070
|
-
childPid: child.pid,
|
|
1071
|
-
childGroupPid: child.pid,
|
|
1072
|
-
label: 'shell-job-powershell',
|
|
1073
|
-
});
|
|
1342
|
+
await awaitSpawnReady(child, 'PowerShell background task');
|
|
1074
1343
|
} catch (e) {
|
|
1344
|
+
try { unlinkSync(wrappedTempPath); } catch {}
|
|
1345
|
+
try { unlinkSync(innerTempPath); } catch {}
|
|
1075
1346
|
return { jobId, kind: 'bash', status: 'failed', error: `failed to spawn PowerShell background task: ${e?.message || e}` };
|
|
1076
1347
|
}
|
|
1077
1348
|
const childPid = child.pid;
|
|
1078
1349
|
if (!Number.isFinite(childPid) || childPid <= 0) {
|
|
1079
1350
|
return { jobId, kind: 'bash', status: 'failed', error: 'PowerShell background task spawn returned no pid' };
|
|
1080
1351
|
}
|
|
1081
|
-
_installShellJobsExitHook();
|
|
1082
|
-
_registerLiveJobPid(childPid, jobId);
|
|
1083
1352
|
const detail = {
|
|
1084
1353
|
jobId,
|
|
1085
1354
|
kind: 'bash',
|
|
@@ -1103,7 +1372,81 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
1103
1372
|
ownerHostPid: resolveJobOwnerHostPid(clientHostPid),
|
|
1104
1373
|
startedAt: new Date().toISOString(),
|
|
1105
1374
|
};
|
|
1106
|
-
|
|
1375
|
+
let terminal = false;
|
|
1376
|
+
let runtimeOwned = false;
|
|
1377
|
+
const onRuntimeError = (error) => {
|
|
1378
|
+
if (terminal) return;
|
|
1379
|
+
terminal = true;
|
|
1380
|
+
detail.status = 'failed';
|
|
1381
|
+
detail.terminationPending = true;
|
|
1382
|
+
detail.error = `PowerShell background task process error: ${error?.message || error}`;
|
|
1383
|
+
detail.finishedAt = new Date().toISOString();
|
|
1384
|
+
try { writeDetailFn(detail); } catch {}
|
|
1385
|
+
if (runtimeOwned) void (async () => {
|
|
1386
|
+
const rollback = await rollbackSpawnedChild(child, { timeoutMs: rollbackTimeoutMs });
|
|
1387
|
+
if (rollback.confirmed) {
|
|
1388
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, childPid, {
|
|
1389
|
+
onConfirmed: () => {
|
|
1390
|
+
detail.terminationPending = false;
|
|
1391
|
+
try { writeDetailFn(detail); } catch {}
|
|
1392
|
+
},
|
|
1393
|
+
});
|
|
1394
|
+
} else {
|
|
1395
|
+
detail.error += `; termination unconfirmed after ${rollbackTimeoutMs}ms (process remains tracked)`;
|
|
1396
|
+
try { writeDetailFn(detail); } catch {}
|
|
1397
|
+
trackChildUntilConfirmedExit(child, jobId, () => {
|
|
1398
|
+
detail.terminationPending = false;
|
|
1399
|
+
try { writeDetailFn(detail); } catch {}
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
})();
|
|
1403
|
+
};
|
|
1404
|
+
adoptSpawnErrorHandler(child, onRuntimeError);
|
|
1405
|
+
if (terminal) {
|
|
1406
|
+
const rollback = await rollbackSpawnedChild(child, { timeoutMs: rollbackTimeoutMs });
|
|
1407
|
+
detail.terminationPending = !rollback.confirmed;
|
|
1408
|
+
try { writeDetailFn(detail); } catch {}
|
|
1409
|
+
try { unlinkSync(wrappedTempPath); } catch {}
|
|
1410
|
+
try { unlinkSync(innerTempPath); } catch {}
|
|
1411
|
+
return rollback.confirmed
|
|
1412
|
+
? detail
|
|
1413
|
+
: {
|
|
1414
|
+
...detail,
|
|
1415
|
+
rollbackPending: trackChildUntilConfirmedExit(child, jobId, () => {
|
|
1416
|
+
detail.terminationPending = false;
|
|
1417
|
+
try { writeDetailFn(detail); } catch {}
|
|
1418
|
+
}),
|
|
1419
|
+
error: `${detail.error}; termination unconfirmed after ${rollbackTimeoutMs}ms (process remains tracked)`,
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
try {
|
|
1423
|
+
writeDetailFn(detail);
|
|
1424
|
+
} catch (e) {
|
|
1425
|
+
terminal = true;
|
|
1426
|
+
const rollback = await rollbackSpawnedChild(child, { timeoutMs: rollbackTimeoutMs });
|
|
1427
|
+
child.removeListener('error', onRuntimeError);
|
|
1428
|
+
child[SPAWN_ERROR_GUARD]?.discard?.();
|
|
1429
|
+
try { unlinkSync(wrappedTempPath); } catch {}
|
|
1430
|
+
try { unlinkSync(innerTempPath); } catch {}
|
|
1431
|
+
const failure = { jobId, kind: 'bash', status: 'failed', error: `failed to persist PowerShell background task: ${e?.message || e}` };
|
|
1432
|
+
if (!rollback.confirmed) {
|
|
1433
|
+
failure.rollbackPending = trackChildUntilConfirmedExit(child, jobId);
|
|
1434
|
+
failure.error += `; termination unconfirmed after ${rollbackTimeoutMs}ms (process remains tracked)`;
|
|
1435
|
+
}
|
|
1436
|
+
return failure;
|
|
1437
|
+
}
|
|
1438
|
+
startChildGuardian({
|
|
1439
|
+
childPid,
|
|
1440
|
+
childGroupPid: childPid,
|
|
1441
|
+
label: 'shell-job-powershell',
|
|
1442
|
+
});
|
|
1443
|
+
_installShellJobsExitHook();
|
|
1444
|
+
_registerLiveJobPid(childPid, jobId);
|
|
1445
|
+
runtimeOwned = true;
|
|
1446
|
+
releaseShellJobOwnershipWhenQuiescent(jobId, childPid, {
|
|
1447
|
+
allowLateLease: true,
|
|
1448
|
+
deferUntilRootExit: true,
|
|
1449
|
+
});
|
|
1107
1450
|
if (Number(timeoutMs) > 0) {
|
|
1108
1451
|
const timer = setTimeout(() => { refreshShellJob(jobId); }, Math.min(TIMER_MAX_MS, timeoutMs + 25));
|
|
1109
1452
|
if (typeof timer.unref === 'function') timer.unref();
|