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.
- package/CHANGELOG.md +57 -0
- package/dist/agent-runtime/provider-origin.d.ts +7 -0
- package/dist/agent-runtime/provider-origin.d.ts.map +1 -1
- package/dist/agent-runtime/run-operation-lifecycle.d.ts.map +1 -1
- package/dist/application/directory-inbox-contract.d.ts +129 -0
- package/dist/application/directory-inbox-contract.d.ts.map +1 -0
- package/dist/application/directory-inbox.d.ts +3 -0
- package/dist/application/directory-inbox.d.ts.map +1 -0
- package/dist/application/process-lifecycle-transitions.d.ts +175 -0
- package/dist/application/process-lifecycle-transitions.d.ts.map +1 -0
- package/dist/application/process-lifecycle.d.ts +3 -131
- package/dist/application/process-lifecycle.d.ts.map +1 -1
- package/dist/entrypoints/agent-runtime/harness.js +1 -1
- package/dist/entrypoints/agent-runtime.js +1 -1
- package/dist/entrypoints/application/directory-inbox.d.ts +11 -0
- package/dist/entrypoints/application/directory-inbox.d.ts.map +1 -0
- package/dist/entrypoints/application/directory-inbox.js +221 -0
- package/dist/entrypoints/application.d.ts +3 -1
- package/dist/entrypoints/application.d.ts.map +1 -1
- package/dist/entrypoints/application.js +151 -56
- package/dist/entrypoints/server.js +6 -219
- package/dist/index-5jrd0yng.js +114 -0
- package/dist/{index-7f68ghdn.js → index-8zja4pkf.js} +23 -1
- package/dist/index-tm88avnj.js +221 -0
- package/llms/application-directory-inbox.txt +9 -0
- package/llms/application.2.txt +7 -5
- package/llms/application.txt +88 -24
- package/llms/stitchkit.txt +4 -3
- package/llms/{upgrading-0.37.0-0.50.0.txt → upgrading-0.37.0-0.53.0.txt} +25 -3
- package/llms/{upgrading-0.53.0-0.67.0.txt → upgrading-0.55.0-0.68.0.txt} +53 -25
- package/llms/{upgrading-0.68.0-0.94.0.txt → upgrading-0.69.0-0.95.0.txt} +45 -53
- package/llms/upgrading.txt +3 -3
- package/llms-full.txt +141 -32
- package/llms.txt +6 -5
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
package/llms/application.2.txt
CHANGED
|
@@ -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
|
|
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` / `
|
|
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 |
|