@nowcrew/daemon 0.6.8 → 0.6.9

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
@@ -378,7 +378,7 @@ CREW_EXECUTION_MAX_QUEUED_TOTAL=128
378
378
  CREW_EXECUTION_MAX_STARTING_TOTAL=1
379
379
  CREW_EXECUTION_MAX_STARTING_PER_RUNTIME=1
380
380
  CREW_EXECUTION_START_GAP_MS=3000
381
- CREW_EXECUTION_STARTUP_TIMEOUT_MS=60000
381
+ CREW_EXECUTION_STARTUP_TIMEOUT_MS=120000
382
382
  ```
383
383
 
384
384
  `MAX_PARALLEL_TOTAL` is the machine-wide process cap shared by protocol-v1 and legacy work.
@@ -0,0 +1,54 @@
1
+ const STAGE_LINE_PREFIX_CAP = 512;
2
+ const STAGE_PATTERN = /^\[codex-app-server\] stage=(spawn|initialize|thread_start|thread_resume|model_init) status=(start|ok|error|timeout) attempt=(\d+) elapsed_ms=(\d+)(?:\s|$)/;
3
+ function parseBoundedStageLine(line) {
4
+ const match = STAGE_PATTERN.exec(line);
5
+ if (match === null)
6
+ return null;
7
+ const [, rawStage, status, rawAttempt, rawStageMs] = match;
8
+ const attempt = Number(rawAttempt);
9
+ const stageMs = Number(rawStageMs);
10
+ if (!Number.isSafeInteger(attempt) || attempt < 1
11
+ || !Number.isSafeInteger(stageMs) || stageMs < 0)
12
+ return null;
13
+ if (rawStage === "thread_resume") {
14
+ return { stage: "thread_start", status: status, attempt, stageMs, resumed: true };
15
+ }
16
+ return {
17
+ stage: rawStage,
18
+ status: status,
19
+ attempt,
20
+ stageMs,
21
+ ...(rawStage === "thread_start" ? { resumed: false } : {}),
22
+ };
23
+ }
24
+ export class CodexStartupStageParser {
25
+ onStage;
26
+ pendingPrefix = "";
27
+ constructor(onStage) {
28
+ this.onStage = onStage;
29
+ }
30
+ push(chunk) {
31
+ let remaining = chunk;
32
+ while (remaining.length > 0) {
33
+ const newline = remaining.indexOf("\n");
34
+ const segment = newline < 0 ? remaining : remaining.slice(0, newline);
35
+ const available = STAGE_LINE_PREFIX_CAP - this.pendingPrefix.length;
36
+ if (available > 0)
37
+ this.pendingPrefix += segment.slice(0, available);
38
+ if (newline < 0)
39
+ return;
40
+ this.emitPending();
41
+ remaining = remaining.slice(newline + 1);
42
+ }
43
+ }
44
+ finish() {
45
+ if (this.pendingPrefix.length > 0)
46
+ this.emitPending();
47
+ }
48
+ emitPending() {
49
+ const stage = parseBoundedStageLine(this.pendingPrefix.replace(/\r$/, ""));
50
+ this.pendingPrefix = "";
51
+ if (stage !== null)
52
+ this.onStage(stage);
53
+ }
54
+ }
package/dist/config.js CHANGED
@@ -17,7 +17,7 @@ export const DEFAULT_EXECUTION_LIMITS = Object.freeze({
17
17
  maxStartingTotal: 1,
18
18
  maxStartingPerRuntime: 1,
19
19
  startupGapMs: 3_000,
20
- startupTimeoutMs: 60_000,
20
+ startupTimeoutMs: 120_000,
21
21
  });
22
22
  // Fits the largest mandatory v1 lifecycle envelope (UUID + timestamps + outcome facts) with margin.
23
23
  export const MIN_EXECUTION_EVENT_BYTES = 512;
@@ -20,6 +20,7 @@ import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
20
20
  import { dslog } from "./slog.js";
21
21
  import { evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
22
22
  import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
23
+ import { CodexStartupStageParser } from "./codex-startup-stage.js";
23
24
  function memoryPruneSnapshotFields(snapshot) {
24
25
  const fields = {};
25
26
  for (const [label, fact] of Object.entries(snapshot)) {
@@ -615,10 +616,30 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
615
616
  callbacks.onConsole?.(chunk);
616
617
  });
617
618
  let stderrTail = "";
619
+ const codexStartupStageState = { last: null };
620
+ const codexStartupStageParser = runtime.name === "codex"
621
+ ? new CodexStartupStageParser((stage) => {
622
+ codexStartupStageState.last = stage;
623
+ dslog("runtime.start_stage", "Codex runtime 启动阶段更新", {
624
+ level: stage.status === "error" || stage.status === "timeout" ? "ERROR" : "INFO",
625
+ execution_id: input.executionId,
626
+ runtime: runtime.name,
627
+ stage: stage.stage,
628
+ status: stage.status,
629
+ attempt: stage.attempt,
630
+ stage_ms: stage.stageMs,
631
+ startup_ms: Date.now() - runtimeLaunchAt,
632
+ ...(stage.stage === "thread_start" ? { resumed: stage.resumed } : {}),
633
+ });
634
+ })
635
+ : null;
618
636
  child.stderr.on("data", (data) => {
619
637
  process.stderr.write(data);
620
- stderrTail = (stderrTail + String(data)).slice(-STDERR_TAIL_CAP);
638
+ const text = String(data);
639
+ stderrTail = (stderrTail + text).slice(-STDERR_TAIL_CAP);
640
+ codexStartupStageParser?.push(text);
621
641
  });
642
+ child.stderr.on("end", () => codexStartupStageParser?.finish());
622
643
  let runtimeExit;
623
644
  try {
624
645
  if (startupReservation !== null) {
@@ -629,7 +650,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
629
650
  runtimeReadySignal.then(() => ({ kind: "ready" })),
630
651
  child.exit.then((exit) => ({ kind: "exit", exit })),
631
652
  new Promise((resolve) => {
632
- startupTimer = setTimeout(() => resolve({ kind: "timeout" }), dependencies.startupTimeoutMs ?? 60_000);
653
+ startupTimer = setTimeout(() => resolve({ kind: "timeout" }), dependencies.startupTimeoutMs ?? 120_000);
633
654
  }),
634
655
  ]), dependencies.cancellation);
635
656
  }
@@ -639,11 +660,18 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
639
660
  }
640
661
  if (startupOutcome.kind !== "ready") {
641
662
  if (startupOutcome.kind === "timeout") {
663
+ const lastCodexStartupStage = codexStartupStageState.last;
642
664
  dslog("runtime.start_timeout", "runtime 启动超时", {
643
665
  level: "ERROR",
644
666
  execution_id: input.executionId,
645
667
  runtime: runtime.name,
646
- startup_timeout_ms: dependencies.startupTimeoutMs ?? 60_000,
668
+ startup_timeout_ms: dependencies.startupTimeoutMs ?? 120_000,
669
+ ...(lastCodexStartupStage === null ? {} : {
670
+ last_stage: lastCodexStartupStage.stage,
671
+ last_stage_status: lastCodexStartupStage.status,
672
+ last_stage_ms: lastCodexStartupStage.stageMs,
673
+ attempt: lastCodexStartupStage.attempt,
674
+ }),
647
675
  });
648
676
  await child.cancel?.();
649
677
  }
package/dist/main.js CHANGED
File without changes
@@ -468,7 +468,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
468
468
  completionReject(new Error("Codex produced no semantic progress within the startup window"));
469
469
  void cancel();
470
470
  });
471
- activeStage = "turn_start";
471
+ activeStage = "model_init";
472
472
  stageStartedAt = Date.now();
473
473
  const started = await rpc.request("turn/start", {
474
474
  threadId,
package/dist/serve.js CHANGED
@@ -741,7 +741,7 @@ export function serve(config, opts = {}) {
741
741
  try {
742
742
  const data2 = req.type === "fs:list" ? await listWorkspace(root, req.path)
743
743
  : req.type === "fs:read" ? await readWorkspaceFile(root, req.path)
744
- : req.type === "skills:list" ? await listSkills(config.agentsRoot, req.handle)
744
+ : req.type === "skills:list" ? await listSkills(req.runtime)
745
745
  : { models: await listRuntimeModels(req.type === "probe-models" ? (req.runtime ?? "") : "") };
746
746
  reply({ ok: true, data: data2 });
747
747
  }
package/dist/skills.js CHANGED
@@ -1,17 +1,35 @@
1
1
  /**
2
- * 枚举某 agent 可用的 skill,供 Profile 的 SKILLS 区展示。两个来源:
3
- * - workspace 私有:<agentsRoot>/<handle>/skills/<name>/SKILL.md
4
- * - 全局共享: ~/.claude/skills/<name>/SKILL.md (可经 CREW_GLOBAL_SKILLS_DIR 覆盖)
2
+ * 枚举某 agent 可用的全局 skill,供 Profile 的 SKILLS 区展示。
3
+ *
4
+ * 目录由 runtime 决定——各 runtime 的原生发现路径互不相同:
5
+ * - codex: $CODEX_HOME/skills (未设时 ~/.codex/skills) + ~/.agents/skills
6
+ * - claude: ~/.claude/skills
7
+ * runtime 缺省(老 server 不下发)时按 claude 回落,与历史行为一致。
8
+ * CREW_GLOBAL_SKILLS_DIR 若设置则覆盖以上全部,只扫它。
5
9
  *
6
10
  * 每个 skill = 一个目录,内含 SKILL.md;从其 YAML frontmatter 取 name/description,
7
11
  * 缺失则回退到目录名。只读、容错(目录不存在/无 frontmatter 都不报错)。
12
+ *
13
+ * 不扫 agent 工作区:项目技能由 project-skills/reconciler 投影到
14
+ * <agentRoot>/.agents/skills 与 <agentRoot>/.crew/claude-skills/.claude/skills,
15
+ * 那两处是 reconciler 独占(每次整目录原子替换),且已由「项目技能」区单独展示。
8
16
  */
9
17
  import { readdir, readFile, stat } from "node:fs/promises";
10
18
  import { join } from "node:path";
11
19
  import { homedir } from "node:os";
12
20
  import { parseSkillFrontmatter } from "./skill-frontmatter.js";
13
- const globalSkillsDir = () => process.env.CREW_GLOBAL_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
14
- async function readSkillsFrom(dir, scope) {
21
+ /** runtime 的原生全局 skill 目录;顺序即优先级(重名时靠前的胜出) */
22
+ function globalSkillsDirs(runtime) {
23
+ const override = process.env.CREW_GLOBAL_SKILLS_DIR;
24
+ if (override)
25
+ return [override];
26
+ const home = homedir();
27
+ if (runtime === "codex") {
28
+ return [join(process.env.CODEX_HOME ?? join(home, ".codex"), "skills"), join(home, ".agents", "skills")];
29
+ }
30
+ return [join(home, ".claude", "skills")];
31
+ }
32
+ async function readSkillsFrom(dir) {
15
33
  const names = await readdir(dir).catch(() => []);
16
34
  const skills = [];
17
35
  for (const name of names) {
@@ -23,17 +41,20 @@ async function readSkillsFrom(dir, scope) {
23
41
  continue;
24
42
  const md = await readFile(skillMd, "utf8").catch(() => "");
25
43
  const fm = parseSkillFrontmatter(md);
26
- skills.push({ scope, name: fm.name || name, description: fm.description ?? "" });
44
+ skills.push({ scope: "global", name: fm.name || name, description: fm.description ?? "" });
27
45
  }
28
46
  return skills;
29
47
  }
30
- /** 列出 agent workspace 私有 + 全局 skill,按 name 排序。 */
31
- export async function listSkills(agentsRoot, handle) {
32
- const [ws, global] = await Promise.all([
33
- readSkillsFrom(join(agentsRoot, handle, "skills"), "workspace"),
34
- readSkillsFrom(globalSkillsDir(), "global"),
35
- ]);
36
- const all = [...ws, ...global];
37
- all.sort((a, b) => (a.scope !== b.scope ? (a.scope === "workspace" ? -1 : 1) : a.name.localeCompare(b.name)));
38
- return all;
48
+ /** 列出该 runtime 的全局 skill,按 name 去重后排序。 */
49
+ export async function listSkills(runtime) {
50
+ const perDir = await Promise.all(globalSkillsDirs(runtime).map((dir) => readSkillsFrom(dir)));
51
+ // 同名 skill 可能同时存在于多个 root( ~/.codex/skills 与 ~/.agents/skills),
52
+ // 取先命中的:既符合 runtime 的目录优先级,也避免前端 key 冲突。
53
+ const byName = new Map();
54
+ for (const skills of perDir) {
55
+ for (const skill of skills)
56
+ if (!byName.has(skill.name))
57
+ byName.set(skill.name, skill);
58
+ }
59
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
39
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -16,9 +16,16 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
+ "scripts": {
20
+ "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
21
+ "build": "tsc -p tsconfig.json",
22
+ "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
23
+ "test": "vitest run",
24
+ "typecheck": "tsc --noEmit"
25
+ },
19
26
  "dependencies": {
20
27
  "@agentclientprotocol/sdk": "1.2.1",
21
- "@nowcrew/cli": "^0.4.13",
28
+ "@nowcrew/cli": "^0.4.14",
22
29
  "cross-spawn": "^7.0.6",
23
30
  "ws": "^8",
24
31
  "yaml": "^2.8.1",
@@ -34,11 +41,5 @@
34
41
  "tsx": "^4.19.0",
35
42
  "typescript": "^5.6.0",
36
43
  "vitest": "^2.1.0"
37
- },
38
- "scripts": {
39
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
40
- "build": "tsc -p tsconfig.json",
41
- "test": "vitest run",
42
- "typecheck": "tsc --noEmit"
43
44
  }
44
- }
45
+ }