stitchkit 0.94.1 → 0.95.1

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/agent-runtime/provider-origin.d.ts +7 -0
  3. package/dist/agent-runtime/provider-origin.d.ts.map +1 -1
  4. package/dist/agent-runtime/run-operation-lifecycle.d.ts.map +1 -1
  5. package/dist/application/directory-inbox-contract.d.ts +129 -0
  6. package/dist/application/directory-inbox-contract.d.ts.map +1 -0
  7. package/dist/application/directory-inbox.d.ts +3 -0
  8. package/dist/application/directory-inbox.d.ts.map +1 -0
  9. package/dist/application/process-lifecycle-transitions.d.ts +175 -0
  10. package/dist/application/process-lifecycle-transitions.d.ts.map +1 -0
  11. package/dist/application/process-lifecycle.d.ts +3 -131
  12. package/dist/application/process-lifecycle.d.ts.map +1 -1
  13. package/dist/entrypoints/agent-runtime/harness.js +1 -1
  14. package/dist/entrypoints/agent-runtime.js +1 -1
  15. package/dist/entrypoints/application/directory-inbox.d.ts +11 -0
  16. package/dist/entrypoints/application/directory-inbox.d.ts.map +1 -0
  17. package/dist/entrypoints/application/directory-inbox.js +221 -0
  18. package/dist/entrypoints/application.d.ts +3 -1
  19. package/dist/entrypoints/application.d.ts.map +1 -1
  20. package/dist/entrypoints/application.js +151 -56
  21. package/dist/entrypoints/server.js +6 -219
  22. package/dist/index-5jrd0yng.js +114 -0
  23. package/dist/{index-7f68ghdn.js → index-8zja4pkf.js} +23 -1
  24. package/dist/index-tm88avnj.js +221 -0
  25. package/llms/application-directory-inbox.txt +9 -0
  26. package/llms/application.2.txt +7 -5
  27. package/llms/application.txt +88 -24
  28. package/llms/stitchkit.txt +4 -3
  29. package/llms/{upgrading-0.37.0-0.50.0.txt → upgrading-0.37.0-0.53.0.txt} +25 -3
  30. package/llms/{upgrading-0.53.0-0.67.0.txt → upgrading-0.55.0-0.68.0.txt} +53 -25
  31. package/llms/{upgrading-0.68.0-0.94.0.txt → upgrading-0.69.0-0.95.0.txt} +45 -53
  32. package/llms/upgrading.txt +3 -3
  33. package/llms-full.txt +141 -32
  34. package/llms.txt +6 -5
  35. package/package.json +5 -1
@@ -21,6 +21,9 @@ import {
21
21
  webSocketLane
22
22
  } from "../index-p3vsv1fb.js";
23
23
  import"../index-y91ry9h2.js";
24
+ import {
25
+ createFileStateStore
26
+ } from "../index-tm88avnj.js";
24
27
  import {
25
28
  withDeadline
26
29
  } from "../index-b9aefrqd.js";
