@rivet-dev/agentos-core 0.2.22 → 0.4.0
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 +9 -9
- package/dist/agent-os.d.ts +71 -198
- package/dist/agent-os.js +480 -1206
- package/dist/agentos-package.d.ts +15 -4
- package/dist/agentos-package.js +1 -22
- package/dist/cron/cron-manager.js +0 -27
- package/dist/cron/types.d.ts +0 -12
- package/dist/default-software/coreutils.aospkg +4 -0
- package/dist/default-software/default-software.json +10 -0
- package/dist/default-software/diffutils.aospkg +0 -0
- package/dist/default-software/findutils.aospkg +0 -0
- package/dist/default-software/gawk.aospkg +0 -0
- package/dist/default-software/grep.aospkg +0 -0
- package/dist/default-software/gzip.aospkg +0 -0
- package/dist/default-software/sed.aospkg +0 -0
- package/dist/default-software/tar.aospkg +0 -0
- package/dist/default-software.d.ts +6 -5
- package/dist/default-software.js +17 -6
- package/dist/generated/VmLimitsConfig.d.ts +0 -2
- package/dist/host-dir-mount.d.ts +1 -1
- package/dist/host-dir-mount.js +1 -1
- package/dist/language-execution.d.ts +6 -2
- package/dist/options-schema.d.ts +10 -48
- package/dist/options-schema.js +10 -47
- package/dist/output-replay.d.ts +18 -0
- package/dist/output-replay.js +75 -0
- package/dist/packages.d.ts +0 -9
- package/dist/packages.js +0 -39
- package/dist/process-forest.d.ts +7 -0
- package/dist/process-forest.js +24 -0
- package/dist/runtime-compat.d.ts +13 -0
- package/dist/runtime-compat.js +3 -9
- package/dist/sidecar/binary.d.ts +1 -1
- package/dist/sidecar/binary.js +4 -4
- package/dist/sidecar/native-process-client.d.ts +1 -1
- package/dist/sidecar/rpc-client.d.ts +12 -18
- package/dist/sidecar/rpc-client.js +385 -188
- package/dist/types.d.ts +3 -11
- package/package.json +9 -45
- package/dist/generated/AcpLimitsConfig.d.ts +0 -20
- package/dist/generated/AcpLimitsConfig.js +0 -2
- package/dist/json-rpc.d.ts +0 -50
- package/dist/json-rpc.js +0 -22
- package/dist/session-api.d.ts +0 -180
- package/dist/session-api.js +0 -1
- package/dist/sidecar/agentos-protocol.d.ts +0 -719
- package/dist/sidecar/agentos-protocol.js +0 -1331
|
@@ -1,12 +1,94 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { constants as osConstants } from "node:os";
|
|
3
3
|
import { posix as posixPath } from "node:path";
|
|
4
|
+
import { SidecarRejectedError } from "@rivet-dev/agentos-runtime-core/sidecar-errors";
|
|
4
5
|
const SYNTHETIC_PID_BASE = 1_000_000;
|
|
5
6
|
const MISSING_EXIT_EVENT_GRACE_MS = 500;
|
|
6
7
|
const PROTECTED_READ_ONLY_GUEST_ROOTS = ["/etc/agentos"];
|
|
7
8
|
const TRAILING_OUTPUT_DRAIN_INTERVAL_MS = 10;
|
|
8
9
|
const TRAILING_OUTPUT_DRAIN_MAX_MS = 250;
|
|
9
10
|
const TRAILING_OUTPUT_DRAIN_QUIET_TURNS = 2;
|
|
11
|
+
const EXEC_TERMINATION_CONFIRMATION_MS = 30_000;
|
|
12
|
+
// Termination proof must remain observable even after an ordinary wait failed.
|
|
13
|
+
// Weak keys keep this private adapter from retaining completed public process handles.
|
|
14
|
+
const execTermination = new WeakMap();
|
|
15
|
+
async function killAndConfirmManagedProcess(proc) {
|
|
16
|
+
let confirmationTimer;
|
|
17
|
+
try {
|
|
18
|
+
const control = execTermination.get(proc);
|
|
19
|
+
const exit = control ? control.wait() : proc.wait();
|
|
20
|
+
// Normalize synchronous and asynchronous signal failures, without delaying exit
|
|
21
|
+
// observation behind a slow acknowledgement or an in-flight launch.
|
|
22
|
+
const kill = Promise.resolve().then(() => control ? control.kill() : proc.kill(9));
|
|
23
|
+
const confirmed = await Promise.race([
|
|
24
|
+
exit.then(() => true),
|
|
25
|
+
// A late deterministic rejection proves there was no admitted process.
|
|
26
|
+
kill
|
|
27
|
+
.then(() => (control?.needsCleanup() === false ? 0 : exit))
|
|
28
|
+
.then(() => true),
|
|
29
|
+
new Promise((resolve) => {
|
|
30
|
+
confirmationTimer = setTimeout(() => resolve(false), EXEC_TERMINATION_CONFIRMATION_MS);
|
|
31
|
+
}),
|
|
32
|
+
]).catch((error) => {
|
|
33
|
+
throw new Error(`termination_failed: process ${proc.pid} could not confirm exit after SIGKILL: ${String(error)}`);
|
|
34
|
+
});
|
|
35
|
+
if (!confirmed) {
|
|
36
|
+
throw new Error(`termination_failed: process ${proc.pid} did not confirm exit within ${EXEC_TERMINATION_CONFIRMATION_MS}ms after SIGKILL`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (confirmationTimer !== undefined) {
|
|
41
|
+
clearTimeout(confirmationTimer);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** @internal Shared completion policy for shell-string and structured-argv execution. */
|
|
46
|
+
export async function runManagedProcessToCompletion(proc, prepareStdin, deadlineMs) {
|
|
47
|
+
const run = async () => {
|
|
48
|
+
await prepareStdin();
|
|
49
|
+
return proc.wait();
|
|
50
|
+
};
|
|
51
|
+
if (deadlineMs === undefined) {
|
|
52
|
+
try {
|
|
53
|
+
return await run();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (proc.exitCode === null &&
|
|
57
|
+
execTermination.get(proc)?.needsCleanup() !== false) {
|
|
58
|
+
await killAndConfirmManagedProcess(proc);
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const timedOut = Symbol("execution timeout");
|
|
64
|
+
let timer;
|
|
65
|
+
const deadline = new Promise((resolve) => {
|
|
66
|
+
timer = setTimeout(() => resolve(timedOut), Math.max(0, deadlineMs - Date.now()));
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
let result;
|
|
70
|
+
try {
|
|
71
|
+
result = await Promise.race([run(), deadline]);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (proc.exitCode === null &&
|
|
75
|
+
execTermination.get(proc)?.needsCleanup() !== false) {
|
|
76
|
+
await killAndConfirmManagedProcess(proc);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (result !== timedOut) {
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
await killAndConfirmManagedProcess(proc);
|
|
84
|
+
throw new Error(`timeout: process ${proc.pid} exceeded its execution deadline and was stopped`);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
if (timer !== undefined) {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
10
92
|
function shouldLogStructuredSidecarEvent(name) {
|
|
11
93
|
const normalized = name.toLowerCase();
|
|
12
94
|
return (normalized === "limit_warning" ||
|
|
@@ -217,8 +299,8 @@ export class NativeSidecarKernelProxy {
|
|
|
217
299
|
permissions;
|
|
218
300
|
commandPermissions;
|
|
219
301
|
loopbackExemptPorts;
|
|
220
|
-
//
|
|
221
|
-
//
|
|
302
|
+
// Boot packages are resent when mounts change. Runtime-linked packages are
|
|
303
|
+
// retained by the sidecar and are not mirrored in this client.
|
|
222
304
|
packages;
|
|
223
305
|
packagesMountAt;
|
|
224
306
|
hostFunctionShimCommands;
|
|
@@ -284,15 +366,9 @@ export class NativeSidecarKernelProxy {
|
|
|
284
366
|
this.commandDrivers.set(name, "wasmvm");
|
|
285
367
|
}
|
|
286
368
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
* each payload, so a linked package omitted here would be silently
|
|
291
|
-
* unprojected from `/opt/agentos` by the next `mountFs`/`unmountFs`.
|
|
292
|
-
*/
|
|
293
|
-
registerLinkedPackage(descriptor) {
|
|
294
|
-
if (!this.packages.some((pkg) => pkg.path === descriptor.path)) {
|
|
295
|
-
this.packages.push({ path: descriptor.path });
|
|
369
|
+
unregisterCommandGuestPaths(commandNames) {
|
|
370
|
+
for (const name of commandNames) {
|
|
371
|
+
this.commandDrivers.delete(name);
|
|
296
372
|
}
|
|
297
373
|
}
|
|
298
374
|
async dispose() {
|
|
@@ -301,27 +377,41 @@ export class NativeSidecarKernelProxy {
|
|
|
301
377
|
}
|
|
302
378
|
this.disposed = true;
|
|
303
379
|
this.eventPumpAbortController.abort();
|
|
304
|
-
await this.mountReconfigurePromise?.catch(() => { });
|
|
305
380
|
const liveProcesses = [...this.trackedProcesses.values()].filter((entry) => entry.exitCode === null);
|
|
306
|
-
|
|
307
|
-
|
|
381
|
+
// Wake ordinary waits before any teardown RPC can stall. Termination is
|
|
382
|
+
// still unconfirmed; disposing a VM cannot supply a guest exit status.
|
|
308
383
|
for (const entry of liveProcesses) {
|
|
309
|
-
|
|
310
|
-
// The sidecar dispose path already performs TERM/KILL escalation for any
|
|
311
|
-
// guest executions that are still live. Resolve local waiters eagerly so
|
|
312
|
-
// VM teardown does not hang on killed ACP adapter processes that never
|
|
313
|
-
// surface a terminal process_exited event back to the JS bridge.
|
|
314
|
-
this.finishProcess(entry, 143);
|
|
315
|
-
}
|
|
384
|
+
this.handleBackgroundProcessError(entry, new Error("VM disposed before process exit was observed"));
|
|
316
385
|
}
|
|
386
|
+
await this.mountReconfigurePromise?.catch((error) => {
|
|
387
|
+
console.error("[agentOS] mount reconfiguration failed during disposal", error);
|
|
388
|
+
});
|
|
389
|
+
await Promise.all(liveProcesses
|
|
390
|
+
.filter((entry) => !entry.launchRejected)
|
|
391
|
+
.map((entry) => this.signalProcess(entry, 15).catch((error) => {
|
|
392
|
+
console.error(`[agentOS] process ${entry.pid} teardown signal failed`, error);
|
|
393
|
+
})));
|
|
394
|
+
let disposalFailed = false;
|
|
395
|
+
let disposalError;
|
|
396
|
+
await this.client.disposeVm(this.session, this.vm).catch((error) => {
|
|
397
|
+
disposalFailed = true;
|
|
398
|
+
disposalError = error;
|
|
399
|
+
console.error("[agentOS] VM disposal failed; guest termination is unconfirmed", error);
|
|
400
|
+
});
|
|
317
401
|
// Only tear down the shared sidecar process when this proxy owns it. VMs
|
|
318
402
|
// leased from an `AgentOsSidecar` handle share one process, which is
|
|
319
403
|
// disposed when the handle is disposed.
|
|
320
404
|
if (this.ownsClient) {
|
|
321
|
-
await this.client.dispose().catch(() => {
|
|
405
|
+
await this.client.dispose().catch((error) => {
|
|
406
|
+
console.error("[agentOS] owned sidecar disposal failed", error);
|
|
407
|
+
});
|
|
322
408
|
}
|
|
323
|
-
await this.eventPump.catch(() => {
|
|
324
|
-
|
|
409
|
+
await this.eventPump.catch((error) => {
|
|
410
|
+
console.error("[agentOS] process event pump teardown failed", error);
|
|
411
|
+
});
|
|
412
|
+
await this.onDispose?.().catch((error) => {
|
|
413
|
+
console.error("[agentOS] VM disposal callback failed", error);
|
|
414
|
+
});
|
|
325
415
|
// Drop all per-VM tracking state so a disposed proxy retains nothing.
|
|
326
416
|
for (const entry of this.trackedProcesses.values()) {
|
|
327
417
|
entry.onStdout.clear();
|
|
@@ -332,6 +422,8 @@ export class NativeSidecarKernelProxy {
|
|
|
332
422
|
this.signalStates.clear();
|
|
333
423
|
this.signalRefreshes.clear();
|
|
334
424
|
this.localMounts.length = 0;
|
|
425
|
+
if (disposalFailed)
|
|
426
|
+
throw disposalError;
|
|
335
427
|
}
|
|
336
428
|
/** Test-only snapshot of the per-VM tracking collection sizes. */
|
|
337
429
|
__trackingSizesForTest() {
|
|
@@ -357,33 +449,24 @@ export class NativeSidecarKernelProxy {
|
|
|
357
449
|
}
|
|
358
450
|
const stdoutChunks = [];
|
|
359
451
|
const stderrChunks = [];
|
|
452
|
+
const deadlineMs = typeof options?.timeout === "number" &&
|
|
453
|
+
Number.isFinite(options.timeout) &&
|
|
454
|
+
options.timeout >= 0
|
|
455
|
+
? Date.now() + options.timeout
|
|
456
|
+
: undefined;
|
|
360
457
|
const effectiveCwd = options?.cwd ?? this.defaultExecCwd ?? this.cwd;
|
|
361
458
|
const parsedCommand = parseSimpleExecCommand(command);
|
|
362
459
|
const runAndCapture = async (proc, stdinOverride, readExitCode) => {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
await proc.closeStdin();
|
|
374
|
-
const waitPromise = proc.wait();
|
|
375
|
-
const shellExitCode = typeof options?.timeout === "number"
|
|
376
|
-
? await new Promise((resolve) => {
|
|
377
|
-
const timer = setTimeout(() => {
|
|
378
|
-
proc.kill(9);
|
|
379
|
-
void proc.wait().then(resolve);
|
|
380
|
-
}, options.timeout);
|
|
381
|
-
void waitPromise.then((code) => {
|
|
382
|
-
clearTimeout(timer);
|
|
383
|
-
resolve(code);
|
|
384
|
-
});
|
|
385
|
-
})
|
|
386
|
-
: await waitPromise;
|
|
460
|
+
const shellExitCode = await runManagedProcessToCompletion(proc, async () => {
|
|
461
|
+
if (stdinOverride !== undefined) {
|
|
462
|
+
await proc.writeStdin(stdinOverride);
|
|
463
|
+
}
|
|
464
|
+
else if (options?.stdin !== undefined) {
|
|
465
|
+
await proc.writeStdin(options.stdin);
|
|
466
|
+
}
|
|
467
|
+
// Non-interactive runs must observe EOF after any provided input.
|
|
468
|
+
await proc.closeStdin();
|
|
469
|
+
}, deadlineMs);
|
|
387
470
|
const exitCode = readExitCode
|
|
388
471
|
? await readExitCode().catch(() => shellExitCode)
|
|
389
472
|
: shellExitCode;
|
|
@@ -435,25 +518,19 @@ export class NativeSidecarKernelProxy {
|
|
|
435
518
|
async execArgv(command, args = [], options) {
|
|
436
519
|
const stdoutChunks = [];
|
|
437
520
|
const stderrChunks = [];
|
|
521
|
+
const deadlineMs = typeof options?.timeout === "number" &&
|
|
522
|
+
Number.isFinite(options.timeout) &&
|
|
523
|
+
options.timeout >= 0
|
|
524
|
+
? Date.now() + options.timeout
|
|
525
|
+
: undefined;
|
|
438
526
|
const effectiveCwd = options?.cwd ?? this.defaultExecCwd ?? this.cwd;
|
|
439
527
|
const runAndCapture = async (proc) => {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
? await new Promise((resolve) => {
|
|
447
|
-
const timer = setTimeout(() => {
|
|
448
|
-
proc.kill(9);
|
|
449
|
-
void proc.wait().then(resolve);
|
|
450
|
-
}, options.timeout);
|
|
451
|
-
void waitPromise.then((code) => {
|
|
452
|
-
clearTimeout(timer);
|
|
453
|
-
resolve(code);
|
|
454
|
-
});
|
|
455
|
-
})
|
|
456
|
-
: await waitPromise;
|
|
528
|
+
const exitCode = await runManagedProcessToCompletion(proc, async () => {
|
|
529
|
+
if (options?.stdin !== undefined) {
|
|
530
|
+
await proc.writeStdin(options.stdin);
|
|
531
|
+
}
|
|
532
|
+
await proc.closeStdin();
|
|
533
|
+
}, deadlineMs);
|
|
457
534
|
await drainTrailingProcessOutputTurn();
|
|
458
535
|
return {
|
|
459
536
|
exitCode,
|
|
@@ -478,6 +555,9 @@ export class NativeSidecarKernelProxy {
|
|
|
478
555
|
}));
|
|
479
556
|
}
|
|
480
557
|
spawn(command, args, options) {
|
|
558
|
+
if (this.disposed || this.pumpError) {
|
|
559
|
+
throw new Error(`not_ready: cannot spawn on a ${this.disposed ? "disposed VM" : "failed process event stream"}`);
|
|
560
|
+
}
|
|
481
561
|
let spawnCommand = command;
|
|
482
562
|
let spawnArgs = [...args];
|
|
483
563
|
const shellOption = options?.shell;
|
|
@@ -492,11 +572,9 @@ export class NativeSidecarKernelProxy {
|
|
|
492
572
|
}
|
|
493
573
|
const pid = this.nextSyntheticPid++;
|
|
494
574
|
const processId = `proc-${pid}`;
|
|
495
|
-
let
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
resolveWait = resolve;
|
|
499
|
-
rejectWait = reject;
|
|
575
|
+
let confirmExit;
|
|
576
|
+
const confirmedExit = new Promise((resolve) => {
|
|
577
|
+
confirmExit = resolve;
|
|
500
578
|
});
|
|
501
579
|
const entry = {
|
|
502
580
|
pid,
|
|
@@ -513,15 +591,17 @@ export class NativeSidecarKernelProxy {
|
|
|
513
591
|
...(options?.env ?? {}),
|
|
514
592
|
...(options?.streamStdin ? { AGENTOS_KEEP_STDIN_OPEN: "1" } : {}),
|
|
515
593
|
},
|
|
594
|
+
retainOutput: options?.retainOutput ?? false,
|
|
516
595
|
startTime: Date.now(),
|
|
517
596
|
exitTime: null,
|
|
518
597
|
hostPid: null,
|
|
519
598
|
exitCode: null,
|
|
520
599
|
started: false,
|
|
600
|
+
launchRejected: false,
|
|
521
601
|
startPromise: Promise.resolve(),
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
602
|
+
waitError: null,
|
|
603
|
+
wakeWait: null,
|
|
604
|
+
confirmExit,
|
|
525
605
|
onStdout: new Set(options?.onStdout ? [options.onStdout] : []),
|
|
526
606
|
onStderr: new Set(options?.onStderr ? [options.onStderr] : []),
|
|
527
607
|
pendingStdin: options?.stdin === undefined ? [] : [options.stdin],
|
|
@@ -537,33 +617,46 @@ export class NativeSidecarKernelProxy {
|
|
|
537
617
|
this.updateTrackedProcessSnapshot(entry);
|
|
538
618
|
const proc = {
|
|
539
619
|
pid,
|
|
620
|
+
processId,
|
|
540
621
|
writeStdin: (data) => {
|
|
541
622
|
if (entry.exitCode !== null) {
|
|
542
623
|
return Promise.resolve();
|
|
543
624
|
}
|
|
625
|
+
if (entry.waitError)
|
|
626
|
+
return Promise.reject(entry.waitError);
|
|
544
627
|
entry.pendingStdin.push(data);
|
|
545
628
|
return this.flushPendingStdin(entry).catch((error) => {
|
|
546
629
|
this.handleBackgroundProcessError(entry, error);
|
|
630
|
+
throw error;
|
|
547
631
|
});
|
|
548
632
|
},
|
|
549
633
|
closeStdin: () => {
|
|
634
|
+
if (entry.waitError)
|
|
635
|
+
return Promise.reject(entry.waitError);
|
|
550
636
|
entry.pendingCloseStdin = true;
|
|
551
637
|
return this.closeTrackedStdin(entry).catch((error) => {
|
|
552
638
|
this.handleBackgroundProcessError(entry, error);
|
|
639
|
+
throw error;
|
|
553
640
|
});
|
|
554
641
|
},
|
|
555
642
|
kill: (signal = 15) => {
|
|
556
|
-
if (entry.exitCode !== null) {
|
|
643
|
+
if (entry.exitCode !== null || entry.launchRejected) {
|
|
557
644
|
return;
|
|
558
645
|
}
|
|
559
646
|
entry.pendingKillSignal = signal;
|
|
560
|
-
void entry.startPromise
|
|
561
|
-
|
|
647
|
+
void entry.startPromise
|
|
648
|
+
.then(async () => {
|
|
649
|
+
if (entry.exitCode !== null ||
|
|
650
|
+
entry.launchRejected ||
|
|
651
|
+
entry.pendingKillSignal === null) {
|
|
562
652
|
return;
|
|
563
653
|
}
|
|
564
654
|
const pendingSignal = entry.pendingKillSignal;
|
|
565
655
|
entry.pendingKillSignal = null;
|
|
566
656
|
await this.signalProcess(entry, pendingSignal);
|
|
657
|
+
})
|
|
658
|
+
.catch((error) => {
|
|
659
|
+
this.handleBackgroundProcessError(entry, error);
|
|
567
660
|
});
|
|
568
661
|
},
|
|
569
662
|
wait: async () => {
|
|
@@ -575,13 +668,29 @@ export class NativeSidecarKernelProxy {
|
|
|
575
668
|
return entry.exitCode;
|
|
576
669
|
},
|
|
577
670
|
};
|
|
671
|
+
execTermination.set(proc, {
|
|
672
|
+
wait: () => confirmedExit,
|
|
673
|
+
needsCleanup: () => !entry.launchRejected,
|
|
674
|
+
kill: async () => {
|
|
675
|
+
await entry.startPromise;
|
|
676
|
+
if (entry.launchRejected)
|
|
677
|
+
return;
|
|
678
|
+
// A failed ordinary wait does not close this independent control path.
|
|
679
|
+
await this.signalProcess(entry, 9);
|
|
680
|
+
},
|
|
681
|
+
});
|
|
578
682
|
entry.startPromise = this.startTrackedProcess(entry).catch((error) => {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
683
|
+
entry.launchRejected ||=
|
|
684
|
+
!entry.started && error instanceof SidecarRejectedError;
|
|
685
|
+
this.handleBackgroundProcessError(entry, error);
|
|
686
|
+
if (entry.launchRejected) {
|
|
687
|
+
// Rejected admission is not a process exit. Release this non-process's
|
|
688
|
+
// tracking while retaining the rejection on the returned handle.
|
|
689
|
+
this.processes.delete(entry.pid);
|
|
690
|
+
void this.releaseProcessTrackingAfterDrain(entry).catch((error) => {
|
|
691
|
+
console.error(`[agentOS] rejected process ${entry.pid} cleanup failed`, error);
|
|
692
|
+
});
|
|
583
693
|
}
|
|
584
|
-
this.finishProcess(entry, 1);
|
|
585
694
|
});
|
|
586
695
|
return proc;
|
|
587
696
|
}
|
|
@@ -919,6 +1028,7 @@ export class NativeSidecarKernelProxy {
|
|
|
919
1028
|
},
|
|
920
1029
|
cwd: options?.cwd,
|
|
921
1030
|
streamStdin: true,
|
|
1031
|
+
retainOutput: true,
|
|
922
1032
|
onStdout: (chunk) => {
|
|
923
1033
|
const sanitized = sanitizeNativeShellOutput(chunk);
|
|
924
1034
|
if (!sanitized) {
|
|
@@ -1040,9 +1150,12 @@ export class NativeSidecarKernelProxy {
|
|
|
1040
1150
|
shell.kill();
|
|
1041
1151
|
throw error;
|
|
1042
1152
|
}
|
|
1043
|
-
void shell
|
|
1044
|
-
|
|
1045
|
-
|
|
1153
|
+
void shell
|
|
1154
|
+
.wait()
|
|
1155
|
+
.catch((error) => {
|
|
1156
|
+
console.error(`[agentOS] terminal ${shell.pid} wait failed`, error);
|
|
1157
|
+
})
|
|
1158
|
+
.finally(cleanup);
|
|
1046
1159
|
return shell.pid;
|
|
1047
1160
|
}
|
|
1048
1161
|
readFile(path) {
|
|
@@ -1211,10 +1324,8 @@ export class NativeSidecarKernelProxy {
|
|
|
1211
1324
|
if (this.disposed) {
|
|
1212
1325
|
return;
|
|
1213
1326
|
}
|
|
1214
|
-
//
|
|
1215
|
-
//
|
|
1216
|
-
// Omitting them here strips the `/opt/agentos` projections and host-function
|
|
1217
|
-
// shims from the VM as a side effect of a runtime mount change.
|
|
1327
|
+
// Re-send boot packages and binding shims. The sidecar preserves
|
|
1328
|
+
// runtime-linked packages independently of this client payload.
|
|
1218
1329
|
await this.client.configureVm(this.session, this.vm, {
|
|
1219
1330
|
mounts: this.desiredSidecarMounts(),
|
|
1220
1331
|
permissions: this.permissions,
|
|
@@ -1241,6 +1352,10 @@ export class NativeSidecarKernelProxy {
|
|
|
1241
1352
|
}
|
|
1242
1353
|
}
|
|
1243
1354
|
snapshotProcesses() {
|
|
1355
|
+
return this.snapshotProcessTopology().map((record) => record.info);
|
|
1356
|
+
}
|
|
1357
|
+
/** Internal, host-side topology; never part of the guest or public DTO wire. */
|
|
1358
|
+
snapshotProcessTopology() {
|
|
1244
1359
|
return this.buildProcessSnapshot();
|
|
1245
1360
|
}
|
|
1246
1361
|
findListener(request) {
|
|
@@ -1358,17 +1473,25 @@ export class NativeSidecarKernelProxy {
|
|
|
1358
1473
|
}
|
|
1359
1474
|
async startTrackedProcess(entry) {
|
|
1360
1475
|
await this.waitForMountReconfigure();
|
|
1476
|
+
if (this.disposed || this.pumpError) {
|
|
1477
|
+
entry.launchRejected = true; // No Execute request was sent.
|
|
1478
|
+
throw new Error("not_ready: process launch cancelled before admission because the VM is unavailable");
|
|
1479
|
+
}
|
|
1361
1480
|
const started = await this.client.execute(this.session, this.vm, {
|
|
1362
1481
|
processId: entry.processId,
|
|
1363
1482
|
command: entry.command,
|
|
1364
1483
|
args: entry.args,
|
|
1365
1484
|
env: entry.env,
|
|
1366
1485
|
cwd: entry.cwd,
|
|
1486
|
+
retainOutput: entry.retainOutput,
|
|
1367
1487
|
});
|
|
1368
1488
|
entry.hostPid = started.pid;
|
|
1369
1489
|
entry.started = true;
|
|
1490
|
+
entry.wakeWait?.();
|
|
1370
1491
|
this.updateTrackedProcessSnapshot(entry);
|
|
1371
|
-
void this.refreshProcessSnapshot().catch(() => {
|
|
1492
|
+
void this.refreshProcessSnapshot().catch((error) => {
|
|
1493
|
+
console.error("[agentOS] post-launch process snapshot failed", error);
|
|
1494
|
+
});
|
|
1372
1495
|
// Signal metadata is advisory and must not hold process startup behind a
|
|
1373
1496
|
// second sidecar RPC. Very short hostFunction commands can finish while the
|
|
1374
1497
|
// original execute response is still in flight; synchronously refreshing
|
|
@@ -1400,14 +1523,19 @@ export class NativeSidecarKernelProxy {
|
|
|
1400
1523
|
continue;
|
|
1401
1524
|
}
|
|
1402
1525
|
entry.outputGeneration += 1;
|
|
1403
|
-
void this.refreshProcessSnapshot().catch(() => {
|
|
1526
|
+
void this.refreshProcessSnapshot().catch((error) => {
|
|
1527
|
+
console.error("[agentOS] process output snapshot refresh failed", error);
|
|
1528
|
+
});
|
|
1404
1529
|
this.scheduleSignalStateRefresh(entry);
|
|
1405
1530
|
const chunk = event.payload.chunk;
|
|
1406
1531
|
const listeners = event.payload.channel === "stdout"
|
|
1407
1532
|
? entry.onStdout
|
|
1408
1533
|
: entry.onStderr;
|
|
1409
1534
|
for (const listener of listeners) {
|
|
1410
|
-
listener(chunk
|
|
1535
|
+
listener(chunk, {
|
|
1536
|
+
sequence: event.payload.sequence,
|
|
1537
|
+
timestampMs: event.payload.timestamp_ms,
|
|
1538
|
+
});
|
|
1411
1539
|
}
|
|
1412
1540
|
continue;
|
|
1413
1541
|
}
|
|
@@ -1416,7 +1544,10 @@ export class NativeSidecarKernelProxy {
|
|
|
1416
1544
|
if (!entry) {
|
|
1417
1545
|
continue;
|
|
1418
1546
|
}
|
|
1419
|
-
void this.refreshProcessSnapshot().catch(() => {
|
|
1547
|
+
void this.refreshProcessSnapshot().catch((error) => {
|
|
1548
|
+
console.error("[agentOS] post-exit process snapshot failed", error);
|
|
1549
|
+
});
|
|
1550
|
+
entry.confirmExit(event.payload.exit_code);
|
|
1420
1551
|
this.finishProcess(entry, event.payload.exit_code);
|
|
1421
1552
|
continue;
|
|
1422
1553
|
}
|
|
@@ -1430,20 +1561,25 @@ export class NativeSidecarKernelProxy {
|
|
|
1430
1561
|
}
|
|
1431
1562
|
this.pumpError =
|
|
1432
1563
|
error instanceof Error ? error : new Error(String(error));
|
|
1564
|
+
console.error("[agentOS] process event stream failed", this.pumpError);
|
|
1433
1565
|
for (const entry of this.trackedProcesses.values()) {
|
|
1434
1566
|
if (entry.exitCode !== null) {
|
|
1435
1567
|
continue;
|
|
1436
1568
|
}
|
|
1437
|
-
|
|
1438
|
-
for (const listener of entry.onStderr) {
|
|
1439
|
-
listener(stderr);
|
|
1440
|
-
}
|
|
1441
|
-
this.finishProcess(entry, 1);
|
|
1569
|
+
this.handleBackgroundProcessError(entry, this.pumpError);
|
|
1442
1570
|
}
|
|
1443
1571
|
return;
|
|
1444
1572
|
}
|
|
1445
1573
|
}
|
|
1446
1574
|
}
|
|
1575
|
+
/** Internal: replay carries the same authoritative exit proof as a live event. */
|
|
1576
|
+
reconcileReplayExit(processId, exitCode) {
|
|
1577
|
+
const entry = this.trackedProcessesById.get(processId);
|
|
1578
|
+
if (!entry)
|
|
1579
|
+
return;
|
|
1580
|
+
entry.confirmExit(exitCode);
|
|
1581
|
+
this.finishProcess(entry, exitCode);
|
|
1582
|
+
}
|
|
1447
1583
|
finishProcess(entry, exitCode) {
|
|
1448
1584
|
if (entry.exitCode !== null) {
|
|
1449
1585
|
return;
|
|
@@ -1451,13 +1587,15 @@ export class NativeSidecarKernelProxy {
|
|
|
1451
1587
|
entry.exitCode = exitCode;
|
|
1452
1588
|
entry.exitTime = Date.now();
|
|
1453
1589
|
this.updateTrackedProcessSnapshot(entry);
|
|
1454
|
-
entry.
|
|
1590
|
+
entry.wakeWait?.();
|
|
1455
1591
|
// Release per-process tracking now that the process has terminated so these
|
|
1456
1592
|
// maps/Sets don't grow without bound. Defer the release until trailing
|
|
1457
1593
|
// output has drained: `wait()`'s drain and late `process_output` events
|
|
1458
1594
|
// still need the entry + its listeners during the drain window. The exited
|
|
1459
1595
|
// record lives on in `processes` for listing.
|
|
1460
|
-
void this.releaseProcessTrackingAfterDrain(entry)
|
|
1596
|
+
void this.releaseProcessTrackingAfterDrain(entry).catch((error) => {
|
|
1597
|
+
console.error(`[agentOS] process ${entry.pid} tracking cleanup failed`, error);
|
|
1598
|
+
});
|
|
1461
1599
|
}
|
|
1462
1600
|
async releaseProcessTrackingAfterDrain(entry) {
|
|
1463
1601
|
try {
|
|
@@ -1476,35 +1614,67 @@ export class NativeSidecarKernelProxy {
|
|
|
1476
1614
|
if (entry.exitCode !== null) {
|
|
1477
1615
|
return Promise.resolve(entry.exitCode);
|
|
1478
1616
|
}
|
|
1617
|
+
if (entry.waitError) {
|
|
1618
|
+
return Promise.reject(entry.waitError);
|
|
1619
|
+
}
|
|
1479
1620
|
if (entry.waitWithFallbackPromise !== null) {
|
|
1480
1621
|
return entry.waitWithFallbackPromise;
|
|
1481
1622
|
}
|
|
1482
1623
|
entry.waitWithFallbackPromise = (async () => {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1624
|
+
while (entry.exitCode === null && !entry.waitError && !this.disposed) {
|
|
1625
|
+
// One cancellable wake per process, rather than another Promise.race
|
|
1626
|
+
// reaction retained on an unresolved exit promise every 50ms.
|
|
1627
|
+
await new Promise((resolve) => {
|
|
1628
|
+
const timer = setTimeout(() => {
|
|
1629
|
+
entry.wakeWait = null;
|
|
1630
|
+
resolve();
|
|
1631
|
+
}, 50);
|
|
1632
|
+
entry.wakeWait = () => {
|
|
1633
|
+
clearTimeout(timer);
|
|
1634
|
+
entry.wakeWait = null;
|
|
1635
|
+
resolve();
|
|
1636
|
+
};
|
|
1637
|
+
});
|
|
1638
|
+
if (entry.exitCode !== null || entry.waitError || this.disposed) {
|
|
1639
|
+
break;
|
|
1640
|
+
}
|
|
1641
|
+
if (!entry.started) {
|
|
1642
|
+
continue;
|
|
1491
1643
|
}
|
|
1492
1644
|
try {
|
|
1493
|
-
|
|
1645
|
+
// Metadata cannot hold terminal delivery hostage. Keep a wake armed
|
|
1646
|
+
// while the coalesced snapshot RPC is in flight, too.
|
|
1647
|
+
let wake;
|
|
1648
|
+
const terminal = new Promise((resolve) => {
|
|
1649
|
+
wake = resolve;
|
|
1650
|
+
entry.wakeWait = resolve;
|
|
1651
|
+
});
|
|
1652
|
+
try {
|
|
1653
|
+
await Promise.race([this.refreshProcessSnapshot(), terminal]);
|
|
1654
|
+
}
|
|
1655
|
+
finally {
|
|
1656
|
+
if (entry.wakeWait === wake)
|
|
1657
|
+
entry.wakeWait = null;
|
|
1658
|
+
}
|
|
1659
|
+
if (entry.exitCode !== null || entry.waitError || this.disposed)
|
|
1660
|
+
break;
|
|
1494
1661
|
const snapshot = this.sidecarProcessSnapshot.find((candidate) => candidate.processId === entry.processId);
|
|
1495
1662
|
if (snapshot?.status === "exited") {
|
|
1496
|
-
|
|
1663
|
+
if (snapshot.exitCode === undefined || snapshot.exitCode === null) {
|
|
1664
|
+
this.handleBackgroundProcessError(entry, new Error("exited snapshot has no exit status"));
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
entry.confirmExit(snapshot.exitCode);
|
|
1668
|
+
this.finishProcess(entry, snapshot.exitCode);
|
|
1669
|
+
}
|
|
1497
1670
|
break;
|
|
1498
1671
|
}
|
|
1499
1672
|
if (snapshot) {
|
|
1500
1673
|
entry.hostExitObservedAt = null;
|
|
1501
1674
|
continue;
|
|
1502
1675
|
}
|
|
1503
|
-
//
|
|
1504
|
-
//
|
|
1505
|
-
// authoritative VM snapshot for a full grace window, treat it as
|
|
1506
|
-
// reaped even if the `pid` returned at launch was only a kernel/shared
|
|
1507
|
-
// runtime identifier rather than a probeable host PID.
|
|
1676
|
+
// A missing snapshot can mean reaping, lost metadata, or a launch race.
|
|
1677
|
+
// None provides an exit status. Bound the grace period and fail closed.
|
|
1508
1678
|
if (!snapshot) {
|
|
1509
1679
|
const now = Date.now();
|
|
1510
1680
|
if (entry.hostExitObservedAt === null) {
|
|
@@ -1512,31 +1682,26 @@ export class NativeSidecarKernelProxy {
|
|
|
1512
1682
|
continue;
|
|
1513
1683
|
}
|
|
1514
1684
|
if (now - entry.hostExitObservedAt >= MISSING_EXIT_EVENT_GRACE_MS) {
|
|
1515
|
-
this.
|
|
1685
|
+
this.handleBackgroundProcessError(entry, new Error("process disappeared from snapshots without an exit event"));
|
|
1516
1686
|
break;
|
|
1517
1687
|
}
|
|
1518
1688
|
}
|
|
1519
1689
|
}
|
|
1520
|
-
catch {
|
|
1521
|
-
|
|
1690
|
+
catch (error) {
|
|
1691
|
+
this.handleBackgroundProcessError(entry, error);
|
|
1522
1692
|
}
|
|
1523
1693
|
}
|
|
1524
|
-
|
|
1694
|
+
if (entry.exitCode !== null)
|
|
1695
|
+
return entry.exitCode;
|
|
1696
|
+
throw (entry.waitError ??
|
|
1697
|
+
new Error(`termination_failed: process ${entry.pid} VM disposed before exit was observed`));
|
|
1525
1698
|
})().finally(() => {
|
|
1526
1699
|
entry.waitWithFallbackPromise = null;
|
|
1527
1700
|
});
|
|
1528
1701
|
return entry.waitWithFallbackPromise;
|
|
1529
1702
|
}
|
|
1530
1703
|
async signalProcess(entry, signal) {
|
|
1531
|
-
|
|
1532
|
-
await this.client.killProcess(this.session, this.vm, entry.processId, toSidecarSignalName(signal));
|
|
1533
|
-
}
|
|
1534
|
-
catch (error) {
|
|
1535
|
-
if (isNoSuchProcessError(error) || isUnknownVmError(error)) {
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
throw error;
|
|
1539
|
-
}
|
|
1704
|
+
await this.client.killProcess(this.session, this.vm, entry.processId, toSidecarSignalName(signal));
|
|
1540
1705
|
}
|
|
1541
1706
|
flushPendingStdin(entry) {
|
|
1542
1707
|
if (entry.stdinFlushPromise !== null) {
|
|
@@ -1544,6 +1709,8 @@ export class NativeSidecarKernelProxy {
|
|
|
1544
1709
|
}
|
|
1545
1710
|
entry.stdinFlushPromise = entry.startPromise
|
|
1546
1711
|
.then(async () => {
|
|
1712
|
+
if (entry.waitError)
|
|
1713
|
+
throw entry.waitError;
|
|
1547
1714
|
if (entry.exitCode !== null) {
|
|
1548
1715
|
return;
|
|
1549
1716
|
}
|
|
@@ -1556,14 +1723,17 @@ export class NativeSidecarKernelProxy {
|
|
|
1556
1723
|
}
|
|
1557
1724
|
})
|
|
1558
1725
|
.catch((error) => {
|
|
1559
|
-
if (
|
|
1726
|
+
if (entry.exitCode !== null &&
|
|
1727
|
+
(isNoSuchProcessError(error) || isUnknownVmError(error))) {
|
|
1560
1728
|
return;
|
|
1561
1729
|
}
|
|
1562
1730
|
throw error;
|
|
1563
1731
|
})
|
|
1564
1732
|
.finally(() => {
|
|
1565
1733
|
entry.stdinFlushPromise = null;
|
|
1566
|
-
if (entry.pendingStdin.length > 0 &&
|
|
1734
|
+
if (entry.pendingStdin.length > 0 &&
|
|
1735
|
+
entry.exitCode === null &&
|
|
1736
|
+
!entry.waitError) {
|
|
1567
1737
|
void this.flushPendingStdin(entry).catch((error) => {
|
|
1568
1738
|
this.handleBackgroundProcessError(entry, error);
|
|
1569
1739
|
});
|
|
@@ -1573,6 +1743,8 @@ export class NativeSidecarKernelProxy {
|
|
|
1573
1743
|
}
|
|
1574
1744
|
async closeTrackedStdin(entry) {
|
|
1575
1745
|
await entry.startPromise;
|
|
1746
|
+
if (entry.waitError)
|
|
1747
|
+
throw entry.waitError;
|
|
1576
1748
|
await this.flushPendingStdin(entry);
|
|
1577
1749
|
if (entry.exitCode !== null || !entry.pendingCloseStdin) {
|
|
1578
1750
|
return;
|
|
@@ -1582,43 +1754,48 @@ export class NativeSidecarKernelProxy {
|
|
|
1582
1754
|
await this.client.closeStdin(this.session, this.vm, entry.processId);
|
|
1583
1755
|
}
|
|
1584
1756
|
catch (error) {
|
|
1585
|
-
if (
|
|
1757
|
+
if (entry.exitCode !== null &&
|
|
1758
|
+
(isNoSuchProcessError(error) || isUnknownVmError(error))) {
|
|
1586
1759
|
return;
|
|
1587
1760
|
}
|
|
1588
1761
|
throw error;
|
|
1589
1762
|
}
|
|
1590
1763
|
}
|
|
1591
1764
|
handleBackgroundProcessError(entry, error) {
|
|
1592
|
-
if (this.disposed ||
|
|
1593
|
-
isNoSuchProcessError(error) ||
|
|
1594
|
-
isUnknownVmError(error)) {
|
|
1595
|
-
return;
|
|
1596
|
-
}
|
|
1597
1765
|
if (entry.exitCode !== null) {
|
|
1598
|
-
|
|
1766
|
+
// An I/O failure after exit must never rewrite the guest's real status.
|
|
1767
|
+
if (!isNoSuchProcessError(error) && !isUnknownVmError(error)) {
|
|
1768
|
+
this.emitBackgroundProcessError(entry, error);
|
|
1769
|
+
}
|
|
1599
1770
|
return;
|
|
1600
1771
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
isNoSuchProcessError(error) ||
|
|
1607
|
-
isUnknownVmError(error)) {
|
|
1608
|
-
return entry.exitCode ?? 1;
|
|
1772
|
+
if (entry.waitError) {
|
|
1773
|
+
if (error !== entry.waitError && error !== entry.waitError.cause) {
|
|
1774
|
+
this.emitBackgroundProcessError(entry, error);
|
|
1775
|
+
}
|
|
1776
|
+
return;
|
|
1609
1777
|
}
|
|
1610
|
-
|
|
1611
|
-
entry.
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1778
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
1779
|
+
entry.waitError = entry.launchRejected
|
|
1780
|
+
? cause
|
|
1781
|
+
: new Error(`termination_failed: process ${entry.pid} failed before exit was confirmed: ${cause.message}`, { cause });
|
|
1782
|
+
entry.pendingStdin.length = 0;
|
|
1783
|
+
entry.pendingCloseStdin = false;
|
|
1784
|
+
entry.wakeWait?.();
|
|
1785
|
+
// Keep the entry and signal routing alive: failure is not proof of exit.
|
|
1786
|
+
this.emitBackgroundProcessError(entry, entry.waitError);
|
|
1616
1787
|
}
|
|
1617
1788
|
emitBackgroundProcessError(entry, error) {
|
|
1618
1789
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1790
|
+
console.error(`[agentOS] process ${entry.pid} operation failed`, normalized);
|
|
1619
1791
|
const stderr = new TextEncoder().encode(`${normalized.message}\n`);
|
|
1620
1792
|
for (const handler of entry.onStderr) {
|
|
1621
|
-
|
|
1793
|
+
try {
|
|
1794
|
+
handler(stderr);
|
|
1795
|
+
}
|
|
1796
|
+
catch (callbackError) {
|
|
1797
|
+
console.error(`[agentOS] process ${entry.pid} stderr callback failed`, callbackError);
|
|
1798
|
+
}
|
|
1622
1799
|
}
|
|
1623
1800
|
}
|
|
1624
1801
|
async readdirRecursiveLocal(fs, path, maxDepth) {
|
|
@@ -1826,8 +2003,12 @@ export class NativeSidecarKernelProxy {
|
|
|
1826
2003
|
};
|
|
1827
2004
|
}
|
|
1828
2005
|
buildProcessSnapshot() {
|
|
1829
|
-
void this.refreshProcessSnapshot().catch(() => {
|
|
1830
|
-
|
|
2006
|
+
void this.refreshProcessSnapshot().catch((error) => {
|
|
2007
|
+
console.warn("agentOS process snapshot refresh failed", error);
|
|
2008
|
+
});
|
|
2009
|
+
const records = [];
|
|
2010
|
+
const seenProcessIds = new Set();
|
|
2011
|
+
const trackedKeys = new Set();
|
|
1831
2012
|
const displayPidByKernelPid = new Map();
|
|
1832
2013
|
for (const entry of this.sidecarProcessSnapshot) {
|
|
1833
2014
|
const tracked = this.trackedProcessesById.get(entry.processId);
|
|
@@ -1837,7 +2018,7 @@ export class NativeSidecarKernelProxy {
|
|
|
1837
2018
|
}
|
|
1838
2019
|
for (const entry of this.sidecarProcessSnapshot) {
|
|
1839
2020
|
const tracked = this.trackedProcessesById.get(entry.processId);
|
|
1840
|
-
const displayPid =
|
|
2021
|
+
const displayPid = tracked?.pid ?? entry.pid;
|
|
1841
2022
|
const displayPpid = displayPidByKernelPid.get(entry.ppid) ?? entry.ppid;
|
|
1842
2023
|
const displayPgid = displayPidByKernelPid.get(entry.pgid) ?? entry.pgid;
|
|
1843
2024
|
const displaySid = displayPidByKernelPid.get(entry.sid) ?? entry.sid;
|
|
@@ -1846,51 +2027,67 @@ export class NativeSidecarKernelProxy {
|
|
|
1846
2027
|
this.observedProcessStartTimes.get(processKey) ??
|
|
1847
2028
|
Date.now();
|
|
1848
2029
|
this.observedProcessStartTimes.set(processKey, startTime);
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
: tracked
|
|
1861
|
-
|
|
2030
|
+
seenProcessIds.add(entry.processId);
|
|
2031
|
+
if (tracked)
|
|
2032
|
+
trackedKeys.add(`kernel:${entry.pid}`);
|
|
2033
|
+
records.push({
|
|
2034
|
+
key: `kernel:${entry.pid}`,
|
|
2035
|
+
parentKey: `kernel:${entry.ppid}`,
|
|
2036
|
+
info: {
|
|
2037
|
+
pid: displayPid,
|
|
2038
|
+
ppid: displayPpid,
|
|
2039
|
+
pgid: displayPgid,
|
|
2040
|
+
sid: displaySid,
|
|
2041
|
+
driver: tracked?.driver ?? entry.driver,
|
|
2042
|
+
command: tracked?.command ?? entry.command,
|
|
2043
|
+
args: tracked?.args ?? entry.args,
|
|
2044
|
+
cwd: tracked?.cwd ?? entry.cwd,
|
|
2045
|
+
status: tracked
|
|
2046
|
+
? tracked.exitCode !== null
|
|
2047
|
+
? "exited"
|
|
2048
|
+
: "running"
|
|
1862
2049
|
: entry.status === "exited"
|
|
1863
2050
|
? "exited"
|
|
1864
2051
|
: "running",
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2052
|
+
exitCode: tracked?.exitCode ?? entry.exitCode,
|
|
2053
|
+
startTime,
|
|
2054
|
+
exitTime: tracked?.exitTime ?? null,
|
|
2055
|
+
},
|
|
1868
2056
|
});
|
|
1869
2057
|
}
|
|
1870
2058
|
for (const entry of this.trackedProcesses.values()) {
|
|
1871
|
-
if (
|
|
2059
|
+
if (seenProcessIds.has(entry.processId)) {
|
|
1872
2060
|
continue;
|
|
1873
2061
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2062
|
+
trackedKeys.add(`tracked:${entry.pid}`);
|
|
2063
|
+
records.push({
|
|
2064
|
+
key: `tracked:${entry.pid}`,
|
|
2065
|
+
parentKey: null,
|
|
2066
|
+
info: {
|
|
2067
|
+
pid: entry.pid,
|
|
2068
|
+
ppid: 0,
|
|
2069
|
+
pgid: entry.pid,
|
|
2070
|
+
sid: entry.pid,
|
|
2071
|
+
driver: entry.driver,
|
|
2072
|
+
command: entry.command,
|
|
2073
|
+
args: entry.args,
|
|
2074
|
+
cwd: entry.cwd,
|
|
2075
|
+
status: entry.exitCode === null ? "running" : "exited",
|
|
2076
|
+
exitCode: entry.exitCode,
|
|
2077
|
+
startTime: entry.startTime,
|
|
2078
|
+
exitTime: entry.exitTime,
|
|
2079
|
+
},
|
|
1887
2080
|
});
|
|
1888
2081
|
}
|
|
1889
2082
|
this.processes.clear();
|
|
1890
|
-
for (const
|
|
1891
|
-
|
|
2083
|
+
for (const { info, key } of records) {
|
|
2084
|
+
// The legacy numeric lookup cannot represent collisions. Prefer the
|
|
2085
|
+
// tracked process for control lookup; tree/list preserve every record.
|
|
2086
|
+
if (!this.processes.has(info.pid) || trackedKeys.has(key)) {
|
|
2087
|
+
this.processes.set(info.pid, info);
|
|
2088
|
+
}
|
|
1892
2089
|
}
|
|
1893
|
-
return
|
|
2090
|
+
return records.sort((left, right) => left.info.pid - right.info.pid);
|
|
1894
2091
|
}
|
|
1895
2092
|
dispatchRead(path, handler, includeLocalMounts = true) {
|
|
1896
2093
|
const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
|
|
@@ -2129,7 +2326,7 @@ export class AgentOsSidecarClient {
|
|
|
2129
2326
|
this.createId = options.createId ?? randomUUID;
|
|
2130
2327
|
this.now = options.now ?? Date.now;
|
|
2131
2328
|
}
|
|
2132
|
-
/** Open a physical sidecar ownership scope
|
|
2329
|
+
/** Open a physical sidecar ownership scope for one client connection. */
|
|
2133
2330
|
async createOwnershipSession(options = {}) {
|
|
2134
2331
|
this.assertActive();
|
|
2135
2332
|
const sessionId = this.createId();
|