@nowcrew/daemon 0.5.30 → 0.5.31

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.
@@ -0,0 +1,40 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
3
+ function optionValue(args, long, short) {
4
+ for (let index = 0; index < args.length; index += 1) {
5
+ const value = args[index];
6
+ if (value.startsWith(`${long}=`))
7
+ return value.slice(long.length + 1);
8
+ if (value === long || (short && value === short)) {
9
+ const next = args[index + 1];
10
+ return next && !next.startsWith("-") ? next : null;
11
+ }
12
+ }
13
+ return null;
14
+ }
15
+ export function resolveClaudeWrapperSession(args, createId = randomUUID) {
16
+ if (args.includes("--continue") || args.includes("-c")) {
17
+ throw new Error("NowCrew remote requires an explicit session ID; use --resume <session-id> instead of --continue");
18
+ }
19
+ if (args.includes("--fork-session")) {
20
+ throw new Error("NowCrew remote cannot identify a fork chosen inside Claude; start a new session or resume without --fork-session");
21
+ }
22
+ const sessionId = optionValue(args, "--session-id") ?? optionValue(args, "--resume", "-r") ?? createId();
23
+ if (!UUID.test(sessionId)) {
24
+ throw new Error("Claude remote sessions require a UUID in --session-id or --resume");
25
+ }
26
+ const hasSessionSelector = args.some((value) => value === "--session-id" || value.startsWith("--session-id=")
27
+ || value === "--resume" || value.startsWith("--resume=") || value === "-r");
28
+ return {
29
+ sessionId,
30
+ args: hasSessionSelector ? [...args] : ["--session-id", sessionId, ...args],
31
+ };
32
+ }
33
+ export function buildCodexWrappedInvocation(userArgs, socketPath = "") {
34
+ const hasRemote = userArgs.some((value) => value === "--remote" || value.startsWith("--remote="));
35
+ return {
36
+ bin: "codex",
37
+ args: hasRemote ? [...userArgs] : ["--remote", socketPath ? `unix://${socketPath}` : "unix://", ...userArgs],
38
+ env: process.env,
39
+ };
40
+ }
package/dist/runner.js CHANGED
@@ -106,6 +106,10 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
106
106
  }, { onActivity, onConsole }, {
107
107
  launchRuntime: dependencies.launchRuntime ?? ((request) => launchSupervisedRuntime(request, dependencies.startSupervisor, dependencies.cancellation, dependencies.platform)),
108
108
  ...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
109
+ ...(dependencies.startupGate === undefined ? {} : { startupGate: dependencies.startupGate }),
110
+ ...(dependencies.startupTimeoutMs === undefined
111
+ ? {}
112
+ : { startupTimeoutMs: dependencies.startupTimeoutMs }),
109
113
  });
110
114
  const activities = [...local.activities];
