@nowcrew/daemon 0.6.14 → 0.6.15

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 CHANGED
@@ -371,9 +371,10 @@ CREW_RUNTIME_SAFE=1
371
371
  CREW_EXECUTION_MAX_PROMPT_BYTES=256000
372
372
  CREW_EXECUTION_MAX_TIMEOUT_MS=10800000
373
373
  CREW_EXECUTION_MAX_EVENT_BYTES=64000
374
- CREW_MAX_PARALLEL=4
374
+ CREW_MAX_PARALLEL=10
375
+ CREW_SCHEDULED_MAX_PARALLEL=4
375
376
  CREW_EXECUTION_MAX_QUEUED_PER_AGENT=32
376
- CREW_EXECUTION_MAX_PARALLEL_TOTAL=4
377
+ CREW_EXECUTION_MAX_PARALLEL_TOTAL=10
377
378
  CREW_EXECUTION_MAX_QUEUED_TOTAL=128
378
379
  CREW_EXECUTION_MAX_STARTING_TOTAL=1
379
380
  CREW_EXECUTION_MAX_STARTING_PER_RUNTIME=1
@@ -381,7 +382,9 @@ CREW_EXECUTION_START_GAP_MS=3000
381
382
  CREW_EXECUTION_STARTUP_TIMEOUT_MS=120000
382
383
  ```
383
384
 
384
- `MAX_PARALLEL_TOTAL` is the machine-wide process cap shared by protocol-v1 and legacy work.
385
+ `CREW_MAX_PARALLEL` limits normal executions per Agent. `CREW_SCHEDULED_MAX_PARALLEL` separately
386
+ limits scheduled executions per Agent. `MAX_PARALLEL_TOTAL` is the machine-wide process cap shared by
387
+ protocol-v1 and legacy work.
385
388
  Runtime startup is a separate FIFO gate: by default only one Claude, Codex, or Kimi process may be
386
389
  initializing at a time, and launches are spaced by three seconds. A process leaves the startup gate
387
390
  only after its runtime-specific ready event; startup timeout cancels the owned process tree.
package/dist/config.js CHANGED
@@ -10,9 +10,10 @@ export const DEFAULT_EXECUTION_LIMITS = Object.freeze({
10
10
  maxPromptBytes: 256_000,
11
11
  maxTimeoutMs: 3 * 60 * 60_000,
12
12
  maxEventBytes: 64_000,
13
- maxParallelPerAgent: 4,
13
+ maxParallelPerAgent: 10,
14
+ maxParallelScheduledPerAgent: 4,
14
15
  maxQueuedPerAgent: 32,
15
- maxParallelTotal: 4,
16
+ maxParallelTotal: 10,
16
17
  maxQueuedTotal: 128,
17
18
  maxStartingTotal: 1,
18
19
  maxStartingPerRuntime: 1,
@@ -68,6 +69,7 @@ export function loadConfig(env = process.env) {
68
69
  maxTimeoutMs: integerEnvAtMost(env, "CREW_EXECUTION_MAX_TIMEOUT_MS", DEFAULT_EXECUTION_LIMITS.maxTimeoutMs, MAX_EXECUTION_TIMEOUT_MS),
69
70
  maxEventBytes: integerEnvAtLeast(env, "CREW_EXECUTION_MAX_EVENT_BYTES", DEFAULT_EXECUTION_LIMITS.maxEventBytes, MIN_EXECUTION_EVENT_BYTES),
70
71
  maxParallelPerAgent: positiveIntegerEnv(env, "CREW_MAX_PARALLEL", DEFAULT_EXECUTION_LIMITS.maxParallelPerAgent),
72
+ maxParallelScheduledPerAgent: positiveIntegerEnv(env, "CREW_SCHEDULED_MAX_PARALLEL", DEFAULT_EXECUTION_LIMITS.maxParallelScheduledPerAgent),
71
73
  maxQueuedPerAgent: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_QUEUED_PER_AGENT", DEFAULT_EXECUTION_LIMITS.maxQueuedPerAgent),
72
74
  maxParallelTotal: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_PARALLEL_TOTAL", DEFAULT_EXECUTION_LIMITS.maxParallelTotal),
73
75
  maxQueuedTotal: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_QUEUED_TOTAL", DEFAULT_EXECUTION_LIMITS.maxQueuedTotal),
@@ -231,7 +231,10 @@ function admission(spec, config, dependencies, at) {
231
231
  || dependencies.facts.queuedTotal < 0) {
232
232
  return { rejected: rejection(spec.executionId, "invalid_spec", "Invalid local resource facts", at) };
233
233
  }
234
- const agentAtCapacity = dependencies.facts.activeForAgent >= config.executionLimits.maxParallelPerAgent;
234
+ const maxParallelForAgent = spec.context.scheduledRunId === undefined
235
+ ? config.executionLimits.maxParallelPerAgent
236
+ : config.executionLimits.maxParallelScheduledPerAgent;
237
+ const agentAtCapacity = dependencies.facts.activeForAgent >= maxParallelForAgent;
235
238
  const machineAtCapacity = dependencies.facts.activeTotal >= config.executionLimits.maxParallelTotal;
236
239
  const slotInvalid = dependencies.slot?.state === "ready"
237
240
  ? agentAtCapacity || machineAtCapacity
@@ -4,7 +4,7 @@ import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { defaultProcessController } from "./execution-journal.js";
6
6
  import { createJournalLease, createJournalLeaseRegistry, JournalLockedError, } from "./execution-journal-lock.js";
7
- const HOST_EXECUTION_SLOT_COUNT = 4;
7
+ const HOST_EXECUTION_SLOT_COUNT = 10;
8
8
  const HOST_STARTUP_GAP_MS = 3_000;
9
9
  const RETRY_MS = 50;
10
10
  export function defaultHostExecutionCoordinatorRoot(userHome = homedir()) {
@@ -215,8 +215,8 @@ export function createHostExecutionCoordinator(options = {}) {
215
215
  }
216
216
  export function hostCoordinatedSlotManager(local, host) {
217
217
  return {
218
- reserve: (handle, kind) => {
219
- const localReservation = local.reserve(handle, kind);
218
+ reserve: (handle, kind, executionClass) => {
219
+ const localReservation = local.reserve(handle, kind, executionClass);
220
220
  if (!localReservation.accepted)
221
221
  return localReservation;
222
222
  const hostReservation = host.reserveExecution(localReservation.ready);
package/dist/prompt.js CHANGED
@@ -234,14 +234,14 @@ ${taskAndScheduleCommands}`;
234
234
 
