@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.
- package/dist/control-request-types.js +1 -0
- package/dist/machine-info.js +2 -0
- package/dist/runtime-health.js +158 -0
- package/dist/serve.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/machine-info.js
CHANGED
|
@@ -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;
|