@agent-device/proxy 0.0.0-stage → 0.21.23

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.
@@ -0,0 +1,1147 @@
1
+ import { i as redactDiagnosticData, n as createRequestCanceledError, t as AppError } from "./errors.mjs";
2
+ import crypto from "node:crypto";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { Readable } from "node:stream";
8
+ import { pipeline } from "node:stream/promises";
9
+ //#region ../host-kit/src/internal/diagnostics.ts
10
+ const diagnosticsStorage = new AsyncLocalStorage();
11
+ function updateDiagnosticsScope(options) {
12
+ const scope = diagnosticsStorage.getStore();
13
+ if (!scope) return;
14
+ Object.assign(scope, options);
15
+ }
16
+ function getDiagnosticsMeta() {
17
+ const scope = diagnosticsStorage.getStore();
18
+ if (!scope) return {};
19
+ const { diagnosticId, requestId, session, command, debug, flushOnSuccess } = scope;
20
+ return {
21
+ diagnosticId,
22
+ requestId,
23
+ session,
24
+ command,
25
+ debug,
26
+ flushOnSuccess
27
+ };
28
+ }
29
+ function emitDiagnostic(event) {
30
+ const scope = diagnosticsStorage.getStore();
31
+ if (!scope) return;
32
+ const payload = {
33
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
34
+ level: event.level ?? "info",
35
+ phase: event.phase,
36
+ session: scope.session,
37
+ requestId: scope.requestId,
38
+ command: scope.command,
39
+ durationMs: event.durationMs,
40
+ data: event.data ? redactScopeData(scope, event.data) : void 0
41
+ };
42
+ scope.events.push(payload);
43
+ scope.phaseCounts.set(event.phase, (scope.phaseCounts.get(event.phase) ?? 0) + 1);
44
+ if (!scope.debug && !scope.traceLogPath) return;
45
+ const fileLine = `${JSON.stringify(payload)}\n`;
46
+ try {
47
+ if (scope.debug && scope.logPath) {
48
+ appendDiagnosticLine(scope, scope.logPath, fileLine);
49
+ scope.liveWrittenEventCount = scope.events.length;
50
+ }
51
+ if (scope.traceLogPath) appendDiagnosticLine(scope, scope.traceLogPath, fileLine);
52
+ if (scope.debug && !scope.logPath && !scope.traceLogPath) process.stderr.write(`[agent-device][diag] ${fileLine}`);
53
+ } catch {}
54
+ }
55
+ /** Longest-first so an overlapping shorter literal cannot truncate a longer one. */
56
+ function sortedScopeSensitiveValues(scope) {
57
+ scope.sortedSensitiveValues ??= [...scope.sensitiveValues].sort((left, right) => right.length - left.length);
58
+ return scope.sortedSensitiveValues;
59
+ }
60
+ function redactScopeData(scope, input) {
61
+ return replaceSensitiveValues(redactDiagnosticData(input), sortedScopeSensitiveValues(scope));
62
+ }
63
+ function replaceSensitiveValues(value, sensitiveValues) {
64
+ if (typeof value === "string") return sensitiveValues.reduce((output, sensitive) => output.replaceAll(sensitive, "[REDACTED]"), value);
65
+ if (Array.isArray(value)) return value.map((entry) => replaceSensitiveValues(entry, sensitiveValues));
66
+ if (!value || typeof value !== "object") return value;
67
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, replaceSensitiveValues(entry, sensitiveValues)]));
68
+ }
69
+ function appendDiagnosticLine(scope, logPath, line) {
70
+ const dir = path.dirname(logPath);
71
+ if (!scope.ensuredDirectories.has(dir)) {
72
+ fs.mkdirSync(dir, { recursive: true });
73
+ scope.ensuredDirectories.add(dir);
74
+ }
75
+ fs.appendFileSync(logPath, line, "utf8");
76
+ }
77
+ //#endregion
78
+ //#region ../kernel/src/source-value.ts
79
+ const BOOLEAN_TRUE_VALUES = /* @__PURE__ */ new Set([
80
+ "1",
81
+ "true",
82
+ "yes",
83
+ "on"
84
+ ]);
85
+ const BOOLEAN_FALSE_VALUES = /* @__PURE__ */ new Set([
86
+ "0",
87
+ "false",
88
+ "no",
89
+ "off"
90
+ ]);
91
+ function parseBooleanLiteral(value) {
92
+ const normalized = value.trim().toLowerCase();
93
+ if (BOOLEAN_TRUE_VALUES.has(normalized)) return true;
94
+ if (BOOLEAN_FALSE_VALUES.has(normalized)) return false;
95
+ }
96
+ //#endregion
97
+ //#region ../host-kit/src/internal/exec.ts
98
+ const BARE_COMMAND_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
99
+ const EXEC_DIAGNOSTIC_ARG_LIMIT = 6;
100
+ const commandExecutorOverrideScope = new AsyncLocalStorage();
101
+ const requestCommandEnvScope = new AsyncLocalStorage();
102
+ /** Effective toolchain selection; an explicitly empty request value selects `xcode-select`. */
103
+ function commandDeveloperDir() {
104
+ const requestEnv = requestCommandEnvScope.getStore();
105
+ if (requestEnv && Object.hasOwn(requestEnv, "DEVELOPER_DIR")) return requestEnv.DEVELOPER_DIR || void 0;
106
+ return process.env.DEVELOPER_DIR || void 0;
107
+ }
108
+ function resolveSpawnEnv(env) {
109
+ const requestEnv = requestCommandEnvScope.getStore();
110
+ if (!requestEnv) return env;
111
+ return {
112
+ ...env ?? process.env,
113
+ ...requestEnv
114
+ };
115
+ }
116
+ async function runCmd(cmd, args, options = {}) {
117
+ const overrideResult = commandExecutorOverrideScope.getStore()?.(cmd, args, options);
118
+ if (overrideResult) return coerceExecResult(await overrideResult);
119
+ return await runSpawnedCommand(cmd, args, options);
120
+ }
121
+ /**
122
+ * Normalize an exec result produced outside this module. Tool providers,
123
+ * executor overrides, and SDK-supplied adb executors are plain callbacks whose
124
+ * results cross an unchecked boundary; coercing once here lets downstream code
125
+ * trust the ExecResult types instead of re-wrapping fields defensively at every
126
+ * use. A non-number exitCode coerces to 1 — the same failure branch such a
127
+ * result already landed in at every `exitCode !== 0` guard.
128
+ */
129
+ function coerceExecResult(result) {
130
+ const stdout = typeof result.stdout === "string" ? result.stdout : String(result.stdout ?? "");
131
+ const stderr = typeof result.stderr === "string" ? result.stderr : String(result.stderr ?? "");
132
+ const exitCode = typeof result.exitCode === "number" ? result.exitCode : 1;
133
+ if (stdout === result.stdout && stderr === result.stderr && exitCode === result.exitCode) return result;
134
+ return {
135
+ ...result,
136
+ stdout,
137
+ stderr,
138
+ exitCode
139
+ };
140
+ }
141
+ function runSpawnedCommand(cmd, args, options = {}) {
142
+ const executable = normalizeExecutableCommand(cmd);
143
+ const execTrace = createExecTraceContext();
144
+ return new Promise((resolve, reject) => {
145
+ const child = spawn(executable, args, {
146
+ cwd: options.cwd,
147
+ env: resolveSpawnEnv(options.env),
148
+ stdio: [
149
+ "pipe",
150
+ "pipe",
151
+ "pipe"
152
+ ],
153
+ detached: options.detached,
154
+ windowsHide: true,
155
+ shell: false
156
+ });
157
+ options.onSpawn?.(child);
158
+ let stdout = "";
159
+ const stdoutChunks = options.binaryStdout ? [] : void 0;
160
+ let stderr = "";
161
+ let didTimeout = false;
162
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
163
+ let timeoutHandle = null;
164
+ let settled = false;
165
+ function finish() {
166
+ if (settled) return false;
167
+ settled = true;
168
+ if (timeoutHandle) clearTimeout(timeoutHandle);
169
+ abort.dispose();
170
+ destroyCommandStreams(child);
171
+ execTrace.emitForegroundCompletion(cmd, args);
172
+ return true;
173
+ }
174
+ function fail(error) {
175
+ if (finish()) reject(error);
176
+ }
177
+ function settle(code) {
178
+ if (!finish()) return;
179
+ const finalExitCode = code ?? 1;
180
+ if (!abort.didAbort && didTimeout && timeoutMs) {
181
+ reject(createTimeoutError(executable, cmd, args, timeoutMs, finalExitCode, stdout, stderr));
182
+ return;
183
+ }
184
+ const failure = commandCloseFailure(abort, executable, cmd, args, finalExitCode, options.allowFailure, stdout, stderr);
185
+ if (failure) {
186
+ reject(failure);
187
+ return;
188
+ }
189
+ resolve({
190
+ stdout,
191
+ stderr,
192
+ exitCode: finalExitCode,
193
+ stdoutBuffer: stdoutChunks ? Buffer.concat(stdoutChunks) : void 0
194
+ });
195
+ }
196
+ const settlement = createCommandKillSettlement({
197
+ killProcessTree: () => killProcessTree(child, options),
198
+ settle
199
+ });
200
+ const abort = watchCommandAbort(options, settlement.requestKill);
201
+ timeoutHandle = timeoutMs ? setTimeout(() => {
202
+ didTimeout = true;
203
+ settlement.requestKill();
204
+ }, timeoutMs) : null;
205
+ if (!options.binaryStdout) child.stdout.setEncoding("utf8");
206
+ child.stderr.setEncoding("utf8");
207
+ writeChildStdin(child, options.stdin).catch((error) => {
208
+ if (abort.didAbort || didTimeout) return;
209
+ if (isEpipeError(error)) return;
210
+ fail(createStdinError(executable, cmd, args, error));
211
+ killProcessTree(child, options);
212
+ });
213
+ child.stdout.on("data", (chunk) => {
214
+ if (options.binaryStdout) {
215
+ stdoutChunks?.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
216
+ return;
217
+ }
218
+ const text = String(chunk);
219
+ stdout += text;
220
+ options.onStdoutChunk?.(text);
221
+ });
222
+ child.stderr.on("data", (chunk) => {
223
+ const text = String(chunk);
224
+ stderr += text;
225
+ options.onStderrChunk?.(text);
226
+ });
227
+ child.on("error", (err) => {
228
+ fail(spawnRejectionError(abort, executable, cmd, args, err));
229
+ });
230
+ child.once("exit", settlement.recordExit);
231
+ child.once("close", settle);
232
+ });
233
+ }
234
+ function runCmdSync(cmd, args, options = {}) {
235
+ const executable = normalizeExecutableCommand(cmd);
236
+ const result = spawnSync(executable, args, {
237
+ cwd: options.cwd,
238
+ env: resolveSpawnEnv(options.env),
239
+ stdio: [
240
+ "pipe",
241
+ "pipe",
242
+ "pipe"
243
+ ],
244
+ encoding: options.binaryStdout ? void 0 : "utf8",
245
+ input: options.stdin,
246
+ timeout: normalizeTimeoutMs(options.timeoutMs),
247
+ windowsHide: true,
248
+ shell: false,
249
+ ...options.maxBuffer !== void 0 ? { maxBuffer: options.maxBuffer } : {}
250
+ });
251
+ if (result.error) {
252
+ const code = result.error.code;
253
+ if (code === "ETIMEDOUT") throw new AppError("COMMAND_FAILED", `${executable} timed out after ${normalizeTimeoutMs(options.timeoutMs)}ms`, {
254
+ cmd,
255
+ args,
256
+ timeoutMs: normalizeTimeoutMs(options.timeoutMs)
257
+ }, result.error);
258
+ if (code === "ENOENT") throw createMissingToolError(executable, cmd, result.error);
259
+ throw createCommandFailedError(executable, cmd, args, result.error);
260
+ }
261
+ const stdoutBuffer = options.binaryStdout ? Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? "") : void 0;
262
+ const stdout = options.binaryStdout ? "" : typeof result.stdout === "string" ? result.stdout : (result.stdout ?? "").toString();
263
+ const stderr = typeof result.stderr === "string" ? result.stderr : (result.stderr ?? "").toString();
264
+ const exitCode = result.status ?? 1;
265
+ if (exitCode !== 0 && !options.allowFailure) throw createExitError(executable, cmd, args, exitCode, stdout, stderr);
266
+ return {
267
+ stdout,
268
+ stderr,
269
+ exitCode,
270
+ stdoutBuffer
271
+ };
272
+ }
273
+ function createExecTraceContext() {
274
+ const diagnosticsMeta = getDiagnosticsMeta();
275
+ const diagnosticsDebugEnabled = diagnosticsMeta.debug === true;
276
+ const envTraceEnabled = parseBooleanLiteral(process.env.AGENT_DEVICE_EXEC_TRACE ?? "") === true;
277
+ if (!diagnosticsDebugEnabled && !envTraceEnabled) return createDisabledExecTraceContext();
278
+ if (envTraceEnabled && diagnosticsMeta.flushOnSuccess !== true) updateDiagnosticsScope({ flushOnSuccess: true });
279
+ const startedAtMs = Date.now();
280
+ let completionEmitted = false;
281
+ return {
282
+ emitForegroundCompletion: (cmd, args) => {
283
+ if (completionEmitted) return;
284
+ completionEmitted = true;
285
+ emitExecCommandDiagnostic({
286
+ cmd,
287
+ args,
288
+ startedAtMs
289
+ });
290
+ },
291
+ emitBackgroundSpawn: (cmd, args) => {
292
+ emitExecCommandDiagnostic({
293
+ cmd,
294
+ args,
295
+ data: { event: "spawn" }
296
+ });
297
+ },
298
+ emitBackgroundCompletion: (cmd, args, event) => {
299
+ if (completionEmitted) return;
300
+ completionEmitted = true;
301
+ emitExecCommandDiagnostic({
302
+ cmd,
303
+ args,
304
+ startedAtMs,
305
+ data: { event }
306
+ });
307
+ }
308
+ };
309
+ }
310
+ function createDisabledExecTraceContext() {
311
+ return {
312
+ emitForegroundCompletion: () => {},
313
+ emitBackgroundSpawn: () => {},
314
+ emitBackgroundCompletion: () => {}
315
+ };
316
+ }
317
+ function emitExecCommandDiagnostic(params) {
318
+ const argsPrefix = params.args.slice(0, EXEC_DIAGNOSTIC_ARG_LIMIT);
319
+ emitDiagnostic({
320
+ level: "debug",
321
+ phase: "exec_command",
322
+ durationMs: params.startedAtMs === void 0 ? void 0 : Math.max(0, Date.now() - params.startedAtMs),
323
+ data: {
324
+ command: params.cmd,
325
+ argsPrefix,
326
+ ...params.args.length > argsPrefix.length ? { omittedArgCount: params.args.length - argsPrefix.length } : {},
327
+ ...params.data ?? {}
328
+ }
329
+ });
330
+ }
331
+ function normalizeExecutableCommand(cmd) {
332
+ const candidate = normalizeExecutableLookup(cmd);
333
+ if (!candidate) throw new AppError("INVALID_ARGS", `Invalid executable command: ${JSON.stringify(cmd)}`, {
334
+ cmd,
335
+ hint: "Use a bare command name from PATH or an absolute executable path."
336
+ });
337
+ return candidate;
338
+ }
339
+ function createSpawnError(executable, cmd, args, err) {
340
+ if (err.code === "ENOENT") return createMissingToolError(executable, cmd, err);
341
+ return createCommandFailedError(executable, cmd, args, err);
342
+ }
343
+ function createMissingToolError(executable, cmd, cause) {
344
+ return new AppError("TOOL_MISSING", `${executable} not found in PATH`, { cmd }, cause);
345
+ }
346
+ function createCommandFailedError(executable, cmd, args, cause) {
347
+ return new AppError("COMMAND_FAILED", `Failed to run ${executable}`, {
348
+ cmd,
349
+ args
350
+ }, cause);
351
+ }
352
+ function createStdinError(executable, cmd, args, cause) {
353
+ return new AppError("COMMAND_FAILED", `Failed to write stdin for ${executable}`, {
354
+ cmd,
355
+ args
356
+ }, cause instanceof Error ? cause : void 0);
357
+ }
358
+ function createCommandCanceledError(executable, cmd, args) {
359
+ return createRequestCanceledError({
360
+ cmd,
361
+ args,
362
+ executable
363
+ });
364
+ }
365
+ function createTimeoutError(executable, cmd, args, timeoutMs, exitCode, stdout, stderr) {
366
+ return new AppError("COMMAND_FAILED", `${executable} timed out after ${timeoutMs}ms`, {
367
+ cmd,
368
+ args,
369
+ stdout,
370
+ stderr,
371
+ exitCode,
372
+ timeoutMs
373
+ });
374
+ }
375
+ function createExitError(executable, cmd, args, exitCode, stdout, stderr) {
376
+ return new AppError("COMMAND_FAILED", `${executable} exited with code ${exitCode}`, execFailureDetails({
377
+ stdout,
378
+ stderr,
379
+ exitCode
380
+ }, {
381
+ cmd,
382
+ args
383
+ }));
384
+ }
385
+ const XCRUN_TOOL_NOT_FOUND_STDERR = /xcrun: error: unable to find utility "([^"]+)"/;
386
+ function xcrunToolNotFoundDetails(stderr) {
387
+ const tool = XCRUN_TOOL_NOT_FOUND_STDERR.exec(stderr)?.[1];
388
+ if (!tool) return {};
389
+ const developerDir = commandDeveloperDir();
390
+ const source = developerDir ? `DEVELOPER_DIR ${developerDir}` : "the developer dir selected by xcode-select (see xcode-select -p)";
391
+ return {
392
+ reason: "xcrun-tool-not-found",
393
+ developerDir: developerDir ?? null,
394
+ hint: `xcrun could not find ${tool} in ${source}. Export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer (or your Xcode's path) before running agent-device, or run sudo xcode-select -s /Applications/Xcode.app.`
395
+ };
396
+ }
397
+ /**
398
+ * Exit-failure details for normalizeError, including null exit codes from signalled processes.
399
+ */
400
+ function execFailureDetails(result, extra) {
401
+ return {
402
+ stdout: result.stdout,
403
+ stderr: result.stderr,
404
+ exitCode: result.exitCode,
405
+ processExitError: true,
406
+ ...extra,
407
+ ...xcrunToolNotFoundDetails(result.stderr)
408
+ };
409
+ }
410
+ function spawnRejectionError(abort, executable, cmd, args, err) {
411
+ return abort.didAbort ? createCommandCanceledError(executable, cmd, args) : createSpawnError(executable, cmd, args, err);
412
+ }
413
+ function commandCloseFailure(abort, executable, cmd, args, exitCode, allowFailure, stdout, stderr) {
414
+ if (abort.didAbort) return createCommandCanceledError(executable, cmd, args);
415
+ if (exitCode !== 0 && !allowFailure) return createExitError(executable, cmd, args, exitCode, stdout, stderr);
416
+ return null;
417
+ }
418
+ function normalizeExecutableLookup(cmd) {
419
+ const candidate = cmd.trim();
420
+ if (!candidate || candidate.includes("\0")) return null;
421
+ if (path.isAbsolute(candidate)) return candidate;
422
+ if (candidate.includes("/") || candidate.includes("\\")) return null;
423
+ return BARE_COMMAND_RE.test(candidate) ? candidate : null;
424
+ }
425
+ function normalizeTimeoutMs(value) {
426
+ if (!Number.isFinite(value)) return void 0;
427
+ const timeout = Math.floor(value);
428
+ if (timeout <= 0) return void 0;
429
+ return timeout;
430
+ }
431
+ function createCommandKillSettlement(input) {
432
+ let killRequested = false;
433
+ let exited = false;
434
+ let exitCode = null;
435
+ const settleIfKilledAndGone = () => {
436
+ if (killRequested && exited) input.settle(exitCode);
437
+ };
438
+ return {
439
+ requestKill: () => {
440
+ killRequested = true;
441
+ input.killProcessTree();
442
+ settleIfKilledAndGone();
443
+ },
444
+ recordExit: (code) => {
445
+ exited = true;
446
+ exitCode = code ?? 1;
447
+ settleIfKilledAndGone();
448
+ }
449
+ };
450
+ }
451
+ function watchCommandAbort(options, onKill) {
452
+ let didAbort = false;
453
+ const onAbort = () => {
454
+ didAbort = true;
455
+ onKill();
456
+ };
457
+ if (options.signal?.aborted) onAbort();
458
+ else options.signal?.addEventListener("abort", onAbort, { once: true });
459
+ return {
460
+ get didAbort() {
461
+ return didAbort;
462
+ },
463
+ dispose: () => {
464
+ options.signal?.removeEventListener("abort", onAbort);
465
+ }
466
+ };
467
+ }
468
+ /**
469
+ * Best-effort process-group signalling, shared by host-kit callers. This lives below
470
+ * host-process.ts to avoid an import cycle. Only positive integer pids are accepted:
471
+ * zero targets this process's group, and negative values can target unrelated processes.
472
+ */
473
+ function signalProcessGroupBestEffort(pid, signal) {
474
+ if (!Number.isInteger(pid) || pid <= 0) return false;
475
+ try {
476
+ process.kill(-pid, signal);
477
+ return true;
478
+ } catch {
479
+ return false;
480
+ }
481
+ }
482
+ function killProcessTree(child, options) {
483
+ if (!options.kill) {
484
+ signalProcessTree(child, options.detached, "SIGKILL");
485
+ return;
486
+ }
487
+ signalProcessTree(child, options.detached, options.kill.signal);
488
+ const escalation = setTimeout(() => signalProcessTree(child, options.detached, "SIGKILL"), options.kill.graceMs);
489
+ escalation.unref();
490
+ child.once("exit", () => clearTimeout(escalation));
491
+ }
492
+ function signalProcessTree(child, detached, signal) {
493
+ if (detached && child.pid && process.platform !== "win32") {
494
+ signalProcessGroupBestEffort(child.pid, signal);
495
+ return;
496
+ }
497
+ if (child.exitCode !== null || child.signalCode !== null) return;
498
+ child.kill(signal);
499
+ }
500
+ /**
501
+ * A kill that cannot reach an inherited-pipe holder must at least stop this process
502
+ * from holding the other end of those pipes open after it has settled.
503
+ */
504
+ function destroyCommandStreams(child) {
505
+ child.stdin?.destroy();
506
+ child.stdout?.destroy();
507
+ child.stderr?.destroy();
508
+ }
509
+ async function writeChildStdin(child, stdin) {
510
+ if (!child.stdin) return;
511
+ if (stdin === void 0) {
512
+ child.stdin?.end();
513
+ return;
514
+ }
515
+ await pipeline(Readable.from([stdin]), child.stdin);
516
+ }
517
+ function isEpipeError(error) {
518
+ return error instanceof Error && "code" in error && error.code === "EPIPE";
519
+ }
520
+ //#endregion
521
+ //#region ../host-kit/src/internal/timeouts.ts
522
+ function sleep(ms) {
523
+ return new Promise((resolve) => setTimeout(resolve, ms));
524
+ }
525
+ //#endregion
526
+ //#region ../host-kit/src/internal/host-process.ts
527
+ const PS_TIMEOUT_MS = 1e3;
528
+ const HOST_PS_COMMAND = process.platform === "win32" ? "ps" : "/bin/ps";
529
+ function hostEnvironment() {
530
+ return process.env;
531
+ }
532
+ function readHostEnvironmentVariable(name) {
533
+ return hostEnvironment()[name];
534
+ }
535
+ function writeHostStderr(value) {
536
+ process.stderr.write(value);
537
+ }
538
+ function hostCurrentWorkingDirectory() {
539
+ return process.cwd();
540
+ }
541
+ function hostNodeExecutablePath() {
542
+ return process.execPath;
543
+ }
544
+ function hostNodeVersion() {
545
+ return process.version;
546
+ }
547
+ function hostPlatform() {
548
+ return process.platform;
549
+ }
550
+ function hostProcessId() {
551
+ return process.pid;
552
+ }
553
+ function isProcessAlive(pid) {
554
+ if (!Number.isInteger(pid) || pid <= 0) return false;
555
+ try {
556
+ process.kill(pid, 0);
557
+ return true;
558
+ } catch (error) {
559
+ return error.code === "EPERM";
560
+ }
561
+ }
562
+ function isProcessGroupAlive(pid) {
563
+ if (!Number.isInteger(pid) || pid <= 0) return false;
564
+ try {
565
+ process.kill(-pid, 0);
566
+ return true;
567
+ } catch (error) {
568
+ return error.code === "EPERM";
569
+ }
570
+ }
571
+ /**
572
+ * Reads who owns a pid, off the event loop, with a budget the caller names. The three fields
573
+ * are read concurrently so a loaded host pays one budget for the set rather than one per
574
+ * field. Use this rather than the synchronous probes when the answer decides whether a
575
+ * process is signaled: waiting is reversible and signaling the wrong process is not.
576
+ */
577
+ async function readProcessIdentityFacts(pid, timeoutMs = PS_TIMEOUT_MS) {
578
+ const [startTime, command, state] = await Promise.all([
579
+ readProcessFieldAsync(pid, "lstart=", timeoutMs),
580
+ readProcessFieldAsync(pid, "command=", timeoutMs),
581
+ readProcessFieldAsync(pid, "state=", timeoutMs)
582
+ ]);
583
+ return {
584
+ startTime,
585
+ command,
586
+ zombie: state === null ? null : state.startsWith("Z")
587
+ };
588
+ }
589
+ function readProcessStartTime(pid) {
590
+ return readProcessField(pid, "lstart=");
591
+ }
592
+ function readProcessCommand(pid) {
593
+ return readProcessField(pid, "command=");
594
+ }
595
+ function isProcessZombie(pid) {
596
+ return readProcessField(pid, "state=")?.startsWith("Z") ?? false;
597
+ }
598
+ function readHostProcessIdentityObservations(pids) {
599
+ const observations = /* @__PURE__ */ new Map();
600
+ const selected = uniquePositivePids(pids);
601
+ if (selected.length === 0) return observations;
602
+ try {
603
+ const result = runCmdSync("ps", [
604
+ "-p",
605
+ selected.join(","),
606
+ "-o",
607
+ "pid=,state=,lstart="
608
+ ], {
609
+ allowFailure: true,
610
+ timeoutMs: PS_TIMEOUT_MS
611
+ });
612
+ if (result.exitCode !== 0) return observations;
613
+ for (const line of result.stdout.split("\n")) {
614
+ const match = /^\s*(\d+)\s+(\S+)\s+(.+?)\s*$/.exec(line);
615
+ if (!match) continue;
616
+ const pid = Number.parseInt(match[1], 10);
617
+ if (!Number.isInteger(pid) || pid <= 0) continue;
618
+ observations.set(pid, {
619
+ state: match[2],
620
+ startTime: match[3]
621
+ });
622
+ }
623
+ } catch {}
624
+ return observations;
625
+ }
626
+ function readProcessField(pid, field) {
627
+ if (!Number.isInteger(pid) || pid <= 0) return null;
628
+ try {
629
+ return processFieldValue(runCmdSync(HOST_PS_COMMAND, [
630
+ "-p",
631
+ String(pid),
632
+ "-o",
633
+ field
634
+ ], {
635
+ allowFailure: true,
636
+ timeoutMs: PS_TIMEOUT_MS
637
+ }));
638
+ } catch {
639
+ return null;
640
+ }
641
+ }
642
+ async function readProcessFieldAsync(pid, field, timeoutMs) {
643
+ if (!Number.isInteger(pid) || pid <= 0) return null;
644
+ try {
645
+ return processFieldValue(await runCmd(HOST_PS_COMMAND, [
646
+ "-p",
647
+ String(pid),
648
+ "-o",
649
+ field
650
+ ], {
651
+ allowFailure: true,
652
+ timeoutMs
653
+ }));
654
+ } catch {
655
+ return null;
656
+ }
657
+ }
658
+ function processFieldValue(result) {
659
+ if (result.exitCode !== 0) return null;
660
+ const value = result.stdout.trim();
661
+ return value.length > 0 ? value : null;
662
+ }
663
+ function parseHostProcessList(stdout) {
664
+ const processes = [];
665
+ for (const line of stdout.split("\n")) {
666
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
667
+ if (!match) continue;
668
+ const pid = Number.parseInt(match[1], 10);
669
+ const ppid = Number.parseInt(match[2], 10);
670
+ const command = match[3];
671
+ if (!Number.isInteger(pid) || pid <= 0) continue;
672
+ processes.push({
673
+ pid,
674
+ ppid: Number.isInteger(ppid) && ppid > 0 ? ppid : void 0,
675
+ command
676
+ });
677
+ }
678
+ return processes;
679
+ }
680
+ async function listHostProcesses(options) {
681
+ const result = await (options.runCommand ?? runCmd)(options.runCommand ? "ps" : HOST_PS_COMMAND, [
682
+ "-ax",
683
+ "-o",
684
+ "pid=,ppid=,command="
685
+ ], {
686
+ allowFailure: true,
687
+ timeoutMs: options.timeoutMs
688
+ });
689
+ if (result.exitCode !== 0) return [];
690
+ return parseHostProcessList(result.stdout);
691
+ }
692
+ function expandProcessTree(rootPids, processes) {
693
+ const selected = new Set(uniquePositivePids(rootPids));
694
+ let changed = true;
695
+ while (changed) {
696
+ changed = false;
697
+ for (const processInfo of processes) {
698
+ if (processInfo.ppid === void 0 || !selected.has(processInfo.ppid)) continue;
699
+ if (selected.has(processInfo.pid)) continue;
700
+ selected.add(processInfo.pid);
701
+ changed = true;
702
+ }
703
+ }
704
+ return processes.filter((processInfo) => selected.has(processInfo.pid));
705
+ }
706
+ function uniquePositivePids(values, options = {}) {
707
+ return [...new Set(values)].filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== options.excludePid);
708
+ }
709
+ function signalPidsBestEffort(pidsToSignal, signal) {
710
+ const pids = uniquePositivePids(pidsToSignal, { excludePid: process.pid });
711
+ let signaled = 0;
712
+ for (const pid of pids) try {
713
+ process.kill(pid, signal);
714
+ signaled += 1;
715
+ } catch {}
716
+ return signaled;
717
+ }
718
+ async function waitForProcessExit(pid, timeoutMs) {
719
+ if (!isProcessAlive(pid)) return true;
720
+ const start = Date.now();
721
+ while (Date.now() - start < timeoutMs) {
722
+ await sleep(50);
723
+ if (!isProcessAlive(pid)) return true;
724
+ }
725
+ return !isProcessAlive(pid);
726
+ }
727
+ async function stopPidsWithEscalation(options) {
728
+ const pids = uniquePositivePids(options.pids, { excludePid: process.pid });
729
+ if (pids.length === 0) return;
730
+ signalPidsBestEffort(pids, "SIGTERM");
731
+ await Promise.all(pids.map(async (pid) => await waitForProcessExit(pid, options.termTimeoutMs)));
732
+ signalPidsBestEffort(pids.filter((pid) => isProcessAlive(pid)), "SIGKILL");
733
+ await Promise.all(pids.map(async (pid) => await waitForProcessExit(pid, options.killTimeoutMs)));
734
+ }
735
+ const SYSCTL_TIMEOUT_MS = 1e3;
736
+ const APPLE_SILICON_SYSCTL = ["-n", "hw.optional.arm64"];
737
+ let pendingHostCpuArch;
738
+ let settledHostCpuArch;
739
+ /**
740
+ * The machine's native CPU architecture in Apple naming (`arm64`, `x86_64`), which is what
741
+ * simulators on a Mac run by default. Other CPUs keep Node's `process.arch` name. Resolved once
742
+ * per process.
743
+ */
744
+ function readHostCpuArch() {
745
+ if (settledHostCpuArch !== void 0) return Promise.resolve(settledHostCpuArch);
746
+ pendingHostCpuArch ??= resolveHostCpuArch(process.platform, process.arch).then(settleHostCpuArch);
747
+ return pendingHostCpuArch;
748
+ }
749
+ /**
750
+ * {@link readHostCpuArch} for a caller that cannot await. It shares the same per-process value,
751
+ * resolving it with a blocking `sysctl` only when nothing has resolved it yet.
752
+ */
753
+ function readHostCpuArchSync() {
754
+ return settledHostCpuArch ?? settleHostCpuArch(hostCpuArchName(process.platform === "darwin" && isAppleSiliconMacSync(), process.arch));
755
+ }
756
+ function settleHostCpuArch(arch) {
757
+ settledHostCpuArch ??= arch;
758
+ return settledHostCpuArch;
759
+ }
760
+ async function resolveHostCpuArch(platform, nodeArch) {
761
+ return hostCpuArchName(platform === "darwin" && await isAppleSiliconMac(), nodeArch);
762
+ }
763
+ function hostCpuArchName(appleSilicon, nodeArch) {
764
+ if (appleSilicon) return "arm64";
765
+ return nodeArch === "x64" ? "x86_64" : nodeArch;
766
+ }
767
+ async function isAppleSiliconMac() {
768
+ try {
769
+ const result = await runCmd("/usr/sbin/sysctl", APPLE_SILICON_SYSCTL, {
770
+ allowFailure: true,
771
+ timeoutMs: SYSCTL_TIMEOUT_MS
772
+ });
773
+ return result.exitCode === 0 && result.stdout.trim() === "1";
774
+ } catch {
775
+ return false;
776
+ }
777
+ }
778
+ function isAppleSiliconMacSync() {
779
+ try {
780
+ const result = runCmdSync("/usr/sbin/sysctl", APPLE_SILICON_SYSCTL, {
781
+ allowFailure: true,
782
+ timeoutMs: SYSCTL_TIMEOUT_MS
783
+ });
784
+ return result.exitCode === 0 && result.stdout.trim() === "1";
785
+ } catch {
786
+ return false;
787
+ }
788
+ }
789
+ //#endregion
790
+ //#region ../host-kit/src/internal/owned-process-reaper.ts
791
+ const DEFAULT_TERM_TIMEOUT_MS = 1500;
792
+ const DEFAULT_KILL_TIMEOUT_MS = 1e3;
793
+ async function reapOwnedProcessRecordsAtStartup(store, options = {}) {
794
+ const summary = createReapSummary();
795
+ const policy = createReapPolicy(options);
796
+ for (const entry of store.read()) {
797
+ if (!isDecodedRecord(entry)) continue;
798
+ const retained = await reapDecodedEntry(entry, policy, summary);
799
+ if (retained.length === 0) store.clear(entry.scope);
800
+ else if (retained.length !== entry.records.length) store.replace(entry.scope, retained);
801
+ }
802
+ return Object.freeze(summary);
803
+ }
804
+ function isDecodedRecord(entry) {
805
+ return entry.status === "decoded";
806
+ }
807
+ function createReapSummary() {
808
+ return {
809
+ inspected: 0,
810
+ inspectedPids: [],
811
+ terminated: 0,
812
+ terminatedPids: [],
813
+ missing: 0,
814
+ missingPids: [],
815
+ ownershipLost: 0,
816
+ ownershipLostPids: [],
817
+ skippedForOpenWebSession: false
818
+ };
819
+ }
820
+ function createReapPolicy(options) {
821
+ return {
822
+ openWebSession: (options.openWebSessionNames ?? []).length > 0,
823
+ purposes: options.purposes ? new Set(options.purposes) : void 0,
824
+ termTimeoutMs: options.termTimeoutMs ?? DEFAULT_TERM_TIMEOUT_MS,
825
+ killTimeoutMs: options.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS
826
+ };
827
+ }
828
+ async function reapDecodedEntry(entry, policy, summary) {
829
+ const retained = [];
830
+ for (const record of entry.records) {
831
+ if (shouldRetainWithoutInspection(entry.scope.kind, record, policy)) {
832
+ retained.push(record);
833
+ if (isOpenWebSessionSkip(entry.scope.kind, record, policy)) summary.skippedForOpenWebSession = true;
834
+ continue;
835
+ }
836
+ summary.inspected += 1;
837
+ summary.inspectedPids.push(record.pid);
838
+ appendReapOutcome(summary, record, await reapOwnedProcess(record, policy), retained);
839
+ }
840
+ return retained;
841
+ }
842
+ function shouldRetainWithoutInspection(scope, record, policy) {
843
+ return !isSelectedPurpose(record, policy) || isOpenWebSessionSkip(scope, record, policy);
844
+ }
845
+ function isSelectedPurpose(record, policy) {
846
+ return policy.purposes === void 0 || policy.purposes.has(record.purpose);
847
+ }
848
+ function isOpenWebSessionSkip(scope, record, policy) {
849
+ return scope === "daemon" && record.purpose === "managed-web-browser" && policy.openWebSession;
850
+ }
851
+ function appendReapOutcome(summary, record, outcome, retained) {
852
+ if (outcome === "terminated") {
853
+ summary.terminated += 1;
854
+ summary.terminatedPids.push(record.pid);
855
+ return;
856
+ }
857
+ if (outcome === "missing") {
858
+ summary.missing += 1;
859
+ summary.missingPids.push(record.pid);
860
+ return;
861
+ }
862
+ summary.ownershipLost += 1;
863
+ summary.ownershipLostPids.push(record.pid);
864
+ retained.push(record);
865
+ }
866
+ async function reapOwnedProcess(record, policy) {
867
+ const initial = inspectOwnedProcess(record);
868
+ if (initial !== void 0) return initial;
869
+ signalBestEffort(record.pid, initialSignalFor(record));
870
+ return await escalateOwnedProcess(record, policy);
871
+ }
872
+ function inspectOwnedProcess(record) {
873
+ if (record.pid === process.pid || !isProcessAlive(record.pid) || isProcessZombie(record.pid)) return "missing";
874
+ return matchesRecord(record) ? void 0 : "ownership-lost";
875
+ }
876
+ function initialSignalFor(record) {
877
+ return record.purpose === "simctl-screen-recording" ? "SIGINT" : "SIGTERM";
878
+ }
879
+ async function escalateOwnedProcess(record, policy) {
880
+ const termOutcome = await waitForOwnedProcessExit(record, policy.termTimeoutMs);
881
+ if (termOutcome === "exited") return "terminated";
882
+ if (termOutcome === "ownership-lost") return "ownership-lost";
883
+ signalBestEffort(record.pid, "SIGKILL");
884
+ return await waitForOwnedProcessExit(record, policy.killTimeoutMs) === "exited" ? "terminated" : "ownership-lost";
885
+ }
886
+ async function waitForOwnedProcessExit(record, timeoutMs) {
887
+ await waitForProcessExit(record.pid, timeoutMs);
888
+ if (!isProcessAlive(record.pid) || isProcessZombie(record.pid)) return "exited";
889
+ return matchesRecord(record) ? "owned-alive" : "ownership-lost";
890
+ }
891
+ function matchesRecord(record) {
892
+ return readProcessStartTime(record.pid) === record.startTime && readProcessCommand(record.pid) === record.command;
893
+ }
894
+ function signalBestEffort(pid, signal) {
895
+ try {
896
+ process.kill(pid, signal);
897
+ } catch {}
898
+ }
899
+ //#endregion
900
+ //#region ../host-kit/src/internal/atomic-file.ts
901
+ /**
902
+ * Gives a specialized durable publisher a canonical temp path and cleanup
903
+ * ownership while it performs its own open/fsync/safety protocol.
904
+ */
905
+ function withAtomicPublishTempPathSync(destination, publish) {
906
+ const temporaryPath = createAtomicPublishTempPath(destination);
907
+ try {
908
+ return publish(temporaryPath);
909
+ } finally {
910
+ try {
911
+ fs.rmSync(temporaryPath, { force: true });
912
+ } catch (error) {
913
+ emitDiagnostic({
914
+ level: "warn",
915
+ phase: "atomic_file_cleanup_failed",
916
+ data: {
917
+ destination,
918
+ temporaryPath,
919
+ error: error instanceof Error ? error.message : String(error)
920
+ }
921
+ });
922
+ }
923
+ }
924
+ }
925
+ /** Returns the canonical same-directory temp path used by atomic publishers. */
926
+ function createAtomicPublishTempPath(destination) {
927
+ return path.join(path.dirname(destination), `.${path.basename(destination)}.${process.pid}-${crypto.randomUUID()}.tmp`);
928
+ }
929
+ //#endregion
930
+ //#region ../host-kit/src/internal/owned-process-record.ts
931
+ const RECORD_FILE_NAME = "owned-processes.json";
932
+ const RECORD_VERSION = 1;
933
+ function createOwnedProcessRecordStore(options) {
934
+ const stateDir = path.resolve(options.stateDir);
935
+ const sessionsDir = path.resolve(options.sessionsDir);
936
+ const daemonPath = path.join(stateDir, RECORD_FILE_NAME);
937
+ const resolvePath = (scope) => {
938
+ if (scope.kind === "daemon") return daemonPath;
939
+ const sessionDir = path.resolve(options.resolveSessionDir(scope.sessionId));
940
+ if (path.dirname(sessionDir) !== sessionsDir) throw new TypeError("owned process session scope must resolve directly under the sessions dir");
941
+ return path.join(sessionDir, RECORD_FILE_NAME);
942
+ };
943
+ return Object.freeze({
944
+ replace(scope, records) {
945
+ const normalized = normalizeRecords(records);
946
+ const recordPath = resolvePath(scope);
947
+ if (normalized.length === 0) {
948
+ clearRecordPath(recordPath);
949
+ return;
950
+ }
951
+ const directory = path.dirname(recordPath);
952
+ fs.mkdirSync(directory, { recursive: true });
953
+ withAtomicPublishTempPathSync(recordPath, (temporaryPath) => {
954
+ const descriptor = fs.openSync(temporaryPath, "wx", 384);
955
+ try {
956
+ fs.writeFileSync(descriptor, `${JSON.stringify({
957
+ version: RECORD_VERSION,
958
+ processes: normalized
959
+ })}\n`, "utf8");
960
+ fs.fsyncSync(descriptor);
961
+ } finally {
962
+ fs.closeSync(descriptor);
963
+ }
964
+ assertSafeRecordDestination(recordPath);
965
+ fs.renameSync(temporaryPath, recordPath);
966
+ });
967
+ },
968
+ clear(scope) {
969
+ clearRecordPath(resolvePath(scope));
970
+ },
971
+ read() {
972
+ return readRecordFiles(sessionsDir, daemonPath);
973
+ }
974
+ });
975
+ }
976
+ function readOwnedProcessRecordFile(recordPath, scope) {
977
+ let value;
978
+ try {
979
+ assertSafeRecordDestination(recordPath);
980
+ value = JSON.parse(fs.readFileSync(recordPath, "utf8"));
981
+ } catch (error) {
982
+ return {
983
+ scope,
984
+ path: recordPath,
985
+ status: "invalid",
986
+ records: [],
987
+ message: errorMessage(error)
988
+ };
989
+ }
990
+ if (!isRecordFile(value)) return {
991
+ scope,
992
+ path: recordPath,
993
+ status: "invalid",
994
+ records: [],
995
+ message: "owned process record must contain version 1 and a processes array"
996
+ };
997
+ try {
998
+ return {
999
+ scope,
1000
+ path: recordPath,
1001
+ status: "decoded",
1002
+ records: normalizeRecords(value.processes)
1003
+ };
1004
+ } catch (error) {
1005
+ return {
1006
+ scope,
1007
+ path: recordPath,
1008
+ status: "invalid",
1009
+ records: [],
1010
+ message: errorMessage(error)
1011
+ };
1012
+ }
1013
+ }
1014
+ function readRecordFiles(sessionsDir, daemonPath) {
1015
+ const entries = [];
1016
+ if (fileExists(daemonPath)) entries.push(readOwnedProcessRecordFile(daemonPath, { kind: "daemon" }));
1017
+ if (!fs.existsSync(sessionsDir)) return entries;
1018
+ for (const entry of fs.readdirSync(sessionsDir, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
1019
+ if (!entry.isDirectory()) continue;
1020
+ const recordPath = path.join(sessionsDir, entry.name, RECORD_FILE_NAME);
1021
+ if (fileExists(recordPath)) entries.push(readOwnedProcessRecordFile(recordPath, {
1022
+ kind: "session",
1023
+ sessionId: entry.name
1024
+ }));
1025
+ }
1026
+ return entries;
1027
+ }
1028
+ function normalizeRecords(records) {
1029
+ const seen = /* @__PURE__ */ new Set();
1030
+ const normalized = [];
1031
+ for (const record of records) {
1032
+ if (!isValidOwnedProcessRecord(record)) throw new TypeError("owned process record has an invalid identity or purpose");
1033
+ if (seen.has(record.pid)) throw new TypeError(`duplicate owned process pid ${record.pid}`);
1034
+ seen.add(record.pid);
1035
+ normalized.push(Object.freeze({
1036
+ pid: record.pid,
1037
+ startTime: record.startTime,
1038
+ command: record.command,
1039
+ purpose: record.purpose
1040
+ }));
1041
+ }
1042
+ return normalized.sort((left, right) => left.pid - right.pid);
1043
+ }
1044
+ function isValidOwnedProcessRecord(record) {
1045
+ return Number.isInteger(record.pid) && record.pid > 0 && typeof record.startTime === "string" && record.startTime.length > 0 && typeof record.command === "string" && record.command.length > 0 && typeof record.purpose === "string" && record.purpose.length > 0;
1046
+ }
1047
+ function isRecordFile(value) {
1048
+ return typeof value === "object" && value !== null && "version" in value && value.version === RECORD_VERSION && "processes" in value && Array.isArray(value.processes);
1049
+ }
1050
+ function clearRecordPath(recordPath) {
1051
+ try {
1052
+ assertSafeRecordDestination(recordPath);
1053
+ fs.unlinkSync(recordPath);
1054
+ } catch (error) {
1055
+ if (!isMissingFile(error)) throw error;
1056
+ }
1057
+ }
1058
+ function assertSafeRecordDestination(recordPath) {
1059
+ try {
1060
+ const stats = fs.lstatSync(recordPath);
1061
+ if (stats.isSymbolicLink()) throw new Error("Refusing to follow an owned process record symlink");
1062
+ if (!stats.isFile()) throw new Error("Owned process record is not a regular file");
1063
+ } catch (error) {
1064
+ if (!isMissingFile(error)) throw error;
1065
+ }
1066
+ }
1067
+ function fileExists(filePath) {
1068
+ try {
1069
+ return fs.lstatSync(filePath).isFile();
1070
+ } catch {
1071
+ return false;
1072
+ }
1073
+ }
1074
+ function isMissingFile(error) {
1075
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
1076
+ }
1077
+ function errorMessage(error) {
1078
+ return error instanceof Error ? error.message : "owned process record is invalid";
1079
+ }
1080
+ //#endregion
1081
+ //#region ../host-kit/src/internal/owner-identity.ts
1082
+ /** A process id accepted by Node's native signal API. */
1083
+ function isProcessPid(value) {
1084
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 2147483647;
1085
+ }
1086
+ function readCurrentOwnerIdentity() {
1087
+ return {
1088
+ pid: process.pid,
1089
+ startTime: readProcessStartTime(process.pid)
1090
+ };
1091
+ }
1092
+ function ownerIdentityMatches(left, right) {
1093
+ return left.pid === right.pid && left.startTime === right.startTime;
1094
+ }
1095
+ /**
1096
+ * Positive proof that two records name DIFFERENT processes. Deliberately not
1097
+ * the negation of {@link ownerIdentityMatches}: equal pids with an unreadable
1098
+ * start time on either side are unproven rather than different, so a caller
1099
+ * that acts on this can never mistake one owner for two and hand its resource
1100
+ * away.
1101
+ */
1102
+ function ownerIdentityDiffers(left, right) {
1103
+ if (left.pid !== right.pid) return true;
1104
+ return Boolean(left.startTime && right.startTime && left.startTime !== right.startTime);
1105
+ }
1106
+ /**
1107
+ * This is deliberately proof-oriented, in both directions. A filesystem read
1108
+ * error is not proof that an owner state directory disappeared, so callers
1109
+ * must surface it as an unknown owner rather than treating the resource as
1110
+ * free. Likewise a failed `ps` read (it shells out with a short timeout and
1111
+ * loses under CPU contention) is not proof the owner died, so it never
1112
+ * condemns a pid that kill(pid, 0) says is alive. Death is only concluded
1113
+ * from positive evidence: the pid is gone or the process is a zombie (already
1114
+ * terminated, merely unreaped). A different readable start time proves PID
1115
+ * reuse, but remains a distinct result so each ownership policy must decide
1116
+ * explicitly whether that proof permits recovery. An unreadable start time is
1117
+ * not proof of reuse. An owner recorded without a start time stays fail-closed
1118
+ * while its pid is alive: there is no same-clock-domain proof of birth order (wall-clock
1119
+ * arithmetic over `ps etime` shifts under clock steps), and misreading a live
1120
+ * owner as recycled would let a waiter steal a held resource.
1121
+ */
1122
+ function classifyOwnerLiveness(params) {
1123
+ return classifyOwnerLivenessFromObservation(params);
1124
+ }
1125
+ function classifyOwnerLivenessFromObservation(params, observation) {
1126
+ const { owner, stateDir } = params;
1127
+ if (!isProcessPid(owner.pid)) return "unknown";
1128
+ if (!isProcessAlive(owner.pid)) return "owner-process-dead";
1129
+ if (observation !== void 0 ? observation?.state.startsWith("Z") : isProcessZombie(owner.pid)) return "owner-process-dead";
1130
+ if (owner.startTime) {
1131
+ const currentStartTime = observation !== void 0 ? observation?.startTime ?? null : readProcessStartTime(owner.pid);
1132
+ if (currentStartTime !== null && currentStartTime !== owner.startTime) return "owner-process-reused";
1133
+ }
1134
+ return stateDir ? classifyOwnerStateDirectory(stateDir) : "live";
1135
+ }
1136
+ function classifyOwnerStateDirectory(stateDir) {
1137
+ try {
1138
+ fs.statSync(stateDir);
1139
+ return "live";
1140
+ } catch (error) {
1141
+ const code = error?.code;
1142
+ if (code === "ENOENT" || code === "ENOTDIR") return "owner-state-dir-gone";
1143
+ return "unknown";
1144
+ }
1145
+ }
1146
+ //#endregion
1147
+ export { classifyOwnerLiveness, classifyOwnerLivenessFromObservation, createOwnedProcessRecordStore, expandProcessTree, hostCurrentWorkingDirectory, hostEnvironment, hostNodeExecutablePath, hostNodeVersion, hostPlatform, hostProcessId, isProcessAlive, isProcessGroupAlive, isProcessPid, isProcessZombie, listHostProcesses, ownerIdentityDiffers, ownerIdentityMatches, readCurrentOwnerIdentity, readHostCpuArch, readHostCpuArchSync, readHostEnvironmentVariable, readHostProcessIdentityObservations, readOwnedProcessRecordFile, readProcessCommand, readProcessIdentityFacts, readProcessStartTime, reapOwnedProcessRecordsAtStartup, signalPidsBestEffort, signalProcessGroupBestEffort, stopPidsWithEscalation, uniquePositivePids, waitForProcessExit, writeHostStderr };