@nowcrew/daemon 0.6.73 → 0.6.75

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,123 @@
1
+ /**
2
+ * claude 模型清单探测:上报 CLI 族别名(fable/opus/sonnet/haiku,恒指本机已装版本的最新
3
+ * 对应模型),并把每个别名解析成具体模型 id 放进展示名——用户反馈"只写 latest 根本不知道
4
+ * 实际是什么模型"。
5
+ *
6
+ * 解析方式:claude 没有模型枚举命令,但 `--print` 会话启动时的 `system/init` 事件明文携带
7
+ * 解析后的 `model`(2.1.258 实测 opus→claude-opus-5、fable→claude-fable-5-1)。探测把
8
+ * ANTHROPIC_BASE_URL 指向本机不可达端口(零 API 调用、零 token),`--setting-sources=`
9
+ * 关掉用户 hooks/项目配置,读到 init 行即 SIGKILL。结果按 CLI 版本缓存:只有全部别名都
10
+ * 解析成功才缓存,失败的下次请求重试。
11
+ */
12
+ import { execFile, spawn } from "node:child_process";
13
+ import { tmpdir } from "node:os";
14
+ import { createInterface } from "node:readline";
15
+ import { promisify } from "node:util";
16
+ import { isWin } from "../platform.js";
17
+ const execFileP = promisify(execFile);
18
+ const PROBE_TIMEOUT_MS = 8_000;
19
+ // 本机丢弃端口:连不上也无所谓,init 事件在任何网络请求之前就已输出。
20
+ const UNROUTABLE_API_BASE = "http://127.0.0.1:9";
21
+ const CLAUDE_ALIASES = [
22
+ ["fable", "Fable"],
23
+ ["sonnet", "Sonnet"],
24
+ ["opus", "Opus"],
25
+ ["haiku", "Haiku"],
26
+ ];
27
+ const DEFAULT_ALIAS = "sonnet";
28
+ const cacheByVersion = new Map();
29
+ /** "2.1.258 (Claude Code)" → "2.1.258";空/异常输出 → ""。 */
30
+ export function normalizeClaudeVersion(raw) {
31
+ return raw.replace(/\s*\(Claude Code\)\s*$/, "").trim();
32
+ }
33
+ export async function listClaudeModels(deps = defaultDeps) {
34
+ const version = normalizeClaudeVersion(await deps.probeVersion());
35
+ const cached = version ? cacheByVersion.get(version) : undefined;
36
+ if (cached)
37
+ return cached;
38
+ const resolved = await Promise.all(CLAUDE_ALIASES.map(async ([alias]) => {
39
+ try {
40
+ return await deps.resolveAlias(alias);
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }));
46
+ const suffix = version ? ` · Claude Code ${version}` : "";
47
+ const models = CLAUDE_ALIASES.map(([alias, name], index) => {
48
+ const concrete = resolved[index];
49
+ return {
50
+ id: alias,
51
+ label: `${name} (latest${concrete ? `: ${concrete}` : ""})${suffix}`,
52
+ ...(alias === DEFAULT_ALIAS ? { default: true } : {}),
53
+ };
54
+ });
55
+ if (version && resolved.every((model) => model !== null))
56
+ cacheByVersion.set(version, models);
57
+ return models;
58
+ }
59
+ /** 仅测试用:清空版本缓存。 */
60
+ export function resetClaudeModelCache() {
61
+ cacheByVersion.clear();
62
+ }
63
+ async function probeVersion() {
64
+ const { stdout } = await execFileP("claude", ["--version"], {
65
+ shell: isWin(),
66
+ timeout: PROBE_TIMEOUT_MS,
67
+ killSignal: "SIGKILL",
68
+ });
69
+ return stdout;
70
+ }
71
+ function resolveAlias(alias) {
72
+ return new Promise((resolve) => {
73
+ const child = spawn("claude", [
74
+ "--model", alias,
75
+ "--print", "--output-format", "stream-json", "--verbose",
76
+ "--setting-sources=",
77
+ "ping",
78
+ ], {
79
+ cwd: tmpdir(),
80
+ env: {
81
+ ...process.env,
82
+ ANTHROPIC_BASE_URL: UNROUTABLE_API_BASE,
83
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
84
+ },
85
+ shell: isWin(),
86
+ stdio: ["ignore", "pipe", "ignore"],
87
+ });
88
+ let settled = false;
89
+ const finish = (value) => {
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ clearTimeout(timer);
94
+ try {
95
+ child.kill("SIGKILL");
96
+ }
97
+ catch { /* 已退出 */ }
98
+ resolve(value);
99
+ };
100
+ const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
101
+ child.on("error", () => finish(null));
102
+ child.on("exit", () => finish(null));
103
+ const lines = createInterface({ input: child.stdout });
104
+ lines.on("line", (line) => {
105
+ const model = initModelOf(line);
106
+ if (model !== null)
107
+ finish(model);
108
+ });
109
+ });
110
+ }
111
+ /** stream-json 一行 → system/init 事件里的 model;其他行返回 null。 */
112
+ export function initModelOf(line) {
113
+ try {
114
+ const event = JSON.parse(line);
115
+ if (event.type !== "system" || event.subtype !== "init")
116
+ return null;
117
+ return typeof event.model === "string" && event.model.trim() ? event.model.trim() : null;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ const defaultDeps = { probeVersion, resolveAlias };
@@ -31,6 +31,7 @@ const ERROR_MESSAGE_CAP = 2_000;
31
31
  const STDERR_TAIL_CAP = 1_200;
32
32
  const STDERR_LINE_CAPTURE_CAP = 1_200;
33
33
  const STDERR_LINE_OMITTED = "[stderr line omitted: exceeded capture limit]\n";
34
+ const API_RETRY_DETAIL_CAP = 240;
34
35
  const MAX_INITIALIZE_ATTEMPTS = 2;
35
36
  // 模型网关瞬态故障(过载/限流)导致 turn 失败时,整轮重试(15s→45s 递进退避):codex 自身
36
37
  // 的重试窗口只有 ~10-30s,网关过载往往持续数分钟,这里再兜一层,否则 agent 直接失败
@@ -188,6 +189,21 @@ function logStage(stage, status, attempt, startedAt, detail) {
188
189
  process.stderr.write(`[codex-app-server] stage=${stage} status=${status} attempt=${attempt}`
189
190
  + ` elapsed_ms=${Date.now() - startedAt}${suffix}\n`);
190
191
  }
192
+ function logApiRetry(turnAttempt, apiRetryAttempt, detail, secrets) {
193
+ const safeDetail = detail === undefined
194
+ ? undefined
195
+ : safeErrorMessage(detail, secrets).slice(0, API_RETRY_DETAIL_CAP);
196
+ process.stderr.write(`[codex-app-server] stage=api_retry status=retry attempt=${apiRetryAttempt}`
197
+ + ` turn_attempt=${turnAttempt}`
198
+ + (safeDetail === undefined ? "" : ` error=${JSON.stringify(safeDetail)}`)
199
+ + "\n");
200
+ }
201
+ function logApiRetrySummary(turnAttempt, apiRetryCount) {
202
+ if (apiRetryCount === 0)
203
+ return;
204
+ process.stderr.write(`[codex-app-server] stage=api_retry status=summary turn_attempt=${turnAttempt}`
205
+ + ` api_retry_count=${apiRetryCount}\n`);
206
+ }
191
207
  function jsonLine(event) {
192
208
  if (process.stdout.write(`${JSON.stringify(event)}\n`))
193
209
  return Promise.resolve();
@@ -445,6 +461,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
445
461
  let threadId = null;
446
462
  let announcedThreadId = null;
447
463
  let turnId = null;
464
+ let apiRetryCount = 0;
448
465
  let turnHasUnsafeSideEffect = false;
449
466
  const startedItemIds = new Set();
450
467
  let lastUsage;
@@ -489,10 +506,17 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
489
506
  }
490
507
  if (method === "turn/completed")
491
508
  completionResolve(params);
492
- if (method === "error" && params.willRetry === false) {
493
- const detail = params.error?.message;
494
- if (detail)
495
- process.stderr.write(sanitizeCodexDiagnostic(`Codex turn error: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`, secrets));
509
+ if (method === "error") {
510
+ const notification = (params !== null && typeof params === "object" ? params : {});
511
+ const detail = notification.error?.message;
512
+ if (notification.willRetry === true) {
513
+ apiRetryCount += 1;
514
+ logApiRetry(attempt, apiRetryCount, detail, secrets);
515
+ }
516
+ else if (notification.willRetry === false && detail) {
517
+ process.stderr.write(sanitizeCodexDiagnostic(`Codex turn error: ${detail.slice(0, ERROR_MESSAGE_CAP)}`
518
+ + ` turn_attempt=${attempt} api_retry_count=${apiRetryCount}\n`, secrets));
519
+ }
496
520
  }
497
521
  }, completionReject, secrets);
498
522
  let treeStopPromise = null;
@@ -608,10 +632,12 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
608
632
  },
609
633
  }),
