@nowcrew/daemon 0.5.51 → 0.5.52

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 @@
1
+ export {};
@@ -17,6 +17,7 @@ import { executionBackendCapability } from "./execution-backend.js";
17
17
  import { probeKimiAcp } from "./runtimes/kimi-acp-runner.js";
18
18
  import { probeHermesAcp } from "./runtimes/hermes.js";
19
19
  import { probeOpenCodeRun } from "./runtimes/opencode.js";
20
+ import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
20
21
  import { probeDeepSeekHarnessAcp } from "./runtimes/deepseek-harness.js";
21
22
  export { executableRuntimes } from "./runtime-capabilities.js";
22
23
  const execFileP = promisify(execFile);
@@ -31,6 +32,7 @@ export const DAEMON_CAPABILITIES = [
31
32
  "execution_machine_queue_v1",
32
33
  "execution_agent_memory_policy_v1",
33
34
  "project_skills_v1",
35
+ RUNTIME_HEALTH_PROBE_CAPABILITY,
34
36
  ];
35
37
  export const daemonCapabilities = (runtimePlatform = process.platform) => runtimePlatform === "darwin" || runtimePlatform === "linux"
36
38
  ? DAEMON_CAPABILITIES
@@ -0,0 +1,158 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { z } from "zod";
4
+ import { augmentedPath } from "./runtime-path.js";
5
+ export const RUNTIME_HEALTH_PROBE_CAPABILITY = "runtime_health_probe_v1";
6
+ export const RUNTIME_HEALTH_NAMES = ["claude", "codex", "opencode", "kimi"];
7
+ const RuntimeProbeTargetSchema = z.object({
8
+ runtime: z.enum(RUNTIME_HEALTH_NAMES),
9
+ modelId: z.string().min(1).max(200).nullable(),
10
+ configFingerprint: z.string().min(1).max(200).nullable(),
11
+ }).strict();
12
+ export const RuntimeProbeRequestSchema = z.object({
13
+ runtimes: z.array(RuntimeProbeTargetSchema).min(1).max(RUNTIME_HEALTH_NAMES.length),
14
+ timeoutMs: z.number().int().min(10).max(60_000),
15
+ }).strict().superRefine((value, context) => {
16
+ const seen = new Set();
17
+ for (let index = 0; index < value.runtimes.length; index += 1) {
18
+ const runtime = value.runtimes[index]?.runtime;
19
+ if (runtime === undefined || !seen.has(runtime)) {
20
+ if (runtime !== undefined)
21
+ seen.add(runtime);
22
+ continue;
23
+ }
24
+ context.addIssue({
25
+ code: z.ZodIssueCode.custom,
26
+ path: ["runtimes", index, "runtime"],
27
+ message: "duplicate runtime",
28
+ });
29
+ }
30
+ });
31
+ export const RuntimeProbeFrameSchema = z.object({
32
+ type: z.literal("runtime-health:probe"),
33
+ reqId: z.string().min(1).max(128),
34
+ runtimes: z.array(RuntimeProbeTargetSchema).min(1).max(RUNTIME_HEALTH_NAMES.length),
35
+ timeoutMs: z.number().int().min(10).max(60_000),
36
+ }).strict();
37
+ const execFileAsync = promisify(execFile);
38
+ const defaultExecutor = async (file, args, options) => {
39
+ const result = await execFileAsync(file, [...args], options);
40
+ return { stdout: String(result.stdout), stderr: String(result.stderr) };
41
+ };
42
+ const adapterOf = (_runtime) => "executable";
43
+ function modelArgs(runtime, modelId) {
44
+ const model = modelId === null ? [] : ["--model", modelId];
45
+ if (runtime === "claude")
46
+ return ["--print", "--output-format", "json", ...model, "Reply only OK."];
47
+ if (runtime === "codex")
48
+ return ["exec", "--json", "--skip-git-repo-check", ...model, "Reply only OK."];
49
+ if (runtime === "opencode")
50
+ return ["run", "--format", "json", ...model, "Reply only OK."];
51
+ if (runtime === "kimi")
52
+ return ["--print", "--output-format", "json", ...model, "Reply only OK."];
53
+ return [];
54
+ }
55
+ function failureText(error) {
56
+ if (!error || typeof error !== "object")
57
+ return String(error);
58
+ const value = error;
59
+ return [value.message, value.stdout, value.stderr].filter((part) => typeof part === "string")
60
+ .join("\n").toLowerCase().slice(0, 32_000);
61
+ }
62
+ function failureCode(error) {
63
+ const text = failureText(error);
64
+ if (/quota|insufficient[_ ]quota|credit balance/.test(text))
65
+ return "quota_exhausted";
66
+ if (/\b401\b|unauthori[sz]ed|invalid api[_ -]?key|authentication failed/.test(text))
67
+ return "auth_failed";
68
+ if (/model.{0,80}(not found|does not exist|unknown)/.test(text))
69
+ return "model_not_found";
70
+ if (/model.{0,80}(mapping|alias).{0,40}(invalid|missing|failed)/.test(text))
71
+ return "mapping_invalid";
72
+ if (/\b429\b|rate.?limit|too many requests/.test(text))
73
+ return "rate_limited";
74
+ if (/\b5\d\d\b|upstream|bad gateway|service unavailable/.test(text))
75
+ return "upstream_error";
76
+ return "upstream_error";
77
+ }
78
+ const isMissing = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
79
+ const isAbort = (error) => typeof error === "object" && error !== null && error.name === "AbortError";
80
+ async function runBounded(execute, file, args, timeoutMs, env) {
81
+ const controller = new AbortController();
82
+ let timer;
83
+ try {
84
+ return await Promise.race([
85
+ execute(file, args, {
86
+ env,
87
+ signal: controller.signal,
88
+ timeout: timeoutMs,
89
+ maxBuffer: 64 * 1024,
90
+ }),
91
+ new Promise((_resolve, reject) => {
92
+ timer = setTimeout(() => {
93
+ controller.abort();
94
+ reject(Object.assign(new Error("runtime health probe timed out"), { name: "AbortError" }));
95
+ }, timeoutMs);
96
+ }),
97
+ ]);
98
+ }
99
+ finally {
100
+ if (timer !== undefined)
101
+ clearTimeout(timer);
102
+ }
103
+ }
104
+ export async function probeRuntimeHealth(request, dependencies = {}) {
105
+ const parsed = RuntimeProbeRequestSchema.parse(request);
106
+ const execute = dependencies.execute ?? defaultExecutor;
107
+ const now = dependencies.now ?? (() => new Date());
108
+ const monotonicNow = dependencies.monotonicNow ?? (() => performance.now());
109
+ const sourceEnv = dependencies.env ?? process.env;
110
+ const env = { ...sourceEnv, PATH: augmentedPath(sourceEnv) };
111
+ return Promise.all(parsed.runtimes.map(async (target) => {
112
+ const started = monotonicNow();
113
+ const checkedAt = now().toISOString();
114
+ const base = {
115
+ runtime: target.runtime,
116
+ adapter: adapterOf(target.runtime),
117
+ modelId: target.modelId,
118
+ configFingerprint: target.configFingerprint,
119
+ checkedAt,
120
+ };
121
+ const latencyMs = () => Math.max(0, Math.round(monotonicNow() - started));
122
+ try {
123
+ await runBounded(execute, target.runtime, ["--version"], parsed.timeoutMs, env);
124
+ }
125
+ catch (error) {
126
+ return {
127
+ ...base,
128
+ installation: isMissing(error) ? "missing" : "timeout",
129
+ model: "not_checked",
130
+ latencyMs: latencyMs(),
131
+ detailCode: isMissing(error) ? "executable_missing" : "version_timeout",
132
+ };
133
+ }
134
+ if (base.adapter !== "executable") {
135
+ return { ...base, installation: "available", model: "not_checked", latencyMs: latencyMs(), detailCode: null };
136
+ }
137
+ try {
138
+ await runBounded(execute, target.runtime, modelArgs(target.runtime, target.modelId), parsed.timeoutMs, env);
139
+ return { ...base, installation: "available", model: "ok", latencyMs: latencyMs(), detailCode: null };
140
+ }
141
+ catch (error) {
142
+ const code = isAbort(error) ? "timeout" : failureCode(error);
143
+ return { ...base, installation: "available", model: code, latencyMs: latencyMs(), detailCode: code };
144
+ }
145
+ }));
146
+ }
147
+ export async function handleRuntimeProbeFrame(decoded, probe = probeRuntimeHealth) {
148
+ const frame = RuntimeProbeFrameSchema.safeParse(decoded);
149
+ if (!frame.success)
150
+ return null;
151
+ try {
152
+ const data = await probe({ runtimes: frame.data.runtimes, timeoutMs: frame.data.timeoutMs });
153
+ return { reqId: frame.data.reqId, ok: true, data };
154
+ }
155
+ catch {
156
+ return { reqId: frame.data.reqId, ok: false, error: "runtime_health_probe_failed" };
157
+ }
158
+ }
package/dist/serve.js CHANGED
@@ -42,6 +42,7 @@ import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
42
42
  import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
43
43
  import { createProjectSkillsReconciler, ProjectProjectionError, } from "./project-skills/reconciler.js";
44
44
  import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
45
+ import { handleRuntimeProbeFrame, probeRuntimeHealth } from "./runtime-health.js";
45
46
  // normalize.ts 的活动种类 → activity 枚举
46
47
  const ACTIVITY_MAP = {
47
48
  init: "working", text: "thinking", reading: "reading", sending: "sending",
@@ -698,6 +699,16 @@ export function serve(config, opts = {}) {
698
699
  catch { /* server 会按请求超时处理 */ }
699
700
  return;
700
701
  }
702
+ if (msg.type === "runtime-health:probe") {
703
+ const result = await handleRuntimeProbeFrame(msg, opts.runtimeHealth?.probe ?? probeRuntimeHealth);
704
+ if (result !== null) {
705
+ try {
706
+ ws?.send(JSON.stringify({ type: "fs:result", ...result }));
707
+ }
708
+ catch { /* server 会超时 */ }
709
+ }
710
+ return;
711
+ }
701
712
  // 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
702
713
  if (msg.type === "raft:inspect" || msg.type === "raft:import") {
703
714
  const req = msg;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.51",
3
+ "version": "0.5.52",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",