235
235
  ## 并行与记忆(CRITICAL — 防冲突 + 防上下文膨胀)
236
236
  你**可能同时在处理多个任务**(你自己的多个并行进程)。为避免互相覆盖、并保持上下文精简,严格遵守:
237
- - **普通任务默认只写两处**:① 本任务的隔离 cwd(代码/草稿);② 本任务的工作日志 **\`$CREW_TASK_LOG\`**(每任务一份,别人不会碰)。默认不要修改共享 \`$CREW_HOME/MEMORY.md\` 或 notes/;只有当前用户明确要求更新共享记忆时,才按下方条件写入相关共享文件。在工作日志里记:当前阶段 / 下一步 / 卡在谁 / 关键结论(每条尽量 ≤3 行)。
238
- - **当前请求明确要求更新记忆,或当前消息是对本线程紧邻记忆方案的无歧义确认时**,可以按最小范围直接写共享记忆;先读取相关索引和 note,语义已存在时不重复追加,不要把 Agent 自己判断值得记住视为用户授权。
237
+ - **普通任务默认只写两处**:① 本任务的隔离 cwd(代码/草稿);② 本任务的工作日志 **\`$CREW_TASK_LOG\`**(每任务一份,别人不会碰)。默认不要修改共享 \`$CREW_HOME/MEMORY.md\` 或 notes/;只有当前用户明确要求更新或记住长期记忆,或明确表达具体且跨后续任务持续生效的规则、偏好或约束时,才按下方条件写入相关共享文件。在工作日志里记:当前阶段 / 下一步 / 卡在谁 / 关键结论(每条尽量 ≤3 行)。
238
+ - **当前请求明确要求更新或记住长期记忆,明确表达需要跨后续任务持续生效的具体规则、偏好或约束,或当前消息是对本线程紧邻记忆方案的无歧义确认时**,可以按最小范围直接写共享记忆。例如“以后所有测试报告统一按五部分写”属于明确长期意图,即使没有使用“记忆”一词。先读取相关索引和 note,语义已存在时不重复追加,不要把 Agent 自己判断值得记住视为用户授权。
239
239
  - 本节的直接更新共享记忆和相关 task 流转仅适用于用户消息触发的普通交互执行。memory_prune 和 scheduled 执行只遵守各自唤醒指令,不得依据本节认领、创建、重开或推进 task。
240
240
  - 普通交互执行将写共享记忆时,使用当前线程唯一的普通 task:已有非终态 task 时先按归属规则确保当前 Agent 成功认领;当前线程没有 task 时先创建并认领一个普通 task;已有 task 为 done/closed 时不得由 Agent 重开,done 报告当前轻量方案无法安全复用终态 task,closed 保持人工终态。遇到终态或归属问题时报告冲突且不得创建第二个 task 或新的任务类型。完成写入后,只有任务本身的全部工作已完成时才推进终态以触发 prune。
241
241
  - 提出共享记忆修改方案并等待用户确认时,该 task 仍有剩余工作,保持 in_review 或其它非终态;收到无歧义确认后再执行写入,并仅在全部工作完成后推进终态。
242
242
  - 普通执行直接更新后,仍然保留任务结束后的专门收尾执行,不因已经写过而跳过 prune。
243
243
  - 新建 note 必须说明现有分类为何不适用;MEMORY.md 默认只更新 Knowledge Index,不得默认修改 Role、Core Goals 或 Active Context;只有用户明确要求或任务本身就是档案/状态维护时例外。
244
- - 未明确要求、记忆意图含糊或仅由 Agent 自己判断值得记住时,不直接写共享记忆,也不把含糊内容写成长期记忆候选。含糊表达本身不作为长期记忆候选,但不得因此跳过整个 task 的 memory_prune;任务按正常状态流程完成。收尾只能处理该任务其它符合现有收尾规则的稳定内容,不允许让收尾代替用户补充记忆授权。
244
+ - 仅对本次或当前任务生效的要求不持久化。未明确要求、记忆意图含糊或仅由 Agent 自己判断值得记住时,不直接写共享记忆,也不把含糊内容写成长期记忆候选。例如“以后注意一下”等缺少具体内容或适用范围的表达仍属于含糊意图。含糊表达本身不作为长期记忆候选,但不得因此跳过整个 task 的 memory_prune;任务按正常状态流程完成。收尾只能处理该任务其它符合现有收尾规则的稳定内容,不允许让收尾代替用户补充记忆授权。
245
245
  - 若用户明确要求本次不要修改共享记忆,同样不写共享记忆或长期记忆候选。任务尚未完成时,把该禁止要求作为运行状态写入 $CREW_TASK_LOG;该运行状态不是长期记忆候选,后续执行必须保留该状态。若用户之后在同一任务中明确撤销禁止,移除该运行状态并按最新明确授权处理。完成 task 前检查该状态:Agent 可自行改状态时,使用 crew task update <taskId> --status done --skip-memory-prune。CLI 返回状态由 Server 管理时沿用上述 Server 管理任务的安全回退:不重试、不绕过权限;收尾会读取该运行状态并禁止共享记忆写入,但本路径不提供机器级 skip 保证。不要只在回复里声明不修改。
246
246
  - **跨任务协调看实时清单**:用 \`crew task list --mine\` 查你当前所有任务及其状态(谁在排队、谁在评审)。例:你一次只能测一个页面时,据此判断"正在测 #1、#2 排队",在 #2 的工作日志里标注"等 #1 完成"。
247
247
  - 需要更多背景时再 \`Read $CREW_HOME/notes/<topic>.md\`;不要一次把所有笔记读进上下文。
package/dist/serve.js CHANGED
@@ -71,7 +71,8 @@ export function serve(config, opts = {}) {
71
71
  let detectedExecutionRuntimes = [];
72
72
  let runtimeFacts = null;
73
73
  const runtimeProbe = createRuntimeProbeCoordinator();
74
- const hostCoordinator = opts.execution?.hostCoordinator ?? createHostExecutionCoordinator();
74
+ const hostCoordinator = opts.execution?.hostCoordinator
75
+ ?? createHostExecutionCoordinator({ executionSlots: config.executionLimits.maxParallelTotal });
75
76
  const localSlots = createSharedSlotManager(config.executionLimits);
76
77
  const sharedSlots = hostCoordinatedSlotManager(localSlots, hostCoordinator);
77
78
  const runtimeStartupGate = hostCoordinatedStartupGate(createRuntimeStartupGate(config.executionLimits), hostCoordinator);
@@ -496,12 +497,14 @@ export function serve(config, opts = {}) {
496
497
  task_key: spec.workspace.taskKey,
497
498
  });
498
499
  }
499
- const reservation = sharedSlots.reserve(spec.agent.handle, "execution");
500
+ const executionClass = spec.context.scheduledRunId === undefined ? "normal" : "scheduled";
501
+ const reservation = sharedSlots.reserve(spec.agent.handle, "execution", executionClass);
500
502
  if (!reservation.accepted) {
501
503
  dslog("execution.machine_queue_rejected", "机器执行队列已满", {
502
504
  level: "WARN",
503
505
  execution_id: spec.executionId,
504
506
  agent_handle: spec.agent.handle,
507
+ execution_class: executionClass,
505
508
  ...reservation.facts,
506
509
  });
507
510
  safeExecutionSend(ExecutionRejectedSchema.parse({
@@ -517,6 +520,7 @@ export function serve(config, opts = {}) {
517
520
  dslog("execution.machine_queued", "execution 已进入机器队列", {
518
521
  execution_id: spec.executionId,
519
522
  agent_handle: spec.agent.handle,
523
+ execution_class: executionClass,
520
524
  ...reservation.facts,
521
525
  });
522
526
  }
@@ -534,6 +538,7 @@ export function serve(config, opts = {}) {
534
538
  ...(activeSameTask > 1 ? { level: "WARN" } : {}),
535
539
  execution_id: spec.executionId,
536
540
  agent_handle: spec.agent.handle,
541
+ execution_class: executionClass,
537
542
  task_key: spec.workspace.taskKey,
538
543
  active_same_task: activeSameTask,
539
544
  ...reservation.facts,
@@ -602,6 +607,7 @@ export function serve(config, opts = {}) {
602
607
  dslog("execution.machine_slot_ready", "execution 获得机器执行名额", {
603
608
  execution_id: spec.executionId,
604
609
  agent_handle: spec.agent.handle,
610
+ execution_class: executionClass,
605
611
  queue_ms: Date.now() - machineQueueEnteredAt,
606
612
  active_total: snapshot.activeTotal,
607
613
  queued_total: snapshot.queuedTotal,
@@ -824,7 +830,7 @@ export function serve(config, opts = {}) {
824
830
  }
825
831
  }
826
832
  running.add(key);
827
- legacyReservation = sharedSlots.reserve(msg.agentHandle, "legacy");
833
+ legacyReservation = sharedSlots.reserve(msg.agentHandle, "legacy", scheduled === null ? "normal" : "scheduled");
828
834
  if (legacyReservation.isQueued()) {
829
835
  const snapshot = sharedSlots.snapshot();
830
836
  dslog("run.machine_queued", "legacy run 已进入机器队列", {
@@ -1,13 +1,18 @@
1
1
  export function createSharedSlotManager(limits) {
2
- const activeByHandle = new Map();
2
+ const activeByHandleAndClass = new Map();
3
3
  const queue = [];
4
4
  let activeTotal = 0;
5
5
  let exclusive = false;
6
6
  const queuedFor = (handle) => queue.filter((entry) => !entry.released && entry.handle === handle).length;
7
+ const activeKey = (handle, executionClass) => `${handle}:${executionClass}`;
8
+ const maxParallelFor = (executionClass) => executionClass === "scheduled"
9
+ ? limits.maxParallelScheduledPerAgent
10
+ : limits.maxParallelPerAgent;
7
11
  const promote = () => {
8
12
  while (activeTotal < limits.maxParallelTotal) {
9
13
  const index = queue.findIndex((entry) => !entry.released
10
- && (activeByHandle.get(entry.handle) ?? 0) < limits.maxParallelPerAgent);
14
+ && (activeByHandleAndClass.get(activeKey(entry.handle, entry.executionClass)) ?? 0)
15
+ < maxParallelFor(entry.executionClass));
11
16
  if (index < 0)
12
17
  return;
13
18
  const [next] = queue.splice(index, 1);
@@ -15,13 +20,15 @@ export function createSharedSlotManager(limits) {
15
20
  continue;
16
21
  next.promoted = true;
17
22
  activeTotal += 1;
18
- activeByHandle.set(next.handle, (activeByHandle.get(next.handle) ?? 0) + 1);
23
+ const key = activeKey(next.handle, next.executionClass);
24
+ activeByHandleAndClass.set(key, (activeByHandleAndClass.get(key) ?? 0) + 1);
19
25
  next.resolve();
20
26
  }
21
27
  };
22
28
  return {
23
- reserve: (handle, kind) => {
24
- const activeForAgent = activeByHandle.get(handle) ?? 0;
29
+ reserve: (handle, kind, executionClass = "normal") => {
30
+ const key = activeKey(handle, executionClass);
31
+ const activeForAgent = activeByHandleAndClass.get(key) ?? 0;
25
32
  const queuedForAgent = queuedFor(handle);
26
33
  const facts = {
27
34
  activeForAgent,
@@ -39,7 +46,7 @@ export function createSharedSlotManager(limits) {
39
46
  };
40
47
  }
41
48
  const canStartImmediately = activeTotal < limits.maxParallelTotal
42
- && activeForAgent < limits.maxParallelPerAgent
49
+ && activeForAgent < maxParallelFor(executionClass)
43
50
  && queue.length === 0;
44
51
  if (kind === "execution" && !canStartImmediately && (queuedForAgent >= limits.maxQueuedPerAgent
45
52
  || queue.length >= limits.maxQueuedTotal)) {
@@ -55,6 +62,7 @@ export function createSharedSlotManager(limits) {
55
62
  const ready = new Promise((resolve) => { resolveReady = resolve; });
56
63
  const entry = {
57
64
  handle,
65
+ executionClass,
58
66
  released: false,
59
67
  promoted: false,
60
68
  resolve: resolveReady,
@@ -73,11 +81,11 @@ export function createSharedSlotManager(limits) {
73
81
  entry.released = true;
74
82
  if (entry.promoted) {
75
83
  activeTotal = Math.max(0, activeTotal - 1);
76
- const nextForAgent = Math.max(0, (activeByHandle.get(handle) ?? 1) - 1);
84
+ const nextForAgent = Math.max(0, (activeByHandleAndClass.get(key) ?? 1) - 1);
77
85
  if (nextForAgent === 0)
78
- activeByHandle.delete(handle);
86
+ activeByHandleAndClass.delete(key);
79
87
  else
80
- activeByHandle.set(handle, nextForAgent);
88
+ activeByHandleAndClass.set(key, nextForAgent);
81
89
  }
82
90
  else {
83
91
  const index = queue.indexOf(entry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.14",
3
+ "version": "0.6.15",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",