610
634
  });
635
+ logApiRetrySummary(attempt, apiRetryCount);
611
636
  if (completed.turn?.status === "completed")
612
637
  return { code: 0, initializeTimedOut: false, unsafeToRetry: false };
613
638
  const detail = completed.turn?.error?.message ?? `turn status ${completed.turn?.status ?? "unknown"}`;
614
- process.stderr.write(sanitizeCodexDiagnostic(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`, secrets));
639
+ process.stderr.write(sanitizeCodexDiagnostic(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}`
640
+ + ` turn_attempt=${attempt} api_retry_count=${apiRetryCount}\n`, secrets));
615
641
  const status = completed.turn?.status;
616
642
  const retryReason = cancelling ? undefined
617
643
  : status === "interrupted" ? "collab_interrupted"
@@ -633,7 +659,8 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
633
659
  else {
634
660
  logStage(activeStage, "error", attempt, stageStartedAt);
635
661
  }
636
- process.stderr.write(`Codex app-server execution failed: ${safeErrorMessage(error, secrets)}\n`);
662
+ process.stderr.write(`Codex app-server execution failed: ${safeErrorMessage(error, secrets)}`
663
+ + ` turn_attempt=${attempt} api_retry_count=${apiRetryCount}\n`);
637
664
  return { code: 1, initializeTimedOut, unsafeToRetry: false };
