@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.
Files changed (41) hide show
  1. package/README.md +23 -0
  2. package/dist/attachments.js +196 -0
  3. package/dist/computer-cli.js +72 -12
  4. package/dist/computer-profile-lock.js +395 -0
  5. package/dist/computer-profile.js +189 -20
  6. package/dist/config.js +2 -1
  7. package/dist/console.js +175 -9
  8. package/dist/execution-event-limit.js +1 -1
  9. package/dist/execution-journal-lock.js +199 -40
  10. package/dist/execution-journal.js +42 -4
  11. package/dist/execution-protocol.js +21 -1
  12. package/dist/execution-recovery.js +71 -0
  13. package/dist/execution-runner.js +68 -77
  14. package/dist/execution-supervisor.js +79 -31
  15. package/dist/external-output.js +114 -0
  16. package/dist/i18n.js +5 -5
  17. package/dist/list-models.js +41 -5
  18. package/dist/local-executor.js +103 -14
  19. package/dist/machine-info.js +6 -1
  20. package/dist/main.js +23 -8
  21. package/dist/origin-decision.js +3 -1
  22. package/dist/prompt.js +4 -1
  23. package/dist/runner.js +14 -9
  24. package/dist/runtime-cancellation.js +74 -0
  25. package/dist/runtime-capabilities.js +38 -0
  26. package/dist/runtime-path.js +60 -0
  27. package/dist/runtimes/claude.js +9 -4
  28. package/dist/runtimes/codex-app-server-runner.js +340 -0
  29. package/dist/runtimes/codex.js +10 -4
  30. package/dist/runtimes/kimi-acp-runner.js +117 -17
  31. package/dist/runtimes/kimi.js +2 -0
  32. package/dist/runtimes/progress-watchdog.js +26 -0
  33. package/dist/serve-lifecycle.js +82 -0
  34. package/dist/serve.js +212 -212
  35. package/dist/session.js +1 -1
  36. package/dist/shared-execution-slots.js +68 -0
  37. package/dist/shutdown-deadline.js +32 -0
  38. package/dist/slog.js +34 -20
  39. package/dist/supervised-runtime.js +104 -0
  40. package/dist/websocket-shutdown.js +53 -0
  41. 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 executionReady = executionJournal.reconcileAfterRestart().then(() => true, (error) => {
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
- isRequested: () => existing.requested,
137
- requested: existing.promise,
138
- register: (cancel) => {
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
- const state = cancellations.get(executionId);
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
- // - running:正在跑的「agent:任务」去重键(同一任务重复唤醒才跳过)
241
- // - agentSlots:每 agent 当前并行数;超过配置上限的进 FIFO 队列(不丢)。
149
+ // scheduled 重复 run 仍由 running 去重;普通同线程 wake 用 legacyTaskTails 串成 FIFO
150
+ // agent 超过并行上限的任务继续进入 sharedQueues(不丢)。
242
151
  const running = new Set();
243
- const acquireSlot = async (handle) => {
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 = new WebSocket(wsUrl);
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 executionReady.then((ready) => ready ? sendSnapshot(`reconnect-${randomUUID()}`) : undefined);
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
- const recovered = await executionReady;
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 = reserveSharedSlot(spec.agent.handle, "execution");
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
- reservation.release();
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
- reservation.release();
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
- running.add(key);
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
- log(`\n${"─".repeat(56)}`);
553
- log(`🔔 唤醒 agent=${msg.agentHandle} reason=${msg.reason ?? "?"}`);
554
- log(` channel = ${msg.channelId}`);
555
- log(` thread = ${threadLabel ? `${threadLabel} (要求线程内回复)` : "(无,顶层回复)"}`);
556
- if (incoming)
557
- log(`📥 来信 @${from}: ${incoming.replace(/\s+/g, " ").slice(0, 200)}`);
558
- let actSeq = 0;
559
- const reportActivity = (a) => {
560
- const det = a.detail ? a.detail.replace(/\s+/g, " ").trim() : "";
561
- // 发消息时尽量打印回复正文/目标(--content "..." 或 heredoc 首行)
562
- let line = ` · ${a.label}`;
563
- if (a.kind === "sending") {
564
- const m = det.match(/--content\s+"([^"]*)"/) || det.match(/<<'?\w+'?\s*(.*)/);
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
- releaseSlot(msg.agentHandle);
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(connect, backoff);
750
+ reconnectTimer = setTimeout(() => {
751
+ reconnectTimer = null;
752
+ connect();
753
+ }, backoff);
795
754
  backoff = Math.min(backoff * 2, maxBackoff);
796
755
  });
797
- ws.on("error", () => ws?.close());
756
+ ws.on("error", () => {
757
+ if (!stopped)
758
+ ws?.close();
759
+ });
798
760
  }
799
- connect();
800
- return {
801
- stop: () => {
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
- ws?.close();
804
- for (const executionId of executionRuns.keys())
805
- requestCancellation(executionId);
806
- void Promise.race([
807
- Promise.allSettled([...executionRuns.values()]).then(() => true),
808
- new Promise((resolve) => setTimeout(() => resolve(false), 10_000)),
809
- ]).then((settled) => settled ? executionJournal.close() : undefined);
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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * per-task claude 会话元数据 —— 让同一任务的重复唤醒用 `--resume` 复用上下文(省 token)。
2
+ * per-task runtime 会话元数据 —— 让同一任务的重复唤醒复用原生上下文(省 token)。
3
3
  *
4
4
  * 存在本任务隔离运行目录下(<runDir>/.crew-session.json):天然按 taskKey 隔离、落盘防 daemon
5
5
  * 重启丢失。读取一律容错 → 不存在/损坏/字段非法都回退 null(= 当作首轮冷启动),绝不阻断运行。
@@ -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
+ }