@nowcrew/daemon 0.5.19 → 0.5.21
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/dist/attachments.js +47 -12
- package/dist/computer-cli.js +72 -12
- package/dist/computer-profile-lock.js +395 -0
- package/dist/computer-profile.js +189 -20
- package/dist/config.js +2 -1
- package/dist/console.js +175 -9
- package/dist/daemon-startup-error.js +30 -0
- package/dist/execution-backend.js +35 -2
- package/dist/execution-journal-lock.js +199 -40
- package/dist/execution-journal.js +42 -4
- package/dist/execution-protocol.js +2 -0
- package/dist/execution-recovery.js +95 -0
- package/dist/execution-runner.js +49 -91
- package/dist/execution-supervisor-child.js +51 -0
- package/dist/execution-supervisor.js +83 -33
- package/dist/external-output.js +28 -0
- package/dist/i18n.js +7 -5
- package/dist/local-executor.js +66 -15
- package/dist/machine-info.js +2 -1
- package/dist/main.js +28 -8
- package/dist/runner.js +11 -6
- package/dist/runtime-cancellation.js +74 -0
- package/dist/runtime-path.js +8 -4
- package/dist/runtimes/claude.js +8 -4
- package/dist/runtimes/codex.js +8 -4
- package/dist/serve-lifecycle.js +82 -0
- package/dist/serve.js +189 -220
- package/dist/shared-execution-slots.js +68 -0
- package/dist/shutdown-deadline.js +32 -0
- package/dist/slog.js +34 -20
- package/dist/supervised-runtime.js +104 -0
- package/dist/websocket-shutdown.js +53 -0
- package/dist/win32-job-object.js +193 -0
- package/package.json +5 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, open, readFile, readdir, rename, rm, rmdir, stat, unlink, } from "node:fs/promises";
|
|
2
|
+
import { renameSync } from "node:fs";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
const OwnerSchema = z.object({
|
|
6
7
|
pid: z.number().int().positive(),
|
|
@@ -8,9 +9,15 @@ const OwnerSchema = z.object({
|
|
|
8
9
|
token: z.string().uuid(),
|
|
9
10
|
}).strict();
|
|
10
11
|
export class JournalLockedError extends Error {
|
|
11
|
-
|
|
12
|
+
journalPath;
|
|
13
|
+
ownerPid;
|
|
14
|
+
constructor(message, diagnostics = {}) {
|
|
12
15
|
super(message);
|
|
13
16
|
this.name = "JournalLockedError";
|
|
17
|
+
if (diagnostics.journalPath !== undefined)
|
|
18
|
+
this.journalPath = diagnostics.journalPath;
|
|
19
|
+
if (diagnostics.ownerPid !== undefined)
|
|
20
|
+
this.ownerPid = diagnostics.ownerPid;
|
|
14
21
|
}
|
|
15
22
|
}
|
|
16
23
|
export class JournalLockCorruptionError extends Error {
|
|
@@ -24,6 +31,7 @@ export const defaultJournalLockFileSystem = {
|
|
|
24
31
|
readFile: (path) => readFile(path, "utf8"),
|
|
25
32
|
readdir,
|
|
26
33
|
rename,
|
|
34
|
+
renameSync,
|
|
27
35
|
rm,
|
|
28
36
|
rmdir,
|
|
29
37
|
stat,
|
|
@@ -44,6 +52,141 @@ export function createJournalLeaseRegistry() {
|
|
|
44
52
|
return { leases: new Map() };
|
|
45
53
|
}
|
|
46
54
|
const codeOf = (error) => error instanceof Error && "code" in error ? error.code : undefined;
|
|
55
|
+
const ownerFileName = (token) => `owner.${token}.json`;
|
|
56
|
+
const releasedLockName = (token) => `.journal.released.${token}.lock`;
|
|
57
|
+
const RELEASED_LOCK_PATTERN = /^\.journal\.released\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.lock$/i;
|
|
58
|
+
const MAX_RELEASED_LOCK_CLEANUP = 8;
|
|
59
|
+
const throwIfAborted = (signal) => {
|
|
60
|
+
if (signal?.aborted)
|
|
61
|
+
throw signal.reason ?? new Error("Journal close aborted");
|
|
62
|
+
};
|
|
63
|
+
function awaitAbortable(operation, signal) {
|
|
64
|
+
if (signal === undefined)
|
|
65
|
+
return operation;
|
|
66
|
+
throwIfAborted(signal);
|
|
67
|
+
return new Promise((resolveOperation, rejectOperation) => {
|
|
68
|
+
let settled = false;
|
|
69
|
+
const settle = (continuation) => {
|
|
70
|
+
if (settled)
|
|
71
|
+
return;
|
|
72
|
+
settled = true;
|
|
73
|
+
signal.removeEventListener("abort", onAbort);
|
|
74
|
+
continuation();
|
|
75
|
+
};
|
|
76
|
+
const onAbort = () => settle(() => {
|
|
77
|
+
rejectOperation(signal.reason ?? new Error("Journal close aborted"));
|
|
78
|
+
});
|
|
79
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
80
|
+
if (signal.aborted)
|
|
81
|
+
onAbort();
|
|
82
|
+
operation.then((value) => settle(() => resolveOperation(value)), (error) => settle(() => rejectOperation(error)));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function parseLockOwner(lockDirectory, fileName, fileSystem) {
|
|
86
|
+
const path = join(lockDirectory, fileName);
|
|
87
|
+
try {
|
|
88
|
+
const owner = OwnerSchema.parse(JSON.parse(await fileSystem.readFile(path)));
|
|
89
|
+
if (fileName !== ownerFileName(owner.token))
|
|
90
|
+
throw new Error("owner token does not match filename");
|
|
91
|
+
return owner;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw new JournalLockCorruptionError(path, error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function inspectJournalLock(options) {
|
|
98
|
+
const fileSystem = options.fileSystem ?? defaultJournalLockFileSystem;
|
|
99
|
+
const lockDirectory = join(options.directory, ".journal.lock");
|
|
100
|
+
const orphanGraceMs = options.orphanGraceMs ?? 30_000;
|
|
101
|
+
const now = options.now ?? (() => new Date());
|
|
102
|
+
const missingDuringOwnerRead = (error) => codeOf(error) === "ENOENT"
|
|
103
|
+
|| (error instanceof Error && codeOf(error.cause) === "ENOENT");
|
|
104
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
105
|
+
let names;
|
|
106
|
+
try {
|
|
107
|
+
names = (await fileSystem.readdir(lockDirectory)).map(String).sort();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (codeOf(error) === "ENOENT") {
|
|
111
|
+
return { status: "unlocked", detail: "lock directory does not exist" };
|
|
112
|
+
}
|
|
113
|
+
return { status: "corrupt", detail: `lock directory cannot be read: ${error.message}` };
|
|
114
|
+
}
|
|
115
|
+
if (names.length === 0) {
|
|
116
|
+
try {
|
|
117
|
+
const lockStat = await fileSystem.stat(lockDirectory);
|
|
118
|
+
const ageMs = now().valueOf() - lockStat.mtimeMs;
|
|
119
|
+
return ageMs < orphanGraceMs
|
|
120
|
+
? { status: "installing", detail: "lock owner installation is in progress" }
|
|
121
|
+
: { status: "stale", detail: "empty lock directory exceeded the owner installation grace period" };
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (codeOf(error) === "ENOENT" && attempt === 0)
|
|
125
|
+
continue;
|
|
126
|
+
if (codeOf(error) === "ENOENT") {
|
|
127
|
+
return { status: "unlocked", detail: "lock directory disappeared during inspection" };
|
|
128
|
+
}
|
|
129
|
+
return { status: "corrupt", detail: `empty lock directory cannot be inspected: ${error.message}` };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (names.length !== 1 || !/^owner\.[0-9a-f-]+\.json$/i.test(names[0])) {
|
|
133
|
+
return { status: "corrupt", detail: "lock directory must contain one owner" };
|
|
134
|
+
}
|
|
135
|
+
let owner;
|
|
136
|
+
try {
|
|
137
|
+
owner = await parseLockOwner(lockDirectory, names[0], fileSystem);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (attempt === 0 && missingDuringOwnerRead(error))
|
|
141
|
+
continue;
|
|
142
|
+
return { status: "corrupt", detail: error.message };
|
|
143
|
+
}
|
|
144
|
+
let inspection;
|
|
145
|
+
try {
|
|
146
|
+
const identity = await options.inspectIdentity(owner.pid);
|
|
147
|
+
inspection = identity === owner.processIdentity
|
|
148
|
+
? {
|
|
149
|
+
status: "owned",
|
|
150
|
+
ownerPid: owner.pid,
|
|
151
|
+
ownerAlive: true,
|
|
152
|
+
detail: `owned by live process ${owner.pid}`,
|
|
153
|
+
}
|
|
154
|
+
: {
|
|
155
|
+
status: "stale",
|
|
156
|
+
ownerPid: owner.pid,
|
|
157
|
+
ownerAlive: false,
|
|
158
|
+
detail: `owner process ${owner.pid} is absent or its identity changed`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
inspection = {
|
|
163
|
+
status: "corrupt",
|
|
164
|
+
ownerPid: owner.pid,
|
|
165
|
+
detail: `owner process cannot be inspected: ${error.message}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const confirmedOwner = await parseLockOwner(lockDirectory, names[0], fileSystem);
|
|
170
|
+
const confirmedNames = (await fileSystem.readdir(lockDirectory)).map(String).sort();
|
|
171
|
+
const unchanged = JSON.stringify(confirmedOwner) === JSON.stringify(owner)
|
|
172
|
+
&& JSON.stringify(confirmedNames) === JSON.stringify(names);
|
|
173
|
+
if (!unchanged && attempt === 0)
|
|
174
|
+
continue;
|
|
175
|
+
if (!unchanged)
|
|
176
|
+
return { status: "corrupt", detail: "lock snapshot changed during bounded inspection" };
|
|
177
|
+
return inspection;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (attempt === 0 && missingDuringOwnerRead(error))
|
|
181
|
+
continue;
|
|
182
|
+
if (missingDuringOwnerRead(error)) {
|
|
183
|
+
return { status: "corrupt", detail: "lock snapshot remained unstable after bounded inspection" };
|
|
184
|
+
}
|
|
185
|
+
return { status: "corrupt", detail: error.message };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { status: "corrupt", detail: "lock snapshot remained unstable after bounded inspection" };
|
|
189
|
+
}
|
|
47
190
|
function processLeaseMap(registry) {
|
|
48
191
|
return registry.leases;
|
|
49
192
|
}
|
|
@@ -52,21 +195,29 @@ export function createJournalLease(options) {
|
|
|
52
195
|
const registry = processLeaseMap(options.registry ?? defaultRegistry);
|
|
53
196
|
const now = options.now ?? (() => new Date());
|
|
54
197
|
const orphanGraceMs = options.orphanGraceMs ?? 30_000;
|
|
198
|
+
const journalPath = resolve(options.directory);
|
|
55
199
|
const lockDirectory = join(options.directory, ".journal.lock");
|
|
56
200
|
let attached = false;
|
|
57
201
|
let status = "open";
|
|
58
|
-
const
|
|
59
|
-
const parseOwner = async (fileName) => {
|
|
60
|
-
const path = join(lockDirectory, fileName);
|
|
202
|
+
const cleanupReleasedDirectory = async (path) => {
|
|
61
203
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
throw new Error("owner token does not match filename");
|
|
65
|
-
return owner;
|
|
204
|
+
await fileSystem.rm(path, { recursive: true, force: true });
|
|
205
|
+
await options.syncDirectory(options.directory);
|
|
66
206
|
}
|
|
67
|
-
catch
|
|
68
|
-
|
|
207
|
+
catch { /* released tombstones never block ownership or reverse a committed close */ }
|
|
208
|
+
};
|
|
209
|
+
const cleanupReleasedDirectories = async () => {
|
|
210
|
+
let names;
|
|
211
|
+
try {
|
|
212
|
+
names = (await fileSystem.readdir(options.directory)).map(String).sort();
|
|
69
213
|
}
|
|
214
|
+
catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await Promise.all(names
|
|
218
|
+
.filter((name) => RELEASED_LOCK_PATTERN.test(name))
|
|
219
|
+
.slice(0, MAX_RELEASED_LOCK_CLEANUP)
|
|
220
|
+
.map((name) => cleanupReleasedDirectory(join(options.directory, name))));
|
|
70
221
|
};
|
|
71
222
|
const readOwner = async () => {
|
|
72
223
|
let names;
|
|
@@ -81,7 +232,7 @@ export function createJournalLease(options) {
|
|
|
81
232
|
if (names.length === 0) {
|
|
82
233
|
const lockStat = await fileSystem.stat(lockDirectory);
|
|
83
234
|
if (now().valueOf() - lockStat.mtimeMs < orphanGraceMs) {
|
|
84
|
-
throw new JournalLockedError("Execution journal lock owner installation is in progress");
|
|
235
|
+
throw new JournalLockedError("Execution journal lock owner installation is in progress", { journalPath });
|
|
85
236
|
}
|
|
86
237
|
try {
|
|
87
238
|
await fileSystem.rmdir(lockDirectory);
|
|
@@ -96,7 +247,7 @@ export function createJournalLease(options) {
|
|
|
96
247
|
if (names.length !== 1 || !/^owner\.[0-9a-f-]+\.json$/i.test(names[0])) {
|
|
97
248
|
throw new JournalLockCorruptionError(lockDirectory, new Error("lock directory must contain one owner"));
|
|
98
249
|
}
|
|
99
|
-
return { owner: await
|
|
250
|
+
return { owner: await parseLockOwner(lockDirectory, names[0], fileSystem), fileName: names[0] };
|
|
100
251
|
};
|
|
101
252
|
const validateInstalledOwner = async (lease) => {
|
|
102
253
|
const observed = await readOwner();
|
|
@@ -151,9 +302,6 @@ export function createJournalLease(options) {
|
|
|
151
302
|
lockDirectorySynced: false,
|
|
152
303
|
executionsDirectorySynced: false,
|
|
153
304
|
releasing: false,
|
|
154
|
-
ownerRemoved: false,
|
|
155
|
-
lockDirectoryRemoved: false,
|
|
156
|
-
releaseSynced: false,
|
|
157
305
|
};
|
|
158
306
|
registry.set(options.directory, lease);
|
|
159
307
|
attached = true;
|
|
@@ -169,7 +317,7 @@ export function createJournalLease(options) {
|
|
|
169
317
|
continue;
|
|
170
318
|
const identity = await options.inspectIdentity(observed.owner.pid);
|
|
171
319
|
if (identity === observed.owner.processIdentity) {
|
|
172
|
-
throw new JournalLockedError(`Execution journal is locked by process ${observed.owner.pid}
|
|
320
|
+
throw new JournalLockedError(`Execution journal is locked by process ${observed.owner.pid}`, { journalPath, ownerPid: observed.owner.pid });
|
|
173
321
|
}
|
|
174
322
|
await options.hooks?.beforeRemoveObservedOwner?.(observed.owner);
|
|
175
323
|
try {
|
|
@@ -196,37 +344,48 @@ export function createJournalLease(options) {
|
|
|
196
344
|
}
|
|
197
345
|
};
|
|
198
346
|
const acquire = async () => {
|
|
199
|
-
if (status !== "open")
|
|
200
|
-
throw new JournalLockedError(`Journal lease is ${status}
|
|
347
|
+
if (status !== "open") {
|
|
348
|
+
throw new JournalLockedError(`Journal lease is ${status}`, { journalPath });
|
|
349
|
+
}
|
|
201
350
|
if (attached) {
|
|
202
351
|
const lease = registry.get(options.directory);
|
|
203
|
-
if (lease === undefined)
|
|
204
|
-
throw new JournalLockedError("In-process journal lease is missing");
|
|
352
|
+
if (lease === undefined) {
|
|
353
|
+
throw new JournalLockedError("In-process journal lease is missing", { journalPath });
|
|
354
|
+
}
|
|
205
355
|
await finishInstall(lease);
|
|
206
356
|
return;
|
|
207
357
|
}
|
|
208
358
|
const existing = registry.get(options.directory);
|
|
209
359
|
if (existing !== undefined) {
|
|
210
|
-
if (existing.releasing)
|
|
211
|
-
throw new JournalLockedError("Journal lease release is pending"
|
|
360
|
+
if (existing.releasing) {
|
|
361
|
+
throw new JournalLockedError("Journal lease release is pending", {
|
|
362
|
+
journalPath,
|
|
363
|
+
ownerPid: existing.owner.pid,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
212
366
|
await finishInstall(existing);
|
|
213
367
|
existing.refs += 1;
|
|
214
368
|
attached = true;
|
|
215
369
|
return;
|
|
216
370
|
}
|
|
371
|
+
void cleanupReleasedDirectories();
|
|
217
372
|
await install();
|
|
218
373
|
};
|
|
219
|
-
const close = async () => {
|
|
374
|
+
const close = async (closeOptions = {}) => {
|
|
220
375
|
if (status === "closed")
|
|
221
376
|
return;
|
|
377
|
+
const signal = closeOptions.signal;
|
|
378
|
+
throwIfAborted(signal);
|
|
222
379
|
if (!attached) {
|
|
223
380
|
status = "closed";
|
|
224
381
|
return;
|
|
225
382
|
}
|
|
226
383
|
const lease = registry.get(options.directory);
|
|
227
|
-
if (lease === undefined)
|
|
228
|
-
throw new JournalLockedError("In-process journal lease is missing");
|
|
384
|
+
if (lease === undefined) {
|
|
385
|
+
throw new JournalLockedError("In-process journal lease is missing", { journalPath });
|
|
386
|
+
}
|
|
229
387
|
if (status === "open" && lease.refs > 1) {
|
|
388
|
+
throwIfAborted(signal);
|
|
230
389
|
lease.refs -= 1;
|
|
231
390
|
attached = false;
|
|
232
391
|
status = "closed";
|
|
@@ -234,28 +393,28 @@ export function createJournalLease(options) {
|
|
|
234
393
|
}
|
|
235
394
|
status = "closing";
|
|
236
395
|
lease.releasing = true;
|
|
237
|
-
|
|
238
|
-
await
|
|
239
|
-
lease.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
await
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
lease.releaseSynced = true;
|
|
248
|
-
}
|
|
396
|
+
await awaitAbortable((async () => {
|
|
397
|
+
await validateInstalledOwner(lease);
|
|
398
|
+
await options.hooks?.beforeReleaseOwner?.(lease.owner, {
|
|
399
|
+
...(signal === undefined ? {} : { signal }),
|
|
400
|
+
});
|
|
401
|
+
await validateInstalledOwner(lease);
|
|
402
|
+
})(), signal);
|
|
403
|
+
throwIfAborted(signal);
|
|
404
|
+
const releasedDirectory = join(options.directory, releasedLockName(lease.owner.token));
|
|
405
|
+
fileSystem.renameSync(lockDirectory, releasedDirectory);
|
|
249
406
|
lease.refs -= 1;
|
|
250
407
|
registry.delete(options.directory);
|
|
251
408
|
attached = false;
|
|
252
409
|
status = "closed";
|
|
410
|
+
void cleanupReleasedDirectory(releasedDirectory);
|
|
253
411
|
};
|
|
254
412
|
return {
|
|
255
413
|
acquire,
|
|
256
414
|
assertUsable: () => {
|
|
257
|
-
if (status !== "open")
|
|
258
|
-
throw new JournalLockedError(`Execution journal lease is ${status}
|
|
415
|
+
if (status !== "open") {
|
|
416
|
+
throw new JournalLockedError(`Execution journal lease is ${status}`, { journalPath });
|
|
417
|
+
}
|
|
259
418
|
},
|
|
260
419
|
close,
|
|
261
420
|
};
|
|
@@ -558,7 +558,7 @@ export function createExecutionJournal(agentsRoot, options = {}) {
|
|
|
558
558
|
return { kind: "created", ...entry };
|
|
559
559
|
});
|
|
560
560
|
},
|
|
561
|
-
startGuarded: async (executionId, processStartedAt, startDormant) => {
|
|
561
|
+
startGuarded: async (executionId, processStartedAt, startDormant, hooks) => {
|
|
562
562
|
validateExecutionId(executionId);
|
|
563
563
|
TimestampSchema.parse(processStartedAt);
|
|
564
564
|
return serialized(async () => {
|
|
@@ -567,9 +567,21 @@ export function createExecutionJournal(agentsRoot, options = {}) {
|
|
|
567
567
|
return { kind: "existing", entry: await confirmDurable(entry) };
|
|
568
568
|
}
|
|
569
569
|
const dormant = await startDormant();
|
|
570
|
+
let abortPromise = null;
|
|
571
|
+
const abort = () => {
|
|
572
|
+
if (abortPromise === null) {
|
|
573
|
+
try {
|
|
574
|
+
abortPromise = Promise.resolve(dormant.abort());
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
abortPromise = Promise.reject(error);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return abortPromise;
|
|
581
|
+
};
|
|
570
582
|
const abortWith = async (error) => {
|
|
571
583
|
try {
|
|
572
|
-
await
|
|
584
|
+
await abort();
|
|
573
585
|
}
|
|
574
586
|
catch (abortError) {
|
|
575
587
|
throw new AggregateError([error, abortError], "Dormant runtime abort failed");
|
|
@@ -591,6 +603,9 @@ export function createExecutionJournal(agentsRoot, options = {}) {
|
|
|
591
603
|
});
|
|
592
604
|
await writeRecord(updated);
|
|
593
605
|
try {
|
|
606
|
+
const gate = hooks?.beforeRelease?.({ entry: updated, handle: dormant, abort });
|
|
607
|
+
if (gate !== undefined)
|
|
608
|
+
await gate;
|
|
594
609
|
await dormant.release();
|
|
595
610
|
}
|
|
596
611
|
catch (error) {
|
|
@@ -671,8 +686,31 @@ export function createExecutionJournal(agentsRoot, options = {}) {
|
|
|
671
686
|
}
|
|
672
687
|
}),
|
|
673
688
|
prune: async () => serialized(pruneInternal),
|
|
674
|
-
close: async () => {
|
|
675
|
-
|
|
689
|
+
close: async (closeOptions = {}) => {
|
|
690
|
+
const signal = closeOptions.signal;
|
|
691
|
+
let started = false;
|
|
692
|
+
const closing = runSerialized(directory, async () => {
|
|
693
|
+
started = true;
|
|
694
|
+
if (signal?.aborted)
|
|
695
|
+
throw signal.reason ?? new Error("Journal close aborted");
|
|
696
|
+
await lease.close(closeOptions);
|
|
697
|
+
});
|
|
698
|
+
if (signal === undefined)
|
|
699
|
+
return closing;
|
|
700
|
+
await new Promise((resolveClose, rejectClose) => {
|
|
701
|
+
const settle = (settler) => {
|
|
702
|
+
signal.removeEventListener("abort", onAbort);
|
|
703
|
+
settler();
|
|
704
|
+
};
|
|
705
|
+
const onAbort = () => {
|
|
706
|
+
if (!started)
|
|
707
|
+
settle(() => rejectClose(signal.reason ?? new Error("Journal close aborted")));
|
|
708
|
+
};
|
|
709
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
710
|
+
if (signal.aborted)
|
|
711
|
+
onAbort();
|
|
712
|
+
closing.then(() => settle(resolveClose), (error) => settle(() => rejectClose(error)));
|
|
713
|
+
});
|
|
676
714
|
},
|
|
677
715
|
};
|
|
678
716
|
}
|
|
@@ -122,6 +122,7 @@ export const ExecutionStartSchema = z.object({
|
|
|
122
122
|
threadId: z.string().min(1).optional(),
|
|
123
123
|
wakeMessageId: z.string().min(1).optional(),
|
|
124
124
|
externalResponseSessionId: ExecutionIdSchema.optional(),
|
|
125
|
+
answerStream: z.boolean().optional(),
|
|
125
126
|
attachments: z.array(ExecutionAttachmentSchema).max(20).optional(),
|
|
126
127
|
}).strict(),
|
|
127
128
|
reporting: z.object({
|
|
@@ -222,6 +223,7 @@ const RawExecutionCompletedSchema = z.object({
|
|
|
222
223
|
model: z.string().optional(),
|
|
223
224
|
resumed: z.boolean(),
|
|
224
225
|
finalText: z.string().optional(),
|
|
226
|
+
externalAnswer: z.string().min(1).optional(),
|
|
225
227
|
boundImDecision: z.enum(["notify", "silent"]).optional(),
|
|
226
228
|
usage: ExecutionUsageSchema.optional(),
|
|
227
229
|
startedAt: TimestampSchema,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { JournalLockedError } from "./execution-journal.js";
|
|
3
|
+
import { DaemonAlreadyRunningError } from "./daemon-startup-error.js";
|
|
4
|
+
export async function reconcileExecutionJournal(journal, dependencies) {
|
|
5
|
+
try {
|
|
6
|
+
await journal.reconcileAfterRestart();
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
const agentsRoot = resolve(dependencies.agentsRoot);
|
|
10
|
+
const errorRecord = typeof error === "object" && error !== null
|
|
11
|
+
? error
|
|
12
|
+
: {};
|
|
13
|
+
const journalPath = typeof errorRecord.journalPath === "string"
|
|
14
|
+
? resolve(errorRecord.journalPath)
|
|
15
|
+
: join(agentsRoot, ".crew", "executions");
|
|
16
|
+
const ownerPid = typeof errorRecord.ownerPid === "number" ? errorRecord.ownerPid : undefined;
|
|
17
|
+
const errorType = error instanceof Error ? error.name : typeof error;
|
|
18
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
19
|
+
const alreadyRunning = error instanceof JournalLockedError && ownerPid !== undefined;
|
|
20
|
+
const diagnostics = {
|
|
21
|
+
server_url: dependencies.serverUrl,
|
|
22
|
+
agents_root: agentsRoot,
|
|
23
|
+
journal_path: journalPath,
|
|
24
|
+
owner_pid: ownerPid,
|
|
25
|
+
...(dependencies.profileName === undefined ? {} : { profile_name: dependencies.profileName }),
|
|
26
|
+
error_type: errorType,
|
|
27
|
+
error_message: errorMessage,
|
|
28
|
+
};
|
|
29
|
+
const failureEvent = alreadyRunning ? "daemon.already_running" : "execution.recovery_failed";
|
|
30
|
+
const failureMessage = alreadyRunning ? "daemon 已在运行" : "execution journal 恢复失败";
|
|
31
|
+
const failureLevel = alreadyRunning ? "WARN" : "ERROR";
|
|
32
|
+
dependencies.log(failureEvent, failureMessage, {
|
|
33
|
+
level: failureLevel,
|
|
34
|
+
...diagnostics,
|
|
35
|
+
});
|
|
36
|
+
dependencies.writeStderr(`${JSON.stringify({
|
|
37
|
+
level: failureLevel,
|
|
38
|
+
event_type: failureEvent,
|
|
39
|
+
message: failureMessage,
|
|
40
|
+
...diagnostics,
|
|
41
|
+
})}\n`);
|
|
42
|
+
const reportCleanupFailure = (stage, cleanupError) => {
|
|
43
|
+
const cleanupErrorType = cleanupError instanceof Error ? cleanupError.name : typeof cleanupError;
|
|
44
|
+
const cleanupErrorMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
45
|
+
const cleanupDiagnostics = {
|
|
46
|
+
server_url: dependencies.serverUrl,
|
|
47
|
+
agents_root: agentsRoot,
|
|
48
|
+
journal_path: journalPath,
|
|
49
|
+
owner_pid: ownerPid,
|
|
50
|
+
cleanup_stage: stage,
|
|
51
|
+
error_type: cleanupErrorType,
|
|
52
|
+
error_message: cleanupErrorMessage,
|
|
53
|
+
...(alreadyRunning
|
|
54
|
+
? { startup_error_type: errorType, startup_error_message: errorMessage }
|
|
55
|
+
: { recovery_error_type: errorType, recovery_error_message: errorMessage }),
|
|
56
|
+
};
|
|
57
|
+
const cleanupEvent = alreadyRunning
|
|
58
|
+
? "daemon.already_running_cleanup_failed"
|
|
59
|
+
: "execution.recovery_cleanup_failed";
|
|
60
|
+
const cleanupMessage = alreadyRunning
|
|
61
|
+
? "daemon 重复启动后的清理失败"
|
|
62
|
+
: "execution journal 恢复失败后的清理失败";
|
|
63
|
+
dependencies.log(cleanupEvent, cleanupMessage, { level: "ERROR", ...cleanupDiagnostics });
|
|
64
|
+
dependencies.writeStderr(`${JSON.stringify({
|
|
65
|
+
level: "ERROR",
|
|
66
|
+
event_type: cleanupEvent,
|
|
67
|
+
message: cleanupMessage,
|
|
68
|
+
...cleanupDiagnostics,
|
|
69
|
+
})}\n`);
|
|
70
|
+
};
|
|
71
|
+
try {
|
|
72
|
+
await dependencies.flush();
|
|
73
|
+
}
|
|
74
|
+
catch (cleanupError) {
|
|
75
|
+
reportCleanupFailure("slog_flush", cleanupError);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await journal.close();
|
|
79
|
+
}
|
|
80
|
+
catch (cleanupError) {
|
|
81
|
+
reportCleanupFailure("journal_close", cleanupError);
|
|
82
|
+
}
|
|
83
|
+
if (alreadyRunning) {
|
|
84
|
+
throw new DaemonAlreadyRunningError({
|
|
85
|
+
ownerPid,
|
|
86
|
+
agentsRoot,
|
|
87
|
+
journalPath,
|
|
88
|
+
serverUrl: dependencies.serverUrl,
|
|
89
|
+
...(dependencies.profileName === undefined ? {} : { profileName: dependencies.profileName }),
|
|
90
|
+
cause: error,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|