638
665
  }
639
666
  finally {
package/dist/serve.js CHANGED
@@ -5,10 +5,11 @@
5
5
  import { WebSocket } from "ws";
6
6
  import { join } from "node:path";
7
7
  import { randomUUID } from "node:crypto";
8
- import { initSlog, dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
8
+ import { dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
9
9
  import { mergeRunAgentResults, reportScheduledStartFailure, runAgent } from "./runner.js";
10
10
  import { buildOriginDecisionRetryPrompt, buildScheduledPrompt } from "./prompt.js";
11
- import { collectMachineHello, cliVersion, daemonCapabilities, daemonVersion, detectExecutionRuntimesWithSignal, } from "./machine-info.js";
11
+ import { collectMachineHello, daemonCapabilities, detectExecutionRuntimesWithSignal, } from "./machine-info.js";
12
+ import { initReleaseSlog, reportDaemonConnectionClose, reportReleaseError } from "./release-channel-guard.js";
12
13
  import { conservativeExecutionRuntimes, createRuntimeProbeCoordinator, } from "./runtime-probe.js";
13
14
  import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
14
15
  import { listSkills } from "./skills.js";
@@ -311,7 +312,7 @@ export function serve(config, opts = {}) {
311
312
  reservation.release();
312
313
  };
313
314
  let connectedAt = 0; // 本次 WS 连接建立时刻(断开日志算在线时长用)
314
- initSlog(config.serverUrl, config.machineToken, { daemonVersion: daemonVersion(), cliVersion: cliVersion(), ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), agentsRoot: config.agentsRoot });
315
+ initReleaseSlog({ serverUrl: config.serverUrl, machineToken: config.machineToken, ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), agentsRoot: config.agentsRoot });
315
316
  dslog("daemon.start", "daemon 常驻模式启动", { server_url: config.serverUrl, runtime: config.runtimeBin });
316
317
  // 并行调度:同一 agent 可并行处理多个【不同任务】(线程/频道),每任务隔离 cwd+work-log。
317
318
  // scheduled 重复 run 仍由 running 去重;普通同线程 wake 用 legacyTaskTails 串成 FIFO。
@@ -854,6 +855,8 @@ export function serve(config, opts = {}) {
854
855
  dslog("daemon.ws_auth_rejected", "控制面拒绝鉴权:机器凭证无效或已吊销", { level: "ERROR" });
855
856
  backoff = maxBackoff; // 退避到最大,停止每秒重连刷屏
856
857
  }
858
+ if (reportReleaseError(msg.code, log))
859
+ backoff = maxBackoff;
857
860
  return;
858
861
  }
