@nowcrew/daemon 0.5.18 → 0.5.20
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/README.md +23 -0
- package/dist/attachments.js +196 -0
- 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/execution-event-limit.js +1 -1
- package/dist/execution-journal-lock.js +199 -40
- package/dist/execution-journal.js +42 -4
- package/dist/execution-protocol.js +21 -1
- package/dist/execution-recovery.js +71 -0
- package/dist/execution-runner.js +68 -77
- package/dist/execution-supervisor.js +79 -31
- package/dist/external-output.js +114 -0
- package/dist/i18n.js +5 -5
- package/dist/list-models.js +41 -5
- package/dist/local-executor.js +103 -14
- package/dist/machine-info.js +6 -1
- package/dist/main.js +23 -8
- package/dist/origin-decision.js +3 -1
- package/dist/prompt.js +4 -1
- package/dist/runner.js +14 -9
- package/dist/runtime-cancellation.js +74 -0
- package/dist/runtime-capabilities.js +38 -0
- package/dist/runtime-path.js +60 -0
- package/dist/runtimes/claude.js +9 -4
- package/dist/runtimes/codex-app-server-runner.js +340 -0
- package/dist/runtimes/codex.js +10 -4
- package/dist/runtimes/kimi-acp-runner.js +117 -17
- package/dist/runtimes/kimi.js +2 -0
- package/dist/runtimes/progress-watchdog.js +26 -0
- package/dist/serve-lifecycle.js +82 -0
- package/dist/serve.js +212 -212
- package/dist/session.js +1 -1
- 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/package.json +3 -3
package/dist/serve.js
CHANGED
|
@@ -22,6 +22,11 @@ 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",
|
|
@@ -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,134 +128,33 @@ 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);
|
|
238
147
|
dslog("daemon.start", "daemon 常驻模式启动", { server_url: config.serverUrl, runtime: config.runtimeBin });
|
|
239
148
|
// 并行调度:同一 agent 可并行处理多个【不同任务】(线程/频道),每任务隔离 cwd+work-log。
|
|
240
|
-
//
|
|
241
|
-
//
|
|
149
|
+
// scheduled 重复 run 仍由 running 去重;普通同线程 wake 用 legacyTaskTails 串成 FIFO。
|
|
150
|
+
// 每 agent 超过并行上限的任务继续进入 sharedQueues(不丢)。
|
|
242
151
|
const running = new Set();
|
|
243
|
-
const
|
|
244
|
-
await reserveSharedSlot(handle, "legacy").ready;
|
|
245
|
-
};
|
|
246
|
-
const releaseSlot = (handle) => {
|
|
247
|
-
sharedActive.set(handle, Math.max(0, (sharedActive.get(handle) ?? 1) - 1));
|
|
248
|
-
promoteNext(handle);
|
|
249
|
-
};
|
|
152
|
+
const legacyTaskTails = new Map();
|
|
250
153
|
const log = (s) => process.stdout.write(formatDaemonLogLine(s) + "\n");
|
|
251
154
|
function connect() {
|
|
252
155
|
if (stopped)
|
|
253
156
|
return;
|
|
254
|
-
ws =
|
|
157
|
+
ws = createWebSocket(wsUrl);
|
|
255
158
|
ws.on("open", () => {
|
|
256
159
|
backoff = 1000;
|
|
257
160
|
connectedAt = Date.now();
|
|
@@ -278,7 +181,7 @@ export function serve(config, opts = {}) {
|
|
|
278
181
|
})
|
|
279
182
|
.catch(() => { });
|
|
280
183
|
opts.onOpen?.(ws);
|
|
281
|
-
void
|
|
184
|
+
void sendSnapshot(`reconnect-${randomUUID()}`);
|
|
282
185
|
void replayExecutionTelemetry().catch((error) => {
|
|
283
186
|
dslog("execution.telemetry_replay_failed", "execution telemetry 重放失败", {
|
|
284
187
|
level: "ERROR", error_message: error.message,
|
|
@@ -286,6 +189,8 @@ export function serve(config, opts = {}) {
|
|
|
286
189
|
});
|
|
287
190
|
});
|
|
288
191
|
ws.on("message", async (data) => {
|
|
192
|
+
if (stopped)
|
|
193
|
+
return;
|
|
289
194
|
let decoded;
|
|
290
195
|
try {
|
|
291
196
|
decoded = JSON.parse(data.toString());
|
|
@@ -315,7 +220,8 @@ export function serve(config, opts = {}) {
|
|
|
315
220
|
}
|
|
316
221
|
const frame = parsedExecution.data;
|
|
317
222
|
executionFrameQueue = executionFrameQueue.then(async () => {
|
|
318
|
-
|
|
223
|
+
if (stopped)
|
|
224
|
+
return;
|
|
319
225
|
if (frame.type === "execution:completion-ack") {
|
|
320
226
|
await executionJournal.acknowledgeCompletion(frame.executionId).catch(() => { });
|
|
321
227
|
return;
|
|
@@ -342,14 +248,6 @@ export function serve(config, opts = {}) {
|
|
|
342
248
|
return;
|
|
343
249
|
}
|
|
344
250
|
const spec = frame;
|
|
345
|
-
if (!recovered) {
|
|
346
|
-
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
347
|
-
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
348
|
-
reason: "resource_limit", message: "Local execution journal recovery failed",
|
|
349
|
-
at: new Date().toISOString(),
|
|
350
|
-
}));
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
251
|
const hash = hashExecutionSpec(spec);
|
|
354
252
|
const knownHash = knownExecutionHashes.get(spec.executionId);
|
|
355
253
|
if (knownHash !== undefined) {
|
|
@@ -369,6 +267,8 @@ export function serve(config, opts = {}) {
|
|
|
369
267
|
return;
|
|
370
268
|
}
|
|
371
269
|
const durableExisting = await executionJournal.get(spec.executionId);
|
|
270
|
+
if (stopped)
|
|
271
|
+
return;
|
|
372
272
|
if (durableExisting !== null) {
|
|
373
273
|
if (durableExisting.specHash !== hash) {
|
|
374
274
|
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
@@ -383,19 +283,22 @@ export function serve(config, opts = {}) {
|
|
|
383
283
|
return;
|
|
384
284
|
}
|
|
385
285
|
knownExecutionHashes.set(spec.executionId, hash);
|
|
386
|
-
const reservation =
|
|
286
|
+
const reservation = sharedSlots.reserve(spec.agent.handle, "execution");
|
|
387
287
|
executionReservations.set(spec.executionId, reservation);
|
|
388
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
|
+
};
|
|
389
295
|
let availableRuntimes;
|
|
390
296
|
try {
|
|
391
297
|
availableRuntimes = opts.execution?.availableRuntimes?.()
|
|
392
298
|
?? await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes));
|
|
393
299
|
}
|
|
394
300
|
catch (error) {
|
|
395
|
-
|
|
396
|
-
cancellations.delete(spec.executionId);
|
|
397
|
-
executionReservations.delete(spec.executionId);
|
|
398
|
-
knownExecutionHashes.delete(spec.executionId);
|
|
301
|
+
cleanupExecutionReservation();
|
|
399
302
|
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
400
303
|
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
401
304
|
reason: "resource_limit", message: `Runtime detection failed: ${error.message}`,
|
|
@@ -403,6 +306,10 @@ export function serve(config, opts = {}) {
|
|
|
403
306
|
}));
|
|
404
307
|
return;
|
|
405
308
|
}
|
|
309
|
+
if (stopped) {
|
|
310
|
+
cleanupExecutionReservation();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
406
313
|
const execution = executeProtocol(config, spec, {
|
|
407
314
|
...opts.execution?.dependencies,
|
|
408
315
|
journal: executionJournal,
|
|
@@ -416,11 +323,8 @@ export function serve(config, opts = {}) {
|
|
|
416
323
|
}),
|
|
417
324
|
cancellation,
|
|
418
325
|
}).finally(() => {
|
|
419
|
-
|
|
420
|
-
cancellations.delete(spec.executionId);
|
|
421
|
-
executionReservations.delete(spec.executionId);
|
|
326
|
+
cleanupExecutionReservation();
|
|
422
327
|
executionRuns.delete(spec.executionId);
|
|
423
|
-
knownExecutionHashes.delete(spec.executionId);
|
|
424
328
|
});
|
|
425
329
|
executionRuns.set(spec.executionId, execution);
|
|
426
330
|
void execution.catch(() => { });
|
|
@@ -436,8 +340,23 @@ export function serve(config, opts = {}) {
|
|
|
436
340
|
&& "type" in decoded && decoded.type === "agent:start"
|
|
437
341
|
? LegacyAgentStartSchema.safeParse(decoded)
|
|
438
342
|
: null;
|
|
439
|
-
if (legacy !== null && !legacy.success)
|
|
343
|
+
if (legacy !== null && !legacy.success) {
|
|
344
|
+
// 帧结构不合法(如 server 端已升级到 execution v1 但本 daemon 仍按 legacy 校验)。
|
|
345
|
+
// 曾经静默 return——只表现为"@了 agent 没反应",且完全没有排查线索。记录字段路径和
|
|
346
|
+
// zod 错误码,不记录消息正文/附件等可能敏感的字段值。
|
|
347
|
+
const raw = decoded;
|
|
348
|
+
dslog("run.legacy_frame_rejected", "agent:start 帧未通过 legacy schema 校验,已丢弃", {
|
|
349
|
+
level: "WARN",
|
|
350
|
+
agent_handle: typeof raw.agentHandle === "string" ? raw.agentHandle : undefined,
|
|
351
|
+
channel_id: typeof raw.channelId === "string" ? raw.channelId : undefined,
|
|
352
|
+
reason: typeof raw.reason === "string" ? raw.reason : undefined,
|
|
353
|
+
issue_count: legacy.error.issues.length,
|
|
354
|
+
issue_paths: legacy.error.issues
|
|
355
|
+
.map((issue) => `${issue.path.join(".")}:${issue.code}`)
|
|
356
|
+
.join(","),
|
|
357
|
+
});
|
|
440
358
|
return;
|
|
359
|
+
}
|
|
441
360
|
const msg = (legacy?.success ? legacy.data : decoded);
|
|
442
361
|
// 控制面鉴权拒绝:server 端 resolveToken 未命中有效的 machine 凭证(失效/被吊销/
|
|
443
362
|
// 库已重置)。不能静默丢弃这帧——否则只表现为神秘的「每 1s 重连」循环。打印可执行
|
|
@@ -534,69 +453,94 @@ export function serve(config, opts = {}) {
|
|
|
534
453
|
wake_origin: msg.wake?.origin ?? null,
|
|
535
454
|
content_preview: (msg.wake?.content ?? "").replace(/\s+/g, " ").slice(0, 120),
|
|
536
455
|
});
|
|
537
|
-
if (running.has(key)) {
|
|
456
|
+
if (scheduled && running.has(key)) {
|
|
538
457
|
log(`↩︎ 跳过(该任务已在运行): ${key}`);
|
|
539
458
|
dslog("run.dedupe_skip", "跳过唤醒:该任务已在运行", { level: "WARN", ...runKeys });
|
|
540
459
|
return;
|
|
541
460
|
}
|
|
542
|
-
|
|
543
|
-
// 并行槽:同 agent 超过配置上限的任务在此排队(不丢),有空位再跑。
|
|
544
|
-
const slotWaitStart = Date.now();
|
|
545
|
-
await acquireSlot(msg.agentHandle);
|
|
546
|
-
const queueMs = Date.now() - slotWaitStart;
|
|
547
|
-
const threadLabel = threadId ?? null;
|
|
548
|
-
const from = msg.wake?.senderHandle ?? "?";
|
|
549
|
-
const incoming = msg.wake?.content ?? "";
|
|
550
|
-
dslog("run.start", `开始运行 ${msg.agentHandle}`, { ...runKeys, queue_ms: queueMs });
|
|
461
|
+
const queueWaitStart = Date.now();
|
|
551
462
|
const runStartedAt = Date.now();
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
line = ` 💬 回复${threadLabel ? `(thread ${threadLabel})` : ""}: ${m ? m[1].slice(0, 160) : det.slice(0, 120)}`;
|
|
566
|
-
}
|
|
567
|
-
else if (det) {
|
|
568
|
-
line += ` ${det.slice(0, 80)}`;
|
|
569
|
-
}
|
|
570
|
-
log(line);
|
|
571
|
-
try {
|
|
572
|
-
ws?.send(JSON.stringify({
|
|
573
|
-
type: "agent:activity",
|
|
574
|
-
agentHandle: msg.agentHandle,
|
|
575
|
-
channelId: msg.channelId,
|
|
576
|
-
activity: ACTIVITY_MAP[a.kind] ?? "working",
|
|
577
|
-
detail: a.detail || a.label,
|
|
578
|
-
seq: actSeq++,
|
|
579
|
-
}));
|
|
580
|
-
}
|
|
581
|
-
catch { /* ws 非 OPEN,忽略 */ }
|
|
582
|
-
};
|
|
583
|
-
// 终端透传:把底层 claude 的每条 console 行按线程上送给 server(独立 seq,落库+广播给 web 终端窗口)。
|
|
584
|
-
let conSeq = 0;
|
|
585
|
-
const reportConsole = (c) => {
|
|
586
|
-
try {
|
|
587
|
-
ws?.send(JSON.stringify({
|
|
588
|
-
type: "agent:console",
|
|
589
|
-
agentHandle: msg.agentHandle,
|
|
590
|
-
channelId: msg.channelId,
|
|
591
|
-
threadId: threadId ?? null,
|
|
592
|
-
stream: c.stream,
|
|
593
|
-
text: c.text,
|
|
594
|
-
seq: conSeq++,
|
|
595
|
-
}));
|
|
596
|
-
}
|
|
597
|
-
catch { /* ws 非 OPEN,忽略 */ }
|
|
598
|
-
};
|
|
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;
|
|
474
|
+
let legacyQueueDone = null;
|
|
475
|
+
let finishLegacyQueue = () => { };
|
|
599
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
|
+
};
|
|
600
544
|
// 线程聚合:触发消息即任务线程根,你的确认+后续所有回复都要发到它的线程里,
|
|
601
545
|
// 不要发顶层——这样 task 讨论全部聚合在该 thread 下。
|
|
602
546
|
const threadHint = threadId
|
|
@@ -653,7 +597,7 @@ export function serve(config, opts = {}) {
|
|
|
653
597
|
...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
|
|
654
598
|
...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
|
|
655
599
|
...(attemptWake ? { wake: attemptWake } : {}),
|
|
656
|
-
}, reportActivity, reportConsole);
|
|
600
|
+
}, reportActivity, reportConsole, { cancellation: controller.cancellation });
|
|
657
601
|
});
|
|
658
602
|
const result = mergeRunAgentResults(guarded.results);
|
|
659
603
|
// 本轮 token 用量上报:runner 已从 result 事件提取(含缓存读/写细分),
|
|
@@ -734,6 +678,9 @@ export function serve(config, opts = {}) {
|
|
|
734
678
|
}
|
|
735
679
|
}
|
|
736
680
|
catch (e) {
|
|
681
|
+
if (controller.cancellation.isRequested() && !(e instanceof RuntimeCancelledError)) {
|
|
682
|
+
legacyStopError = e;
|
|
683
|
+
}
|
|
737
684
|
log(`❌ runAgent 失败: ${e.message}`);
|
|
738
685
|
dslog("run.error", `runAgent 失败: ${e.message}`, {
|
|
739
686
|
level: "ERROR", ...runKeys, duration_ms: Date.now() - runStartedAt,
|
|
@@ -768,7 +715,16 @@ export function serve(config, opts = {}) {
|
|
|
768
715
|
}
|
|
769
716
|
finally {
|
|
770
717
|
running.delete(key);
|
|
771
|
-
|
|
718
|
+
legacyReservation?.release();
|
|
719
|
+
finishLegacyQueue();
|
|
720
|
+
if (legacyQueueDone && legacyTaskTails.get(key) === legacyQueueDone) {
|
|
721
|
+
legacyTaskTails.delete(key);
|
|
722
|
+
}
|
|
723
|
+
legacyRuns.delete(runId);
|
|
724
|
+
if (legacyStopError === undefined)
|
|
725
|
+
finishLegacyRun();
|
|
726
|
+
else
|
|
727
|
+
failLegacyRun(legacyStopError);
|
|
772
728
|
void flushSlog(); // 每轮收尾冲一次,保证 run.end 尽快可查
|
|
773
729
|
}
|
|
774
730
|
});
|
|
@@ -791,22 +747,66 @@ export function serve(config, opts = {}) {
|
|
|
791
747
|
running_tasks: [...running].join(","),
|
|
792
748
|
});
|
|
793
749
|
void flushSlog();
|
|
794
|
-
setTimeout(
|
|
750
|
+
reconnectTimer = setTimeout(() => {
|
|
751
|
+
reconnectTimer = null;
|
|
752
|
+
connect();
|
|
753
|
+
}, backoff);
|
|
795
754
|
backoff = Math.min(backoff * 2, maxBackoff);
|
|
796
755
|
});
|
|
797
|
-
ws.on("error", () =>
|
|
756
|
+
ws.on("error", () => {
|
|
757
|
+
if (!stopped)
|
|
758
|
+
ws?.close();
|
|
759
|
+
});
|
|
798
760
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
761
|
+
const ready = (async () => {
|
|
762
|
+
await reconcileExecutionJournal(executionJournal, {
|
|
763
|
+
agentsRoot: config.agentsRoot,
|
|
764
|
+
serverUrl: config.serverUrl,
|
|
765
|
+
log: dslog,
|
|
766
|
+
flush: flushSlog,
|
|
767
|
+
writeStderr: (line) => process.stderr.write(line),
|
|
768
|
+
});
|
|
769
|
+
connect();
|
|
770
|
+
})();
|
|
771
|
+
const stop = () => {
|
|
772
|
+
if (stopPromise !== null)
|
|
773
|
+
return stopPromise;
|
|
774
|
+
stopPromise = (async () => {
|
|
802
775
|
stopped = true;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
776
|
+
const deadline = createShutdownDeadline(shutdownTimeoutMs);
|
|
777
|
+
if (reconnectTimer !== null) {
|
|
778
|
+
clearTimeout(reconnectTimer);
|
|
779
|
+
reconnectTimer = null;
|
|
780
|
+
}
|
|
781
|
+
const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
|
|
782
|
+
const pending = [
|
|
783
|
+
webSocketClosed,
|
|
784
|
+
executionFrameQueue,
|
|
785
|
+
...executionRuns.values(),
|
|
786
|
+
...[...legacyRuns.values()].map((run) => run.done),
|
|
787
|
+
...(testShutdown.barrier === null ? [] : [testShutdown.barrier]),
|
|
788
|
+
];
|
|
789
|
+
try {
|
|
790
|
+
for (const executionId of executionRuns.keys())
|
|
791
|
+
requestCancellation(executionId);
|
|
792
|
+
for (const run of legacyRuns.values())
|
|
793
|
+
run.controller.request();
|
|
794
|
+
await deadline.waitFor(Promise.all(pending));
|
|
795
|
+
await executionJournal.close({ signal: deadline.signal });
|
|
796
|
+
}
|
|
797
|
+
finally {
|
|
798
|
+
deadline.dispose();
|
|
799
|
+
}
|
|
800
|
+
})();
|
|
801
|
+
return stopPromise;
|
|
802
|
+
};
|
|
803
|
+
return {
|
|
804
|
+
ready,
|
|
805
|
+
stop,
|
|
806
|
+
shutdownSnapshot: () => ({
|
|
807
|
+
activeExecutionCount: executionRuns.size,
|
|
808
|
+
activeLegacyCount: legacyRuns.size,
|
|
809
|
+
deadlineMs: shutdownTimeoutMs,
|
|
810
|
+
}),
|
|
811
811
|
};
|
|
812
812
|
}
|
package/dist/session.js
CHANGED
|
@@ -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
|
+
}
|