111
115
  if (!input.scheduled && (runtime === "codex" || runtime === "kimi")
@@ -0,0 +1,91 @@
1
+ export function createRuntimeStartupGate(limits, now = Date.now) {
2
+ const activeByRuntime = new Map();
3
+ const queue = [];
4
+ let activeTotal = 0;
5
+ let lastLaunchAt = Number.NEGATIVE_INFINITY;
6
+ const promote = () => {
7
+ while (activeTotal < limits.maxStartingTotal) {
8
+ const index = queue.findIndex((entry) => !entry.released
9
+ && (activeByRuntime.get(entry.runtime) ?? 0) < limits.maxStartingPerRuntime);
10
+ if (index < 0)
11
+ return;
12
+ const [next] = queue.splice(index, 1);
13
+ if (!next || next.released)
14
+ continue;
15
+ next.promoted = true;
16
+ activeTotal += 1;
17
+ activeByRuntime.set(next.runtime, (activeByRuntime.get(next.runtime) ?? 0) + 1);
18
+ const grant = () => {
19
+ next.timer = null;
20
+ if (next.released)
21
+ return;
22
+ next.launchGranted = true;
23
+ lastLaunchAt = now();
24
+ next.resolve();
25
+ };
26
+ const delay = Math.max(0, lastLaunchAt + limits.startupGapMs - now());
27
+ if (delay === 0)
28
+ grant();
29
+ else
30
+ next.timer = setTimeout(grant, delay);
31
+ }
32
+ };
33
+ return {
34
+ reserve: (runtime) => {
35
+ let resolveReady;
36
+ const ready = new Promise((resolve) => { resolveReady = resolve; });
37
+ const entry = {
38
+ runtime,
39
+ released: false,
40
+ promoted: false,
41
+ launchGranted: false,
42
+ timer: null,
43
+ resolve: resolveReady,
44
+ };
45
+ queue.push(entry);
46
+ promote();
47
+ return {
48
+ ready,
49
+ isQueued: () => !entry.launchGranted && !entry.released,
50
+ release: () => {
51
+ if (entry.released)
52
+ return;
53
+ entry.released = true;
54
+ if (entry.timer !== null)
55
+ clearTimeout(entry.timer);
56
+ if (entry.promoted) {
57
+ activeTotal = Math.max(0, activeTotal - 1);
58
+ const nextForRuntime = Math.max(0, (activeByRuntime.get(runtime) ?? 1) - 1);
59
+ if (nextForRuntime === 0)
60
+ activeByRuntime.delete(runtime);
61
+ else
62
+ activeByRuntime.set(runtime, nextForRuntime);
63
+ }
64
+ else {
65
+ const index = queue.indexOf(entry);
66
+ if (index >= 0)
67
+ queue.splice(index, 1);
68
+ }
69
+ promote();
70
+ },
71
+ };
72
+ },
73
+ snapshot: () => ({
74
+ startingTotal: activeTotal,
75
+ queuedTotal: queue.length,
76
+ startingByRuntime: {
77
+ claude: activeByRuntime.get("claude") ?? 0,
78
+ codex: activeByRuntime.get("codex") ?? 0,
79
+ kimi: activeByRuntime.get("kimi") ?? 0,
80
+ },
81
+ }),
82
+ };
83
+ }
84
+ export function isRuntimeReadyEvent(runtime, event) {
85
+ if (typeof event !== "object" || event === null)
86
+ return false;
87
+ const value = event;
88
+ if (runtime === "claude")
89
+ return value.type === "system" && value.subtype === "init";
90
+ return value.type === "thread.started";
91
+ }
package/dist/serve.js CHANGED
@@ -28,6 +28,8 @@ import { closeWebSocketWithinDeadline } from "./websocket-shutdown.js";
28
28
  import { reconcileExecutionJournal } from "./execution-recovery.js";
29
29
  import { createSharedSlotManager } from "./shared-execution-slots.js";
30
30
  import { createCompletionRetransmitter } from "./completion-retransmitter.js";
31
+ import { createRuntimeStartupGate } from "./runtime-startup-gate.js";
32
+ import { createHostExecutionCoordinator, hostCoordinatedSlotManager, hostCoordinatedStartupGate, } from "./host-execution-coordinator.js";
31
33
  // normalize.ts 的活动种类 → activity 枚举
32
34
  const ACTIVITY_MAP = {
33
35
  init: "working", text: "thinking", reading: "reading", sending: "sending",
@@ -60,7 +62,9 @@ export function serve(config, opts = {}) {
60
62
  const executeProtocol = opts.execution?.runExecution ?? runExecution;
61
63
  let detectedExecutionRuntimes = [];
62
64
  let runtimeFacts = null;
63
- const sharedSlots = createSharedSlotManager(config.executionLimits);
65
+ const hostCoordinator = opts.execution?.hostCoordinator ?? createHostExecutionCoordinator();
66
+ const sharedSlots = hostCoordinatedSlotManager(createSharedSlotManager(config.executionLimits), hostCoordinator);
67
+ const runtimeStartupGate = hostCoordinatedStartupGate(createRuntimeStartupGate(config.executionLimits), hostCoordinator);
64
68
  const knownExecutionHashes = new Map();
65
69
  const executionReservations = new Map();
66
70
  const executionRuns = new Map();
@@ -115,8 +119,11 @@ export function serve(config, opts = {}) {
115
119
  if ((entry.state === "completed" || entry.state === "interrupted") && entry.completion !== null) {
116
120
  return { executionId: entry.executionId, state: entry.state, completion: entry.completion, updatedAt: entry.updatedAt };
117
121
  }
122
+ if (entry.state === "running" && entry.runtimeReadyAt !== null) {
123
+ return { executionId: entry.executionId, state: "running", updatedAt: entry.runtimeReadyAt };
124
+ }
118
125
  if (entry.state === "accepted" || entry.state === "running") {
119
- return { executionId: entry.executionId, state: entry.state, updatedAt: entry.updatedAt };
126
+ return { executionId: entry.executionId, state: "accepted", updatedAt: entry.acceptedAt };
120
127
  }
121
128
  return null;
122
129
  };
@@ -137,10 +144,10 @@ export function serve(config, opts = {}) {
137
144
  state: acceptanceState, effectivePermission: entry.effectivePermission ?? "workspace_write",
138
145
  at: entry.acceptedAt,
139
146
  });
140
- if (entry.state === "running" && entry.processStartedAt !== null) {
147
+ if (entry.state === "running" && entry.runtimeReadyAt !== null) {
141
148
  safeExecutionSend({
142
149
  type: "execution:started", protocolVersion: 1,
143
- executionId: entry.executionId, at: entry.processStartedAt,
150
+ executionId: entry.executionId, at: entry.runtimeReadyAt,
144
151
  });
145
152
  }
146
153
  }
@@ -335,8 +342,30 @@ export function serve(config, opts = {}) {
335
342
  }
336
343
  return;
337
344
  }
338
- knownExecutionHashes.set(spec.executionId, hash);
339
345
  const reservation = sharedSlots.reserve(spec.agent.handle, "execution");
346
+ if (!reservation.accepted) {
347
+ dslog("execution.machine_queue_rejected", "机器执行队列已满", {
348
+ level: "WARN",
349
+ execution_id: spec.executionId,
350
+ agent_handle: spec.agent.handle,
351
+ ...reservation.facts,
352
+ });
353
+ safeExecutionSend(ExecutionRejectedSchema.parse({
354
+ type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
355
+ reason: "resource_limit", message: "Local machine execution queue is full",
356
+ at: new Date().toISOString(),
357
+ }));
358
+ return;
359
+ }
360
+ knownExecutionHashes.set(spec.executionId, hash);
361
+ const machineQueueEnteredAt = Date.now();
362
+ if (reservation.isQueued()) {
363
+ dslog("execution.machine_queued", "execution 已进入机器队列", {
364
+ execution_id: spec.executionId,
365
+ agent_handle: spec.agent.handle,
366
+ ...reservation.facts,
367
+ });
368
+ }
340
369
  executionReservations.set(spec.executionId, reservation);
341
370
  const cancellation = cancellationFor(spec.executionId);
342
371
  const cleanupExecutionReservation = () => {
@@ -371,8 +400,22 @@ export function serve(config, opts = {}) {
371
400
  ...reservation.facts,
372
401
  },
373
402
  report: reportExecutionFrame,
403
+ startupGate: runtimeStartupGate,
404
+ startupTimeoutMs: config.executionLimits.startupTimeoutMs,
374
405
  ...(reservation.state === undefined ? {} : {
375
- slot: { state: reservation.state, ready: reservation.ready },
406
+ slot: {
407
+ state: reservation.state,
408
+ ready: reservation.ready.then(() => {
409
+ const snapshot = sharedSlots.snapshot();
410
+ dslog("execution.machine_slot_ready", "execution 获得机器执行名额", {
411
+ execution_id: spec.executionId,
412
+ agent_handle: spec.agent.handle,
413
+ queue_ms: Date.now() - machineQueueEnteredAt,
414
+ active_total: snapshot.activeTotal,
415
+ queued_total: snapshot.queuedTotal,
416
+ });
417
+ }),
418
+ },
376
419
  }),
377
420
  cancellation,
378
421
  }).finally(() => {
@@ -539,12 +582,26 @@ export function serve(config, opts = {}) {
539
582
  }
540
583
  running.add(key);
541
584
  legacyReservation = sharedSlots.reserve(msg.agentHandle, "legacy");
585
+ if (legacyReservation.isQueued()) {
586
+ const snapshot = sharedSlots.snapshot();
587
+ dslog("run.machine_queued", "legacy run 已进入机器队列", {
588
+ ...runKeys,
589
+ active_total: snapshot.activeTotal,
590
+ queued_total: snapshot.queuedTotal,
591
+ });
592
+ }
542
593
  await awaitWithCancellation(legacyReservation.ready, controller.cancellation);
543
594
  const queueMs = Date.now() - queueWaitStart;
595
+ const machineSnapshot = sharedSlots.snapshot();
544
596
  const threadLabel = threadId ?? null;
545
597
  const from = msg.wake?.senderHandle ?? "?";
546
598
  const incoming = msg.wake?.content ?? "";
547
- dslog("run.start", `开始运行 ${msg.agentHandle}`, { ...runKeys, queue_ms: queueMs });
599
+ dslog("run.start", `开始运行 ${msg.agentHandle}`, {
600
+ ...runKeys,
601
+ queue_ms: queueMs,
602
+ active_total: machineSnapshot.activeTotal,
603
+ queued_total: machineSnapshot.queuedTotal,
604
+ });
548
605
  log(`\n${"─".repeat(56)}`);
549
606
  log(`🔔 唤醒 agent=${msg.agentHandle} reason=${msg.reason ?? "?"}`);
550
607
  log(` channel = ${msg.channelId}`);
@@ -650,7 +707,11 @@ export function serve(config, opts = {}) {
650
707
  ...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
651
708
  ...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
652
709
  ...(attemptWake ? { wake: attemptWake } : {}),
653
- }, reportActivity, reportConsole, { cancellation: controller.cancellation });
710
+ }, reportActivity, reportConsole, {
711
+ cancellation: controller.cancellation,
712
+ startupGate: runtimeStartupGate,
713
+ startupTimeoutMs: config.executionLimits.startupTimeoutMs,
714
+ });
654
715
  });
