stitchkit 0.103.9 → 0.103.10

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,213 @@
1
+ import {
2
+ loadDarwinBinding
3
+ } from "./index-d4pa2bgs.js";
4
+ import {
5
+ isRecord
6
+ } from "./index-9jfdm5tg.js";
7
+
8
+ // src/internal/process-instance.ts
9
+ import { readFile as readFile2, readlink } from "node:fs/promises";
10
+ import { platform as platform2 } from "node:os";
11
+ import { z } from "zod";
12
+
13
+ // src/internal/process-identity.ts
14
+ import { execFile } from "node:child_process";
15
+ import { readFile } from "node:fs/promises";
16
+ import { platform } from "node:os";
17
+ import { promisify } from "node:util";
18
+ var detectedIdentity;
19
+ async function firstReadableLine(paths) {
20
+ for (const path of paths) {
21
+ try {
22
+ const text = (await readFile(path, "utf8")).trim();
23
+ if (text.length > 0)
24
+ return text;
25
+ } catch {}
26
+ }
27
+ return null;
28
+ }
29
+ var run = promisify(execFile);
30
+ function parsePlatformUuid(dump) {
31
+ return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(dump)?.[1] ?? null;
32
+ }
33
+ async function readDarwinPlatformUuid(command = "ioreg") {
34
+ try {
35
+ const { stdout } = await run(command, ["-rd1", "-c", "IOPlatformExpertDevice"], {
36
+ timeout: 2000,
37
+ maxBuffer: 1 << 20,
38
+ encoding: "utf8"
39
+ });
40
+ return parsePlatformUuid(stdout);
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ async function machineIdentity(declared) {
46
+ if (declared !== undefined)
47
+ return declared;
48
+ if (detectedIdentity !== undefined)
49
+ return detectedIdentity;
50
+ detectedIdentity = platform() === "darwin" ? await readDarwinPlatformUuid() : await firstReadableLine(["/etc/machine-id", "/var/lib/dbus/machine-id"]);
51
+ return detectedIdentity;
52
+ }
53
+ async function probeLiveness(pid, reach = (target) => process.kill(target, 0)) {
54
+ try {
55
+ reach(pid);
56
+ } catch (error) {
57
+ return isRecord(error) && error.code === "ESRCH" ? "gone" : "alive";
58
+ }
59
+ return await isZombieProcess(pid) ? "gone" : "alive";
60
+ }
61
+ async function isZombieProcess(pid, seam = {}) {
62
+ const { procRoot = "/proc", psCommand = "ps" } = seam;
63
+ try {
64
+ const stat = await readFile(`${procRoot}/${pid}/stat`, "utf8");
65
+ return stat.slice(stat.lastIndexOf(")") + 1).trimStart().startsWith("Z");
66
+ } catch {}
67
+ try {
68
+ const { stdout } = await run(psCommand, ["-o", "state=", "-p", String(pid)], {
69
+ timeout: 2000,
70
+ maxBuffer: 1 << 16,
71
+ encoding: "utf8"
72
+ });
73
+ return stdout.trim().startsWith("Z");
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ // src/internal/process-instance.ts
80
+ var ProcessInstanceSchema2 = z.object({
81
+ platform: z.enum(["linux", "darwin"]),
82
+ bootId: z.string().min(1),
83
+ namespace: z.string().min(1),
84
+ startId: z.string().regex(/^\d+$/)
85
+ }).strict();
86
+ var ProcessInstanceObservationSchema = z.discriminatedUnion("state", [
87
+ z.object({ state: z.literal("observed"), instance: ProcessInstanceSchema2 }),
88
+ z.object({ state: z.literal("unavailable"), cause: z.unknown() })
89
+ ]);
90
+ async function observeProcessInstance2(pid) {
91
+ z.number().int().positive().max(2147483647).parse(pid);
92
+ return observeProcessInstanceAt(pid, "/proc");
93
+ }
94
+ function linuxProcessStart(stat, pid) {
95
+ if (!stat.startsWith(`${pid} (`))
96
+ return null;
97
+ const end = stat.lastIndexOf(")");
98
+ if (end < 0)
99
+ return null;
100
+ const start = stat.slice(end + 1).trim().split(/\s+/)[19];
101
+ return start !== undefined && /^\d+$/.test(start) ? start : null;
102
+ }
103
+ async function readProcessInstance(pid, procRoot = "/proc") {
104
+ const result = await observeProcessInstanceAt(pid, procRoot);
105
+ return result.state === "observed" ? result.instance : null;
106
+ }
107
+ async function observeProcessInstanceAt(pid, procRoot) {
108
+ try {
109
+ if (platform2() === "darwin") {
110
+ return {
111
+ state: "observed",
112
+ instance: ProcessInstanceSchema2.parse(loadDarwinBinding().processIdentity(pid))
113
+ };
114
+ }
115
+ if (platform2() !== "linux")
116
+ throw new Error("Process lifetime observation requires Linux or Darwin");
117
+ const [bootId, namespace, ownStatus, timeNamespace, ownStat, stat] = await Promise.all([
118
+ readFile2(`${procRoot}/sys/kernel/random/boot_id`, "utf8"),
119
+ readlink(`${procRoot}/self/ns/pid`),
120
+ readFile2(`${procRoot}/self/status`, "utf8"),
121
+ readlink(`${procRoot}/self/ns/time`).catch((error) => {
122
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
123
+ return "";
124
+ throw error;
125
+ }),
126
+ readFile2(`${procRoot}/self/stat`, "utf8"),
127
+ readFile2(`${procRoot}/${pid}/stat`, "utf8")
128
+ ]);
129
+ const namespacePids = ownStatus.match(/^NStgid:[ \t]*(.*)$/m)?.[1]?.trim().split(/\s+/);
130
+ if (namespacePids?.length !== 1 || namespacePids[0] !== String(process.pid) || linuxProcessStart(ownStat, process.pid) === null)
131
+ throw new Error("Process namespace evidence is partial or belongs to a foreign proc mount");
132
+ const startId = linuxProcessStart(stat, pid);
133
+ if (startId === null || !bootId.trim())
134
+ throw new Error("Process boot/start evidence is incomplete");
135
+ return {
136
+ state: "observed",
137
+ instance: {
138
+ platform: "linux",
139
+ bootId: bootId.trim(),
140
+ namespace: `${namespace};${timeNamespace}`,
141
+ startId
142
+ }
143
+ };
144
+ } catch (cause) {
145
+ return { state: "unavailable", cause };
146
+ }
147
+ }
148
+ var ProcessOwnerEvidenceSchema = z.object({
149
+ liveness: z.enum(["alive", "gone", "not-probed"]),
150
+ identity: z.enum([
151
+ "matched",
152
+ "different-boot",
153
+ "reused-pid",
154
+ "pid-gone",
155
+ "legacy",
156
+ "unavailable"
157
+ ]),
158
+ cause: z.unknown().optional()
159
+ });
160
+ async function probeProcessOwner2(pid, recorded) {
161
+ z.number().int().positive().max(2147483647).parse(pid);
162
+ if (recorded != null)
163
+ ProcessInstanceSchema2.parse(recorded);
164
+ return probeProcessOwnerWith(pid, recorded, {
165
+ read: readProcessInstance,
166
+ observe: observeProcessInstance2,
167
+ liveness: probeLiveness
168
+ });
169
+ }
170
+ async function probeProcessOwnerWith(pid, recorded, probes = { read: readProcessInstance, liveness: probeLiveness }) {
171
+ if (recorded === undefined)
172
+ return { liveness: await probes.liveness(pid), identity: "legacy" };
173
+ const unavailable = (cause) => ({
174
+ liveness: "not-probed",
175
+ identity: "unavailable",
176
+ cause
177
+ });
178
+ if (recorded === null)
179
+ return unavailable(new Error("Recorded process lifetime is unavailable"));
180
+ const observe = async (target) => {
181
+ if (probes.observe)
182
+ return probes.observe(target);
183
+ const instance = await probes.read(target);
184
+ return instance ? { state: "observed", instance } : {
185
+ state: "unavailable",
186
+ cause: new Error("Process lifetime reader returned no evidence")
187
+ };
188
+ };
189
+ const own = await observe(process.pid);
190
+ if (own.state === "unavailable")
191
+ return unavailable(own.cause);
192
+ const current = own.instance;
193
+ if (recorded.platform !== current.platform)
194
+ return unavailable(new Error("Process platforms differ"));
195
+ if (recorded.bootId !== current.bootId)
196
+ return { liveness: "gone", identity: "different-boot" };
197
+ if (recorded.namespace !== current.namespace)
198
+ return unavailable(new Error("Process namespaces differ"));
199
+ const liveness = await probes.liveness(pid);
200
+ if (liveness === "gone")
201
+ return { liveness, identity: "pid-gone" };
202
+ const observation = await observe(pid);
203
+ if (observation.state === "unavailable")
204
+ return unavailable(observation.cause);
205
+ const observed = observation.instance;
206
+ if (!observed || observed.bootId !== current.bootId || observed.namespace !== current.namespace)
207
+ return unavailable(new Error("Observed boot or namespace changed during probe"));
208
+ if (recorded.startId !== observed.startId)
209
+ return { liveness: "gone", identity: "reused-pid" };
210
+ return { liveness: "alive", identity: "matched" };
211
+ }
212
+
213
+ export { machineIdentity, ProcessInstanceSchema2, observeProcessInstance2, readProcessInstance, probeProcessOwner2 };
@@ -23,6 +23,7 @@ export interface ExclusiveLockDiagnosis {
23
23
  readonly liveness: 'alive' | 'gone' | 'not-probed';
24
24
  readonly identity?: ProcessOwnerEvidence['identity'];
25
25
  readonly owner: ExclusiveLockOwner | null;
26
+ readonly cause?: unknown;
26
27
  }
27
28
  /** A lock this process holds. */
28
29
  export interface HeldExclusiveLock {
@@ -42,7 +43,7 @@ export interface ExclusiveLockAttemptOptions {
42
43
  * How old a lock with no readable owner must be before it is taken. Absent,
43
44
  * an ownerless lock is never taken — it may predate owner records.
44
45
  */
45
- readonly ownerlessGraceMs?: number;
46
+ readonly ownerlessGraceMs?: number | null;
46
47
  }
47
48
  export type ExclusiveLockAttempt = {
48
49
  readonly held: HeldExclusiveLock;
@@ -1 +1 @@
1
- {"version":3,"file":"exclusive-lock.d.ts","sourceRoot":"","sources":["../../src/internal/exclusive-lock.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,oBAAoB,CAAC;AAK5B,gDAAgD;AAChD,QAAA,MAAM,wBAAwB;;;;;;;;;;;;;;iBAO5B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,wCAAwC;AACxC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,cAAc,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;IAC5E,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,YAAY,CAAC;IACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,oBAAoB,CAAC,UAAU,CAAC,CAAC;IACrD,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAC3C;AAED,iCAAiC;AACjC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,kFAAkF;IAClF,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,uFAAuF;IACvF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE,GACpC;IACE,kDAAkD;IAClD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC;CAC7C,CAAC;AAmLN;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAe/B;AAED,gFAAgF;AAChF,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAEpC"}
1
+ {"version":3,"file":"exclusive-lock.d.ts","sourceRoot":"","sources":["../../src/internal/exclusive-lock.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,oBAAoB,CAAC;AAK5B,gDAAgD;AAChD,QAAA,MAAM,wBAAwB;;;;;;;;;;;;;;iBAO5B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,wCAAwC;AACxC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,cAAc,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;IAC5E,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,YAAY,CAAC;IACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,oBAAoB,CAAC,UAAU,CAAC,CAAC;IACrD,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,iCAAiC;AACjC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,kFAAkF;IAClF,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,uFAAuF;IACvF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3C;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE,GACpC;IACE,kDAAkD;IAClD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC;CAC7C,CAAC;AAmLN;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAqB/B;AAED,gFAAgF;AAChF,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAEpC"}
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { probeLiveness } from './process-identity.js';
3
2
  export declare const ProcessInstanceSchema: z.ZodObject<{
4
3
  platform: z.ZodEnum<{
5
4
  darwin: "darwin";
@@ -10,17 +9,55 @@ export declare const ProcessInstanceSchema: z.ZodObject<{
10
9
  startId: z.ZodString;
11
10
  }, z.core.$strict>;
12
11
  export type ProcessInstance = z.infer<typeof ProcessInstanceSchema>;
12
+ /** An unavailable observation preserves its native or validation cause; it never proves death. */
13
+ declare const ProcessInstanceObservationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
14
+ state: z.ZodLiteral<"observed">;
15
+ instance: z.ZodObject<{
16
+ platform: z.ZodEnum<{
17
+ darwin: "darwin";
18
+ linux: "linux";
19
+ }>;
20
+ bootId: z.ZodString;
21
+ namespace: z.ZodString;
22
+ startId: z.ZodString;
23
+ }, z.core.$strict>;
24
+ }, z.core.$strip>, z.ZodObject<{
25
+ state: z.ZodLiteral<"unavailable">;
26
+ cause: z.ZodUnknown;
27
+ }, z.core.$strip>], "state">;
28
+ export type ProcessInstanceObservation = z.infer<typeof ProcessInstanceObservationSchema>;
29
+ export declare function observeProcessInstance(pid: number): Promise<ProcessInstanceObservation>;
13
30
  /** /proc stat field 22 follows a command name that may itself contain spaces and parentheses. */
14
31
  export declare function linuxProcessStart(stat: string, pid: number): string | null;
15
32
  /** @internal procRoot permits kernel-format fixtures without changing a real process or boot. */
16
33
  export declare function readProcessInstance(pid: number, procRoot?: string): Promise<ProcessInstance | null>;
17
- export interface ProcessOwnerEvidence {
18
- readonly liveness: 'alive' | 'gone' | 'not-probed';
19
- readonly identity: 'matched' | 'different-boot' | 'reused-pid' | 'pid-gone' | 'legacy' | 'unavailable';
20
- }
34
+ /** @internal The fixture root is never a public owner option. */
35
+ export declare function observeProcessInstanceAt(pid: number, procRoot: string): Promise<ProcessInstanceObservation>;
36
+ declare const ProcessOwnerEvidenceSchema: z.ZodObject<{
37
+ liveness: z.ZodEnum<{
38
+ alive: "alive";
39
+ gone: "gone";
40
+ "not-probed": "not-probed";
41
+ }>;
42
+ identity: z.ZodEnum<{
43
+ "different-boot": "different-boot";
44
+ legacy: "legacy";
45
+ matched: "matched";
46
+ "pid-gone": "pid-gone";
47
+ "reused-pid": "reused-pid";
48
+ unavailable: "unavailable";
49
+ }>;
50
+ cause: z.ZodOptional<z.ZodUnknown>;
51
+ }, z.core.$strip>;
52
+ export type ProcessOwnerEvidence = z.infer<typeof ProcessOwnerEvidenceSchema>;
21
53
  /** Compare identities only inside one machine (the caller establishes that boundary). */
22
- export declare function probeProcessOwner(pid: number, recorded: ProcessInstance | null | undefined, probes?: {
23
- read: typeof readProcessInstance;
24
- liveness: typeof probeLiveness;
25
- }): Promise<ProcessOwnerEvidence>;
54
+ export declare function probeProcessOwner(pid: number, recorded: ProcessInstance | null | undefined): Promise<ProcessOwnerEvidence>;
55
+ interface ProcessOwnerProbes {
56
+ read(pid: number): Promise<ProcessInstance | null>;
57
+ liveness(pid: number): Promise<'alive' | 'gone'>;
58
+ observe?(pid: number): Promise<ProcessInstanceObservation>;
59
+ }
60
+ /** @internal Injection is for kernel-format controls, not a public ownership assertion. */
61
+ export declare function probeProcessOwnerWith(pid: number, recorded: ProcessInstance | null | undefined, probes?: ProcessOwnerProbes): Promise<ProcessOwnerEvidence>;
62
+ export {};
26
63
  //# sourceMappingURL=process-instance.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"process-instance.d.ts","sourceRoot":"","sources":["../../src/internal/process-instance.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,eAAO,MAAM,qBAAqB;;;;;;;;kBAOvB,CAAC;AACZ,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS1E;AAED,iGAAiG;AACjG,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,QAAQ,SAAU,GACjB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA6CjC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,YAAY,CAAC;IACnD,QAAQ,CAAC,QAAQ,EACb,SAAS,GACT,gBAAgB,GAChB,YAAY,GACZ,UAAU,GACV,QAAQ,GACR,aAAa,CAAC;CACnB;AAED,yFAAyF;AACzF,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,EAC5C,MAAM;IAAK,IAAI;IAAuB,QAAQ;CAAiB,GAC9D,OAAO,CAAC,oBAAoB,CAAC,CAyB/B"}
1
+ {"version":3,"file":"process-instance.d.ts","sourceRoot":"","sources":["../../src/internal/process-instance.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,qBAAqB;;;;;;;;kBAOvB,CAAC;AACZ,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,kGAAkG;AAClG,QAAA,MAAM,gCAAgC;;;;;;;;;;;;;;4BAGpC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gCAAgC,CAAC,CAAC;AAE1F,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,0BAA0B,CAAC,CAGrC;AAED,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS1E;AAED,iGAAiG;AACjG,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,QAAQ,SAAU,GACjB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAGjC;AAED,iEAAiE;AACjE,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,0BAA0B,CAAC,CAsDrC;AAED,QAAA,MAAM,0BAA0B;;;;;;;;;;;;;;;iBAW9B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E,yFAAyF;AACzF,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAQ/B;AAED,UAAU,kBAAkB;IAC1B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IACnD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,CAAC;IACjD,OAAO,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;CAC5D;AAED,2FAA2F;AAC3F,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,EAC5C,MAAM,GAAE,kBAA2E,GAClF,OAAO,CAAC,oBAAoB,CAAC,CA2C/B"}
@@ -18,7 +18,7 @@ export interface ExclusiveLockOptions {
18
18
  timeoutMs?: number;
19
19
  /** Stops the wait; an abort while waiting rejects at once with its reason as the cause. */
20
20
  signal?: AbortSignal;
21
- /** Permission bits of the lock file. Default `0o600`. */
21
+ /** Exact permission bits, independent of umask. Default `0o600`. Shared readers need a common group and traversable parents. */
22
22
  mode?: number;
23
23
  /**
24
24
  * This machine's identity, for a platform that offers none (`/etc/machine-id`
@@ -29,9 +29,10 @@ export interface ExclusiveLockOptions {
29
29
  /**
30
30
  * How old a lock with no readable owner must be before it is taken — the one
31
31
  * case where time decides, left by a process that died between creating the
32
- * file and recording itself. Default 5 000.
32
+ * file and recording itself. Default 5 000; `null` disables age-based reclaim,
33
+ * including abandoned ownerless reclaim guards.
33
34
  */
34
- ownerlessGraceMs?: number;
35
+ ownerlessGraceMs?: number | null;
35
36
  }
36
37
  /** The lock `run` executes under. */
37
38
  export interface ExclusiveLock {
@@ -1 +1 @@
1
- {"version":3,"file":"with-exclusive-lock.d.ts","sourceRoot":"","sources":["../../src/internal/with-exclusive-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gGAAgG;IAChG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,iFAAiF;IACjF,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,qEAAqE;AACrE,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IALnC,IAAI,SAAwB;IACrC,YACW,IAAI,EAAE,cAAc,GAAG,cAAc,EACrC,KAAK,EAAE,MAAM;IACtB,wFAAwF;IAC/E,MAAM,EAAE,kBAAkB,GAAG,IAAI,EAC1C,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAG9B;CACF;AAyCD;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAC5C,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,CAAC,CAAC,CAwDZ"}
1
+ {"version":3,"file":"with-exclusive-lock.d.ts","sourceRoot":"","sources":["../../src/internal/with-exclusive-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gGAAgG;IAChG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gIAAgI;IAChI,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,iFAAiF;IACjF,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,qEAAqE;AACrE,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IALnC,IAAI,SAAwB;IACrC,YACW,IAAI,EAAE,cAAc,GAAG,cAAc,EACrC,KAAK,EAAE,MAAM;IACtB,wFAAwF;IAC/E,MAAM,EAAE,kBAAkB,GAAG,IAAI,EAC1C,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAG9B;CACF;AAyCD;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAC5C,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,CAAC,CAAC,CA4DZ"}
@@ -1 +1 @@
1
- {"version":3,"file":"command-owner.d.ts","sourceRoot":"","sources":["../../src/process/command-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,8BAA8B,EAAS,MAAM,oBAAoB,CAAC;AAGhF,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,YAAY,CAAC;AAGpB,wFAAwF;AACxF,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,oBAAoB,EAC3B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,8BAA8B,KAAK,IAAI;;;;;;;;EA0H1D"}
1
+ {"version":3,"file":"command-owner.d.ts","sourceRoot":"","sources":["../../src/process/command-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,8BAA8B,EAAS,MAAM,oBAAoB,CAAC;AAGhF,OAAO,EAEL,KAAK,oBAAoB,EAI1B,MAAM,YAAY,CAAC;AAIpB,wFAAwF;AACxF,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,oBAAoB,EAC3B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,8BAA8B,KAAK,IAAI;;;;;;;;EA+J1D"}
@@ -1,4 +1,13 @@
1
1
  import { z } from 'zod';
2
+ declare const NativeCommandSettlementSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
3
+ kind: z.ZodLiteral<"exit">;
4
+ exitCode: z.ZodNullable<z.ZodNumber>;
5
+ signal: z.ZodNullable<z.ZodString>;
6
+ }, z.core.$strip>, z.ZodObject<{
7
+ kind: z.ZodLiteral<"error">;
8
+ cause: z.ZodUnknown;
9
+ }, z.core.$strip>], "kind">;
10
+ export type NativeCommandSettlement = z.infer<typeof NativeCommandSettlementSchema>;
2
11
  export declare const NativeCommandOptionsSchema: z.ZodObject<{
3
12
  executable: z.ZodString;
4
13
  args: z.ZodDefault<z.ZodArray<z.ZodString>>;
@@ -15,6 +24,7 @@ export declare const NativeCommandOptionsSchema: z.ZodObject<{
15
24
  stdin: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>>;
16
25
  maxStdinBytes: z.ZodDefault<z.ZodNumber>;
17
26
  onOutput: z.ZodOptional<z.ZodCustom<(bytes: Uint8Array, channel: 'stdout' | 'stderr', signal: AbortSignal) => void | Promise<void>, (bytes: Uint8Array, channel: 'stdout' | 'stderr', signal: AbortSignal) => void | Promise<void>>>;
27
+ onLeaderSettled: z.ZodOptional<z.ZodCustom<(event: NativeCommandSettlement, signal: AbortSignal) => void | Promise<void>, (event: NativeCommandSettlement, signal: AbortSignal) => void | Promise<void>>>;
18
28
  killGraceMs: z.ZodDefault<z.ZodNumber>;
19
29
  cleanupTimeoutMs: z.ZodDefault<z.ZodNumber>;
20
30
  }, z.core.$strict>;
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/process/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;gDAoBtB,UAAU,WACR,QAAQ,GAAG,QAAQ,UACpB,WAAW,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;kBAgB7B,CAAC;AACL,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,0BAA0B,CAAC,CAAC;AACrF,QAAA,MAAM,yBAAyB;;;;;iBAK7B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,qBAAa,kBAAmB,SAAQ,KAAK;aAEzB,IAAI,EAAE,eAAe,GAAG,qBAAqB,GAAG,iBAAiB;IADnF,YACkB,IAAI,EAAE,eAAe,GAAG,qBAAqB,GAAG,iBAAiB,EACjF,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY,EAIvB;CACF"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/process/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,QAAA,MAAM,6BAA6B;;;;;;;2BAOjC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;gDAoBtB,UAAU,WACR,QAAQ,GAAG,QAAQ,UACpB,WAAW,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;uDAKX,uBAAuB,UAAU,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;kBAiBvF,CAAC;AACL,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,0BAA0B,CAAC,CAAC;AACrF,QAAA,MAAM,yBAAyB;;;;;iBAK7B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,qBAAa,kBAAmB,SAAQ,KAAK;aAEzB,IAAI,EAAE,eAAe,GAAG,qBAAqB,GAAG,iBAAiB;IADnF,YACkB,IAAI,EAAE,eAAe,GAAG,qBAAqB,GAAG,iBAAiB,EACjF,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY,EAIvB;CACF"}
@@ -1,6 +1,6 @@
1
1
  import { NativeCommandError } from './contract.js';
2
2
  export declare function signalGroup(pid: number | undefined, signal: NodeJS.Signals): void;
3
- export declare function stopCommandGroup(pid: number | undefined, graceMs: number): Promise<void>;
3
+ export declare function stopCommandGroup(pid: number | undefined, graceMs: number, cleanupTimeoutMs?: number): Promise<void>;
4
4
  export declare function commandCleanupError(cause: unknown): NativeCommandError;
5
5
  export declare function waitForCommandClose<T>(closed: Promise<T>, timeoutMs: number): Promise<T>;
6
6
  //# sourceMappingURL=group.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"group.d.ts","sourceRoot":"","sources":["../../src/process/group.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAOjF;AAYD,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CAItE;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAExF"}
1
+ {"version":3,"file":"group.d.ts","sourceRoot":"","sources":["../../src/process/group.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAWhD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAOjF;AAiCD,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,OAAO,EAAE,MAAM,EACf,gBAAgB,SAAO,GACtB,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CAItE;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAExF"}
@@ -0,0 +1,4 @@
1
+ import type { NativeCommandSettlement, ParsedNativeCommandOptions } from './contract.js';
2
+ /** One bounded external resource settlement, shared by observed exit and terminal failure. */
3
+ export declare function createLeaderSettlement(options: ParsedNativeCommandOptions, signal: AbortSignal): (event: NativeCommandSettlement) => Promise<void>;
4
+ //# sourceMappingURL=leader-settlement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leader-settlement.d.ts","sourceRoot":"","sources":["../../src/process/leader-settlement.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAGtF,8FAA8F;AAC9F,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,0BAA0B,EACnC,MAAM,EAAE,WAAW,WAGJ,uBAAuB,KAAG,OAAO,CAAC,IAAI,CAAC,CA+BvD"}
package/llms/files.txt CHANGED
@@ -30,7 +30,7 @@ available from `stitchkit/contract`.
30
30
  | `ManagedFileInspector` | _type_ | bounded-prefix read/write inspection callback with a finite cancellation signal that cannot own path or size |
31
31
  | `ManagedFileInspectionInput` / `ManagedFileInspection` | _type_ | inspector prefix/name/declared media/signal input and validated metadata-only result |
32
32
  | `writeFileAtomic` / `writeFileAtomicSync` / `WriteFileAtomicOptions` | function / _type_ | replace a file atomically: a random staging name created exclusively (never through a planted link), the mode set on the descriptor before the file is visible (default `0o600`, not masked by the umask), `fsync`, rename; a failure leaves the target and no staging file. The asynchronous form keeps the event loop running |
33
- | `withExclusiveLock` / `ExclusiveLockOptions` / `ExclusiveLock` / `ExclusiveLockOwner` | function / _type_ | run work under an exclusive lock between processes — a file recording its owner (pid, host, machine identity, time and nullable kernel process identity); waits up to `timeoutMs` (default 10 s) and stops on `signal`; a dead owner on this machine is taken over, a live, slow or foreign one never is; an ownerless lock only after `ownerlessGraceMs` |
33
+ | `withExclusiveLock` / `ExclusiveLockOptions` / `ExclusiveLock` / `ExclusiveLockOwner` | function / _type_ | run work under an exclusive lock between processes — a file recording its owner (pid, host, machine identity, time and nullable kernel process identity); waits up to `timeoutMs` (default 10 s) and stops on `signal`; a dead owner on this machine is taken over, a live, slow or foreign one never is; an ownerless lock only after `ownerlessGraceMs` (default 5000 ms, `null` disables age-based reclaim including guards); exact `mode` is independent of umask; shared readers need a common group and traversable directories |
34
34
  | `ExclusiveLockError` | class | `LOCK_TIMEOUT` naming the resource and its holder, or `LOCK_ABORTED` with the signal's reason |
35
35
  | `createChunkSpool` | function | the server half of a chunked upload: parts on disk under `owner` + client-minted `uploadId`, each with a size+sha256 receipt; idempotent `open` and `put`, the first writer of a part wins across processes, `assemble` in order once every part is in, `sweep` of untouched uploads — [guide](../guide/client.md#chunked-uploads) |
36
36
  | `ChunkSpool` / `ChunkSpoolConfig` / `ChunkSpoolKey` / `ChunkSpoolOpen` / `ChunkSpoolPart` / `ChunkSpoolAssembly` | _type_ | the spool, its directory, part size, file limit, meta schema and age; an upload's key; its declaration; one part; the assembled parts with a concatenating `stream()` |
package/llms/process.txt CHANGED
@@ -15,12 +15,16 @@ description: Native IO для Bun и Node, с явно выбранной durabi
15
15
  type: guide
16
16
  status: active
17
17
  created: 2026-10-01 20:44 +07:00
18
- updated: 2026-10-01 21:31 +07:00
18
+ updated: 2026-10-02 00:37 +07:00
19
19
  participants:
20
20
  - role: authored
21
21
  harness: Codex
22
22
  model: GPT-6
23
23
  at: 2026-10-01 20:44 +07:00
24
+ - role: implemented
25
+ harness: Codex
26
+ model: GPT-6
27
+ at: 2026-10-02 00:08 +07:00
24
28
  ---
25
29
 
26
30
  # Native IO
@@ -136,6 +140,10 @@ callbacks owner не запускает. Normal completion ждёт всех byt
136
140
 
137
141
  Cleanup: POSIX process group, TERM, `killGraceMs` (default 100, maximum 10 000), затем KILL;
138
142
  `cleanupTimeoutMs` (default 2000, maximum 30 000) ограничивает ожидание close после сигналов.
143
+ На Darwin временный `EPERM` при сигнале группе может означать zombies, ещё не убранные
144
+ OS reaper. Повтор ограничен `cleanupTimeoutMs`; успех требует доставленного сигнала или
145
+ `ESRCH`, постоянный отказ сохраняется в cause. Закрытие каждого owned pipe и выход leader
146
+ наблюдаются отдельно, включая отсутствие общего `child.close` при teardown.
139
147
  Сохраняется caller/sink причина; failure cleanup — `NativeCommandError` с `COMMAND_CLEANUP` и
140
148
  AggregateError cause. Даже вышедший parent не исключает helper, удерживающий pipe. Windows
141
149
  отказывает с `COMMAND_UNAVAILABLE`; потомок, самостоятельно покинувший группу через setsid,
@@ -150,6 +158,68 @@ shell policy, command admission, логирование, redaction, bytes→text
150
158
  Длительные signal-only команды остаются streaming; bounded tar/capture получают свои явные limits.
151
159
  Импорт leaf не делает установку всего npm package маленькой; выбирайте dependency после измерения
152
160
  packed размера и dependency closure.
161
+ ## Resource-scoped launchers and shared owners
162
+
163
+ `onLeaderSettled(event, signal)` observes leader exit before inherited stdout/stderr pipes
164
+ close. A resource owner can stop its external scope there, allowing pipe drain to complete.
165
+ The callback runs once: `event.kind === 'exit'` carries observed `exitCode`/`signal`; a terminal
166
+ failure uses `kind: 'error'` and preserves its cause. Unavailable executables, output sink
167
+ failure and caller cancellation settle through the same owner. `cleanupTimeoutMs` bounds
168
+ the callback after settlement starts; it never becomes a deadline for signal-only execution.
169
+ Honor the settlement signal; arbitrary user promises cannot be forcibly cancelled.
170
+
171
+ ```ts
172
+ await runNativeCommand({
173
+ executable: launcher,
174
+ args,
175
+ signal: scope.signal,
176
+ onOutput: (bytes, channel, signal) => scope.write(bytes, channel, signal),
177
+ onLeaderSettled: (event, signal) => scope.settle(event, signal),
178
+ });
179
+ ```
180
+
181
+ `observeProcessInstance(pid)` returns `observed` with a `ProcessInstance`, or `unavailable`
182
+ with its cause. `probeProcessOwner(pid, recorded)` compares that evidence on the machine the
183
+ caller established: `matched`, `different-boot`, `reused-pid`, `pid-gone`, `legacy` or
184
+ `unavailable`. Missing/partial/denied evidence is not death. Supervisor tree fencing remains
185
+ the caller's job; a PID lifetime is not proof that its whole workload stopped.
186
+
187
+ An exclusive lock applies exact requested permissions through its creating descriptor,
188
+ independent of umask. Default `0600` stays private. Shared `0640` requires a common group and
189
+ traversable directories; another UID also needs directory write permission to reclaim a
190
+ proven-dead owner's file. Set `ownerlessGraceMs: null` to refuse every age-only reclaim,
191
+ including ownerless reclaim guards. The default remains 5000 ms for existing clients.
192
+ Восстановление stale reclaim guard использует тот же lock owner и следующий guard:
193
+ проверка inode и удаление выполняются под ним. Конкурирующий writer не обходит guard,
194
+ пока прежнее удаление приостановлено; live и unknown lifetime evidence остаются отказом.
195
+
196
+ ## Libraries with a Zod-only runtime
197
+
198
+ Public leaves keep optional peers out of their import closure. Installing `stitchkit` still
199
+ installs the complete package and ky; selective imports alone do not avoid that installation.
200
+ A library can use Stitchkit as a build dependency, bundle its selected public native imports,
201
+ keep Zod external, and publish the resulting implementation with its library artifact.
202
+
203
+ ```ts
204
+ // The library's build entry; all mechanisms remain owned by Stitchkit.
205
+ export { canonicalJson } from 'stitchkit/primitives';
206
+ export { writeFileAtomic, withExclusiveLock } from 'stitchkit/files';
207
+ export { observeProcessInstance, probeProcessOwner, runNativeCommand } from 'stitchkit/process';
208
+ ```
209
+
210
+ Bundle for Node-compatible execution with Zod external. Publish declarations from the same
211
+ version: carry their reachable relative `.d.ts` closure, rather than emitting references to
212
+ `stitchkit` that force clients to install it for types. A library exposing its own DTOs may
213
+ instead emit declarations for only that DTO contract. Keep the library's manifest at runtime
214
+ `dependencies: {}` and `peerDependencies: { zod: ... }` when that is its declared promise.
215
+
216
+ Darwin consumers need the published native binaries at the bundled loader's relative native
217
+ directory. A JS-only bundle is not portable evidence. Qualify the packed library outside its
218
+ build tree in Bun and Node, test its declarations, and check that importing an unbundled
219
+ Stitchkit leaf fails there. A separate schema-only entry must not import the native entry.
220
+
221
+ This delivery removes a runtime kernel installation; it does not remove the build-time
222
+ Stitchkit installation or create an independent lightweight npm package.
153
223
 
154
224
  ==============================================================================
155
225
  # API reference: `stitchkit/process` (docs/api/reference.md)
@@ -165,3 +235,8 @@ Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/nativ
165
235
  | `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink and finite cleanup limits |
166
236
  | `NativeCommandResult` | type | observed nullable exit code/signal and captured stdout/stderr bytes |
167
237
  | `NativeCommandError` | class | COMMAND_LIMIT, COMMAND_UNAVAILABLE or COMMAND_CLEANUP with internal cause |
238
+
239
+ | `NativeCommandSettlement` | type | observed leader `exit` or terminal `error`; `onLeaderSettled` runs once before drain, bounded by cleanupTimeoutMs, with a cancellable settlement signal |
240
+ | `observeProcessInstance` / `ProcessInstanceObservation` | function / type | Linux/Darwin lifetime observation; observed identity or unavailable with its original cause, never guessed absence |
241
+ | `ProcessInstanceSchema` / `ProcessInstance` | schema / type | platform, boot ID, namespace and process start identity from the shared native reader |
242
+ | `probeProcessOwner` / `ProcessOwnerEvidence` | function / type | compare lifetimes on one caller-established machine; matched, boot change, reused PID, gone, legacy or unavailable with cause |