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.
package/CHANGELOG.md CHANGED
@@ -15,6 +15,14 @@ additive**; the first breaking change landed in 0.10.0. Grep the file for
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [0.103.10] - 2026-10-01
19
+
20
+ ### Added
21
+
22
+ - `stitchkit/process`: `onLeaderSettled` observes launcher exit before inherited-pipe drain and settles resource scopes once on terminal failure; callbacks have bounded cancellation and preserve initial causes. Public process identity observation/probing preserves unavailable evidence and its cause from the existing Linux/Darwin owner.
23
+ - `stitchkit/files`: `ownerlessGraceMs: null` explicitly refuses age-based reclaim of unknown locks and guards; exact requested lock permissions survive restrictive umask, and failed descriptor setup cannot remove a replacement inode.
24
+ - Document and qualify neutral library bundling from public imports and published declarations with Zod as its only runtime peer; full-kernel install remains a build-time choice.
25
+
18
26
  ## [0.103.9] - 2026-10-01
19
27
 
20
28
  ### Added
@@ -133,6 +133,11 @@ export declare function fencingPhase(store: AgentRuntimeStore, conversationId: s
133
133
  parts: ({
134
134
  type: "text";
135
135
  text: string;
136
+ } | {
137
+ type: "file";
138
+ mediaType: string;
139
+ reference: string;
140
+ filename?: string | undefined;
136
141
  } | {
137
142
  type: "reasoning";
138
143
  text: string;
@@ -141,11 +146,6 @@ export declare function fencingPhase(store: AgentRuntimeStore, conversationId: s
141
146
  provider: string;
142
147
  data: Record<string, import("zod").JSONType>;
143
148
  } | undefined;
144
- } | {
145
- type: "file";
146
- mediaType: string;
147
- reference: string;
148
- filename?: string | undefined;
149
149
  } | {
150
150
  type: "source";
151
151
  sourceId: string;
@@ -313,6 +313,11 @@ export declare function fencingPhase(store: AgentRuntimeStore, conversationId: s
313
313
  parts: ({
314
314
  type: "text";
315
315
  text: string;
316
+ } | {
317
+ type: "file";
318
+ mediaType: string;
319
+ reference: string;
320
+ filename?: string | undefined;
316
321
  } | {
317
322
  type: "reasoning";
318
323
  text: string;
@@ -321,11 +326,6 @@ export declare function fencingPhase(store: AgentRuntimeStore, conversationId: s
321
326
  provider: string;
322
327
  data: Record<string, import("zod").JSONType>;
323
328
  } | undefined;
324
- } | {
325
- type: "file";
326
- mediaType: string;
327
- reference: string;
328
- filename?: string | undefined;
329
329
  } | {
330
330
  type: "source";
331
331
  sourceId: string;
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  NativeCommandError2,
15
15
  startNativeCommand
16
- } from "../../index-eeqmg7qm.js";
16
+ } from "../../index-6m6a9ty4.js";
17
17
  import"../../index-tgzhq07n.js";
18
18
 
19
19
  // src/agent-runtime/sandbox-bubblewrap.ts
@@ -14,9 +14,10 @@ import {
14
14
  import {
15
15
  createBoundedChannel2
16
16
  } from "../../index-w0eqdhyd.js";
17
+ import"../../index-nybn2err.js";
17
18
  import {
18
19
  attemptExclusiveLock
19
- } from "../../index-7kxw1awn.js";
20
+ } from "../../index-ga597cvy.js";
20
21
  import {
21
22
  isRecord
22
23
  } from "../../index-9jfdm5tg.js";
@@ -9,9 +9,10 @@ import {
9
9
  import {
10
10
  AppError2
11
11
  } from "../index-n53mswt3.js";
12
+ import"../index-nybn2err.js";
12
13
  import {
13
14
  attemptExclusiveLock
14
- } from "../index-7kxw1awn.js";
15
+ } from "../index-ga597cvy.js";
15
16
  import {
16
17
  writeFileAtomic2,
17
18
  writeFileAtomicSync2
@@ -300,7 +301,10 @@ function pause(ms, signal) {
300
301
  async function withExclusiveLock(path, run, options = {}) {
301
302
  const label = options.label ?? path;
302
303
  const timeoutMs = nonNegative("timeoutMs", options.timeoutMs, DEFAULT_TIMEOUT_MS);
303
- const ownerlessGraceMs = nonNegative("ownerlessGraceMs", options.ownerlessGraceMs, DEFAULT_OWNERLESS_GRACE_MS);
304
+ const ownerlessGraceMs = options.ownerlessGraceMs === null ? null : nonNegative("ownerlessGraceMs", options.ownerlessGraceMs, DEFAULT_OWNERLESS_GRACE_MS);
305
+ const mode = options.mode ?? 384;
306
+ if (!Number.isInteger(mode) || mode < 0 || mode > 511)
307
+ throw new RangeError("[stitchkit] withExclusiveLock: mode must be permission bits from 0000 to 0777");
304
308
  const { signal } = options;
305
309
  const deadline = performance.now() + timeoutMs;
306
310
  let delay = FIRST_RETRY_MS;
@@ -309,7 +313,7 @@ async function withExclusiveLock(path, run, options = {}) {
309
313
  throw new ExclusiveLockError("LOCK_ABORTED", label, null, `[stitchkit] waiting for the lock on "${label}" was cancelled`, { cause: signal.reason });
310
314
  }
311
315
  const attempt = await attemptExclusiveLock(path, {
312
- mode: options.mode ?? 384,
316
+ mode,
313
317
  reclaim: true,
314
318
  ownerlessGraceMs,
315
319
  ...options.machineIdentity !== undefined && {
@@ -333,7 +337,7 @@ async function withExclusiveLock(path, run, options = {}) {
333
337
  const remaining = deadline - performance.now();
334
338
  if (remaining <= 0) {
335
339
  const holder = attempt.diagnosis?.owner ?? null;
336
- throw new ExclusiveLockError("LOCK_TIMEOUT", label, holder, `[stitchkit] the lock on "${label}" is held by ${describeHolder(holder)}${describeEvidence(attempt.diagnosis)}; gave up after ${timeoutMs} ms`, { cause: attempt.error });
340
+ throw new ExclusiveLockError("LOCK_TIMEOUT", label, holder, `[stitchkit] the lock on "${label}" is held by ${describeHolder(holder)}${describeEvidence(attempt.diagnosis)}; gave up after ${timeoutMs} ms`, { cause: attempt.diagnosis?.cause ?? attempt.error });
337
341
  }
338
342
  await pause(Math.min(delay * (0.5 + Math.random() / 2), remaining), signal);
339
343
  delay = Math.min(delay * 2, MAX_RETRY_MS);
@@ -1,4 +1,5 @@
1
1
  /** POSIX Node/Bun finite native commands, independent of the agent runtime. */
2
+ export { observeProcessInstance, type ProcessInstance, type ProcessInstanceObservation, ProcessInstanceSchema, type ProcessOwnerEvidence, probeProcessOwner, } from '../internal/process-instance.js';
2
3
  export { runNativeCommand } from '../process/command.js';
3
- export { NativeCommandError, type NativeCommandOptions, type NativeCommandResult, } from '../process/contract.js';
4
+ export { NativeCommandError, type NativeCommandOptions, type NativeCommandResult, type NativeCommandSettlement, } from '../process/contract.js';
4
5
  //# sourceMappingURL=process.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/entrypoints/process.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,kBAAkB,EAClB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,GACzB,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/entrypoints/process.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,OAAO,EACL,sBAAsB,EACtB,KAAK,eAAe,EACpB,KAAK,0BAA0B,EAC/B,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,iBAAiB,GAClB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,kBAAkB,EAClB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,GAC7B,MAAM,qBAAqB,CAAC"}
@@ -1,13 +1,20 @@
1
+ import {
2
+ ProcessInstanceSchema2,
3
+ observeProcessInstance2,
4
+ probeProcessOwner2
5
+ } from "../index-nybn2err.js";
1
6
  import {
2
7
  NativeCommandError2,
3
8
  startNativeCommand
4
- } from "../index-eeqmg7qm.js";
5
-
9
+ } from "../index-6m6a9ty4.js";
6
10
  // src/process/command.ts
7
11
  function runNativeCommand(input) {
8
12
  return startNativeCommand(input).result;
9
13
  }
10
14
  export {
11
15
  NativeCommandError2 as NativeCommandError,
16
+ ProcessInstanceSchema2 as ProcessInstanceSchema,
17
+ observeProcessInstance2 as observeProcessInstance,
18
+ probeProcessOwner2 as probeProcessOwner,
12
19
  runNativeCommand
13
20
  };
@@ -4,6 +4,14 @@ import {
4
4
 
5
5
  // src/process/contract.ts
6
6
  import { z } from "zod";
7
+ var NativeCommandSettlementSchema = z.discriminatedUnion("kind", [
8
+ z.object({
9
+ kind: z.literal("exit"),
10
+ exitCode: z.number().int().nullable(),
11
+ signal: z.string().nullable()
12
+ }),
13
+ z.object({ kind: z.literal("error"), cause: z.unknown() })
14
+ ]);
7
15
  var NativeCommandOptionsSchema = z.strictObject({
8
16
  executable: z.string().min(1),
9
17
  args: z.array(z.string()).default([]),
@@ -17,6 +25,7 @@ var NativeCommandOptionsSchema = z.strictObject({
17
25
  stdin: z.custom((v) => v instanceof Uint8Array).optional(),
18
26
  maxStdinBytes: z.number().int().positive().default(1024 * 1024),
19
27
  onOutput: z.custom((v) => typeof v === "function").optional(),
28
+ onLeaderSettled: z.custom((v) => typeof v === "function").optional(),
20
29
  killGraceMs: z.number().int().nonnegative().max(1e4).default(100),
21
30
  cleanupTimeoutMs: z.number().int().positive().max(30000).default(2000)
22
31
  }).superRefine((input, ctx) => {
@@ -50,6 +59,9 @@ class NativeCommandError2 extends Error {
50
59
  import { spawn } from "node:child_process";
51
60
 
52
61
  // src/process/group.ts
62
+ function deniedOnDarwin(error) {
63
+ return process.platform === "darwin" && error instanceof Error && "code" in error && error.code === "EPERM";
64
+ }
53
65
  function signalGroup(pid, signal) {
54
66
  if (pid === undefined)
55
67
  return;
@@ -69,15 +81,30 @@ function exists(pid) {
69
81
  } catch (error) {
70
82
  if (error instanceof Error && "code" in error && error.code === "ESRCH")
71
83
  return false;
84
+ if (deniedOnDarwin(error))
85
+ return true;
72
86
  throw error;
73
87
  }
74
88
  }
75
- async function stopCommandGroup(pid, graceMs) {
76
- signalGroup(pid, "SIGTERM");
89
+ async function signalUntilSettled(pid, signal, deadline) {
90
+ for (;; ) {
91
+ try {
92
+ signalGroup(pid, signal);
93
+ return;
94
+ } catch (error) {
95
+ if (!deniedOnDarwin(error) || Date.now() >= deadline)
96
+ throw error;
97
+ await new Promise((resolve) => setTimeout(resolve, Math.min(10, deadline - Date.now())));
98
+ }
99
+ }
100
+ }
101
+ async function stopCommandGroup(pid, graceMs, cleanupTimeoutMs = 2000) {
102
+ const deadline = Date.now() + graceMs + cleanupTimeoutMs;
103
+ await signalUntilSettled(pid, "SIGTERM", deadline);
77
104
  const until = Date.now() + graceMs;
78
105
  while (Date.now() < until && exists(pid))
79
106
  await new Promise((resolve) => setTimeout(resolve, Math.min(10, until - Date.now())));
80
- signalGroup(pid, "SIGKILL");
107
+ await signalUntilSettled(pid, "SIGKILL", deadline);
81
108
  }
82
109
  function commandCleanupError(cause) {
83
110
  return new NativeCommandError2("COMMAND_CLEANUP", "Command cleanup did not complete", {
@@ -88,6 +115,39 @@ function waitForCommandClose(closed, timeoutMs) {
88
115
  return raceAbort(closed, AbortSignal.timeout(timeoutMs));
89
116
  }
90
117
 
118
+ // src/process/leader-settlement.ts
119
+ function createLeaderSettlement(options, signal) {
120
+ let pending;
121
+ return (event) => {
122
+ if (pending)
123
+ return pending;
124
+ pending = (async () => {
125
+ if (!options.onLeaderSettled)
126
+ return;
127
+ const controller = new AbortController;
128
+ const timer = setTimeout(() => controller.abort(new Error("Leader settlement deadline exceeded")), options.cleanupTimeoutMs);
129
+ const abort = () => controller.abort(signal.reason);
130
+ if (!signal.aborted)
131
+ signal.addEventListener("abort", abort, { once: true });
132
+ try {
133
+ await raceAbort(Promise.resolve(options.onLeaderSettled(event, controller.signal)), controller.signal);
134
+ } catch (cause) {
135
+ if (signal.aborted && cause === signal.reason)
136
+ throw cause;
137
+ throw commandCleanupError(new AggregateError([event, cause], "Leader settlement failed"));
138
+ } finally {
139
+ clearTimeout(timer);
140
+ signal.removeEventListener("abort", abort);
141
+ controller.abort(new Error("Leader settlement completed"));
142
+ }
143
+ })();
144
+ pending.catch(() => {
145
+ return;
146
+ });
147
+ return pending;
148
+ };
149
+ }
150
+
91
151
  // src/process/command-owner.ts
92
152
  function startNativeCommand(input, onSpawn) {
93
153
  const options = NativeCommandOptionsSchema.parse(input);
@@ -119,6 +179,19 @@ function startNativeCommand(input, onSpawn) {
119
179
  let total = 0;
120
180
  let finished = false;
121
181
  let spawnFailure;
182
+ const settle = createLeaderSettlement(options, controller.signal);
183
+ const leader = new Promise((resolve) => {
184
+ child.once("exit", (exitCode, signal) => resolve({ kind: "exit", exitCode, signal }));
185
+ child.once("error", (cause) => resolve({ kind: "error", cause }));
186
+ });
187
+ const pendingHandles = new Set(["leader", "stdin", "stdout", "stderr"]);
188
+ const released = Promise.all([
189
+ leader.then(() => pendingHandles.delete("leader")),
190
+ ...["stdin", "stdout", "stderr"].map((name) => new Promise((resolve) => child[name].once("close", () => {
191
+ pendingHandles.delete(name);
192
+ resolve();
193
+ })))
194
+ ]);
122
195
  const closed = new Promise((resolve) => {
123
196
  child.once("error", (error) => {
124
197
  spawnFailure = new NativeCommandError2("COMMAND_UNAVAILABLE", "Command could not start", {
@@ -157,7 +230,7 @@ function startNativeCommand(input, onSpawn) {
157
230
  drain(child.stdout, "stdout", stdout),
158
231
  drain(child.stderr, "stderr", stderr)
159
232
  ]);
160
- await raceAbort(drains, controller.signal);
233
+ await raceAbort(Promise.all([drains, leader.then(settle)]), controller.signal);
161
234
  const status = await raceAbort(closed, controller.signal);
162
235
  if (spawnFailure !== undefined)
163
236
  throw spawnFailure;
@@ -167,11 +240,21 @@ function startNativeCommand(input, onSpawn) {
167
240
  child.stdin.destroy();
168
241
  child.stdout.destroy();
169
242
  child.stderr.destroy();
170
- try {
171
- await stopCommandGroup(child.pid, options.killGraceMs);
172
- await waitForCommandClose(closed, options.cleanupTimeoutMs);
173
- } catch (cleanup) {
174
- throw commandCleanupError(new AggregateError([error, cleanup], "Command failed and cleanup failed"));
243
+ const cleanup = await Promise.allSettled([
244
+ settle({ kind: "error", cause: error }),
245
+ stopCommandGroup(child.pid, options.killGraceMs, options.cleanupTimeoutMs).then(async () => {
246
+ try {
247
+ await waitForCommandClose(released, options.cleanupTimeoutMs);
248
+ } catch (cause) {
249
+ throw new Error(`Command handles remained open: ${[...pendingHandles].join(", ")}`, {
250
+ cause
251
+ });
252
+ }
253
+ })
254
+ ]);
255
+ const failures = cleanup.flatMap((item) => item.status === "rejected" && item.reason !== error ? [item.reason] : []);
256
+ if (failures.length) {
257
+ throw commandCleanupError(new AggregateError([error, ...failures], "Command failed and cleanup failed"));
175
258
  }
176
259
  throw error;
177
260
  } finally {
@@ -0,0 +1,184 @@
1
+ import {
2
+ machineIdentity,
3
+ ProcessInstanceSchema2,
4
+ readProcessInstance,
5
+ probeProcessOwner2
6
+ } from "./index-nybn2err.js";
7
+ import {
8
+ isRecord
9
+ } from "./index-9jfdm5tg.js";
10
+
11
+ // src/internal/exclusive-lock.ts
12
+ import { constants } from "node:fs";
13
+ import { lstat, open, unlink } from "node:fs/promises";
14
+ import { hostname } from "node:os";
15
+ import { z } from "zod";
16
+ var ExclusiveLockOwnerSchema = z.object({
17
+ pid: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
18
+ host: z.string().min(1),
19
+ acquiredAt: z.string(),
20
+ machine: z.string().min(1).optional().catch(undefined),
21
+ process: ProcessInstanceSchema2.nullable().optional().catch(null)
22
+ });
23
+ function isCode(error, code) {
24
+ return isRecord(error) && error.code === code;
25
+ }
26
+ function readOwner(text) {
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(text);
30
+ } catch {
31
+ return;
32
+ }
33
+ const owner = ExclusiveLockOwnerSchema.safeParse(parsed);
34
+ return owner.success ? owner.data : undefined;
35
+ }
36
+ function attribute(owner, identity) {
37
+ if (identity !== null && owner.machine !== undefined) {
38
+ return owner.machine === identity ? "this-machine" : "another-machine";
39
+ }
40
+ return owner.host === hostname() ? "this-machine" : "unattributable";
41
+ }
42
+ async function diagnose(owner, declaredIdentity) {
43
+ if (!owner)
44
+ return { attribution: "unattributable", liveness: "not-probed", owner: null };
45
+ const attribution = attribute(owner, await machineIdentity(declaredIdentity));
46
+ if (attribution !== "this-machine")
47
+ return { attribution, liveness: "not-probed", owner };
48
+ return { attribution, ...await probeProcessOwner2(owner.pid, owner.process), owner };
49
+ }
50
+ async function readLockFile(path, onFailure) {
51
+ let handle;
52
+ try {
53
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
54
+ const info = await handle.stat();
55
+ return {
56
+ owner: readOwner(await handle.readFile("utf8")),
57
+ ino: info.ino,
58
+ mtimeMs: info.mtimeMs
59
+ };
60
+ } catch (cause) {
61
+ if (!isCode(cause, "ENOENT"))
62
+ onFailure?.(cause);
63
+ return;
64
+ } finally {
65
+ await handle?.close().catch(() => {
66
+ return;
67
+ });
68
+ }
69
+ }
70
+ async function unlinkIfSame(path, ino) {
71
+ try {
72
+ if ((await lstat(path)).ino !== ino)
73
+ return;
74
+ await unlink(path);
75
+ } catch (error) {
76
+ if (!isCode(error, "ENOENT"))
77
+ throw error;
78
+ }
79
+ }
80
+ async function createOwned(path, options, reclaimed) {
81
+ const identity = await machineIdentity(options.machineIdentity);
82
+ const instance = await readProcessInstance(process.pid);
83
+ const handle = await open(path, "wx", options.mode);
84
+ const owner = {
85
+ pid: process.pid,
86
+ process: instance,
87
+ host: hostname(),
88
+ acquiredAt: new Date().toISOString(),
89
+ ...identity !== null && { machine: identity }
90
+ };
91
+ let ino;
92
+ try {
93
+ ino = (await handle.stat()).ino;
94
+ await handle.chmod(options.mode);
95
+ await handle.writeFile(`${JSON.stringify(owner)}
96
+ `, "utf8");
97
+ } catch (error) {
98
+ await handle.close().catch(() => {
99
+ return;
100
+ });
101
+ if (ino !== undefined)
102
+ await unlinkIfSame(path, ino).catch(() => {
103
+ return;
104
+ });
105
+ throw error;
106
+ }
107
+ const ownedInode = ino;
108
+ let released = false;
109
+ return {
110
+ path,
111
+ owner,
112
+ reclaimed,
113
+ async release() {
114
+ if (released)
115
+ return;
116
+ released = true;
117
+ try {
118
+ await handle.close();
119
+ } finally {
120
+ await unlinkIfSame(path, ownedInode);
121
+ }
122
+ }
123
+ };
124
+ }
125
+ function reclaimable(state, diagnosis, graceMs) {
126
+ if (state.owner)
127
+ return diagnosis.liveness === "gone";
128
+ return typeof graceMs === "number" && Date.now() - state.mtimeMs >= graceMs;
129
+ }
130
+ async function reclaimUnderGuard(path, options) {
131
+ const guardPath = `${path}.reclaim`;
132
+ const acquired = await attemptExclusiveLock(guardPath, {
133
+ ...options,
134
+ ownerlessGraceMs: options.ownerlessGraceMs === undefined ? 5000 : options.ownerlessGraceMs
135
+ });
136
+ if (!("held" in acquired))
137
+ return;
138
+ const guard = acquired.held;
139
+ try {
140
+ const state = await readLockFile(path);
141
+ if (state) {
142
+ const diagnosis = await diagnose(state.owner, options.machineIdentity);
143
+ if (!reclaimable(state, diagnosis, options.ownerlessGraceMs))
144
+ return "refused";
145
+ await unlinkIfSame(path, state.ino);
146
+ }
147
+ return await createOwned(path, options, state !== undefined).catch((error) => {
148
+ if (isCode(error, "EEXIST"))
149
+ return "refused";
150
+ throw error;
151
+ });
152
+ } finally {
153
+ await guard.release().catch(() => {
154
+ return;
155
+ });
156
+ }
157
+ }
158
+ async function attemptExclusiveLock(path, options) {
159
+ try {
160
+ return { held: await createOwned(path, options, false) };
161
+ } catch (error) {
162
+ if (!isCode(error, "EEXIST"))
163
+ throw error;
164
+ if (!options.reclaim)
165
+ return { error };
166
+ let readFailure;
167
+ const state = await readLockFile(path, (cause) => {
168
+ readFailure = cause;
169
+ });
170
+ const diagnosis = {
171
+ ...await diagnose(state?.owner, options.machineIdentity),
172
+ ...readFailure !== undefined && { cause: readFailure }
173
+ };
174
+ if (!state || !reclaimable(state, diagnosis, options.ownerlessGraceMs)) {
175
+ return { error, diagnosis };
176
+ }
177
+ const reclaimed = await reclaimUnderGuard(path, options);
178
+ if (reclaimed !== undefined && reclaimed !== "refused")
179
+ return { held: reclaimed };
180
+ return { error, diagnosis };
181
+ }
182
+ }
183
+
184
+ export { attemptExclusiveLock };