@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
package/dist/serve.js
CHANGED
|
@@ -22,15 +22,20 @@ import { createExecutionTelemetryJournal } from "./execution-telemetry-journal.j
|
|
|
22
22
|
import { ExecutionRejectedSchema, ExecutionSnapshotSchema, LegacyAgentStartSchema, ServerToDaemonExecutionFrameSchema, } from "./execution-protocol.js";
|
|
23
23
|
import { hashExecutionSpec, runExecution, } from "./execution-runner.js";
|
|
24
24
|
import { executionBackendCapability } from "./execution-backend.js";
|
|
25
|
+
import { awaitWithCancellation, createRuntimeCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
|
|
26
|
+
import { createShutdownDeadline, readTestShutdownConfiguration } from "./shutdown-deadline.js";
|
|
27
|
+
import { closeWebSocketWithinDeadline } from "./websocket-shutdown.js";
|
|
28
|
+
import { reconcileExecutionJournal } from "./execution-recovery.js";
|
|
29
|
+
import { createSharedSlotManager } from "./shared-execution-slots.js";
|
|
25
30
|
// normalize.ts 的活动种类 → activity 枚举
|
|
26
31
|
const ACTIVITY_MAP = {
|
|
27
32
|
init: "working", text: "thinking", reading: "reading", sending: "sending",
|
|
28
33
|
checking: "checking", claiming: "claiming", crew: "working", tool: "working",
|
|
29
34
|
tool_result: "working", done: "done", error: "error",
|
|
30
35
|
};
|
|
31
|
-
export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform) {
|
|
36
|
+
export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe) {
|
|
32
37
|
const query = new URLSearchParams({ key: machineToken });
|
|
33
|
-
if (executionBackendCapability(runtimePlatform).supported) {
|
|
38
|
+
if (executionBackendCapability(runtimePlatform, jobObjectProbe).supported) {
|
|
34
39
|
query.set("execution_min", String(EXECUTION_PROTOCOL.min));
|
|
35
40
|
query.set("execution_max", String(EXECUTION_PROTOCOL.max));
|
|
36
41
|
}
|
|
@@ -44,24 +49,23 @@ export function serve(config, opts = {}) {
|
|
|
44
49
|
let ws = null;
|
|
45
50
|
let backoff = 1000;
|
|
46
51
|
const maxBackoff = opts.maxBackoffMs ?? 30_000;
|
|
52
|
+
const testShutdown = readTestShutdownConfiguration(process.env);
|
|
53
|
+
const shutdownTimeoutMs = opts.shutdownTimeoutMs ?? testShutdown.timeoutMs ?? 30_000;
|
|
54
|
+
const createWebSocket = opts.createWebSocket ?? ((url) => new WebSocket(url));
|
|
55
|
+
let reconnectTimer = null;
|
|
56
|
+
let stopPromise = null;
|
|
47
57
|
const executionJournal = opts.execution?.journal ?? createExecutionJournal(config.agentsRoot);
|
|
48
58
|
const executionTelemetry = createExecutionTelemetryJournal(config.agentsRoot);
|
|
49
59
|
const executeProtocol = opts.execution?.runExecution ?? runExecution;
|
|
50
60
|
let detectedExecutionRuntimes = [];
|
|
51
61
|
let runtimeFacts = null;
|
|
52
|
-
const
|
|
53
|
-
dslog("execution.recovery_failed", "execution journal 恢复失败", {
|
|
54
|
-
level: "ERROR", error_message: error.message,
|
|
55
|
-
});
|
|
56
|
-
return false;
|
|
57
|
-
});
|
|
58
|
-
const sharedActive = new Map();
|
|
59
|
-
const sharedQueues = new Map();
|
|
62
|
+
const sharedSlots = createSharedSlotManager(config.executionLimits);
|
|
60
63
|
const knownExecutionHashes = new Map();
|
|
61
64
|
const executionReservations = new Map();
|
|
62
65
|
const executionRuns = new Map();
|
|
63
66
|
let executionFrameQueue = Promise.resolve();
|
|
64
67
|
const cancellations = new Map();
|
|
68
|
+
const legacyRuns = new Map();
|
|
65
69
|
const safeExecutionSend = (frame) => {
|
|
66
70
|
try {
|
|
67
71
|
if (ws?.readyState === WebSocket.OPEN)
|
|
@@ -124,114 +128,19 @@ export function serve(config, opts = {}) {
|
|
|
124
128
|
}
|
|
125
129
|
};
|
|
126
130
|
const cancellationFor = (executionId) => {
|
|
127
|
-
const startStop = (state) => {
|
|
128
|
-
if (!state.requested || state.cancel === null)
|
|
129
|
-
return Promise.resolve();
|
|
130
|
-
state.stopPromise ??= Promise.resolve().then(state.cancel);
|
|
131
|
-
return state.stopPromise;
|
|
132
|
-
};
|
|
133
131
|
const existing = cancellations.get(executionId);
|
|
134
|
-
if (existing !== undefined)
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
existing.cancel = cancel;
|
|
140
|
-
void startStop(existing).catch(() => { });
|
|
141
|
-
},
|
|
142
|
-
waitForStop: () => startStop(existing),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
let resolve;
|
|
146
|
-
const promise = new Promise((done) => { resolve = done; });
|
|
147
|
-
const state = {
|
|
148
|
-
requested: false, resolve, promise,
|
|
149
|
-
cancel: null,
|
|
150
|
-
stopPromise: null,
|
|
151
|
-
};
|
|
152
|
-
cancellations.set(executionId, state);
|
|
153
|
-
return {
|
|
154
|
-
isRequested: () => state.requested,
|
|
155
|
-
requested: promise,
|
|
156
|
-
register: (cancel) => {
|
|
157
|
-
state.cancel = cancel;
|
|
158
|
-
void startStop(state).catch(() => { });
|
|
159
|
-
},
|
|
160
|
-
waitForStop: () => startStop(state),
|
|
161
|
-
};
|
|
132
|
+
if (existing !== undefined)
|
|
133
|
+
return existing.cancellation;
|
|
134
|
+
const controller = createRuntimeCancellation();
|
|
135
|
+
cancellations.set(executionId, controller);
|
|
136
|
+
return controller.cancellation;
|
|
162
137
|
};
|
|
163
138
|
const requestCancellation = (executionId) => {
|
|
164
139
|
cancellationFor(executionId);
|
|
165
|
-
|
|
166
|
-
if (state.requested)
|
|
167
|
-
return;
|
|
168
|
-
state.requested = true;
|
|
169
|
-
state.resolve();
|
|
140
|
+
cancellations.get(executionId).request();
|
|
170
141
|
const reservation = executionReservations.get(executionId);
|
|
171
|
-
if (reservation?.isQueued())
|
|
142
|
+
if (reservation?.isQueued())
|
|
172
143
|
reservation.release();
|
|
173
|
-
}
|
|
174
|
-
if (state.cancel !== null && state.stopPromise === null) {
|
|
175
|
-
state.stopPromise = Promise.resolve().then(state.cancel);
|
|
176
|
-
void state.stopPromise.catch(() => { });
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
const promoteNext = (handle) => {
|
|
180
|
-
const queue = sharedQueues.get(handle) ?? [];
|
|
181
|
-
while ((sharedActive.get(handle) ?? 0) < config.executionLimits.maxParallelPerAgent && queue.length > 0) {
|
|
182
|
-
const next = queue.shift();
|
|
183
|
-
if (next.released)
|
|
184
|
-
continue;
|
|
185
|
-
next.promoted = true;
|
|
186
|
-
sharedActive.set(handle, (sharedActive.get(handle) ?? 0) + 1);
|
|
187
|
-
next.resolve();
|
|
188
|
-
}
|
|
189
|
-
if (queue.length === 0)
|
|
190
|
-
sharedQueues.delete(handle);
|
|
191
|
-
};
|
|
192
|
-
const reserveSharedSlot = (handle, kind) => {
|
|
193
|
-
const active = sharedActive.get(handle) ?? 0;
|
|
194
|
-
const queued = sharedQueues.get(handle)?.length ?? 0;
|
|
195
|
-
const facts = { activeForAgent: active, queuedForAgent: queued };
|
|
196
|
-
if (kind === "execution" && active >= config.executionLimits.maxParallelPerAgent
|
|
197
|
-
&& queued >= config.executionLimits.maxQueuedPerAgent) {
|
|
198
|
-
return { facts, ready: Promise.resolve(), isQueued: () => false, release: () => { } };
|
|
199
|
-
}
|
|
200
|
-
let resolve;
|
|
201
|
-
const ready = new Promise((done) => { resolve = done; });
|
|
202
|
-
const entry = { kind, released: false, promoted: active < config.executionLimits.maxParallelPerAgent, resolve };
|
|
203
|
-
if (entry.promoted) {
|
|
204
|
-
sharedActive.set(handle, active + 1);
|
|
205
|
-
resolve();
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
const queue = sharedQueues.get(handle) ?? [];
|
|
209
|
-
queue.push(entry);
|
|
210
|
-
sharedQueues.set(handle, queue);
|
|
211
|
-
}
|
|
212
|
-
return {
|
|
213
|
-
facts,
|
|
214
|
-
state: entry.promoted ? "ready" : "queued",
|
|
215
|
-
ready,
|
|
216
|
-
isQueued: () => !entry.promoted && !entry.released,
|
|
217
|
-
release: () => {
|
|
218
|
-
if (entry.released)
|
|
219
|
-
return;
|
|
220
|
-
entry.released = true;
|
|
221
|
-
if (entry.promoted) {
|
|
222
|
-
sharedActive.set(handle, Math.max(0, (sharedActive.get(handle) ?? 1) - 1));
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
const queue = sharedQueues.get(handle);
|
|
226
|
-
const index = queue?.indexOf(entry) ?? -1;
|
|
227
|
-
if (queue !== undefined && index >= 0)
|
|
228
|
-
queue.splice(index, 1);
|
|
229
|
-
if (queue?.length === 0)
|
|
230
|
-
sharedQueues.delete(handle);
|
|
231
|
-
}
|
|
232
|
-
promoteNext(handle);
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
144
|
};
|
|
236
145
|
let connectedAt = 0; // 本次 WS 连接建立时刻(断开日志算在线时长用)
|
|
237
146
|
initSlog(config.serverUrl, config.machineToken);
|
|
@@ -241,18 +150,11 @@ export function serve(config, opts = {}) {
|
|
|
241
150
|
// 每 agent 超过并行上限的任务继续进入 sharedQueues(不丢)。
|
|
242
151
|
const running = new Set();
|
|
243
152
|
const legacyTaskTails = new Map();
|
|
244
|
-
const acquireSlot = async (handle) => {
|
|
245
|
-
await reserveSharedSlot(handle, "legacy").ready;
|
|
246
|
-
};
|
|
247
|
-
const releaseSlot = (handle) => {
|
|
248
|
-
sharedActive.set(handle, Math.max(0, (sharedActive.get(handle) ?? 1) - 1));
|
|
249
|
-
promoteNext(handle);
|
|
250
|
-
};
|
|
251
153
|
const log = (s) => process.stdout.write(formatDaemonLogLine(s) + "\n");
|
|
252
154
|
function connect() {
|
|
253
155
|
if (stopped)
|
|
254
156
|
return;
|
|
255
|
-
ws =
|
|
157
|
+
ws = createWebSocket(wsUrl);
|
|
256
158
|
ws.on("open", () => {
|
|
257
159
|
backoff = 1000;
|
|
258
160
|
connectedAt = Date.now();
|
|
@@ -279,7 +181,7 @@ export function serve(config, opts = {}) {
|
|
|
279
181
|
})
|
|
280
182
|
.catch(() => { });
|
|
281
183
|
opts.onOpen?.(ws);
|
|
282
|
-
void
|
|
184
|
+
void sendSnapshot(`reconnect-${randomUUID()}`);
|
|
283
185
|
void replayExecutionTelemetry().catch((error) => {
|
|
284
186
|
dslog("execution.telemetry_replay_failed", "execution telemetry 重放失败", {
|
|
285
187
|
level: "ERROR", error_message: error.message,
|
|
@@ -287,6 +189,8 @@ export function serve(config, opts = {}) {
|
|
|
287
189
|
});
|
|
288
190
|
});
|
|
289
191
|
ws.on("message", async (data) => {
|
|
192
|
+
if (stopped)
|
|
193
|
+
return;
|
|
290
194
|
let decoded;
|
|
291
195
|
try {
|
|
292
196
|
decoded = JSON.parse(data.toString());
|
|
@@ -316,7 +220,8 @@ export function serve(config, opts = {}) {
|
|
|
316
220
|
}
|
|
317
221
|
const frame = parsedExecution.data;
|
|
318
222
|
executionFrameQueue = executionFrameQueue.then(async () => {
|
|
319
|
-
|
|
223
|
+
if (stopped)
|
|
224
|
+
return;
|
|
320
225
|
if (frame.type === "execution:completion-ack") {
|
|
321
226
|
await executionJournal.acknowledgeCompletion(frame.executionId).catch(() => { });
|
|
322
227
|
return;
|
|
@@ -343,14 +248,6 @@ export function serve(config, opts = {}) {
|
|
|
343
248
|
return;
|
|
344
249
|
}
|
|
345
250
|
const spec = frame;
|
|
346
|
-
if (!recovered) {
|
|
347
|
-
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
348
|
-
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
349
|
-
reason: "resource_limit", message: "Local execution journal recovery failed",
|
|
350
|
-
at: new Date().toISOString(),
|
|
351
|
-
}));
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
251
|
const hash = hashExecutionSpec(spec);
|
|
355
252
|
const knownHash = knownExecutionHashes.get(spec.executionId);
|
|
356
253
|
if (knownHash !== undefined) {
|
|
@@ -370,6 +267,8 @@ export function serve(config, opts = {}) {
|
|
|
370
267
|
return;
|
|
371
268
|
}
|
|
372
269
|
const durableExisting = await executionJournal.get(spec.executionId);
|
|
270
|
+
if (stopped)
|
|
271
|
+
return;
|
|
373
272
|
if (durableExisting !== null) {
|
|
374
273
|
if (durableExisting.specHash !== hash) {
|
|
375
274
|
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
@@ -384,19 +283,22 @@ export function serve(config, opts = {}) {
|
|
|
384
283
|
return;
|
|
385
284
|
}
|
|
386
285
|
knownExecutionHashes.set(spec.executionId, hash);
|
|
387
|
-
const reservation =
|
|
286
|
+
const reservation = sharedSlots.reserve(spec.agent.handle, "execution");
|
|
388
287
|
executionReservations.set(spec.executionId, reservation);
|
|
389
288
|
const cancellation = cancellationFor(spec.executionId);
|
|
289
|
+
const cleanupExecutionReservation = () => {
|
|
290
|
+
reservation.release();
|
|
291
|
+
cancellations.delete(spec.executionId);
|
|
292
|
+
executionReservations.delete(spec.executionId);
|
|
293
|
+
knownExecutionHashes.delete(spec.executionId);
|
|
294
|
+
};
|
|
390
295
|
let availableRuntimes;
|
|
391
296
|
try {
|
|
392
297
|
availableRuntimes = opts.execution?.availableRuntimes?.()
|
|
393
298
|
?? await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes));
|
|
394
299
|
}
|
|
395
300
|
catch (error) {
|
|
396
|
-
|
|
397
|
-
cancellations.delete(spec.executionId);
|
|
398
|
-
executionReservations.delete(spec.executionId);
|
|
399
|
-
knownExecutionHashes.delete(spec.executionId);
|
|
301
|
+
cleanupExecutionReservation();
|
|
400
302
|
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
401
303
|
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
402
304
|
reason: "resource_limit", message: `Runtime detection failed: ${error.message}`,
|
|
@@ -404,6 +306,10 @@ export function serve(config, opts = {}) {
|
|
|
404
306
|
}));
|
|
405
307
|
return;
|
|
406
308
|
}
|
|
309
|
+
if (stopped) {
|
|
310
|
+
cleanupExecutionReservation();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
407
313
|
const execution = executeProtocol(config, spec, {
|
|
408
314
|
...opts.execution?.dependencies,
|
|
409
315
|
journal: executionJournal,
|
|
@@ -417,11 +323,8 @@ export function serve(config, opts = {}) {
|
|
|
417
323
|
}),
|
|
418
324
|
cancellation,
|
|
419
325
|
}).finally(() => {
|
|
420
|
-
|
|
421
|
-
cancellations.delete(spec.executionId);
|
|
422
|
-
executionReservations.delete(spec.executionId);
|
|
326
|
+
cleanupExecutionReservation();
|
|
423
327
|
executionRuns.delete(spec.executionId);
|
|
424
|
-
knownExecutionHashes.delete(spec.executionId);
|
|
425
328
|
});
|
|
426
329
|
executionRuns.set(spec.executionId, execution);
|
|
427
330
|
void execution.catch(() => { });
|
|
@@ -556,75 +459,88 @@ export function serve(config, opts = {}) {
|
|
|
556
459
|
return;
|
|
557
460
|
}
|
|
558
461
|
const queueWaitStart = Date.now();
|
|
462
|
+
const runStartedAt = Date.now();
|
|
463
|
+
const controller = createRuntimeCancellation();
|
|
464
|
+
let finishLegacyRun;
|
|
465
|
+
let failLegacyRun;
|
|
466
|
+
const legacyDone = new Promise((resolveDone, rejectDone) => {
|
|
467
|
+
finishLegacyRun = resolveDone;
|
|
468
|
+
failLegacyRun = rejectDone;
|
|
469
|
+
});
|
|
470
|
+
void legacyDone.catch(() => undefined);
|
|
471
|
+
legacyRuns.set(runId, { controller, done: legacyDone });
|
|
472
|
+
let legacyStopError;
|
|
473
|
+
let legacyReservation = null;
|
|
559
474
|
let legacyQueueDone = null;
|
|
560
475
|
let finishLegacyQueue = () => { };
|
|
561
|
-
if (!scheduled) {
|
|
562
|
-
const previous = legacyTaskTails.get(key);
|
|
563
|
-
legacyQueueDone = new Promise((resolve) => { finishLegacyQueue = resolve; });
|
|
564
|
-
legacyTaskTails.set(key, legacyQueueDone);
|
|
565
|
-
if (previous) {
|
|
566
|
-
log(`⏳ 排队(该任务已在运行): ${key}`);
|
|
567
|
-
dslog("run.wake_queued", "唤醒已排队:该任务正在运行", { ...runKeys });
|
|
568
|
-
await previous;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
running.add(key);
|
|
572
|
-
// 并行槽:同 agent 超过配置上限的任务在此排队(不丢),有空位再跑。
|
|
573
|
-
await acquireSlot(msg.agentHandle);
|
|
574
|
-
const queueMs = Date.now() - queueWaitStart;
|
|
575
|
-
const threadLabel = threadId ?? null;
|
|
576
|
-
const from = msg.wake?.senderHandle ?? "?";
|
|
577
|
-
const incoming = msg.wake?.content ?? "";
|
|
578
|
-
dslog("run.start", `开始运行 ${msg.agentHandle}`, { ...runKeys, queue_ms: queueMs });
|
|
579
|
-
const runStartedAt = Date.now();
|
|
580
|
-
log(`\n${"─".repeat(56)}`);
|
|
581
|
-
log(`🔔 唤醒 agent=${msg.agentHandle} reason=${msg.reason ?? "?"}`);
|
|
582
|
-
log(` channel = ${msg.channelId}`);
|
|
583
|
-
log(` thread = ${threadLabel ? `${threadLabel} (要求线程内回复)` : "(无,顶层回复)"}`);
|
|
584
|
-
if (incoming)
|
|
585
|
-
log(`📥 来信 @${from}: ${incoming.replace(/\s+/g, " ").slice(0, 200)}`);
|
|
586
|
-
let actSeq = 0;
|
|
587
|
-
const reportActivity = (a) => {
|
|
588
|
-
const det = a.detail ? a.detail.replace(/\s+/g, " ").trim() : "";
|
|
589
|
-
// 发消息时尽量打印回复正文/目标(--content "..." 或 heredoc 首行)
|
|
590
|
-
let line = ` · ${a.label}`;
|
|
591
|
-
if (a.kind === "sending") {
|
|
592
|
-
const m = det.match(/--content\s+"([^"]*)"/) || det.match(/<<'?\w+'?\s*(.*)/);
|
|
593
|
-
line = ` 💬 回复${threadLabel ? `(thread ${threadLabel})` : ""}: ${m ? m[1].slice(0, 160) : det.slice(0, 120)}`;
|
|
594
|
-
}
|
|
595
|
-
else if (det) {
|
|
596
|
-
line += ` ${det.slice(0, 80)}`;
|
|
597
|
-
}
|
|
598
|
-
log(line);
|
|
599
|
-
try {
|
|
600
|
-
ws?.send(JSON.stringify({
|
|
601
|
-
type: "agent:activity",
|
|
602
|
-
agentHandle: msg.agentHandle,
|
|
603
|
-
channelId: msg.channelId,
|
|
604
|
-
activity: ACTIVITY_MAP[a.kind] ?? "working",
|
|
605
|
-
detail: a.detail || a.label,
|
|
606
|
-
seq: actSeq++,
|
|
607
|
-
}));
|
|
608
|
-
}
|
|
609
|
-
catch { /* ws 非 OPEN,忽略 */ }
|
|
610
|
-
};
|
|
611
|
-
// 终端透传:把底层 claude 的每条 console 行按线程上送给 server(独立 seq,落库+广播给 web 终端窗口)。
|
|
612
|
-
let conSeq = 0;
|
|
613
|
-
const reportConsole = (c) => {
|
|
614
|
-
try {
|
|
615
|
-
ws?.send(JSON.stringify({
|
|
616
|
-
type: "agent:console",
|
|
617
|
-
agentHandle: msg.agentHandle,
|
|
618
|
-
channelId: msg.channelId,
|
|
619
|
-
threadId: threadId ?? null,
|
|
620
|
-
stream: c.stream,
|
|
621
|
-
text: c.text,
|
|
622
|
-
seq: conSeq++,
|
|
623
|
-
}));
|
|
624
|
-
}
|
|
625
|
-
catch { /* ws 非 OPEN,忽略 */ }
|
|
626
|
-
};
|
|
627
476
|
try {
|
|
477
|
+
if (!scheduled) {
|
|
478
|
+
const previous = legacyTaskTails.get(key);
|
|
479
|
+
legacyQueueDone = new Promise((resolve) => { finishLegacyQueue = resolve; });
|
|
480
|
+
legacyTaskTails.set(key, legacyQueueDone);
|
|
481
|
+
if (previous) {
|
|
482
|
+
log(`⏳ 排队(该任务已在运行): ${key}`);
|
|
483
|
+
dslog("run.wake_queued", "唤醒已排队:该任务正在运行", { ...runKeys });
|
|
484
|
+
await awaitWithCancellation(previous, controller.cancellation);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
running.add(key);
|
|
488
|
+
legacyReservation = sharedSlots.reserve(msg.agentHandle, "legacy");
|
|
489
|
+
await awaitWithCancellation(legacyReservation.ready, controller.cancellation);
|
|
490
|
+
const queueMs = Date.now() - queueWaitStart;
|
|
491
|
+
const threadLabel = threadId ?? null;
|
|
492
|
+
const from = msg.wake?.senderHandle ?? "?";
|
|
493
|
+
const incoming = msg.wake?.content ?? "";
|
|
494
|
+
dslog("run.start", `开始运行 ${msg.agentHandle}`, { ...runKeys, queue_ms: queueMs });
|
|
495
|
+
log(`\n${"─".repeat(56)}`);
|
|
496
|
+
log(`🔔 唤醒 agent=${msg.agentHandle} reason=${msg.reason ?? "?"}`);
|
|
497
|
+
log(` channel = ${msg.channelId}`);
|
|
498
|
+
log(` thread = ${threadLabel ? `${threadLabel} (要求线程内回复)` : "(无,顶层回复)"}`);
|
|
499
|
+
if (incoming)
|
|
500
|
+
log(`📥 来信 @${from}: ${incoming.replace(/\s+/g, " ").slice(0, 200)}`);
|
|
501
|
+
let actSeq = 0;
|
|
502
|
+
const reportActivity = (a) => {
|
|
503
|
+
const det = a.detail ? a.detail.replace(/\s+/g, " ").trim() : "";
|
|
504
|
+
// 发消息时尽量打印回复正文/目标(--content "..." 或 heredoc 首行)
|
|
505
|
+
let line = ` · ${a.label}`;
|
|
506
|
+
if (a.kind === "sending") {
|
|
507
|
+
const m = det.match(/--content\s+"([^"]*)"/) || det.match(/<<'?\w+'?\s*(.*)/);
|
|
508
|
+
line = ` 💬 回复${threadLabel ? `(thread ${threadLabel})` : ""}: ${m ? m[1].slice(0, 160) : det.slice(0, 120)}`;
|
|
509
|
+
}
|
|
510
|
+
else if (det) {
|
|
511
|
+
line += ` ${det.slice(0, 80)}`;
|
|
512
|
+
}
|
|
513
|
+
log(line);
|
|
514
|
+
try {
|
|
515
|
+
ws?.send(JSON.stringify({
|
|
516
|
+
type: "agent:activity",
|
|
517
|
+
agentHandle: msg.agentHandle,
|
|
518
|
+
channelId: msg.channelId,
|
|
519
|
+
activity: ACTIVITY_MAP[a.kind] ?? "working",
|
|
520
|
+
detail: a.detail || a.label,
|
|
521
|
+
seq: actSeq++,
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
catch { /* ws 非 OPEN,忽略 */ }
|
|
525
|
+
};
|
|
526
|
+
// 终端透传:把底层 claude 的每条 console 行按线程上送给 server(独立 seq,落库+广播给 web 终端窗口)。
|
|
527
|
+
let conSeq = 0;
|
|
528
|
+
const reportConsole = (c) => {
|
|
529
|
+
try {
|
|
530
|
+
ws?.send(JSON.stringify({
|
|
531
|
+
type: "agent:console",
|
|
532
|
+
agentHandle: msg.agentHandle,
|
|
533
|
+
channelId: msg.channelId,
|
|
534
|
+
threadId: threadId ?? null,
|
|
535
|
+
stream: c.stream,
|
|
536
|
+
text: c.text,
|
|
537
|
+
// 结构化负载(diff/命令/todo…):前端富渲染用;缺省 = 纯文本行
|
|
538
|
+
...(c.payload !== undefined ? { payload: c.payload } : {}),
|
|
539
|
+
seq: conSeq++,
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
catch { /* ws 非 OPEN,忽略 */ }
|
|
543
|
+
};
|
|
628
544
|
// 线程聚合:触发消息即任务线程根,你的确认+后续所有回复都要发到它的线程里,
|
|
629
545
|
// 不要发顶层——这样 task 讨论全部聚合在该 thread 下。
|
|
630
546
|
const threadHint = threadId
|
|
@@ -681,7 +597,7 @@ export function serve(config, opts = {}) {
|
|
|
681
597
|
...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
|
|
682
598
|
...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
|
|
683
599
|
...(attemptWake ? { wake: attemptWake } : {}),
|
|
684
|
-
}, reportActivity, reportConsole);
|
|
600
|
+
}, reportActivity, reportConsole, { cancellation: controller.cancellation });
|
|
685
601
|
});
|
|
686
602
|
const result = mergeRunAgentResults(guarded.results);
|
|
687
603
|
// 本轮 token 用量上报:runner 已从 result 事件提取(含缓存读/写细分),
|
|
@@ -762,6 +678,9 @@ export function serve(config, opts = {}) {
|
|
|
762
678
|
}
|
|
763
679
|
}
|
|
764
680
|
catch (e) {
|
|
681
|
+
if (controller.cancellation.isRequested() && !(e instanceof RuntimeCancelledError)) {
|
|
682
|
+
legacyStopError = e;
|
|
683
|
+
}
|
|
765
684
|
log(`❌ runAgent 失败: ${e.message}`);
|
|
766
685
|
dslog("run.error", `runAgent 失败: ${e.message}`, {
|
|
767
686
|
level: "ERROR", ...runKeys, duration_ms: Date.now() - runStartedAt,
|
|
@@ -796,11 +715,16 @@ export function serve(config, opts = {}) {
|
|
|
796
715
|
}
|
|
797
716
|
finally {
|
|
798
717
|
running.delete(key);
|
|
799
|
-
|
|
718
|
+
legacyReservation?.release();
|
|
800
719
|
finishLegacyQueue();
|
|
801
720
|
if (legacyQueueDone && legacyTaskTails.get(key) === legacyQueueDone) {
|
|
802
721
|
legacyTaskTails.delete(key);
|
|
803
722
|
}
|
|
723
|
+
legacyRuns.delete(runId);
|
|
724
|
+
if (legacyStopError === undefined)
|
|
725
|
+
finishLegacyRun();
|
|
726
|
+
else
|
|
727
|
+
failLegacyRun(legacyStopError);
|
|
804
728
|
void flushSlog(); // 每轮收尾冲一次,保证 run.end 尽快可查
|
|
805
729
|
}
|
|
806
730
|
});
|
|
@@ -823,22 +747,67 @@ export function serve(config, opts = {}) {
|
|
|
823
747
|
running_tasks: [...running].join(","),
|
|
824
748
|
});
|
|
825
749
|
void flushSlog();
|
|
826
|
-
setTimeout(
|
|
750
|
+
reconnectTimer = setTimeout(() => {
|
|
751
|
+
reconnectTimer = null;
|
|
752
|
+
connect();
|
|
753
|
+
}, backoff);
|
|
827
754
|
backoff = Math.min(backoff * 2, maxBackoff);
|
|
828
755
|
});
|
|
829
|
-
ws.on("error", () =>
|
|
756
|
+
ws.on("error", () => {
|
|
757
|
+
if (!stopped)
|
|
758
|
+
ws?.close();
|
|
759
|
+
});
|
|
830
760
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
761
|
+
const ready = (async () => {
|
|
762
|
+
await reconcileExecutionJournal(executionJournal, {
|
|
763
|
+
agentsRoot: config.agentsRoot,
|
|
764
|
+
serverUrl: config.serverUrl,
|
|
765
|
+
...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
|
|
766
|
+
log: dslog,
|
|
767
|
+
flush: flushSlog,
|
|
768
|
+
writeStderr: (line) => process.stderr.write(line),
|
|
769
|
+
});
|
|
770
|
+
connect();
|
|
771
|
+
})();
|
|
772
|
+
const stop = () => {
|
|
773
|
+
if (stopPromise !== null)
|
|
774
|
+
return stopPromise;
|
|
775
|
+
stopPromise = (async () => {
|
|
834
776
|
stopped = true;
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
777
|
+
const deadline = createShutdownDeadline(shutdownTimeoutMs);
|
|
778
|
+
if (reconnectTimer !== null) {
|
|
779
|
+
clearTimeout(reconnectTimer);
|
|
780
|
+
reconnectTimer = null;
|
|
781
|
+
}
|
|
782
|
+
const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
|
|
783
|
+
const pending = [
|
|
784
|
+
webSocketClosed,
|
|
785
|
+
executionFrameQueue,
|
|
786
|
+
...executionRuns.values(),
|
|
787
|
+
...[...legacyRuns.values()].map((run) => run.done),
|
|
788
|
+
...(testShutdown.barrier === null ? [] : [testShutdown.barrier]),
|
|
789
|
+
];
|
|
790
|
+
try {
|
|
791
|
+
for (const executionId of executionRuns.keys())
|
|
792
|
+
requestCancellation(executionId);
|
|
793
|
+
for (const run of legacyRuns.values())
|
|
794
|
+
run.controller.request();
|
|
795
|
+
await deadline.waitFor(Promise.all(pending));
|
|
796
|
+
await executionJournal.close({ signal: deadline.signal });
|
|
797
|
+
}
|
|
798
|
+
finally {
|
|
799
|
+
deadline.dispose();
|
|
800
|
+
}
|
|
801
|
+
})();
|
|
802
|
+
return stopPromise;
|
|
803
|
+
};
|
|
804
|
+
return {
|
|
805
|
+
ready,
|
|
806
|
+
stop,
|
|
807
|
+
shutdownSnapshot: () => ({
|
|
808
|
+
activeExecutionCount: executionRuns.size,
|
|
809
|
+
activeLegacyCount: legacyRuns.size,
|
|
810
|
+
deadlineMs: shutdownTimeoutMs,
|
|
811
|
+
}),
|
|
843
812
|
};
|
|
844
813
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function createSharedSlotManager(limits) {
|
|
2
|
+
const activeByHandle = new Map();
|
|
3
|
+
const queuesByHandle = new Map();
|
|
4
|
+
const promoteNext = (handle) => {
|
|
5
|
+
const queue = queuesByHandle.get(handle) ?? [];
|
|
6
|
+
while ((activeByHandle.get(handle) ?? 0) < limits.maxParallelPerAgent && queue.length > 0) {
|
|
7
|
+
const next = queue.shift();
|
|
8
|
+
if (next.released)
|
|
9
|
+
continue;
|
|
10
|
+
next.promoted = true;
|
|
11
|
+
activeByHandle.set(handle, (activeByHandle.get(handle) ?? 0) + 1);
|
|
12
|
+
next.resolve();
|
|
13
|
+
}
|
|
14
|
+
if (queue.length === 0)
|
|
15
|
+
queuesByHandle.delete(handle);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
reserve: (handle, kind) => {
|
|
19
|
+
const active = activeByHandle.get(handle) ?? 0;
|
|
20
|
+
const queued = queuesByHandle.get(handle)?.length ?? 0;
|
|
21
|
+
const facts = { activeForAgent: active, queuedForAgent: queued };
|
|
22
|
+
if (kind === "execution" && active >= limits.maxParallelPerAgent
|
|
23
|
+
&& queued >= limits.maxQueuedPerAgent) {
|
|
24
|
+
return { facts, ready: Promise.resolve(), isQueued: () => false, release: () => { } };
|
|
25
|
+
}
|
|
26
|
+
let resolveReady;
|
|
27
|
+
const ready = new Promise((resolve) => { resolveReady = resolve; });
|
|
28
|
+
const entry = {
|
|
29
|
+
kind,
|
|
30
|
+
released: false,
|
|
31
|
+
promoted: active < limits.maxParallelPerAgent,
|
|
32
|
+
resolve: resolveReady,
|
|
33
|
+
};
|
|
34
|
+
if (entry.promoted) {
|
|
35
|
+
activeByHandle.set(handle, active + 1);
|
|
36
|
+
resolveReady();
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const queue = queuesByHandle.get(handle) ?? [];
|
|
40
|
+
queue.push(entry);
|
|
41
|
+
queuesByHandle.set(handle, queue);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
facts,
|
|
45
|
+
state: entry.promoted ? "ready" : "queued",
|
|
46
|
+
ready,
|
|
47
|
+
isQueued: () => !entry.promoted && !entry.released,
|
|
48
|
+
release: () => {
|
|
49
|
+
if (entry.released)
|
|
50
|
+
return;
|
|
51
|
+
entry.released = true;
|
|
52
|
+
if (entry.promoted) {
|
|
53
|
+
activeByHandle.set(handle, Math.max(0, (activeByHandle.get(handle) ?? 1) - 1));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const queue = queuesByHandle.get(handle);
|
|
57
|
+
const index = queue?.indexOf(entry) ?? -1;
|
|
58
|
+
if (queue !== undefined && index >= 0)
|
|
59
|
+
queue.splice(index, 1);
|
|
60
|
+
if (queue?.length === 0)
|
|
61
|
+
queuesByHandle.delete(handle);
|
|
62
|
+
}
|
|
63
|
+
promoteNext(handle);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function readTestShutdownConfiguration(env) {
|
|
2
|
+
if (env.NODE_ENV !== "test")
|
|
3
|
+
return { barrier: null };
|
|
4
|
+
const timeoutMs = env.CREW_DAEMON_TEST_SHUTDOWN_TIMEOUT_MS === undefined
|
|
5
|
+
? undefined
|
|
6
|
+
: Number(env.CREW_DAEMON_TEST_SHUTDOWN_TIMEOUT_MS);
|
|
7
|
+
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
|
|
8
|
+
throw new RangeError("CREW_DAEMON_TEST_SHUTDOWN_TIMEOUT_MS must be a positive number");
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
12
|
+
barrier: env.CREW_DAEMON_TEST_SHUTDOWN_BARRIER === "1" ? new Promise(() => { }) : null,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function createShutdownDeadline(timeoutMs) {
|
|
16
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
17
|
+
throw new RangeError("shutdown timeout must be a positive finite number");
|
|
18
|
+
}
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timeoutError = new Error(`Timed out waiting for daemon shutdown after ${timeoutMs}ms`);
|
|
21
|
+
let rejectExpired;
|
|
22
|
+
const expired = new Promise((_resolve, reject) => { rejectExpired = reject; });
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
controller.abort(timeoutError);
|
|
25
|
+
rejectExpired(timeoutError);
|
|
26
|
+
}, timeoutMs);
|
|
27
|
+
return {
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
waitFor: async (operation) => Promise.race([operation, expired]),
|
|
30
|
+
dispose: () => clearTimeout(timer),
|
|
31
|
+
};
|
|
32
|
+
}
|