655
716
  const result = mergeRunAgentResults(guarded.results);
656
717
  // 本轮 token 用量上报:runner 已从 result 事件提取(含缓存读/写细分),
@@ -848,6 +909,7 @@ export function serve(config, opts = {}) {
848
909
  for (const run of legacyRuns.values())
849
910
  run.controller.request();
850
911
  await deadline.waitFor(Promise.all(pending));
912
+ await deadline.waitFor(hostCoordinator.drain());
851
913
  await executionJournal.close({ signal: deadline.signal });
852
914
  }
853
915
  finally {
@@ -1,46 +1,58 @@
1
1
  export function createSharedSlotManager(limits) {
2
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)
3
+ const queue = [];
4
+ let activeTotal = 0;
5
+ const queuedFor = (handle) => queue.filter((entry) => !entry.released && entry.handle === handle).length;
6
+ const promote = () => {
7
+ while (activeTotal < limits.maxParallelTotal) {
8
+ const index = queue.findIndex((entry) => !entry.released
9
+ && (activeByHandle.get(entry.handle) ?? 0) < limits.maxParallelPerAgent);
10
+ if (index < 0)
11
+ return;
12
+ const [next] = queue.splice(index, 1);
13
+ if (!next || next.released)
9
14
  continue;
10
15
  next.promoted = true;
11
- activeByHandle.set(handle, (activeByHandle.get(handle) ?? 0) + 1);
16
+ activeTotal += 1;
17
+ activeByHandle.set(next.handle, (activeByHandle.get(next.handle) ?? 0) + 1);
12
18
  next.resolve();
13
19
  }
14
- if (queue.length === 0)
15
- queuesByHandle.delete(handle);
16
20
  };
17
21
  return {
18
22
  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: () => { } };
23
+ const activeForAgent = activeByHandle.get(handle) ?? 0;
24
+ const queuedForAgent = queuedFor(handle);
25
+ const facts = {
26
+ activeForAgent,
27
+ queuedForAgent,
28
+ activeTotal,
29
+ queuedTotal: queue.length,
30
+ };
31
+ const canStartImmediately = activeTotal < limits.maxParallelTotal
32
+ && activeForAgent < limits.maxParallelPerAgent
33
+ && queue.length === 0;
34
+ if (kind === "execution" && !canStartImmediately && (queuedForAgent >= limits.maxQueuedPerAgent
35
+ || queue.length >= limits.maxQueuedTotal)) {
36
+ return {
37
+ accepted: false,
38
+ facts,
39
+ ready: Promise.resolve(),
40
+ isQueued: () => false,
41
+ release: () => { },
42
+ };
25
43
  }
26
44
  let resolveReady;
27
45
  const ready = new Promise((resolve) => { resolveReady = resolve; });
28
46
  const entry = {
29
- kind,
47
+ handle,
30
48
  released: false,
31
- promoted: active < limits.maxParallelPerAgent,
49
+ promoted: false,
32
50
  resolve: resolveReady,
33
51
  };
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
- }
52
+ queue.push(entry);
53
+ promote();
43
54
  return {
55
+ accepted: true,
44
56
  facts,
45
57
  state: entry.promoted ? "ready" : "queued",
46
58
  ready,
@@ -50,19 +62,22 @@ export function createSharedSlotManager(limits) {
50
62
  return;
51
63
  entry.released = true;
52
64
  if (entry.promoted) {
53
- activeByHandle.set(handle, Math.max(0, (activeByHandle.get(handle) ?? 1) - 1));
65
+ activeTotal = Math.max(0, activeTotal - 1);
66
+ const nextForAgent = Math.max(0, (activeByHandle.get(handle) ?? 1) - 1);
67
+ if (nextForAgent === 0)
68
+ activeByHandle.delete(handle);
69
+ else
70
+ activeByHandle.set(handle, nextForAgent);
54
71
  }
55
72
  else {
56
- const queue = queuesByHandle.get(handle);
57
- const index = queue?.indexOf(entry) ?? -1;
58
- if (queue !== undefined && index >= 0)
73
+ const index = queue.indexOf(entry);
74
+ if (index >= 0)
59
75
  queue.splice(index, 1);
60
- if (queue?.length === 0)
61
- queuesByHandle.delete(handle);
62
76
  }
63
- promoteNext(handle);
77
+ promote();
64
78
  },
65
79
  };
66
80
  },
81
+ snapshot: () => ({ activeTotal, queuedTotal: queue.length }),
67
82
  };
68
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.30",
3
+ "version": "0.5.31",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",