@@ -113,222 +116,6 @@ import {
113
116
  import {
114
117
  isRecord
115
118
  } from "../index-hffjpmq9.js";
116
-
117
- // src/application/file-state-store.ts
118
- import { mkdir, open, readdir, readFile, rename, stat, unlink } from "node:fs/promises";
119
- import { basename, dirname, join } from "node:path";
120
- var ABANDONED_LOCK_MULTIPLIER = 10;
121
- var ABANDONED_HEARTBEATS = 30;
122
- var MIN_HEARTBEAT_MS = 10;
123
- var delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
124
- async function missing(error) {
125
- return error instanceof Error && "code" in error && error.code === "ENOENT";
126
- }
127
- function existing(error) {
128
- return error instanceof Error && "code" in error && error.code === "EEXIST";
129
- }
130
- async function ownerState(lockPath) {
131
- let owner;
132
- try {
133
- owner = JSON.parse(await readFile(lockPath, "utf8"));
134
- } catch {
135
- return "unknown";
136
- }
137
- if (typeof owner !== "object" || owner === null || !("pid" in owner) || typeof owner.pid !== "number" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) {
138
- return "unknown";
139
- }
140
- try {
141
- process.kill(owner.pid, 0);
142
- return "alive";
143
- } catch (error) {
144
- return error instanceof Error && "code" in error && error.code === "ESRCH" ? "gone" : "unknown";
145
- }
146
- }
147
- async function sweepTemporaries(path, staleLockMs) {
148
- const prefix = `${basename(path)}.tmp.`;
149
- const directory = dirname(path);
150
- let entries;
151
- try {
152
- entries = await readdir(directory);
153
- } catch {
154
- return;
155
- }
156
- for (const entry of entries) {
157
- if (!entry.startsWith(prefix))
158
- continue;
159
- const candidate = join(directory, entry);
160
- const info = await stat(candidate).catch(() => null);
161
- if (info && Date.now() - info.mtimeMs > staleLockMs) {
162
- await unlink(candidate).catch(() => {
163
- return;
164
- });
165
- }
166
- }
167
- }
168
- async function reclaim(lockPath, observed, staleLockMs, abandonedAfterMs) {
169
- const guardPath = `${lockPath}.reclaim`;
170
- let guard;
171
- try {
172
- guard = await open(guardPath, "wx");
173
- } catch (error) {
174
- if (!existing(error))
175
- throw error;
176
- const orphan = await stat(guardPath).catch(() => null);
177
- if (orphan && Date.now() - orphan.mtimeMs > staleLockMs) {
178
- await unlink(guardPath).catch(() => {
179
- return;
180
- });
181
- }
182
- return false;
183
- }
184
- try {
185
- const current = await stat(lockPath).catch(() => null);
186
- if (!current || current.dev !== observed.dev || current.ino !== observed.ino || current.mtimeMs !== observed.mtimeMs) {
187
- return false;
188
- }
189
- const owner = await ownerState(lockPath);
190
- const age = Date.now() - current.mtimeMs;
191
- if (owner !== "gone" && age < abandonedAfterMs)
192
- return false;
193
- await unlink(lockPath);
194
- return true;
195
- } finally {
196
- await guard.close().catch(() => {
197
- return;
198
- });
199
- await unlink(guardPath).catch(() => {
200
- return;
201
- });
202
- }
203
- }
204
- function createFileStateStore(path, options) {
205
- const lockPath = `${path}.lock`;
206
- const lockTimeoutMs = options.lockTimeoutMs ?? 1e4;
207
- const staleLockMs = options.staleLockMs ?? 3000;
208
- const retryMs = options.retryMs ?? 10;
209
- const heartbeatMs = Math.max(MIN_HEARTBEAT_MS, Math.floor(staleLockMs / 3));
210
- const abandonedAfterMs = Math.max(staleLockMs * ABANDONED_LOCK_MULTIPLIER, heartbeatMs * ABANDONED_HEARTBEATS);
211
- if (staleLockMs >= lockTimeoutMs) {
212
- throw new Error(`[stitchkit] createFileStateStore: staleLockMs (${staleLockMs}) must be below lockTimeoutMs (${lockTimeoutMs}), or a crashed holder blocks every update until the timeout`);
213
- }
214
- let sweptTemporaries = false;
215
- const parseFile = async () => {
216
- let source;
217
- try {
218
- source = await readFile(path, "utf8");
219
- } catch (error) {
220
- if (await missing(error))
221
- return null;
222
- throw error;
223
- }
224
- try {
225
- return options.schema.parse(JSON.parse(source));
226
- } catch (error) {
227
- await options.onCorrupt?.({ path, error });
228
- if ((options.corrupt ?? "throw") === "empty")
229
- return null;
230
- throw error;
231
- }
232
- };
233
- const acquire = async () => {
234
- await mkdir(dirname(path), { recursive: true });
235
- if (!sweptTemporaries) {
236
- sweptTemporaries = true;
237
- await sweepTemporaries(path, staleLockMs);
238
- }
239
- const deadline = Date.now() + lockTimeoutMs;
240
- while (true) {
241
- try {
242
- const handle = await open(lockPath, "wx");
243
- const token = crypto.randomUUID();
244
- try {
245
- await handle.writeFile(JSON.stringify({ token, pid: process.pid, acquiredAt: Date.now() }));
246
- await handle.sync();
247
- } catch (error) {
248
- await handle.close().catch(() => {
249
- return;
250
- });
251
- await unlink(lockPath).catch(() => {
252
- return;
253
- });
254
- throw error;
255
- }
256
- return {
257
- refresh: async () => {
258
- const now = new Date;
259
- await handle.utimes(now, now);
260
- },
261
- release: async () => {
262
- await handle.close();
263
- try {
264
- const owner = JSON.parse(await readFile(lockPath, "utf8"));
265
- if (isRecord(owner) && owner.token === token)
266
- await unlink(lockPath);
267
- } catch (error) {
268
- if (!await missing(error))
269
- throw error;
270
- }
271
- }
272
- };
273
- } catch (error) {
274
- if (!existing(error))
275
- throw error;
276
- try {
277
- const lock = await stat(lockPath);
278
- if (Date.now() - lock.mtimeMs > staleLockMs) {
279
- if (await reclaim(lockPath, lock, staleLockMs, abandonedAfterMs))
280
- continue;
281
- }
282
- } catch (statError) {
283
- if (await missing(statError))
284
- continue;
285
- throw statError;
286
- }
287
- if (Date.now() >= deadline) {
288
- throw new Error(`[stitchkit] timed out acquiring state lock for ${path}`);
289
- }
290
- await delay(retryMs);
291
- }
292
- }
293
- };
294
- const write = async (state) => {
295
- const validated = options.schema.parse(state);
296
- const temporary = `${path}.tmp.${process.pid}.${crypto.randomUUID()}`;
297
- const handle = await open(temporary, "wx");
298
- try {
299
- try {
300
- await handle.writeFile(`${JSON.stringify(validated)}
301
- `);
302
- await handle.sync();
303
- } finally {
304
- await handle.close();
305
- }
306
- await rename(temporary, path);
307
- } catch (error) {
308
- await unlink(temporary).catch(() => {
309
- return;
310
- });
311
- throw error;
312
- }
313
- };
314
- return {
315
- read: parseFile,
316
- async update(transition) {
317
- const lock = await acquire();
318
- const heartbeat = setInterval(() => void lock.refresh().catch(() => {
319
- return;
320
- }), heartbeatMs);
321
- try {
322
- const next = await transition(await parseFile());
323
- await write(next.state);
324
- return next.result;
325
- } finally {
326
- clearInterval(heartbeat);
327
- await lock.release();
328
- }
329
- }
330
- };
331
- }
332
119
  // src/realtime/registry.ts
333
120
  var authorizedRoom = Symbol("stitchkit.authorized-room");
334
121
  function freezeRoom(name) {
@@ -1001,7 +788,7 @@ function createEventBus(options = {}) {
1001
788
  };
1002
789
  }
1003
790
  // src/server/file.ts
1004
- import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
791
+ import { readFile, stat } from "node:fs/promises";
1005
792
  import { resolve } from "node:path";
1006
793
 
1007
794
  // src/server/mime.ts
@@ -1061,10 +848,10 @@ function staticRoute(prefix, dir) {
1061
848
  const realTarget = await realPathWithinDir(root, target);
1062
849
  if (realTarget === null)
1063
850
  return new Response("Not found", { status: 404 });
1064
- const info = await stat2(realTarget).catch(() => null);
851
+ const info = await stat(realTarget).catch(() => null);
1065
852
  if (!info?.isFile())
1066
853
  return new Response("Not found", { status: 404 });
1067
- return new Response(await readFile2(realTarget), {
854
+ return new Response(await readFile(realTarget), {
1068
855
  headers: {
1069
856
  "Content-Type": mimeForPath(realTarget),
1070
857
  "X-Content-Type-Options": "nosniff"
@@ -0,0 +1,114 @@
1
+ // src/application/directory-inbox-contract.ts
2
+ import { z } from "zod";
3
+ var timestamp = z.string().datetime({ offset: true });
4
+ var key = z.string().min(1).max(255);
5
+ var DirectoryInboxRejectionReasonSchema = z.enum([
6
+ "invalid",
7
+ "too-large",
8
+ "attempt-limit"
9
+ ]);
10
+ var DirectoryInboxStateSchema = z.object({
11
+ schemaVersion: z.literal(1),
12
+ claims: z.array(z.object({
13
+ key,
14
+ attempts: z.number().int().positive(),
15
+ nextAttemptAt: timestamp,
16
+ leaseId: z.string().min(1).max(128).nullable(),
17
+ leaseUntil: timestamp.nullable()
18
+ }).strict()),
19
+ receipts: z.array(z.object({ key, completedAt: timestamp }).strict()),
20
+ rejected: z.array(z.object({
21
+ key,
22
+ reason: DirectoryInboxRejectionReasonSchema,
23
+ detail: z.string().max(1000),
24
+ rejectedAt: timestamp
25
+ }).strict())
26
+ }).strict();
27
+ var emptyDirectoryInboxState = () => ({
28
+ schemaVersion: 1,
29
+ claims: [],
30
+ receipts: [],
31
+ rejected: []
32
+ });
33
+ var iso = (milliseconds) => new Date(milliseconds).toISOString();
34
+ function withRejection(state, rejection, retain) {
35
+ return {
36
+ ...state,
37
+ claims: state.claims.filter((claim) => claim.key !== rejection.key),
38
+ rejected: [
39
+ rejection,
40
+ ...state.rejected.filter((item) => item.key !== rejection.key)
41
+ ].slice(0, retain)
42
+ };
43
+ }
44
+ function claimEntry(state, input) {
45
+ if (state.receipts.some((receipt) => receipt.key === input.key)) {
46
+ return { state, step: { kind: "remove" } };
47
+ }
48
+ if (state.rejected.some((rejection) => rejection.key === input.key)) {
49
+ return { state, step: { kind: "set-aside" } };
50
+ }
51
+ const now = input.now.getTime();
52
+ const claim = state.claims.find((candidate) => candidate.key === input.key);
53
+ if (claim) {
54
+ const leased = claim.leaseUntil !== null && Date.parse(claim.leaseUntil) > now;
55
+ if (leased || Date.parse(claim.nextAttemptAt) > now)
56
+ return { state, step: { kind: "skip" } };
57
+ if (claim.attempts >= input.maxAttempts) {
58
+ const rejection = {
59
+ key: input.key,
60
+ reason: "attempt-limit",
61
+ detail: `taken ${claim.attempts} times without completing`,
62
+ rejectedAt: iso(now)
63
+ };
64
+ return {
65
+ state: withRejection(state, rejection, input.retain),
66
+ step: { kind: "reject", rejection }
67
+ };
68
+ }
69
+ }
70
+ const taken = {
71
+ key: input.key,
72
+ attempts: (claim?.attempts ?? 0) + 1,
73
+ nextAttemptAt: iso(now),
74
+ leaseId: crypto.randomUUID(),
75
+ leaseUntil: iso(now + input.leaseMs)
76
+ };
77
+ return {
78
+ state: {
79
+ ...state,
80
+ claims: [...state.claims.filter((candidate) => candidate.key !== input.key), taken]
81
+ },
82
+ step: { kind: "deliver", claim: taken }
83
+ };
84
+ }
85
+ function completeEntry(state, entryKey, now, retain) {
86
+ return {
87
+ ...state,
88
+ claims: state.claims.filter((claim) => claim.key !== entryKey),
89
+ receipts: [
90
+ { key: entryKey, completedAt: now.toISOString() },
91
+ ...state.receipts.filter((receipt) => receipt.key !== entryKey)
92
+ ].slice(0, retain)
93
+ };
94
+ }
95
+ function releaseEntry(state, claim, retryAt) {
96
+ return {
97
+ ...state,
98
+ claims: state.claims.map((candidate) => candidate.key === claim.key && candidate.leaseId === claim.leaseId ? {
99
+ ...candidate,
100
+ nextAttemptAt: retryAt.toISOString(),
101
+ leaseId: null,
102
+ leaseUntil: null
103
+ } : candidate)
104
+ };
105
+ }
106
+ function forgetMissing(state, present, now) {
107
+ const live = (claim) => claim.leaseUntil !== null && Date.parse(claim.leaseUntil) > now.getTime();
108
+ return {
109
+ ...state,
110
+ claims: state.claims.filter((claim) => present.has(claim.key) || live(claim))
111
+ };
112
+ }
113
+
114
+ export { DirectoryInboxRejectionReasonSchema, DirectoryInboxStateSchema, emptyDirectoryInboxState, withRejection, claimEntry, completeEntry, releaseEntry, forgetMissing };
@@ -1775,6 +1775,27 @@ function markProviderOrigin(error) {
1775
1775
  function hasProviderOrigin(error) {
1776
1776
  return typeof error === "object" && error !== null && providerOrigin.has(error);
1777
1777
  }
1778
+ function markProviderStream(stream) {
1779
+ const reader = stream.getReader();
1780
+ return new ReadableStream({
1781
+ async pull(controller) {
1782
+ let read;
1783
+ try {
1784
+ read = await reader.read();
1785
+ } catch (error) {
1786
+ controller.error(isOwnInputRefusal(error) ? error : markProviderOrigin(error));
1787
+ return;
1788
+ }
1789
+ if (read.done)
1790
+ controller.close();
1791
+ else
1792
+ controller.enqueue(read.value);
1793
+ },
1794
+ cancel(reason) {
1795
+ return reader.cancel(reason);
1796
+ }
1797
+ });
1798
+ }
1778
1799
 
1779
1800
  // src/agent-runtime/run-operation-lifecycle.ts
1780
1801
  function createAgentRunOperationLifecycle(config) {
@@ -1840,7 +1861,8 @@ function createAgentRunOperationLifecycle(config) {
1840
1861
  }
1841
1862
  await startModelRequest(callId, step);
1842
1863
  try {
1843
- return await doStream();
1864
+ const result = await doStream();
1865
+ return { ...result, stream: markProviderStream(result.stream) };
1844
1866
  } catch (error) {
1845
1867
  throw isOwnInputRefusal(error) ? error : markProviderOrigin(error);
1846
1868
  }
@@ -0,0 +1,221 @@
1
+ import {
2
+ isRecord
3
+ } from "./index-hffjpmq9.js";
4
+
5
+ // src/application/file-state-store.ts
6
+ import { mkdir, open, readdir, readFile, rename, stat, unlink } from "node:fs/promises";
7
+ import { basename, dirname, join } from "node:path";
8
+ var ABANDONED_LOCK_MULTIPLIER = 10;
9
+ var ABANDONED_HEARTBEATS = 30;
10
+ var MIN_HEARTBEAT_MS = 10;
11
+ var delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
12
+ async function missing(error) {
13
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
14
+ }
15
+ function existing(error) {
16
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
17
+ }
18
+ async function ownerState(lockPath) {
19
+ let owner;
20
+ try {
21
+ owner = JSON.parse(await readFile(lockPath, "utf8"));
22
+ } catch {
23
+ return "unknown";
24
+ }
25
+ if (typeof owner !== "object" || owner === null || !("pid" in owner) || typeof owner.pid !== "number" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) {
26
+ return "unknown";
27
+ }
28
+ try {
29
+ process.kill(owner.pid, 0);
30
+ return "alive";
31
+ } catch (error) {
32
+ return error instanceof Error && "code" in error && error.code === "ESRCH" ? "gone" : "unknown";
33
+ }
34
+ }
35
+ async function sweepTemporaries(path, staleLockMs) {
36
+ const prefix = `${basename(path)}.tmp.`;
37
+ const directory = dirname(path);
38
+ let entries;
39
+ try {
40
+ entries = await readdir(directory);
41
+ } catch {
42
+ return;
43
+ }
44
+ for (const entry of entries) {
45
+ if (!entry.startsWith(prefix))
46
+ continue;
47
+ const candidate = join(directory, entry);
48
+ const info = await stat(candidate).catch(() => null);
49
+ if (info && Date.now() - info.mtimeMs > staleLockMs) {
50
+ await unlink(candidate).catch(() => {
51
+ return;
52
+ });
53
+ }
54
+ }
55
+ }
56
+ async function reclaim(lockPath, observed, staleLockMs, abandonedAfterMs) {
57
+ const guardPath = `${lockPath}.reclaim`;
58
+ let guard;
59
+ try {
60
+ guard = await open(guardPath, "wx");
61
+ } catch (error) {
62
+ if (!existing(error))
63
+ throw error;
64
+ const orphan = await stat(guardPath).catch(() => null);
65
+ if (orphan && Date.now() - orphan.mtimeMs > staleLockMs) {
66
+ await unlink(guardPath).catch(() => {
67
+ return;
68
+ });
69
+ }
70
+ return false;
71
+ }
72
+ try {
73
+ const current = await stat(lockPath).catch(() => null);
74
+ if (!current || current.dev !== observed.dev || current.ino !== observed.ino || current.mtimeMs !== observed.mtimeMs) {
75
+ return false;
76
+ }
77
+ const owner = await ownerState(lockPath);
78
+ const age = Date.now() - current.mtimeMs;
79
+ if (owner !== "gone" && age < abandonedAfterMs)
80
+ return false;
81
+ await unlink(lockPath);
82
+ return true;
83
+ } finally {
84
+ await guard.close().catch(() => {
85
+ return;
86
+ });
87
+ await unlink(guardPath).catch(() => {
88
+ return;
89
+ });
90
+ }
91
+ }
92
+ function createFileStateStore(path, options) {
93
+ const lockPath = `${path}.lock`;
94
+ const lockTimeoutMs = options.lockTimeoutMs ?? 1e4;
95
+ const staleLockMs = options.staleLockMs ?? 3000;
96
+ const retryMs = options.retryMs ?? 10;
97
+ const heartbeatMs = Math.max(MIN_HEARTBEAT_MS, Math.floor(staleLockMs / 3));
98
+ const abandonedAfterMs = Math.max(staleLockMs * ABANDONED_LOCK_MULTIPLIER, heartbeatMs * ABANDONED_HEARTBEATS);
99
+ if (staleLockMs >= lockTimeoutMs) {
100
+ throw new Error(`[stitchkit] createFileStateStore: staleLockMs (${staleLockMs}) must be below lockTimeoutMs (${lockTimeoutMs}), or a crashed holder blocks every update until the timeout`);
101
+ }
102
+ let sweptTemporaries = false;
103
+ const parseFile = async () => {
104
+ let source;
105
+ try {
106
+ source = await readFile(path, "utf8");
107
+ } catch (error) {
108
+ if (await missing(error))
109
+ return null;
110
+ throw error;
111
+ }
112
+ try {
113
+ return options.schema.parse(JSON.parse(source));
114
+ } catch (error) {
115
+ await options.onCorrupt?.({ path, error });
116
+ if ((options.corrupt ?? "throw") === "empty")
117
+ return null;
118
+ throw error;
119
+ }
120
+ };
121
+ const acquire = async () => {
122
+ await mkdir(dirname(path), { recursive: true });
123
+ if (!sweptTemporaries) {
124
+ sweptTemporaries = true;
125
+ await sweepTemporaries(path, staleLockMs);
126
+ }
127
+ const deadline = Date.now() + lockTimeoutMs;
128
+ while (true) {
129
+ try {
130
+ const handle = await open(lockPath, "wx");
131
+ const token = crypto.randomUUID();
132
+ try {
133
+ await handle.writeFile(JSON.stringify({ token, pid: process.pid, acquiredAt: Date.now() }));
134
+ await handle.sync();
135
+ } catch (error) {
136
+ await handle.close().catch(() => {
137
+ return;
138
+ });
139
+ await unlink(lockPath).catch(() => {
140
+ return;
141
+ });
142
+ throw error;
143
+ }
144
+ return {
145
+ refresh: async () => {
146
+ const now = new Date;
147
+ await handle.utimes(now, now);
148
+ },
149
+ release: async () => {
150
+ await handle.close();
151
+ try {
152
+ const owner = JSON.parse(await readFile(lockPath, "utf8"));
153
+ if (isRecord(owner) && owner.token === token)
154
+ await unlink(lockPath);
155
+ } catch (error) {
156
+ if (!await missing(error))
157
+ throw error;
158
+ }
159
+ }
160
+ };
161
+ } catch (error) {
162
+ if (!existing(error))
163
+ throw error;
164
+ try {
165
+ const lock = await stat(lockPath);
166
+ if (Date.now() - lock.mtimeMs > staleLockMs) {
167
+ if (await reclaim(lockPath, lock, staleLockMs, abandonedAfterMs))
168
+ continue;
169
+ }
170
+ } catch (statError) {
171
+ if (await missing(statError))
172
+ continue;
173
+ throw statError;
174
+ }
175
+ if (Date.now() >= deadline) {
176
+ throw new Error(`[stitchkit] timed out acquiring state lock for ${path}`);
177
+ }
178
+ await delay(retryMs);
179
+ }
180
+ }
181
+ };
182
+ const write = async (state) => {
183
+ const validated = options.schema.parse(state);
184
+ const temporary = `${path}.tmp.${process.pid}.${crypto.randomUUID()}`;
185
+ const handle = await open(temporary, "wx");
186
+ try {
187
+ try {
188
+ await handle.writeFile(`${JSON.stringify(validated)}
189
+ `);
190
+ await handle.sync();
191
+ } finally {
192
+ await handle.close();
193
+ }
194
+ await rename(temporary, path);
195
+ } catch (error) {
196
+ await unlink(temporary).catch(() => {
197
+ return;
198
+ });
199
+ throw error;
200
+ }
201
+ };
202
+ return {
203
+ read: parseFile,
204
+ async update(transition) {
205
+ const lock = await acquire();
206
+ const heartbeat = setInterval(() => void lock.refresh().catch(() => {
207
+ return;
208
+ }), heartbeatMs);
209
+ try {
210
+ const next = await transition(await parseFile());
211
+ await write(next.state);
212
+ return next.result;
213
+ } finally {
214
+ clearInterval(heartbeat);
215
+ await lock.release();
216
+ }
217
+ }
218
+ };
219
+ }
220
+
221
+ export { createFileStateStore };
@@ -0,0 +1,9 @@
1
+ # stitchkit/application/directory-inbox — stitchkit docs slice
2
+
3
+ > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
+ > `llms.txt` in this package.
5
+
6
+ Contains: pointers only
7
+
8
+ Also read:
9
+ - see `llms/application.txt` for the Managed application kernel guide
@@ -290,15 +290,17 @@ cutovers are covered by the executable
290
290
  | `createApplication` | function | compose a validated resource DAG into one startup, readiness, admission and shutdown state machine |
291
291
  | `StateStore` / `StateStoreUpdate` | _type_ | async atomic read/update boundary shared by durable application state machines |
292
292
  | `createProcessLifecycleLedger` / `ProcessLifecycleLedger` / `ProcessLifecycleLedgerConfig` | function / _type_ | durable bounded run history and typed fact subscription |
293
- | `LifecycleRunSchema` / `LifecycleRun` / `LifecycleStateSchema` / `LifecycleState` / `LifecycleTerminationSchema` / `LifecycleTermination` | schema / _type_ | versioned process-run state and termination vocabulary |
294
- | `PreviousExitSchema` / `PreviousExit` / `StartFact` / `ReadyFact` / `ShutdownFact` / `ProcessLifecycleFact` | schema / _type_ | first-boot, hot-reload, clean, forced, handoff and abnormal lifecycle facts |
295
- | `transitionProcessStart` / `transitionProcessReady` / `transitionProcessShutdown` | function | pure idempotent runId+pid-owned transitions |
296
- | `LifecycleTransition` / `TransitionStartInput` / `TransitionReadyInput` / `TransitionShutdownInput` / `SameVersionOverlap` | _type_ | pure transition inputs, the same-build overlap policy (`abnormal` default / `handoff`) and state+fact result |
297
- | `lifecycleLedgerResource` / `LifecycleLedgerResource` / `LifecycleLedgerResourceConfig` | function / _type_ | managed-resource adapter that records start, readiness and shutdown |
293
+ | `LifecycleRunSchema` / `LifecycleRun` / `LifecycleStateSchema` / `LifecycleState` / `LifecycleTerminationSchema` / `LifecycleTermination` | schema / _type_ | versioned process-run state — open (`active`, `draining` with `unavailableAt`) and ended (`clean`, `forced`, `startup-failed`, `hot-reload`, `abnormal`) |
294
+ | `PreviousExitSchema` / `PreviousExit` / `StartFact` / `ReadyFact` / `DrainingFact` / `ShutdownFact` / `ProcessLifecycleFact` | schema / _type_ | first-boot, hot-reload, clean, forced, startup-failed, handoff and abnormal lifecycle facts; `processGapMs` on start, `downtimeMs` (unavailable → ready) on ready |
295
+ | `transitionProcessStart` / `transitionProcessReady` / `transitionProcessDraining` / `transitionProcessShutdown` | function | pure idempotent runId+pid-owned transitions |
296
+ | `LifecycleTransition` / `TransitionStartInput` / `TransitionRunInput` / `TransitionShutdownInput` / `SameVersionOverlap` | _type_ | pure transition inputs (`TransitionRunInput` for ready and draining, extended with `forced` for shutdown), the same-build overlap policy (`abnormal` default / `handoff`) and state+fact result |
297
+ | `lifecycleLedgerResource` / `LifecycleLedgerResource` / `LifecycleLedgerResourceConfig` | function / _type_ | managed-resource adapter that records start, readiness, admission stop (draining) and shutdown |
298
298
  | `createNotificationOutbox` / `NotificationOutbox` / `NotificationOutboxConfig` | function / _type_ | bounded at-least-once queue with durable claims, leases, retries and supersede |
299
299
  | `NotificationOutboxItem` / `NotificationOutboxReceipt` / `NotificationOutboxState` / `NotificationSend` / `NotificationFailureClassification` / `DroppedNotification` / `EnqueueNotification` | _type_ | versioned queue records, stable send identity and delivery decisions |
300
300
  | `notificationOutboxStateSchema` | function | strict persisted-state schema over an application payload schema |
301
301
  | `notificationOutboxResource` / `NotificationOutboxResource` / `NotificationOutboxResourceConfig` | function / _type_ | thin managed-resource scheduler over an outbox handle |
302
+ | `createDirectoryInbox` (from `stitchkit/application/directory-inbox`) / `DirectoryInbox` / `DirectoryInboxConfig` / `DirectoryInboxDelivery` / `DirectoryInboxResource` | function / _type_ | managed resource delivering each `<name>.json` entry another program drops into a directory to the application at least once, after readiness; separate durable taken/done records, leases, bounded retry |
303
+ | `DirectoryInboxStateSchema` / `DirectoryInboxState` / `DirectoryInboxRejection` / `DirectoryInboxRejectionReasonSchema` / `DirectoryInboxRejectionReason` | schema / _type_ | the inbox's claims, receipts and rejections (`invalid`, `too-large`, `attempt-limit`) |
302
304
  | `ApplicationHandle.restart` | method | replace one resource and everything that depends on it, leaving the rest of the graph running and the process epoch unchanged |
303
305
  | `ApplicationRestartInputSchema` / `ApplicationRestartInput` | schema / _type_ | the resource to replace, by id, and optionally `gracePeriodMs` / `forceTimeoutMs` for this restart — the application's own shutdown budget otherwise |
304
306
  | `ApplicationRestartResultSchema` / `ApplicationRestartResult` | schema / _type_ | the subtree that was actually taken down and brought back, in start order, with the outcome, the reason on anything but success, and how long it took |