859
862
  if (msg.type === "project:add"
@@ -1309,11 +1312,8 @@ export function serve(config, opts = {}) {
1309
1312
  completionRetransmitter.pause();
1310
1313
  // 4001 = 控制面应用级「鉴权失败」关闭码(见 server control-plane.ts)。即使上面的
1311
1314
  // error 帧因 close 抢先而丢失,也能据关闭码识别这是凭证失效——退避拉满,不再每秒热循环。
1312
- if (code === 4001) {
1315
+ if (reportDaemonConnectionClose(code, log))
1313
1316
  backoff = maxBackoff;
1314
- log(`🛑 控制面以鉴权失败关闭连接 (code 4001):机器凭证无效或已吊销。`);
1315
- log(` 请在 NowWork 重新 "Add Computer" 获取新连接命令再重跑(当前 --api-key 已失效)。`);
1316
- }
1317
1317
  log(`🔁 控制面断开,${Math.round(backoff / 1000)}s 后重连`);
1318
1318
  // 此刻 server 大概率不可达 → 这条会落 spool,重连 drainSpool 时补传;
1319
1319
  // ts 是现在(断开时刻),排查「daemon 为什么断」以它对齐 server 侧 machine_disconnected。
package/dist/slog.js CHANGED
@@ -59,6 +59,10 @@ export function initSlog(serverUrl, machineToken, identity = {}) {
59
59
  pid: process.pid,
60
60
  daemon_version: identity.daemonVersion,
61
61
  cli_version: identity.cliVersion,
62
+ release_channel: identity.releaseChannel,
63
+ release_tag: identity.releaseTag,
64
+ daemon_git_sha: identity.gitSha,
65
+ daemon_artifact_digest: identity.artifactDigest,
62
66
  daemon_profile: identity.profileName,
63
67
  server_host: serverHost,
64
68
  env: deploymentEnvironment(serverHost),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.73",
3
+ "version": "0.6.75",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -18,8 +18,9 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@agentclientprotocol/sdk": "1.2.1",
21
- "@nowcrew/cli": "^0.4.13",
21
+ "@nowcrew/cli": "0.4.13",
22
22
  "cross-spawn": "^7.0.6",
23
+ "semver": "7.8.5",
23
24
  "ws": "^8",
24
25
  "yaml": "^2.8.1",
25
26
  "zod": "^3.23.0"
@@ -30,6 +31,7 @@
30
31
  "devDependencies": {
31
32
  "@types/cross-spawn": "^6.0.6",
32
33
  "@types/node": "^22.0.0",
34
+ "@types/semver": "7.7.1",
33
35
  "@types/ws": "^8",
34
36
  "tsx": "^4.19.0",
35
37
  "typescript": "^5.6.0",