@nowcrew/daemon 0.5.28 → 0.5.30

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 (68) hide show
  1. package/README.md +4 -0
  2. package/dist/attachments.js +196 -0
  3. package/dist/bound-im-decision.js +22 -0
  4. package/dist/completion-retransmitter.js +77 -0
  5. package/dist/computer-cli.js +274 -0
  6. package/dist/computer-profile-lock.js +395 -0
  7. package/dist/computer-profile.js +364 -0
  8. package/dist/computer-service.js +358 -0
  9. package/dist/config.js +82 -0
  10. package/dist/console-collapse.js +13 -0
  11. package/dist/console-formatter.js +77 -0
  12. package/dist/console-payload.js +73 -0
  13. package/dist/console.js +329 -0
  14. package/dist/daemon-startup-error.js +30 -0
  15. package/dist/execution-backend.js +44 -0
  16. package/dist/execution-event-limit.js +64 -0
  17. package/dist/execution-journal-lock.js +421 -0
  18. package/dist/execution-journal.js +716 -0
  19. package/dist/execution-protocol.js +342 -0
  20. package/dist/execution-recovery.js +95 -0
  21. package/dist/execution-runner.js +659 -0
  22. package/dist/execution-supervisor-child.js +236 -0
  23. package/dist/execution-supervisor.js +316 -0
  24. package/dist/execution-telemetry-journal.js +71 -0
  25. package/dist/external-output.js +114 -0
  26. package/dist/i18n.js +64 -0
  27. package/dist/json-result.js +27 -0
  28. package/dist/list-models.js +92 -0
  29. package/dist/local-executor.js +439 -0
  30. package/dist/log-format.js +10 -0
  31. package/dist/machine-info.js +124 -0
  32. package/dist/main.js +118 -0
  33. package/dist/normalize.js +170 -0
  34. package/dist/origin-decision.js +44 -0
  35. package/dist/platform.js +8 -0
  36. package/dist/prompt.js +307 -0
  37. package/dist/provider-env.js +90 -0
  38. package/dist/runner.js +234 -0
  39. package/dist/runtime-cancellation.js +74 -0
  40. package/dist/runtime-capabilities.js +43 -0
  41. package/dist/runtime-path.js +60 -0
  42. package/dist/runtimes/claude.js +51 -0
  43. package/dist/runtimes/codex-app-server-runner.js +541 -0
  44. package/dist/runtimes/codex-deepseek-catalog.js +7 -0
  45. package/dist/runtimes/codex-deepseek-config.js +50 -0
  46. package/dist/runtimes/codex.js +53 -0
  47. package/dist/runtimes/kimi-acp-runner.js +364 -0
  48. package/dist/runtimes/kimi.js +45 -0
  49. package/dist/runtimes/progress-watchdog.js +26 -0
  50. package/dist/scheduled-report.js +51 -0
  51. package/dist/scheduled-run-report.js +57 -0
  52. package/dist/serve-lifecycle.js +82 -0
  53. package/dist/serve.js +868 -0
  54. package/dist/session.js +82 -0
  55. package/dist/shared-execution-slots.js +68 -0
  56. package/dist/shutdown-deadline.js +32 -0
  57. package/dist/skill-preview.js +21 -0
  58. package/dist/skills.js +56 -0
  59. package/dist/slog.js +228 -0
  60. package/dist/supervised-runtime.js +104 -0
  61. package/dist/token.js +24 -0
  62. package/dist/unified-diff.js +84 -0
  63. package/dist/websocket-shutdown.js +53 -0
  64. package/dist/win32-job-object.js +193 -0
  65. package/dist/workspace-fs.js +80 -0
  66. package/dist/workspace-import.js +127 -0
  67. package/dist/workspace.js +148 -0
  68. package/package.json +1 -1
