@code-yeongyu/senpi 2026.10.24-4 → 2026.10.25-2
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/CHANGELOG.md +32 -0
- package/dist/bundle/chunks/{chunk-NQC7MI2G.js → chunk-2EFVQWXZ.js} +3 -3
- package/dist/bundle/chunks/{chunk-RHHBXIRN.js → chunk-4WZRVZNX.js} +1 -1
- package/dist/bundle/chunks/{chunk-HKUXKTFW.js → chunk-B6TPQDBW.js} +1 -1
- package/dist/bundle/chunks/{chunk-VOBD6FQG.js → chunk-FHXR4ZAC.js} +1 -1
- package/dist/bundle/chunks/{chunk-7IRLFYNM.js → chunk-G2UETS2R.js} +2 -2
- package/dist/bundle/chunks/{chunk-GVJ76VQI.js → chunk-J2U5VDG7.js} +63 -63
- package/dist/bundle/chunks/{chunk-42OY5SZZ.js → chunk-JRXKLIEQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-3VOG6WFU.js → chunk-KJZAY5YH.js} +1 -1
- package/dist/bundle/chunks/{chunk-UT6AJHPK.js → chunk-KUE4AQN6.js} +1 -1
- package/dist/bundle/chunks/{chunk-FWXMMMCV.js → chunk-LYGAV6KU.js} +1 -1
- package/dist/bundle/chunks/{chunk-SGDOZT6B.js → chunk-MXZPAUFB.js} +1 -1
- package/dist/bundle/chunks/chunk-O5RYI3IW.js +2 -0
- package/dist/bundle/chunks/{chunk-II22E3CV.js → chunk-OI4NMCKP.js} +1 -1
- package/dist/bundle/chunks/{chunk-7UUOGZKR.js → chunk-RXKNOEQQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-QH2HSCMX.js → chunk-TTU2RNVX.js} +3 -3
- package/dist/bundle/chunks/{chunk-PXKF2RHD.js → chunk-UFJ2K7Q7.js} +1 -1
- package/dist/bundle/chunks/chunk-VNN2V74Y.js +5 -0
- package/dist/bundle/chunks/{chunk-BMYNMUVZ.js → chunk-W2BKZBYJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-RVYSJBZ7.js → chunk-WFXUCVIR.js} +1 -1
- package/dist/bundle/chunks/{chunk-IUHU5PEC.js → chunk-XBUSVKWC.js} +1 -1
- package/dist/bundle/chunks/{chunk-EWOEVVQT.js → chunk-YN7ELQ43.js} +1 -1
- package/dist/bundle/chunks/{cli-main-UH3MAREK.js → cli-main-SWKFMTZQ.js} +1 -1
- package/dist/bundle/chunks/{host-command-OQR4RDTM.js → host-command-BC6UWP33.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-U36U7JTE.js → host-lifecycle-TN5NEGNF.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +3 -3
- package/dist/bundle/chunks/{host-session-command-IGB27ZPJ.js → host-session-command-MGVBLBRV.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-ZDJGSSZO.js → host-socket-request-AS2OCRLP.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-R5RCSJ2F.js → interactive-mode-SCSROHD5.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-YZEMYUFY.js → multi-session-host-VTTY5MFT.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-5FOXDKUA.js → package-manager-cli-DZFOC3MF.js} +1 -1
- package/dist/bundle/chunks/{role-M2MTVACJ.js → role-JVSBNKGE.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-6K62CZBW.js → rpc-mode-TOXYD4XA.js} +1 -1
- package/dist/bundle/chunks/{rpc-monitor-source-OL5KU6NO.js → rpc-monitor-source-B5NQ4ZVH.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-CLNODAYE.js → runtime-session-V5OO7ZRM.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-XBW4EOG5.js → schedule-command-SLISTG4V.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-54TMCS7Q.js → session-control-endpoint-HFFMBALG.js} +1 -1
- package/dist/bundle/chunks/{session-picker-P2VUU2MQ.js → session-picker-465FVXQG.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +69 -69
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +3 -3
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.js +28 -9
- package/dist/core/engine-turn-limit.d.ts +2 -0
- package/dist/core/engine-turn-limit.js +7 -1
- package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.js +5 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-binding.d.ts +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-commit-boundary.d.ts +6 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-commit-boundary.js +18 -7
- package/dist/core/extensions/builtin/anthropic-subscription/session-commit-divergence.d.ts +23 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-commit-divergence.js +107 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-entry-annotations.d.ts +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-entry-annotations.js +3 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-observability.d.ts +3 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-observability.js +4 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-registry-wiring.js +12 -7
- package/dist/core/extensions/builtin/anthropic-subscription/session-registry.d.ts +3 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-registry.js +3 -2
- package/dist/core/extensions/builtin/anthropic-subscription/session-stream.js +2 -0
- package/dist/core/extensions/builtin/goal/stop-pause.js +2 -1
- package/dist/core/extensions/builtin/loop/session-hold.js +2 -1
- package/dist/core/lifecycle-runtime-facts.js +3 -0
- package/dist/core/lifecycle-session-binding.js +6 -2
- package/dist/core/run-resume-turn.d.ts +1 -0
- package/dist/core/run-resume-turn.js +4 -3
- package/dist/core/skill-access.js +15 -3
- package/dist/core/skill-discovery.d.ts +2 -0
- package/dist/core/skill-discovery.js +1 -1
- package/dist/core/stop-resume-directive.d.ts +1 -1
- package/dist/core/stop-resume-directive.js +10 -1
- package/dist/modes/interactive/interactive-mode.js +1 -0
- package/dist/modes/interactive/pause-footer.d.ts +5 -0
- package/dist/modes/interactive/pause-footer.js +2 -0
- package/dist/modes/rpc/host-reservations.js +3 -2
- package/dist/modes/rpc/host-stalled-evidence.js +2 -1
- package/dist/modes/rpc/tool-child/group-ownership.js +4 -2
- package/dist/utils/process-identity.d.ts +7 -0
- package/dist/utils/process-identity.js +32 -5
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-7ASLQ243.js +0 -2
- package/dist/bundle/chunks/chunk-XUFUKDKJ.js +0 -5
|
@@ -44,8 +44,12 @@ export class LifecycleSessionBinding {
|
|
|
44
44
|
});
|
|
45
45
|
// A crashed receiver may have durable custody but no transcript entry yet. Recovery owns that
|
|
46
46
|
// delivery already; register it before session_start can let the gateway enqueue a second carrier.
|
|
47
|
-
const
|
|
47
|
+
const deliveries = session.sessionManager.reduceHistory(lifecycleDeliveryEntries);
|
|
48
48
|
for (const record of this.controller.obligations.active) {
|
|
49
|
+
// Discover consumed work before session_start can queue another automatic turn. The later
|
|
50
|
+
// recovery pass still writes interruption/tool records, but must not be the first to close admission.
|
|
51
|
+
if (record.ticketId && (record.state === "consumed" || deliveries.has(record.ticketId)))
|
|
52
|
+
this.controller.ledger.interrupted = true;
|
|
49
53
|
if (record.state !== "queued" || !record.payload || !record.ticketId)
|
|
50
54
|
continue;
|
|
51
55
|
const ticketId = record.ticketId;
|
|
@@ -53,7 +57,7 @@ export class LifecycleSessionBinding {
|
|
|
53
57
|
const deliveryId = deliveryIdOf(message);
|
|
54
58
|
if (deliveryId === undefined)
|
|
55
59
|
continue;
|
|
56
|
-
if (
|
|
60
|
+
if (deliveries.has(ticketId))
|
|
57
61
|
session.externalAdmission.observePersisted(message);
|
|
58
62
|
else
|
|
59
63
|
session.externalAdmission.restoreQueued(deliveryId, () =>
|
|
@@ -2,6 +2,7 @@ import type { ImageContent } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { CustomMessage } from "./messages.ts";
|
|
3
3
|
type Message = Pick<CustomMessage, "customType" | "content" | "display" | "details">;
|
|
4
4
|
export interface RunResumePorts {
|
|
5
|
+
readonly resumeCause?: "stop" | "interrupted";
|
|
5
6
|
readonly heldInputs: () => readonly {
|
|
6
7
|
readonly inputId: string;
|
|
7
8
|
readonly text: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stopResumeDirective } from "./stop-resume-directive.js";
|
|
2
2
|
function partsOf(content) {
|
|
3
3
|
return typeof content === "string" ? [{ type: "text", text: content }] : [...content];
|
|
4
4
|
}
|
|
@@ -13,17 +13,18 @@ export async function startRunResumeTurn(ports) {
|
|
|
13
13
|
const inputs = ports.heldInputs();
|
|
14
14
|
const continues = ports.continueInputs();
|
|
15
15
|
const wakes = ports.heldWakes();
|
|
16
|
+
const directive = stopResumeDirective(ports.resumeCause);
|
|
16
17
|
const content = [
|
|
17
18
|
...inputs.flatMap((input) => [
|
|
18
19
|
{ type: "text", text: `The user wrote while the run was paused:\n${input.text}` },
|
|
19
20
|
...(input.images ?? []),
|
|
20
21
|
]),
|
|
21
22
|
...wakes.flatMap((wake) => partsOf(wake.message.content)),
|
|
22
|
-
...partsOf(
|
|
23
|
+
...partsOf(directive.content),
|
|
23
24
|
];
|
|
24
25
|
const sendSettled = Promise.withResolvers();
|
|
25
26
|
const started = ports.turnStarted(sendSettled.promise);
|
|
26
|
-
const sending = ports.send({
|
|
27
|
+
const sending = ports.send({ ...directive, content, display: inputs.length > 0 });
|
|
27
28
|
sending.then(sendSettled.resolve, sendSettled.resolve);
|
|
28
29
|
const turnId = await started;
|
|
29
30
|
const delivered = (async () => {
|
|
@@ -22,15 +22,27 @@ export function insideSkillRoot(path, root) {
|
|
|
22
22
|
export function buildSkillAllowedSet(sources) {
|
|
23
23
|
const roots = new Set();
|
|
24
24
|
const files = new Set();
|
|
25
|
-
|
|
25
|
+
// Package resolution retains the selected root on every discovered skill.
|
|
26
|
+
// Walk each spelling once, without collapsing differently ignored aliases.
|
|
27
|
+
const realpaths = new Map();
|
|
28
|
+
const realpath = (path) => {
|
|
29
|
+
let canonical = realpaths.get(path);
|
|
30
|
+
if (canonical === undefined) {
|
|
31
|
+
canonical = realpathSync.native(path);
|
|
32
|
+
realpaths.set(path, canonical);
|
|
33
|
+
}
|
|
34
|
+
return canonical;
|
|
35
|
+
};
|
|
36
|
+
for (const source of new Set(sources)) {
|
|
26
37
|
try {
|
|
27
|
-
const canonical =
|
|
38
|
+
const canonical = realpath(source);
|
|
28
39
|
if (statSync(canonical).isFile()) {
|
|
29
40
|
files.add(canonical);
|
|
30
41
|
continue;
|
|
31
42
|
}
|
|
32
43
|
collectSkillEntries(source, "pi", undefined, undefined, {
|
|
33
|
-
|
|
44
|
+
realpath,
|
|
45
|
+
onDirectory: (path) => roots.add(realpath(path)),
|
|
34
46
|
});
|
|
35
47
|
}
|
|
36
48
|
catch {
|
|
@@ -9,6 +9,8 @@ export interface SkillWalkOptions {
|
|
|
9
9
|
readonly allowedSet?: SkillAllowedSet;
|
|
10
10
|
readonly ancestry?: ReadonlySet<string>;
|
|
11
11
|
readonly onDirectory?: (path: string) => void;
|
|
12
|
+
/** Optional cache owned by a single discovery pass, never retained on a Skill. */
|
|
13
|
+
readonly realpath?: (path: string) => string;
|
|
12
14
|
}
|
|
13
15
|
export declare function shouldSkipSkillWalkDirectoryName(name: string): boolean;
|
|
14
16
|
/**
|
|
@@ -117,7 +117,7 @@ export function collectSkillEntries(dir, mode, ignoreMatcher, rootDir, options =
|
|
|
117
117
|
let canonical;
|
|
118
118
|
try {
|
|
119
119
|
authorizeSkillPath(dir, options.provenance, options.allowedSet, true);
|
|
120
|
-
canonical = realpathSync.native(dir);
|
|
120
|
+
canonical = (options.realpath ?? realpathSync.native)(dir);
|
|
121
121
|
}
|
|
122
122
|
catch (error) {
|
|
123
123
|
// Leave a blocked root as a candidate so the resource loader retains its
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { CustomMessage } from "./messages.ts";
|
|
2
2
|
export declare const STOP_RESUME_CUSTOM_TYPE = "session-resumed";
|
|
3
|
-
export declare function stopResumeDirective(): Pick<CustomMessage, "customType" | "content" | "display">;
|
|
3
|
+
export declare function stopResumeDirective(cause?: "stop" | "interrupted"): Pick<CustomMessage, "customType" | "content" | "display" | "details">;
|
|
4
4
|
//# sourceMappingURL=stop-resume-directive.d.ts.map
|
|
@@ -2,7 +2,16 @@ export const STOP_RESUME_CUSTOM_TYPE = "session-resumed";
|
|
|
2
2
|
const DIRECTIVE = "<system-reminder>The user stopped the previous turn and has now resumed. Tool calls that the Stop interrupted show " +
|
|
3
3
|
"an aborted result; they were not re-run and their side effects may be partial. Check their effects before repeating " +
|
|
4
4
|
"any of them, then continue the work.</system-reminder>";
|
|
5
|
-
export function stopResumeDirective() {
|
|
5
|
+
export function stopResumeDirective(cause = "stop") {
|
|
6
|
+
if (cause === "interrupted")
|
|
7
|
+
return {
|
|
8
|
+
customType: STOP_RESUME_CUSTOM_TYPE,
|
|
9
|
+
content: "<system-reminder>The previous run was interrupted and has now resumed. Interrupted tool calls may have " +
|
|
10
|
+
"partial side effects and were not re-run. Check their effects before repeating any of them, then continue " +
|
|
11
|
+
"the work.</system-reminder>",
|
|
12
|
+
display: false,
|
|
13
|
+
details: { resumeCause: cause },
|
|
14
|
+
};
|
|
6
15
|
return { customType: STOP_RESUME_CUSTOM_TYPE, content: DIRECTIVE, display: false };
|
|
7
16
|
}
|
|
8
17
|
//# sourceMappingURL=stop-resume-directive.js.map
|
|
@@ -2117,6 +2117,7 @@ export class InteractiveMode {
|
|
|
2117
2117
|
this.showExtensionError(error);
|
|
2118
2118
|
},
|
|
2119
2119
|
});
|
|
2120
|
+
this.setExtensionStatus(PAUSE_STATUS_KEY, pauseFooterText(this.session));
|
|
2120
2121
|
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
2121
2122
|
this.setupAutocompleteProvider();
|
|
2122
2123
|
const extensionRunner = this.session.extensionRunner;
|
|
@@ -3,6 +3,11 @@ interface PauseView {
|
|
|
3
3
|
readonly paused: boolean;
|
|
4
4
|
readonly pausedNoticeCount: number;
|
|
5
5
|
readonly runPaused: boolean;
|
|
6
|
+
readonly pauseView?: {
|
|
7
|
+
readonly records: readonly {
|
|
8
|
+
readonly key: string;
|
|
9
|
+
}[];
|
|
10
|
+
};
|
|
6
11
|
}
|
|
7
12
|
export declare function pauseFooterText(view: PauseView): string | undefined;
|
|
8
13
|
export declare function resumeCommandTarget(view: PauseView): "resume_session" | "session_picker";
|
|
@@ -3,6 +3,8 @@ export function pauseFooterText(view) {
|
|
|
3
3
|
if (!view.paused && !view.runPaused)
|
|
4
4
|
return undefined;
|
|
5
5
|
const held = view.pausedNoticeCount === 1 ? "1 update held" : `${view.pausedNoticeCount} updates held`;
|
|
6
|
+
if (view.pauseView?.records.some((record) => record.key === "interrupted-run"))
|
|
7
|
+
return `Interrupted · ${view.pausedNoticeCount > 0 ? `${held} · ` : ""}type to continue, /resume to resume`;
|
|
6
8
|
if (view.runPaused)
|
|
7
9
|
return `Run paused · ${held} · type to continue, /resume to resume the run`;
|
|
8
10
|
return `Paused · ${held} · type to continue, /resume to resume`;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
23
|
import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
24
24
|
import { join } from "node:path";
|
|
25
|
-
import { processIsLive, processStartTimeMs, readProcessStartTime, sameProcessStartMs, } from "../../utils/process-identity.js";
|
|
25
|
+
import { processIsLive, processIsRunning, processStartTimeMs, readProcessStartTime, sameProcessStartMs, } from "../../utils/process-identity.js";
|
|
26
26
|
import { createHostDaemonPaths, generationPaths, HOST_DAEMON_DIR_ENV, hostDaemonDirectoryPaths, } from "./host-daemon-paths.js";
|
|
27
27
|
import { parseJson, readFileOrUndefined } from "./host-daemon-state.js";
|
|
28
28
|
import { generationHeartbeatStopped } from "./host-stalled-evidence.js";
|
|
@@ -280,7 +280,8 @@ export async function claimOwnerIsLive(owner) {
|
|
|
280
280
|
* different start time. `unproven`: the pid runs but no start time was recorded or none can be read.
|
|
281
281
|
*/
|
|
282
282
|
export async function claimOwnerIdentity(owner) {
|
|
283
|
-
|
|
283
|
+
// An exited, unreaped owner (state Z) makes no more writes: the claim is gone (senpi#4119).
|
|
284
|
+
if (!processIsRunning(owner.pid))
|
|
284
285
|
return "gone";
|
|
285
286
|
const recorded = owner.processStartTime === null ? undefined : processStartTimeMs(owner.processStartTime);
|
|
286
287
|
if (recorded === undefined) {
|
|
@@ -132,7 +132,8 @@ export async function hostChildAlive(generation) {
|
|
|
132
132
|
if (!processIsLive(pid))
|
|
133
133
|
return false;
|
|
134
134
|
const observed = await readProcessIdentity(pid, process.platform, 1_000, processIsLive, "UTC");
|
|
135
|
-
|
|
135
|
+
// `absent` includes an exited, unreaped child (state Z), read in the same async snapshot (senpi#4119).
|
|
136
|
+
if (observed.kind === "absent" || !processIsLive(pid))
|
|
136
137
|
return false;
|
|
137
138
|
const recordedMs = typeof record?.processStartTime === "string" ? processStartTimeMs(record.processStartTime) : undefined;
|
|
138
139
|
const observedMs = observed.kind === "present" ? processStartTimeMs(observed.identity) : undefined;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* own group. A `detached` entry is never swept; the command explicitly opted out of the child's lifetime.
|
|
8
8
|
*/
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
|
-
import {
|
|
10
|
+
import { processIsRunning, processStartTimeMs, readProcessStartTime, sameProcessStartMs, } from "../../../utils/process-identity.js";
|
|
11
11
|
export async function classifyGroup(group, ownGroup, probes) {
|
|
12
12
|
if (group.lifetime === "detached")
|
|
13
13
|
return "detached";
|
|
@@ -70,7 +70,9 @@ function groupHasMembers(pgid) {
|
|
|
70
70
|
}
|
|
71
71
|
export const defaultGroupProbes = {
|
|
72
72
|
groupHasMembers,
|
|
73
|
-
|
|
73
|
+
// An exited, unreaped leader (state Z) has no start identity left to compare, so it must read as exited:
|
|
74
|
+
// its orphaned members keep the group, and the group is swept (senpi#3020, senpi#4119).
|
|
75
|
+
isAlive: processIsRunning,
|
|
74
76
|
startOf: (pid) => readProcessStartTime(pid),
|
|
75
77
|
ownGroup: () => processGroupOf(process.pid),
|
|
76
78
|
signalGroup: (pgid, signal) => {
|
|
@@ -48,6 +48,13 @@ timeZone?: "UTC"): Promise<ProcessIdentityResult>;
|
|
|
48
48
|
export declare function stopValidatedPid(pidFile: DaemonPidFile, signal: NodeJS.Signals): Promise<void>;
|
|
49
49
|
export declare function waitForGone(pidFile: DaemonPidFile, timeoutMs: number): Promise<boolean>;
|
|
50
50
|
export declare function readProcessStartTime(pid: number, platform?: NodeJS.Platform, timeoutMs?: number): Promise<string | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* `processIsLive` minus processes that have exited but are not reaped yet (state Z): a SIGKILLed host
|
|
53
|
+
* stays a zombie until its parent's loop, or init, waits on it, and `kill(pid, 0)` still succeeds for it
|
|
54
|
+
* (senpi#4119). One `ps` snapshot decides; a probe that fails any other way reads as running, so a
|
|
55
|
+
* starved probe on a live process is never taken for a dead one (#1609).
|
|
56
|
+
*/
|
|
57
|
+
export declare function processIsRunning(pid: number): boolean;
|
|
51
58
|
export declare function processIsLive(pid: number): boolean;
|
|
52
59
|
/**
|
|
53
60
|
* Wait for a spawned pid's process identity.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
2
2
|
const WINDOWS_FILETIME_EPOCH = 116444736000000000n;
|
|
3
3
|
export const PROCESS_START_TOLERANCE_MS = 3_000;
|
|
4
4
|
/** FILETIME and C/localized lstart/ISO identities share one millisecond representation. */
|
|
@@ -80,7 +80,7 @@ export class ProcessIdentityUnreadableError extends Error {
|
|
|
80
80
|
* probe is retried within a bounded budget and only then surfaces as
|
|
81
81
|
* ProcessIdentityUnreadableError. It never leaks the raw probe error as a verdict.
|
|
82
82
|
*/
|
|
83
|
-
export async function processMatchesPidFile(pidFile, readStartTime = readProcessStartTime, isLive =
|
|
83
|
+
export async function processMatchesPidFile(pidFile, readStartTime = readProcessStartTime, isLive = processIsRunning, retry = {}) {
|
|
84
84
|
// A record without an identity guard answers only the liveness half of the question: a pid
|
|
85
85
|
// that is gone is gone, and a live one stays unknown so it is never claimed or signalled.
|
|
86
86
|
if (pidFile.processStartTime === null) {
|
|
@@ -129,7 +129,9 @@ timeZone) {
|
|
|
129
129
|
`$process = Get-CimInstance Win32_Process -Filter "ProcessId=${String(pid)}" -ErrorAction Stop; if ($null -eq $process) { Write-Output '__SENPI_ABSENT__'; exit 0 }; $process.CreationDate.ToFileTimeUtc().ToString("D", [Globalization.CultureInfo]::InvariantCulture)`,
|
|
130
130
|
],
|
|
131
131
|
}
|
|
132
|
-
:
|
|
132
|
+
: // State and start time from ONE snapshot: an exited, unreaped process (state Z) keeps its start
|
|
133
|
+
// time but runs nothing, so it has no identity to match (senpi#4119).
|
|
134
|
+
{ executable: "ps", args: ["-o", "stat=,lstart=", "-p", String(pid)] };
|
|
133
135
|
return new Promise((resolve) => {
|
|
134
136
|
const effectiveTimeoutMs = timeoutMs ?? (platform === "win32" ? 1_000 : undefined);
|
|
135
137
|
execFile(command.executable, command.args, {
|
|
@@ -151,9 +153,13 @@ timeZone) {
|
|
|
151
153
|
resolve({ kind: "error", error });
|
|
152
154
|
return;
|
|
153
155
|
}
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
+
const snapshot = stdout.trim();
|
|
157
|
+
if (snapshot === "__SENPI_ABSENT__")
|
|
156
158
|
return resolve({ kind: "absent" });
|
|
159
|
+
const state = platform === "win32" ? "" : (snapshot.split(/\s+/, 1)[0] ?? "");
|
|
160
|
+
if (state.startsWith("Z"))
|
|
161
|
+
return resolve({ kind: "absent" });
|
|
162
|
+
const output = platform === "win32" ? snapshot : snapshot.slice(state.length).trim();
|
|
157
163
|
if (!output || (platform === "win32" && !/^\d+$/.test(output)))
|
|
158
164
|
return resolve({ kind: "error", error: new Error("invalid process identity output") });
|
|
159
165
|
resolve({ kind: "present", identity: timeZone && platform !== "win32" ? `${output} UTC` : output });
|
|
@@ -191,6 +197,27 @@ export async function readProcessStartTime(pid, platform = process.platform, tim
|
|
|
191
197
|
return result.kind === "present" ? result.identity : undefined;
|
|
192
198
|
});
|
|
193
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* `processIsLive` minus processes that have exited but are not reaped yet (state Z): a SIGKILLed host
|
|
202
|
+
* stays a zombie until its parent's loop, or init, waits on it, and `kill(pid, 0)` still succeeds for it
|
|
203
|
+
* (senpi#4119). One `ps` snapshot decides; a probe that fails any other way reads as running, so a
|
|
204
|
+
* starved probe on a live process is never taken for a dead one (#1609).
|
|
205
|
+
*/
|
|
206
|
+
export function processIsRunning(pid) {
|
|
207
|
+
if (!processIsLive(pid))
|
|
208
|
+
return false;
|
|
209
|
+
if (process.platform === "win32")
|
|
210
|
+
return true;
|
|
211
|
+
try {
|
|
212
|
+
const state = execFileSync("ps", ["-o", "stat=", "-p", String(pid)], { encoding: "utf8", timeout: 2_000 });
|
|
213
|
+
return !state.trim().startsWith("Z");
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (error instanceof Error && "status" in error && error.status === 1)
|
|
217
|
+
return false;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
194
221
|
export function processIsLive(pid) {
|
|
195
222
|
try {
|
|
196
223
|
process.kill(pid, 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.25-2",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@earendil-works/chord": "0.99.1",
|
|
74
|
-
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.
|
|
75
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
76
|
-
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.
|
|
74
|
+
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.25-2",
|
|
75
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.25-2",
|
|
76
|
+
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.25-2",
|
|
77
77
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
78
78
|
"chalk": "6.0.0",
|
|
79
79
|
"cross-spawn": "7.0.6",
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
"yaml": "2.9.1",
|
|
93
93
|
"@anthropic-ai/claude-agent-sdk": "0.3.306",
|
|
94
94
|
"@anthropic-ai/sdk": "0.127.0",
|
|
95
|
-
"@code-yeongyu/senpi-codemode": "2026.10.
|
|
96
|
-
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.
|
|
95
|
+
"@code-yeongyu/senpi-codemode": "2026.10.25-2",
|
|
96
|
+
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.25-2",
|
|
97
97
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
98
98
|
"@mozilla/readability": "0.6.0",
|
|
99
99
|
"linkedom": "0.18.13",
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
|
|
2
|
-
import{execFile}from"node:child_process";var WINDOWS_FILETIME_EPOCH=116444736000000000n,PROCESS_START_TOLERANCE_MS=3e3;function processStartTimeMs(identity){if(/^\d+$/.test(identity)){let parsed2=Number((BigInt(identity)-WINDOWS_FILETIME_EPOCH)/10000n);return Number.isFinite(parsed2)?parsed2:void 0}let localIdentity=identity.trim(),korean=/^(\d{4})\uB144\s+(\d{1,2})\uC6D4\s+(\d{1,2})\uC77C\s+[\uC6D4\uD654\uC218\uBAA9\uAE08\uD1A0\uC77C]\uC694\uC77C\s+(\d{1,2})\uC2DC\s+(\d{1,2})\uBD84\s+(\d{1,2})\uCD08$/.exec(localIdentity),japanese=/^[\u65E5\u6708\u706B\u6C34\u6728\u91D1\u571F](?:\u66DC\u65E5)?\s+(\d{1,2})\/\s*(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(localIdentity),parts=korean?korean.slice(1):japanese?[japanese[6],japanese[1],japanese[2],japanese[3],japanese[4],japanese[5]]:void 0;if(parts===void 0){let parsed2=Date.parse(identity);return Number.isFinite(parsed2)?parsed2:void 0}let[year=NaN,month=NaN,day=NaN,hour=NaN,minute=NaN,second=NaN]=parts.map(Number),parsed=new Date(year,month-1,day,hour,minute,second,0);return parsed.getFullYear()===year&&parsed.getMonth()===month-1&&parsed.getDate()===day&&parsed.getHours()===hour&&parsed.getMinutes()===minute&&parsed.getSeconds()===second?parsed.getTime():void 0}function sameProcessStartMs(recorded,observed){return recorded!==void 0&&observed!==void 0&&Number.isFinite(recorded)&&Number.isFinite(observed)&&Math.abs(recorded-observed)<=PROCESS_START_TOLERANCE_MS}function parseDaemonPidFile(text){let parsed;try{parsed=JSON.parse(text)}catch(error){if(error instanceof SyntaxError)return;throw error}let unguarded=parsed!==null&&isRecord(parsed)&&parsed.processStartTime===null;if(!(!isRecord(parsed)||typeof parsed.pid!="number"||!unguarded&&typeof parsed.processStartTime!="string")&&!(!Number.isInteger(parsed.pid)||parsed.pid<=0)){if(unguarded)return{pid:parsed.pid,processStartTime:null};if(!(typeof parsed.processStartTime!="string"||parsed.processStartTime.trim()===""))return{pid:parsed.pid,processStartTime:parsed.processStartTime}}}var ProcessIdentityUnreadableError=class extends Error{constructor(pid,attempts,cause){super(`process identity for live pid ${pid} stayed unreadable after ${attempts} probe attempt(s): ${cause instanceof Error?cause.message:String(cause)}`),this.name="ProcessIdentityUnreadableError",this.pid=pid,this.attempts=attempts,this.cause=cause}};async function processMatchesPidFile(pidFile,readStartTime=readProcessStartTime,isLive=processIsLive,retry={}){if(pidFile.processStartTime===null){if(!isLive(pidFile.pid))return!1;throw new ProcessIdentityUnreadableError(pidFile.pid,0,new Error("pidfile carries no process identity guard"))}let attempts=Math.max(1,retry.attempts??5),delayMs=retry.delayMs??200,lastError;for(let attempt=1;attempt<=attempts;attempt++)try{let current=await readStartTime(pidFile.pid);if(current!==void 0){let recorded=processStartTimeMs(pidFile.processStartTime),observed=processStartTimeMs(current);if(recorded!==void 0&&observed!==void 0)return sameProcessStartMs(recorded,observed)}if(!isLive(pidFile.pid))return!1;lastError=new Error("process identity probe returned no usable identity for a live process"),attempt<attempts&&await new Promise(resolve=>setTimeout(resolve,delayMs))}catch(error){if(!isLive(pidFile.pid))return!1;lastError=error,attempt<attempts&&await new Promise(resolve=>setTimeout(resolve,delayMs))}throw new ProcessIdentityUnreadableError(pidFile.pid,attempts,lastError)}async function readProcessIdentity(pid,platform=process.platform,timeoutMs,isLive=processIsLive,timeZone){let command=platform==="win32"?{executable:"powershell.exe",args:["-NoProfile","-NonInteractive","-Command",`$process = Get-CimInstance Win32_Process -Filter "ProcessId=${String(pid)}" -ErrorAction Stop; if ($null -eq $process) { Write-Output '__SENPI_ABSENT__'; exit 0 }; $process.CreationDate.ToFileTimeUtc().ToString("D", [Globalization.CultureInfo]::InvariantCulture)`]}:{executable:"ps",args:["-o","lstart=","-p",String(pid)]};return new Promise(resolve=>{let effectiveTimeoutMs=timeoutMs??(platform==="win32"?1e3:void 0);execFile(command.executable,command.args,{windowsHide:!0,...effectiveTimeoutMs===void 0?{}:{timeout:effectiveTimeoutMs},...platform==="win32"&&timeZone===void 0?{}:{env:{...process.env,LC_ALL:"C",LANG:"C",...timeZone?{TZ:timeZone}:{}}}},(error,stdout)=>{if(error){let code="code"in error?error.code:void 0;if(platform!=="win32"&&code===1||!isLive(pid)){resolve({kind:"absent"});return}resolve({kind:"error",error});return}let output=stdout.trim();if(output==="__SENPI_ABSENT__")return resolve({kind:"absent"});if(!output||platform==="win32"&&!/^\d+$/.test(output))return resolve({kind:"error",error:new Error("invalid process identity output")});resolve({kind:"present",identity:timeZone&&platform!=="win32"?`${output} UTC`:output})}).once("error",error=>resolve({kind:"error",error}))})}async function readProcessStartTime(pid,platform=process.platform,timeoutMs){return readProcessIdentity(pid,platform,timeoutMs).then(result=>{if(result.kind==="error")throw result.error;return result.kind==="present"?result.identity:void 0})}function processIsLive(pid){try{return process.kill(pid,0),!0}catch(error){if(isNodeErrorCode(error,"ESRCH"))return!1;if(isNodeErrorCode(error,"EPERM"))return!0;throw error}}async function waitForStartTime(pid,timeoutMs,readStartTime=readProcessStartTime,isLive=processIsLive){let deadline=Date.now()+timeoutMs;for(;Date.now()<=deadline;){let startTime;try{startTime=await readStartTime(pid)}catch{}if(startTime)return startTime;await delay(20)}if(!isLive(pid))throw new Error(`spawned daemon pid ${pid} had no process start time`)}function delay(ms){return new Promise(resolveDelay=>setTimeout(resolveDelay,ms))}function isRecord(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function isNodeErrorCode(error,code){return error instanceof Error&&"code"in error&&error.code===code}export{PROCESS_START_TOLERANCE_MS,processStartTimeMs,sameProcessStartMs,parseDaemonPidFile,ProcessIdentityUnreadableError,processMatchesPidFile,readProcessIdentity,readProcessStartTime,processIsLive,waitForStartTime};
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
|
|
2
|
-
import{HOST_DAEMON_DIR_ENV,HOST_DAEMON_LAYOUT,ageOf,createGenerationDirectory,createHostDaemonPaths,generationPaths,hostDaemonDirectoryPaths,isRecord,parseJson,readFileOrUndefined,readJsonObject,sameEndpoint,writeJsonAtomic,writeStateFile}from"./chunk-RHHBXIRN.js";import{HOST_INSTANCE_ID_ENV}from"./chunk-RVYSJBZ7.js";import{ProcessIdentityUnreadableError,parseDaemonPidFile,processIsLive,processMatchesPidFile,processStartTimeMs,readProcessIdentity,readProcessStartTime,sameProcessStartMs}from"./chunk-7ASLQ243.js";var HOST_COLD_START_ENV="SENPI_RPC_HOST_COLD_START",HOST_IDLE_EXIT_MS_ENV="SENPI_RPC_HOST_IDLE_EXIT_MS",DEFAULT_HOST_IDLE_EXIT_MS=9e5,HANDOFF_GRACE_MS_ENV="SENPI_RPC_HANDOFF_GRACE_MS",DEFAULT_HANDOFF_GRACE_MS=6e5;function parseColdStart(value){return value==="transient"||value==="persistent"?value:void 0}function parseIdleExitMs(value){if(value===void 0||!/^\d+$/.test(value.trim()))return;let parsed=Number(value.trim());return Number.isFinite(parsed)&&parsed>0?parsed:void 0}function resolveHostPolicy(settings,env){let record=isRecord2(settings)?settings:{},coldStart=parseColdStart(env[HOST_COLD_START_ENV])??parseColdStart(asOptionalString(record.coldStart))??"transient",idleExitMs=parseIdleExitMs(env[HOST_IDLE_EXIT_MS_ENV])??parseIdleExitMs(asOptionalString(record.idleExitMs))??9e5;return{coldStart,idleExitMs}}var IdleExitDecider=class{constructor(idleExitMs,now=Date.now){this.idleExitMs=idleExitMs,this.now=now}update(activity){return activity.connections>0||activity.activeTurns>0?(this.idleSince=void 0,"active"):this.idleExitMs===Number.POSITIVE_INFINITY?"idle":this.idleSince===void 0?(this.idleSince=this.now(),"idle"):this.now()-this.idleSince>=this.idleExitMs?"exit":"idle"}};function isRecord2(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function asOptionalString(value){return typeof value=="string"?value:typeof value=="number"?String(value):void 0}import{writeSync}from"node:fs";function writeStderrLine(text){try{writeSync(2,`${text}
|
|
3
|
-
`)}catch{}}function supervisorLog(message){writeStderrLine(`senpi rpc host supervisor: ${message}`)}function writeHostReady(socket,coldStart,idleExitMs){writeStderrLine(`senpi rpc host ready on unix://${socket} (coldStart=${coldStart}, idleExitMs=${coldStart==="persistent"?"never":String(idleExitMs)})`)}var loggedUnknownIdentities=new Set;function logUnknownHostIdentity(record,pid){let key=`${record}:${pid}`;loggedUnknownIdentities.has(key)||(loggedUnknownIdentities.add(key),writeStderrLine(JSON.stringify({event:"legacy_host_identity_unknown",record,pid})))}function errorMessage(cause){return cause instanceof Error?cause.message:String(cause)}import{createHash}from"node:crypto";import{mkdir,readdir,readFile,rename,rm as rm2,writeFile}from"node:fs/promises";import{join as join2}from"node:path";import{rm,stat}from"node:fs/promises";import{isAbsolute,join}from"node:path";var STALL_REFUSAL_WINDOW_MS_ENV="SENPI_RPC_STALL_REFUSAL_MS",DEFAULT_STALL_REFUSAL_WINDOW_MS=12e4,STOP_STALL_EVIDENCE_MAX_AGE_MS=6e4,CHILD_STALLED_STOP_MAX_MS_ENV="SENPI_RPC_CHILD_STALLED_STOP_MAX_MS",DEFAULT_CHILD_STALLED_STOP_MAX_MS=6e4;function childStalledStopMaxMs(env=process.env){return parseIdleExitMs(env[CHILD_STALLED_STOP_MAX_MS_ENV])??DEFAULT_CHILD_STALLED_STOP_MAX_MS}function ownGenerationDir(env){let daemonDir=env[HOST_DAEMON_DIR_ENV],instanceId=env[HOST_INSTANCE_ID_ENV];if(!(!daemonDir||!instanceId))return join(daemonDir,"generations",instanceId)}function stallRefusalWindowMs(env=process.env){return parseIdleExitMs(env[STALL_REFUSAL_WINDOW_MS_ENV])??DEFAULT_STALL_REFUSAL_WINDOW_MS}function writeStallEvidence(generationDir,evidence){return writeJsonAtomic(join(generationDir,"host-stalled.json"),evidence)}function writeHeartbeat(generationDir,at){return writeJsonAtomic(join(generationDir,"host-alive.json"),{at})}async function readStallEvidence(generation){let raw=await readJsonObject(generation.stalledFile);if(!(raw===void 0||typeof raw.at!="string"||typeof raw.driftMs!="number"))return{at:raw.at,driftMs:raw.driftMs,processCpuMs:typeof raw.processCpuMs=="number"?raw.processCpuMs:0,heapDeltaMb:typeof raw.heapDeltaMb=="number"?raw.heapDeltaMb:0,...typeof raw.sessionId=="string"?{sessionId:raw.sessionId}:{},...typeof raw.tool=="string"?{tool:raw.tool}:{}}}async function recentStall(generation,now,maxAgeMs){let evidence=await readStallEvidence(generation),age=ageOf(evidence?.at,now);return evidence!==void 0&&age!==void 0&&age<=maxAgeMs?evidence:void 0}async function hostLoopStalled(generation,options){if(await recentStall(generation,options.now,options.windowMs)!==void 0)return!0;let heartbeatAge=ageOf((await readJsonObject(generation.aliveFile))?.at,options.now);return heartbeatAge!==void 0&&heartbeatAge>options.errorMs&&heartbeatAge<options.windowMs}async function generationHeartbeatStopped(generation,now=Date.now()){let age=ageOf((await readJsonObject(generation.aliveFile))?.at,now);return age!==void 0&&age>=stallRefusalWindowMs()}var SAFE_INSTANCE_ID=/^[A-Za-z0-9_-]+$/;async function exists(path){return stat(path).then(()=>!0,()=>!1)}async function holderGenerationEnded(holder,env=process.env){let instance=holder.host_instance;if(instance===void 0||!SAFE_INSTANCE_ID.test(instance)||instance===env[HOST_INSTANCE_ID_ENV])return!1;let daemonDir=holder.host_daemon_dir??env[HOST_DAEMON_DIR_ENV];if(daemonDir===void 0||!isAbsolute(daemonDir))return!1;let directory=hostDaemonDirectoryPaths(daemonDir),generation=generationPaths(directory,instance);return await generationHeartbeatStopped(generation)?!0:holder.host_daemon_dir!==void 0&&await exists(directory.endpointFile)&&!await exists(generation.dir)}async function hostChildAlive(generation){let record=await readJsonObject(generation.childPidFile),pid=record?.pid;if(typeof pid!="number"||!Number.isInteger(pid)||pid<=0)return;if(!processIsLive(pid))return!1;let observed=await readProcessIdentity(pid,process.platform,1e3,processIsLive,"UTC");if(!processIsLive(pid))return!1;let recordedMs=typeof record?.processStartTime=="string"?processStartTimeMs(record.processStartTime):void 0,observedMs=observed.kind==="present"?processStartTimeMs(observed.identity):void 0;return recordedMs!==void 0&&observedMs!==void 0?sameProcessStartMs(recordedMs,observedMs):await generationHeartbeatStopped(generation)?(supervisorLog(`host child pid ${pid} start time unreadable and its heartbeat stopped; releasing generation`),!1):(supervisorLog(`host child pid ${pid} start time unreadable; preserving generation`),!0)}function writeStopProgress(generation,progress){return writeJsonAtomic(generation.stopProgressFile,progress)}async function clearStopProgress(generation){await rm(generation.stopProgressFile,{force:!0}).catch(()=>{})}async function activeStopProgress(generation,now,freshMs){let raw=await readJsonObject(generation.stopProgressFile),age=ageOf(raw?.at,now),untilAt=typeof raw?.untilAt=="string"?Date.parse(raw.untilAt):Number.NaN;if(!(age===void 0||age>freshMs||Number.isNaN(untilAt)||untilAt<=now))return untilAt}var SESSION_PATH_RETRY_AFTER_MS=2e3;function reservationFile(dir,sessionPath){return join2(dir,`${createHash("sha256").update(sessionPath,"utf8").digest("hex").slice(0,16)}.json`)}function createEndpointReservations(host){let told=process.env[HOST_DAEMON_DIR_ENV],dir=told!==void 0&&told.trim()!==""?told:host.socket===void 0?void 0:createHostDaemonPaths({socket:host.socket,agentDir:host.agentDir}).dir;if(dir!==void 0)return createSessionPathReservations({daemonDir:dir,instanceId:host.instanceId,...host.publicSocket===void 0?{}:{publicSocket:host.publicSocket},...host.onFailure?{onFailure:host.onFailure}:{}})}function createSessionPathReservations(options){let paths=hostDaemonDirectoryPaths(options.daemonDir),dir=paths.reservationsDir,pid=options.pid??process.pid,startTime=readProcessStartTime(pid).then(value=>value??null,()=>null),held=new Set,updates=new Map,report=message=>options.onFailure?.(message),publish=async(sessionPath,attached,stillHeld)=>{let owner={instanceId:options.instanceId,...options.publicSocket===void 0?{}:{host_socket:options.publicSocket},pid,processStartTime:await startTime,sessionPath,attached},file=reservationFile(dir,sessionPath);await mkdir(dir,{recursive:!0,mode:448});let staging=`${file}.${pid}.tmp`;if(await writeFile(staging,`${JSON.stringify(owner)}
|
|
4
|
-
`,{mode:384}),stillHeld?.()===!1){await rm2(staging,{force:!0});return}await rename(staging,file)};return{async holderPids(observedStarts){let candidates=(await readSessionPathClaims(dir)).map(({owner})=>owner),generations=await readdir(paths.generationsDir,{withFileTypes:!0}).catch(()=>[]);for(let generation of generations){if(!generation.isDirectory())continue;let record=parseJson(await readFileOrUndefined(join2(paths.generationsDir,generation.name,"host-child.pid")));typeof record?.pid=="number"&&candidates.push({pid:record.pid,processStartTime:typeof record.processStartTime=="string"?record.processStartTime:null,instanceId:generation.name,sessionPath:""})}let byPid=new Map;for(let owner of candidates){if(owner.pid===process.pid)continue;let owners=byPid.get(owner.pid)??[];owners.push(owner),byPid.set(owner.pid,owners)}let live=await Promise.all([...byPid].map(async([pid2,owners])=>{if(!processIsLive(pid2))return;if(observedStarts){if(!observedStarts.has(pid2))return;let observed2=observedStarts.get(pid2);return owners.some(owner=>owner.processStartTime===null?!1:sameProcessStartMs(processStartTimeMs(owner.processStartTime),observed2))?pid2:void 0}let current=await readProcessStartTime(pid2,process.platform,1e3).catch(()=>{}),observed=current===void 0?void 0:processStartTimeMs(current);return observed!==void 0&&owners.some(owner=>owner.processStartTime!==null&&sameProcessStartMs(processStartTimeMs(owner.processStartTime),observed))?pid2:void 0}));return[...new Set(live.filter(value=>value!==void 0))]},async claim(sessionPath,attached=!0){let existing=await readOwner(reservationFile(dir,sessionPath));if(existing&&existing.pid!==pid){let verdict=await standingOwner(existing,paths);if(verdict.kind==="stands")return verdict.owner;if(!(verdict.kind==="released"||await import("./session-write-fence-66EMOO4Y.js").then(({advanceSessionFence})=>advanceSessionFence(sessionPath)).then(()=>!0,cause=>(report(`session file ${sessionPath} could not be fenced for a takeover (${errorMessage2(cause)})`),!1))))return{...existing,current:!1}}try{await publish(sessionPath,attached),held.add(sessionPath)}catch(cause){report(`session path reservation for ${sessionPath} could not be written (${errorMessage2(cause)})`)}},async release(sessionPath){held.delete(sessionPath)&&(await updates.get(sessionPath),updates.delete(sessionPath),await rm2(reservationFile(dir,sessionPath),{force:!0}).catch(cause=>{report(`session path reservation for ${sessionPath} could not be removed (${errorMessage2(cause)})`)}))},setAttached(sessionPath,attached){if(!held.has(sessionPath))return;let update=(updates.get(sessionPath)??Promise.resolve()).then(()=>publish(sessionPath,attached,()=>held.has(sessionPath))).catch(cause=>{report(`session path reservation for ${sessionPath} could not be updated (${errorMessage2(cause)})`)});updates.set(sessionPath,update)}}}async function readSessionPathClaims(dir){let entries=await readdir(dir).catch(()=>[]),claims=[];for(let entry of entries){if(!entry.endsWith(".json"))continue;let file=join2(dir,entry),owner=await readOwner(file);owner&&claims.push({file,owner})}return claims}async function standingOwner(owner,paths){let identity=await claimOwnerIdentity(owner);if(identity==="gone")return{kind:"released"};let current=await currentInstanceId(paths.pointerFile)===owner.instanceId;return!current&&identity==="unproven"&&await heartbeatStopped(paths,owner.instanceId)?{kind:"presumed_done"}:current||owner.attached!==!1?{kind:"stands",owner:{...owner,current}}:{kind:"released"}}async function heartbeatStopped(paths,instanceId){return instanceId===""||instanceId==="."||instanceId===".."||/[\\/]/.test(instanceId)?!1:generationHeartbeatStopped(generationPaths(paths,instanceId))}async function currentInstanceId(pointerFile){let pointer=parseJson(await readFileOrUndefined(pointerFile).catch(()=>{}));return typeof pointer?.instance_id=="string"?pointer.instance_id:void 0}async function readOwner(file){let parsed;try{parsed=JSON.parse(await readFile(file,"utf8"))}catch{return}if(typeof parsed!="object"||parsed===null)return;let{instanceId,pid,processStartTime,sessionPath,attached,host_socket}=parsed;if(!(typeof instanceId!="string"||typeof pid!="number"||typeof sessionPath!="string"))return{instanceId,pid,processStartTime:typeof processStartTime=="string"?processStartTime:null,sessionPath,...typeof host_socket=="string"?{host_socket}:{},...typeof attached=="boolean"?{attached}:{}}}async function claimOwnerIsLive(owner){return await claimOwnerIdentity(owner)!=="gone"}async function claimOwnerIdentity(owner){if(!processIsLive(owner.pid))return"gone";let recorded=owner.processStartTime===null?void 0:processStartTimeMs(owner.processStartTime);if(recorded===void 0)return logUnknownHostIdentity("session-path-claim",owner.pid),"unproven";let current=await readProcessStartTime(owner.pid).catch(()=>{}),observed=current===void 0?void 0:processStartTimeMs(current);return observed===void 0?"unproven":sameProcessStartMs(recorded,observed)?"proven":"gone"}function errorMessage2(cause){return cause instanceof Error?cause.message:String(cause)}import{rename as rename3,rm as rm4}from"node:fs/promises";import{randomUUID}from"node:crypto";import{link,readdir as readdir3,rename as rename2,rm as rm3}from"node:fs/promises";import{readdir as readdir2}from"node:fs/promises";import{readdirSync,readFileSync}from"node:fs";function supportedPlatform(platform){return platform==="darwin"||platform==="linux"?platform:void 0}async function loadProcessTableReader(platform=process.platform,options={}){let supported=supportedPlatform(platform);return supported===void 0||typeof globalThis.Bun>"u"?void 0:supported==="darwin"?await darwinReader(options):linuxReader}var KINFO_PROC_STRIDE=648,KINFO_PID_OFFSET=40,KINFO_PPID_OFFSET=560,KINFO_STAT_OFFSET=36,SZOMB=5,PROC_PIDTASKINFO=4,TASKINFO_RESIDENT_OFFSET=8,TABLE_START_BYTES=1<<20,TABLE_MAX_BYTES=32<<20;function parseKernelProcessTable(table,rowCount,residentKb){if(rowCount<=0||table.byteLength<rowCount*KINFO_PROC_STRIDE)return;let view=new DataView(table.buffer,table.byteOffset,table.byteLength),rows=[],selfSeen=!1;for(let index=0;index<rowCount;index++){let base=index*KINFO_PROC_STRIDE,pid=view.getUint32(base+KINFO_PID_OFFSET,!0);if(!Number.isInteger(pid)||pid<=0)continue;let ppid=view.getUint32(base+KINFO_PPID_OFFSET,!0),zombie=table[base+KINFO_STAT_OFFSET]===SZOMB;if(pid===process.pid){if(ppid!==process.ppid||zombie)return;selfSeen=!0}rows.push({pid,ppid,state:zombie?"Z":"U",rssKb:residentKb(pid)})}return selfSeen?rows:void 0}async function darwinReader(options){let{dlopen,FFIType,ptr}=await import("bun:ffi"),library=dlopen("libSystem.B.dylib",{sysctl:{args:[FFIType.ptr,FFIType.u32,FFIType.ptr,FFIType.ptr,FFIType.ptr,FFIType.i32],returns:FFIType.i32},proc_pidinfo:{args:[FFIType.i32,FFIType.i32,FFIType.u32,FFIType.ptr,FFIType.i32],returns:FFIType.i32}}),mib=new Int32Array([1,14,0]),none=new Uint8Array(1),requested=options.initialTableBytes,table=new Uint8Array(Number.isFinite(requested)&&requested!==void 0&&requested>0?Math.min(Math.floor(requested),TABLE_MAX_BYTES):TABLE_START_BYTES),length=new Uint32Array(1),taskInfo=new Uint8Array(256),taskView=new DataView(taskInfo.buffer),residentKb=pid=>(taskInfo.fill(0),library.symbols.proc_pidinfo(pid,PROC_PIDTASKINFO,0,ptr(taskInfo),taskInfo.byteLength)>0?Math.round(Number(taskView.getBigUint64(TASKINFO_RESIDENT_OFFSET,!0))/1024):0);return()=>{for(;;){if(length[0]=table.byteLength,library.symbols.sysctl(ptr(mib),mib.length,ptr(table),ptr(length),ptr(none),0)!==0){if(table.byteLength>=TABLE_MAX_BYTES)return;let reported=length[0]>table.byteLength?length[0]:0;table=new Uint8Array(Math.min(TABLE_MAX_BYTES,Math.max(table.byteLength*2,reported)));continue}return parseKernelProcessTable(table,Math.floor(length[0]/KINFO_PROC_STRIDE),residentKb)}}}function linuxReader(){let rows=[];for(let entry of readdirSync("/proc")){let pid=Number(entry);if(!Number.isInteger(pid)||pid<=0)continue;let stat3;try{stat3=readFileSync(`/proc/${pid}/stat`,"utf8")}catch{continue}let tail=stat3.slice(stat3.lastIndexOf(")")+2).split(" "),state=tail[0],ppid=Number(tail[1]);if(state===void 0||!Number.isInteger(ppid))continue;let rssPages=Number(tail[21]);rows.push({pid,ppid,state,rssKb:Number.isFinite(rssPages)?Math.round(rssPages*4):0})}return rows}var UNOBSERVED={rss_mb:null,host_rss_mb:null,host_footprint_mb:null,open_fds:null,zombies:null},readers=new Map,footprintReader;function loadFootprintReader(){return footprintReader??=import("./process-footprint-SOBYQLMN.js").then(module=>module.readProcessFootprint),footprintReader}function tableReader(platform){let cached=readers.get(platform);if(cached!==void 0)return cached;let loading=loadProcessTableReader(platform);return readers.set(platform,loading),loading}async function readHostProcessMetrics(pid,platform=process.platform){if(platform==="win32")return UNOBSERVED;let read=await tableReader(platform);if(read===void 0)return UNOBSERVED;let rows=read();if(rows===void 0)return UNOBSERVED;let tree=descendants(rows,pid);if(tree.length===0)return UNOBSERVED;let hostRows=tree.filter(row=>row.pid===pid||row.ppid===pid);return{rss_mb:residentMegabytes(tree),host_rss_mb:residentMegabytes(hostRows),host_footprint_mb:footprintMegabytes(hostRows,await loadFootprintReader()),open_fds:platform==="linux"?await openDescriptors(tree):null,zombies:tree.filter(row=>row.state.startsWith("Z")).length}}function residentMegabytes(rows){return Math.round(rows.reduce((total,row)=>total+row.rssKb,0)/KILOBYTES_PER_MEGABYTE)}function footprintMegabytes(rows,readProcessFootprint){let bytes=0;for(let row of rows){let reading=readProcessFootprint(row.pid);if(reading===void 0)return null;bytes+=reading.bytes}return Math.round(bytes/BYTES_PER_MEGABYTE)}var BYTES_PER_MEGABYTE=1024*1024;function descendants(rows,root){let tree=rows.filter(row=>row.pid===root);if(tree.length===0)return tree;for(let index=0;index<tree.length;index++){let parent=tree[index].pid;for(let row of rows)row.ppid===parent&&!tree.includes(row)&&tree.push(row)}return tree}var KILOBYTES_PER_MEGABYTE=1024;async function openDescriptors(tree){let total=0,counted=!1;for(let row of tree){let entries=await readdir2(`/proc/${row.pid}/fd`).catch(()=>{});entries!==void 0&&(counted=!0,total+=entries.length)}return counted?total:null}async function pruneDeadGenerations(paths,options={}){let pointerText=await readFileOrUndefined(paths.pointerFile).catch(()=>{}),pointer=parseJson(pointerText),pointedAt=typeof pointer?.instance_id=="string"?pointer.instance_id:void 0,generations=[];for(let instanceId of await readdir3(paths.generationsDir).catch(()=>[])){let record=parseJson(await readFileOrUndefined(generationPaths(paths,instanceId).pidFile));typeof record?.pid!="number"||processIsLive(record.pid)||await hostChildAlive(generationPaths(paths,instanceId))||generations.push(instanceId)}let claims=await endClaimsOfEndedGenerations(paths,new Set(generations));for(let instanceId of generations)await rm3(generationPaths(paths,instanceId).dir,{recursive:!0,force:!0}).catch(()=>{});let pointerNamesDead=pointerText!==void 0&&pointedAt!==void 0&&generations.includes(pointedAt);pointerNamesDead&&await options._test?.beforePointerRemoval?.();let pointerRemoved=pointerNamesDead&&await removePointerIfUnchanged(paths,pointerText);return{generations,claims,pointer:pointerRemoved}}async function removePointerIfUnchanged(paths,read){let parked=`${paths.pointerFile}.prune-${process.pid}-${randomUUID()}.tmp`;if(!await rename2(paths.pointerFile,parked).then(()=>!0,()=>!1))return!1;let unchanged=await readFileOrUndefined(parked).catch(()=>{})===read;return unchanged||await link(parked,paths.pointerFile).catch(()=>{}),await rm3(parked,{force:!0}).catch(()=>{}),unchanged}async function endClaimsOfEndedGenerations(paths,ended){let claims=[];for(let claim of await readSessionPathClaims(paths.reservationsDir))await claimEnded(claim.owner,ended)&&(await rm3(claim.file,{force:!0}).catch(()=>{}),claims.push(claim.owner.sessionPath));return claims}async function claimEnded(owner,ended){let identity=await claimOwnerIdentity(owner);return identity==="gone"?!0:identity==="proven"||!ended.has(owner.instanceId)?!1:import("./session-write-fence-66EMOO4Y.js").then(({advanceSessionFence})=>advanceSessionFence(owner.sessionPath)).then(()=>!0,()=>!1)}function memoryOf(metrics){return{rss_mb:metrics.rss_mb,host_rss_mb:metrics.host_rss_mb}}async function readGenerationRows(paths,options={}){let pointer=parseJson(await readFileOrUndefined(paths.pointerFile).catch(()=>{})),currentId=typeof pointer?.instance_id=="string"?pointer.instance_id:void 0,claims=await readSessionPathClaims(paths.reservationsDir),rows=[];for(let instanceId of await readdir3(paths.generationsDir).catch(()=>[])){let record=parseJson(await readFileOrUndefined(generationPaths(paths,instanceId).pidFile));if(typeof record?.pid!="number")continue;let alive=processIsLive(record.pid)||await hostChildAlive(generationPaths(paths,instanceId))===!0;!alive&&options.includeDead!==!0||rows.push({instanceId,generation:typeof record.generation=="number"?record.generation:0,pid:record.pid,engineVersion:typeof record.engineVersion=="string"?record.engineVersion:null,...alive?memoryOf(await readHostProcessMetrics(record.pid)):{rss_mb:null,host_rss_mb:null},sessions:claims.filter(claim=>claim.owner.instanceId===instanceId).length,current:instanceId===currentId,alive})}return rows.sort((left,right)=>left.generation-right.generation)}async function readHostRegistration(paths){let pointer=parseJson(await readFileOrUndefined(paths.pointerFile));if(!(!pointer||typeof pointer.instance_id!="string"))return readGenerationRegistration(paths,pointer.instance_id)}async function readGenerationRegistration(paths,instanceId){let text=await readFileOrUndefined(generationPaths(paths,instanceId).pidFile),record=text===void 0?void 0:parseDaemonPidFile(text);if(record===void 0)return;let parsed=parseJson(text)??{},writer=parseWriter(parsed);return{record,...writer&&{writer},...typeof parsed.socket=="string"&&{socket:parsed.socket},instanceId,generation:typeof parsed.generation=="number"?parsed.generation:0}}async function writeHostRegistration(paths,registration,options={}){options.fresh!==!0&&await pruneDeadGenerations(paths);let{generation,writer}=await writeGenerationRecord(paths,registration);await writeStateFile(`${paths.pointerFile}.${process.pid}.tmp`,{layout:HOST_DAEMON_LAYOUT,instance_id:registration.instanceId,generation_dir:generation.relativeDir,writer}),await rename3(`${paths.pointerFile}.${process.pid}.tmp`,paths.pointerFile)}async function writeGenerationRecord(paths,registration){let generation=generationPaths(paths,registration.instanceId),writer={pid:process.pid,startTime:await thisProcessStartTime()},{build}=registration;return await createGenerationDirectory(generation),await writeStateFile(generation.pidFile,{...registration.record,instance_id:registration.instanceId,generation:registration.generation,...build&&{engineVersion:build.text,engineOrdinal:build.ordinal},launchProfileId:registration.launchProfileId,socket:registration.socket,writer}),{generation,writer}}async function clearHostRegistration(paths){let pointer=parseJson(await readFileOrUndefined(paths.pointerFile));if(typeof pointer?.instance_id=="string"){if(await hostChildAlive(generationPaths(paths,pointer.instance_id)))throw new Error(`refusing to clear generation ${pointer.instance_id} with a live host child`);await endClaimsOfEndedGenerations(paths,new Set([pointer.instance_id])),await rm4(generationPaths(paths,pointer.instance_id).dir,{recursive:!0,force:!0})}await rm4(paths.settingsFile,{force:!0}),await rm4(paths.pointerFile,{force:!0})}async function releaseGeneration(paths,owner){let generation=generationPaths(paths,owner.instanceId);if(await hostChildAlive(generation))return;let record=parseDaemonPidFile(await readFileOrUndefined(generation.pidFile)??"");if(record!==void 0&&record.pid!==owner.pid)return;if(owner.superseded===!0){await rm4(generation.dir,{recursive:!0,force:!0});return}let ownsPointer=parseJson(await readFileOrUndefined(paths.pointerFile))?.instance_id===owner.instanceId;ownsPointer&&await rm4(paths.settingsFile,{force:!0}),await rm4(generation.dir,{recursive:!0,force:!0}),ownsPointer&&await rm4(paths.pointerFile,{force:!0})}async function provenOwner(registered,socket){let record=registered?.record;if(!record||registered?.socket!==void 0&&!sameEndpoint(registered.socket,socket))return;if(record.processStartTime===null){processIsLive(record.pid)&&logUnknownHostIdentity("host.pid",record.pid);return}let identity={pid:record.pid,processStartTime:record.processStartTime};return await processMatchesPidFile(identity,readProcessStartTime).catch(error=>(error instanceof ProcessIdentityUnreadableError&&logUnknownHostIdentity("host.pid",identity.pid),!1))?{...identity,instanceId:registered.instanceId}:void 0}async function writtenByThisProcess(writer,readStartTime){if(writer===void 0||writer.pid!==process.pid||writer.startTime===null)return!1;let startTime=readStartTime?await readStartTime(process.pid).then(value=>value??null,()=>null):await thisProcessStartTime();return sameProcessStartMs(processStartTimeMs(writer.startTime),startTime===null?void 0:processStartTimeMs(startTime))}var selfStartTime;function thisProcessStartTime(){return selfStartTime??=readProcessStartTime(process.pid).then(value=>value??null,()=>null),selfStartTime}function isGenerationId(instanceId){return/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(instanceId)&&!instanceId.includes("..")}function parseWriter(parsed){if(!isRecord(parsed.writer)||typeof parsed.writer.pid!="number")return;let{pid,startTime}=parsed.writer;return{pid,startTime:typeof startTime=="string"?startTime:null}}import{randomUUID as randomUUID2}from"node:crypto";import{watch}from"node:fs";import{readFile as readFile2,rm as rm5}from"node:fs/promises";import{basename,dirname}from"node:path";var STOP_INTENT_MAX_AGE_MS=12e4,CHILD_STOP_TIMEOUT_MS=5e3,CHILD_KILL_EXIT_TIMEOUT_MS=3e4,SENDER_KINDS=new Set(["ensure","supervisor","stop","successor","handoff"]);async function writeStopIntent(generation,intent,log=message=>{process.stderr.write(`${message}
|
|
5
|
-
`)}){try{await writeJsonAtomic(generation.stopIntentFile,intent)}catch(cause){log(`could not record the stop intent for ${generation.dir}: ${cause instanceof Error?cause.message:cause}`)}}async function readStopIntent(generation,options={}){let raw=await readJsonObject(generation.stopIntentFile);if(raw===void 0)return;let intent=parseStopIntent(raw),age=ageOf(raw.at,options.now??Date.now());if(intent!==void 0&&age!==void 0&&age<=(options.maxAgeMs??STOP_INTENT_MAX_AGE_MS))return intent;await clearStopIntent(generation)}async function consumeStopIntent(generation,now=Date.now()){let intent=await readStopIntent(generation,{now});return intent!==void 0&&await clearStopIntent(generation),intent}async function clearStopIntent(generation){await rm5(generation.stopIntentFile,{force:!0}).catch(()=>{})}function layeredSupervisorIntent(outer,supervisor,stop){return outer===void 0?{sender:supervisor,signal:"SIGTERM",...stop}:{sender:outer.sender,chain:[...outer.chain??[outer.sender],supervisor],targetPid:stop.targetPid,reason:`${outer.reason} -> ${stop.reason}`,signal:"SIGTERM",at:stop.at}}function parseStopIntent(raw){let sender=parseSender(raw.sender),chain=Array.isArray(raw.chain)?raw.chain.map(parseSender):void 0;if(!(sender===void 0||chain?.some(entry=>entry===void 0))&&!(typeof raw.reason!="string"||typeof raw.at!="string"))return{sender,...chain?{chain:chain.filter(entry=>entry!==void 0)}:{},targetPid:typeof raw.targetPid=="number"?raw.targetPid:0,reason:raw.reason,signal:typeof raw.signal=="string"?raw.signal:"SIGTERM",at:raw.at}}function parseSender(value){if(typeof value!="object"||value===null)return;let pid=Reflect.get(value,"pid"),kind=Reflect.get(value,"kind"),generation=Reflect.get(value,"generation");if(!(typeof pid!="number"||typeof kind!="string"||!isSenderKind(kind)))return{pid,kind,...typeof generation=="string"?{generation}:{}}}function isSenderKind(kind){return SENDER_KINDS.has(kind)}var DRAIN_REQUEST_POLL_MS=1e3,DRAIN_REQUEST_WATCHED_POLL_MS=5e3;async function writeDrainRequest(file,request){let record={...request,at:new Date().toISOString(),nonce:randomUUID2()};await writeJsonAtomic(file,record)}function watchDrainRequest(file,onRequest,options={}){let stopped=!1,timer,watcher,polling=!1,lastNonce,poll=()=>{stopped||polling||(polling=!0,readDrainRequest(file).then(request=>{stopped||request===void 0||request.nonce===lastNonce||(lastNonce=request.nonce,onRequest(request))},ignore).finally(()=>{polling=!1}))},schedule=intervalMs=>{timer!==void 0&&clearInterval(timer),timer=setInterval(poll,options.pollMs??intervalMs),timer.unref?.()},name=basename(file);try{watcher=watch(dirname(file),{persistent:!1},(_event,changed)=>{(changed===null||changed===name)&&poll()}),watcher.on("error",()=>{watcher?.close(),watcher=void 0,stopped||schedule(DRAIN_REQUEST_POLL_MS)})}catch{watcher=void 0}return schedule(watcher===void 0?DRAIN_REQUEST_POLL_MS:DRAIN_REQUEST_WATCHED_POLL_MS),poll(),()=>{stopped=!0,timer!==void 0&&clearInterval(timer),watcher?.close()}}async function readDrainRequest(file){let raw=await readJsonObject(file);if(raw===void 0)return;let sender=parseSender(raw.sender);if(sender!==void 0&&!(typeof raw.reason!="string"||typeof raw.at!="string"||typeof raw.nonce!="string"))return{sender,reason:raw.reason,at:raw.at,nonce:raw.nonce}}async function writeDrainWatch(file,watch2){await writeJsonAtomic(file,{...watch2,at:new Date().toISOString()})}async function readDrainWatch(file){let raw=await readFileOrUndefined2(file);if(raw!==void 0)try{let parsed=JSON.parse(raw),pid=typeof parsed=="object"&&parsed!==null?Reflect.get(parsed,"pid"):void 0;return typeof pid=="number"&&Number.isInteger(pid)&&pid>0?{pid}:void 0}catch{return}}function hostChildDrainRequestFile(env=process.env){let daemonDir=env[HOST_DAEMON_DIR_ENV],instanceId=env[HOST_INSTANCE_ID_ENV];if(!(!daemonDir||!instanceId))return generationPaths(hostDaemonDirectoryPaths(daemonDir),instanceId).childDrainRequestFile}async function readFileOrUndefined2(file){try{return await readFile2(file,"utf8")}catch{return}}function ignore(){}import{stat as stat2}from"node:fs/promises";var LegacyLockArtifactError=class extends Error{constructor(lockPath){super(`Legacy lock directory is present at ${lockPath}`),this.code="ELEGACY_LOCK_ARTIFACT",this.name="LegacyLockArtifactError",this.lockPath=lockPath}},DEFAULT_RETRIES={retries:100,minTimeout:20,maxTimeout:100};async function openLockDatabase(lockPath){if(typeof globalThis.Bun<"u"){let{Database}=await import("bun:sqlite"),database2=new Database(lockPath,{create:!0});return{exec:sql=>database2.exec(sql),close:()=>database2.close(!1)}}let{DatabaseSync}=await import("node:sqlite"),database=new DatabaseSync(lockPath);return{exec:sql=>database.exec(sql),close:()=>database.close()}}async function acquireOwnershipSafeLock(lockPath,options={}){let retries=options.retries??DEFAULT_RETRIES,deadline=Date.now()+retries.retries*retries.maxTimeout;for(;;){await rejectLegacyDirectory(lockPath);let busyMs=Math.max(1,Math.min(retries.maxTimeout,deadline-Date.now())),database;try{database=await openLockDatabase(lockPath),database.exec(`PRAGMA busy_timeout = ${busyMs}; BEGIN EXCLUSIVE;`);let released=!1,held=database;return async()=>{if(!released){released=!0;try{held.exec("COMMIT;")}finally{held.close()}}}}catch(error){try{database?.close()}catch{}if(await rejectLegacyDirectory(lockPath),isBusy(error)&&Date.now()<deadline){await new Promise(resolve=>setTimeout(resolve,Math.min(retries.minTimeout,Math.max(1,deadline-Date.now()))));continue}throw error}}}async function rejectLegacyDirectory(lockPath){try{if((await stat2(lockPath)).isDirectory())throw new LegacyLockArtifactError(lockPath)}catch(error){if(error instanceof LegacyLockArtifactError||error.code!=="ENOENT")throw error}}function isBusy(error){let message=error instanceof Error?error.message:String(error);return/busy|locked/i.test(message)||error?.code==="SQLITE_BUSY"}export{readHostProcessMetrics,HOST_COLD_START_ENV,HOST_IDLE_EXIT_MS_ENV,DEFAULT_HOST_IDLE_EXIT_MS,HANDOFF_GRACE_MS_ENV,DEFAULT_HANDOFF_GRACE_MS,parseColdStart,parseIdleExitMs,resolveHostPolicy,IdleExitDecider,writeStderrLine,supervisorLog,writeHostReady,logUnknownHostIdentity,errorMessage,STOP_STALL_EVIDENCE_MAX_AGE_MS,CHILD_STALLED_STOP_MAX_MS_ENV,childStalledStopMaxMs,ownGenerationDir,stallRefusalWindowMs,writeStallEvidence,writeHeartbeat,recentStall,hostLoopStalled,holderGenerationEnded,hostChildAlive,writeStopProgress,clearStopProgress,activeStopProgress,SESSION_PATH_RETRY_AFTER_MS,createEndpointReservations,readSessionPathClaims,claimOwnerIsLive,pruneDeadGenerations,readGenerationRows,readHostRegistration,readGenerationRegistration,writeHostRegistration,writeGenerationRecord,clearHostRegistration,releaseGeneration,provenOwner,writtenByThisProcess,thisProcessStartTime,isGenerationId,acquireOwnershipSafeLock,CHILD_STOP_TIMEOUT_MS,CHILD_KILL_EXIT_TIMEOUT_MS,writeStopIntent,readStopIntent,consumeStopIntent,layeredSupervisorIntent,writeDrainRequest,watchDrainRequest,writeDrainWatch,readDrainWatch,hostChildDrainRequestFile};
|