@@ -0,0 +1,395 @@
1
+ import { mkdir, open, opendir, readFile, readdir, rename, rm, rmdir, stat, unlink, } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import { z } from "zod";
5
+ import { defaultProcessController } from "./execution-journal.js";
6
+ const OwnerSchema = z.object({
7
+ pid: z.number().int().positive(),
8
+ processIdentity: z.string().min(1),
9
+ token: z.string().uuid(),
10
+ createdAt: z.number().int().nonnegative(),
11
+ }).strict();
12
+ const DEFAULT_TIMEOUT_MS = 30_000;
13
+ const DEFAULT_RETRY_MS = 25;
14
+ const DEFAULT_TEMP_STALE_AGE_MS = 24 * 60 * 60 * 1_000;
15
+ const DEFAULT_TEMP_SCAN_LIMIT = 64;
16
+ const DEFAULT_TEMP_DELETE_LIMIT = 16;
17
+ const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
18
+ const OWNER_NAME_PATTERN = new RegExp(`^owner\\.${UUID_PATTERN}\\.json$`, "i");
19
+ const TEMP_NAME_PATTERN = new RegExp(`^\\.save\\.lock\\.(${UUID_PATTERN})\\.tmp$`, "i");
20
+ const codeOf = (error) => error instanceof Error && "code" in error ? error.code : undefined;
21
+ const ownerName = (token) => `owner.${token}.json`;
22
+ export const defaultProfileSaveLockFileSystem = {
23
+ mkdir,
24
+ readFile: (path) => readFile(path, "utf8"),
25
+ readDirectoryEntries: async (path, limit) => {
26
+ const directory = await opendir(path);
27
+ const names = [];
28
+ try {
29
+ while (names.length < limit) {
30
+ const entry = await directory.read();
31
+ if (entry === null)
32
+ break;
33
+ names.push(entry.name);
34
+ }
35
+ }
36
+ finally {
37
+ await directory.close();
38
+ }
39
+ return names;
40
+ },
41
+ readdir: async (path) => readdir(path),
42
+ rename,
43
+ rm,
44
+ rmdir,
45
+ stat,
46
+ unlink,
47
+ writeDurableFile: async (path, contents) => {
48
+ const handle = await open(path, "wx", 0o600);
49
+ try {
50
+ await handle.writeFile(contents, "utf8");
51
+ await handle.sync();
52
+ }
53
+ finally {
54
+ await handle.close();
55
+ }
56
+ },
57
+ };
58
+ async function readGeneration(lockDirectory, fileSystem) {
59
+ let names;
60
+ try {
61
+ names = (await fileSystem.readdir(lockDirectory)).sort();
62
+ }
63
+ catch (error) {
64
+ if (codeOf(error) === "ENOENT")
65
+ return { status: "absent" };
66
+ throw error;
67
+ }
68
+ if (names.length === 0)
69
+ return { status: "absent" };
70
+ if (names.length !== 1 || names[0] !== "generation") {
71
+ throw new Error(`Invalid profile save lock: ${lockDirectory}`);
72
+ }
73
+ const generationDirectory = join(lockDirectory, "generation");
74
+ try {
75
+ names = (await fileSystem.readdir(generationDirectory)).sort();
76
+ }
77
+ catch (error) {
78
+ if (codeOf(error) === "ENOENT")
79
+ return { status: "absent" };
80
+ throw error;
81
+ }
82
+ if (names.length === 0)
83
+ return { status: "empty" };
84
+ if (names.length !== 1 || !OWNER_NAME_PATTERN.test(names[0])) {
85
+ throw new Error(`Invalid profile save lock generation: ${generationDirectory}`);
86
+ }
87
+ const fileName = names[0];
88
+ const ownerPath = join(generationDirectory, fileName);
89
+ try {
90
+ const owner = OwnerSchema.parse(JSON.parse(await fileSystem.readFile(ownerPath)));
91
+ if (fileName !== ownerName(owner.token))
92
+ throw new Error("owner token does not match filename");
93
+ return { status: "owned", owner, fileName };
94
+ }
95
+ catch (error) {
96
+ if (codeOf(error) === "ENOENT")
97
+ return { status: "absent" };
98
+ throw new Error(`Invalid profile save lock owner: ${ownerPath}`, { cause: error });
99
+ }
100
+ }
101
+ function sameOwner(left, right) {
102
+ return left.pid === right.pid
103
+ && left.processIdentity === right.processIdentity
104
+ && left.token === right.token
105
+ && left.createdAt === right.createdAt;
106
+ }
107
+ async function requireOwnGeneration(lockDirectory, expected, fileSystem) {
108
+ const observed = await readGeneration(lockDirectory, fileSystem);
109
+ if (observed.status !== "owned" || !sameOwner(observed.owner, expected)) {
110
+ throw new Error(`Profile save lock generation changed during install: ${lockDirectory}`);
111
+ }
112
+ return { fileName: observed.fileName };
113
+ }
114
+ async function removeOwnGeneration(lockDirectory, expected, fileSystem) {
115
+ const observed = await readGeneration(lockDirectory, fileSystem);
116
+ if (observed.status !== "owned" || !sameOwner(observed.owner, expected))
117
+ return false;
118
+ const generationDirectory = join(lockDirectory, "generation");
119
+ try {
120
+ await fileSystem.unlink(join(generationDirectory, observed.fileName));
121
+ }
122
+ catch (error) {
123
+ if (codeOf(error) === "ENOENT")
124
+ return false;
125
+ throw error;
126
+ }
127
+ try {
128
+ await fileSystem.rmdir(generationDirectory);
129
+ }
130
+ catch (error) {
131
+ if (codeOf(error) !== "ENOENT" && codeOf(error) !== "ENOTEMPTY")
132
+ throw error;
133
+ }
134
+ try {
135
+ await fileSystem.rmdir(lockDirectory);
136
+ }
137
+ catch (error) {
138
+ if (codeOf(error) !== "ENOENT" && codeOf(error) !== "ENOTEMPTY")
139
+ throw error;
140
+ }
141
+ return true;
142
+ }
143
+ function positiveInteger(value, name) {
144
+ if (!Number.isSafeInteger(value) || value <= 0)
145
+ throw new RangeError(`${name} must be a positive integer`);
146
+ return value;
147
+ }
148
+ async function inspectTemporaryGeneration(temporaryDirectory, token, fileSystem) {
149
+ let entries;
150
+ try {
151
+ entries = (await fileSystem.readdir(temporaryDirectory)).sort();
152
+ }
153
+ catch (error) {
154
+ return codeOf(error) === "ENOENT" ? null : { status: "unavailable" };
155
+ }
156
+ if (entries.length !== 1 || entries[0] !== ownerName(token)) {
157
+ return { status: "missing-or-corrupt" };
158
+ }
159
+ let contents;
160
+ try {
161
+ contents = await fileSystem.readFile(join(temporaryDirectory, entries[0]));
162
+ }
163
+ catch (error) {
164
+ return codeOf(error) === "ENOENT" ? null : { status: "unavailable" };
165
+ }
166
+ try {
167
+ const owner = OwnerSchema.parse(JSON.parse(contents));
168
+ return owner.token.toLowerCase() === token
169
+ ? { status: "valid", owner }
170
+ : { status: "missing-or-corrupt" };
171
+ }
172
+ catch {
173
+ return { status: "missing-or-corrupt" };
174
+ }
175
+ }
176
+ async function cleanupStaleTemporaryGenerations(options) {
177
+ const names = await options.fileSystem.readDirectoryEntries(options.profilesDirectory, options.scanLimit);
178
+ let deleted = 0;
179
+ for (const name of names) {
180
+ if (deleted >= options.deleteLimit)
181
+ break;
182
+ const match = TEMP_NAME_PATTERN.exec(name);
183
+ const token = match?.[1]?.toLowerCase();
184
+ if (token === undefined)
185
+ continue;
186
+ const temporaryDirectory = join(options.profilesDirectory, name);
187
+ let directoryMtimeMs;
188
+ try {
189
+ directoryMtimeMs = (await options.fileSystem.stat(temporaryDirectory)).mtimeMs;
190
+ }
191
+ catch (error) {
192
+ if (codeOf(error) === "ENOENT")
193
+ continue;
194
+ continue;
195
+ }
196
+ const inspection = await inspectTemporaryGeneration(temporaryDirectory, token, options.fileSystem);
197
+ if (inspection === null || inspection.status === "unavailable")
198
+ continue;
199
+ let removable = false;
200
+ if (inspection.status === "valid") {
201
+ try {
202
+ removable = await options.processController.inspectIdentity(inspection.owner.pid)
203
+ !== inspection.owner.processIdentity;
204
+ }
205
+ catch {
206
+ removable = false;
207
+ }
208
+ }
209
+ else {
210
+ removable = options.now - directoryMtimeMs >= options.staleAgeMs;
211
+ }
212
+ if (!removable)
213
+ continue;
214
+ const confirmed = await inspectTemporaryGeneration(temporaryDirectory, token, options.fileSystem);
215
+ if (confirmed === null || confirmed.status !== inspection.status)
216
+ continue;
217
+ if (inspection.status === "valid") {
218
+ if (confirmed.status !== "valid" || !sameOwner(confirmed.owner, inspection.owner))
219
+ continue;
220
+ }
221
+ else {
222
+ try {
223
+ if ((await options.fileSystem.stat(temporaryDirectory)).mtimeMs !== directoryMtimeMs)
224
+ continue;
225
+ }
226
+ catch {
227
+ continue;
228
+ }
229
+ }
230
+ try {
231
+ await options.fileSystem.rm(temporaryDirectory, { recursive: true, force: true });
232
+ deleted += 1;
233
+ }
234
+ catch {
235
+ // Stale-temp cleanup is bounded best effort and must not block a profile save.
236
+ }
237
+ }
238
+ }
239
+ export async function acquireProfileSaveLock(profilesDirectory, options = {}) {
240
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
241
+ const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
242
+ const now = options.now ?? Date.now;
243
+ const wait = options.wait ?? ((milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds)));
244
+ const processController = options.processController ?? defaultProcessController;
245
+ const fileSystem = options.fileSystem ?? defaultProfileSaveLockFileSystem;
246
+ const tempStaleAgeMs = options.tempStaleAgeMs ?? DEFAULT_TEMP_STALE_AGE_MS;
247
+ const tempScanLimit = positiveInteger(options.tempScanLimit ?? DEFAULT_TEMP_SCAN_LIMIT, "tempScanLimit");
248
+ const tempDeleteLimit = positiveInteger(options.tempDeleteLimit ?? DEFAULT_TEMP_DELETE_LIMIT, "tempDeleteLimit");
249
+ if (!Number.isFinite(tempStaleAgeMs) || tempStaleAgeMs < 0) {
250
+ throw new RangeError("tempStaleAgeMs must be a nonnegative finite number");
251
+ }
252
+ const deadline = now() + timeoutMs;
253
+ const lockDirectory = join(profilesDirectory, ".save.lock");
254
+ const generationDirectory = join(lockDirectory, "generation");
255
+ const token = randomUUID();
256
+ const fileName = ownerName(token);
257
+ const temporaryDirectory = join(profilesDirectory, `.save.lock.${token}.tmp`);
258
+ await fileSystem.mkdir(profilesDirectory, { recursive: true, mode: 0o700 });
259
+ await cleanupStaleTemporaryGenerations({
260
+ profilesDirectory,
261
+ now: now(),
262
+ staleAgeMs: tempStaleAgeMs,
263
+ scanLimit: tempScanLimit,
264
+ deleteLimit: tempDeleteLimit,
265
+ processController,
266
+ fileSystem,
267
+ });
268
+ const processIdentity = await processController.inspectIdentity(process.pid);
269
+ if (processIdentity === null || processIdentity.trim().length === 0) {
270
+ throw new Error("Failed to capture profile save lock owner process identity");
271
+ }
272
+ const owner = OwnerSchema.parse({ pid: process.pid, processIdentity, token, createdAt: now() });
273
+ let temporaryDirectoryCreated = false;
274
+ let installed = false;
275
+ let acquired = false;
276
+ const pause = async () => {
277
+ const remaining = deadline - now();
278
+ if (remaining <= 0)
279
+ throw new Error(`Timed out waiting for profile save lock: ${lockDirectory}`);
280
+ await wait(Math.min(retryMs, remaining));
281
+ };
282
+ try {
283
+ await fileSystem.mkdir(temporaryDirectory, { mode: 0o700 });
284
+ temporaryDirectoryCreated = true;
285
+ await fileSystem.writeDurableFile(join(temporaryDirectory, fileName), `${JSON.stringify(owner)}\n`);
286
+ await options.hooks?.beforeInstall?.(owner);
287
+ for (;;) {
288
+ try {
289
+ await fileSystem.mkdir(lockDirectory, { mode: 0o700 });
290
+ }
291
+ catch (error) {
292
+ if (codeOf(error) !== "EEXIST")
293
+ throw error;
294
+ }
295
+ let renameFailure;
296
+ try {
297
+ await fileSystem.rename(temporaryDirectory, generationDirectory);
298
+ installed = true;
299
+ await requireOwnGeneration(lockDirectory, owner, fileSystem);
300
+ acquired = true;
301
+ let released = false;
302
+ return async () => {
303
+ if (released)
304
+ return;
305
+ if (!await removeOwnGeneration(lockDirectory, owner, fileSystem)) {
306
+ throw new Error(`Profile save lock generation changed before release: ${lockDirectory}`);
307
+ }
308
+ released = true;
309
+ };
310
+ }
311
+ catch (error) {
312
+ if (installed)
313
+ throw error;
314
+ if (codeOf(error) === "ENOENT") {
315
+ await pause();
316
+ continue;
317
+ }
318
+ if (codeOf(error) !== "EEXIST"
319
+ && codeOf(error) !== "ENOTEMPTY"
320
+ && codeOf(error) !== "EPERM"
321
+ && codeOf(error) !== "EACCES")
322
+ throw error;
323
+ renameFailure = error;
324
+ }
325
+ const permissionFailure = codeOf(renameFailure) === "EPERM"
326
+ || codeOf(renameFailure) === "EACCES"
327
+ ? renameFailure
328
+ : null;
329
+ const observed = await readGeneration(lockDirectory, fileSystem);
330
+ if (observed.status === "absent") {
331
+ if (permissionFailure !== null)
332
+ throw permissionFailure;
333
+ await pause();
334
+ continue;
335
+ }
336
+ if (observed.status === "empty") {
337
+ try {
338
+ await fileSystem.rmdir(generationDirectory);
339
+ }
340
+ catch (error) {
341
+ if (permissionFailure !== null)
342
+ throw permissionFailure;
343
+ if (codeOf(error) !== "ENOENT" && codeOf(error) !== "ENOTEMPTY")
344
+ throw error;
345
+ await pause();
346
+ continue;
347
+ }
348
+ try {
349
+ await fileSystem.rmdir(lockDirectory);
350
+ }
351
+ catch (error) {
352
+ if (codeOf(error) !== "ENOENT" && codeOf(error) !== "ENOTEMPTY")
353
+ throw error;
354
+ }
355
+ await pause();
356
+ continue;
357
+ }
358
+ const identity = await processController.inspectIdentity(observed.owner.pid);
359
+ if (identity === observed.owner.processIdentity) {
360
+ await pause();
361
+ continue;
362
+ }
363
+ try {
364
+ await fileSystem.unlink(join(generationDirectory, observed.fileName));
365
+ }
366
+ catch (error) {
367
+ if (codeOf(error) === "ENOENT")
368
+ continue;
369
+ throw error;
370
+ }
371
+ try {
372
+ await fileSystem.rmdir(generationDirectory);
373
+ }
374
+ catch (error) {
375
+ if (codeOf(error) !== "ENOENT" && codeOf(error) !== "ENOTEMPTY")
376
+ throw error;
377
+ }
378
+ try {
379
+ await fileSystem.rmdir(lockDirectory);
380
+ }
381
+ catch (error) {
382
+ if (codeOf(error) !== "ENOENT" && codeOf(error) !== "ENOTEMPTY")
383
+ throw error;
384
+ }
385
+ }
386
+ }
387
+ finally {
388
+ if (!installed && temporaryDirectoryCreated) {
389
+ await fileSystem.rm(temporaryDirectory, { recursive: true, force: true }).catch(() => undefined);
390
+ }
391
+ else if (!acquired) {
392
+ await removeOwnGeneration(lockDirectory, owner, fileSystem).catch(() => undefined);
393
+ }
394
+ }
395
+ }