@nowcrew/daemon 0.5.14 → 0.5.16
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 +57 -3
- package/dist/bound-im-decision.js +22 -0
- package/dist/computer-cli.js +214 -0
- package/dist/computer-profile.js +195 -0
- package/dist/computer-service.js +358 -0
- package/dist/config.js +2 -1
- package/dist/console.js +10 -0
- package/dist/execution-backend.js +11 -0
- package/dist/execution-protocol.js +11 -0
- package/dist/execution-runner.js +38 -11
- package/dist/execution-supervisor.js +48 -5
- package/dist/execution-telemetry-journal.js +71 -0
- package/dist/i18n.js +25 -0
- package/dist/local-executor.js +5 -2
- package/dist/machine-info.js +25 -4
- package/dist/main.js +15 -0
- package/dist/normalize.js +11 -0
- package/dist/origin-decision.js +1 -1
- package/dist/prompt.js +40 -21
- package/dist/runner.js +25 -1
- package/dist/runtime-capabilities.js +5 -0
- package/dist/runtimes/kimi-acp-runner.js +264 -0
- package/dist/runtimes/kimi.js +10 -0
- package/dist/scheduled-report.js +4 -0
- package/dist/scheduled-run-report.js +1 -0
- package/dist/serve.js +55 -10
- package/package.json +3 -2
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { Readable, Writable } from "node:stream";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import spawn from "cross-spawn";
|
|
6
|
+
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
|
|
7
|
+
const ERROR_MESSAGE_CAP = 2_000;
|
|
8
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
9
|
+
function jsonLine(event) {
|
|
10
|
+
if (process.stdout.write(`${JSON.stringify(event)}\n`))
|
|
11
|
+
return Promise.resolve();
|
|
12
|
+
return once(process.stdout, "drain").then(() => undefined);
|
|
13
|
+
}
|
|
14
|
+
function textContent(content) {
|
|
15
|
+
if (typeof content === "string")
|
|
16
|
+
return content;
|
|
17
|
+
if (!Array.isArray(content))
|
|
18
|
+
return "";
|
|
19
|
+
return content.flatMap((part) => {
|
|
20
|
+
if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
|
|
21
|
+
return [part.text];
|
|
22
|
+
}
|
|
23
|
+
if (part && typeof part === "object" && "content" in part
|
|
24
|
+
&& part.content && typeof part.content === "object"
|
|
25
|
+
&& "type" in part.content && part.content.type === "text"
|
|
26
|
+
&& "text" in part.content && typeof part.content.text === "string") {
|
|
27
|
+
return [part.content.text];
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
}).join("\n");
|
|
31
|
+
}
|
|
32
|
+
/** Translate stable ACP updates into daemon-owned NDJSON, without exposing thought chunks. */
|
|
33
|
+
export function mapKimiAcpUpdate(update) {
|
|
34
|
+
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
|
|
35
|
+
return [{ type: "kimi.acp.text_delta", text: update.content.text }];
|
|
36
|
+
}
|
|
37
|
+
if (update.sessionUpdate === "tool_call") {
|
|
38
|
+
return [{
|
|
39
|
+
type: "kimi.acp.tool_call",
|
|
40
|
+
id: update.toolCallId,
|
|
41
|
+
title: update.title,
|
|
42
|
+
...(update.kind === undefined ? {} : { kind: update.kind }),
|
|
43
|
+
...(update.status === undefined ? {} : { status: update.status }),
|
|
44
|
+
...(update.rawInput === undefined ? {} : { input: update.rawInput }),
|
|
45
|
+
}];
|
|
46
|
+
}
|
|
47
|
+
if (update.sessionUpdate === "tool_call_update") {
|
|
48
|
+
const output = textContent(update.content);
|
|
49
|
+
return [{
|
|
50
|
+
type: "kimi.acp.tool_result",
|
|
51
|
+
id: update.toolCallId,
|
|
52
|
+
...(update.status === undefined ? {} : { status: update.status }),
|
|
53
|
+
...(output ? { content: output } : {}),
|
|
54
|
+
}];
|
|
55
|
+
}
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
function safeErrorMessage(error, prompt) {
|
|
59
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
60
|
+
const redacted = prompt && raw.includes(prompt) ? raw.replaceAll(prompt, "[prompt redacted]") : raw;
|
|
61
|
+
return redacted.slice(0, ERROR_MESSAGE_CAP);
|
|
62
|
+
}
|
|
63
|
+
/** Full access may approve an operation, but it must never fabricate an answer to an agent question. */
|
|
64
|
+
export function selectKimiPermission(params) {
|
|
65
|
+
const allowOnce = params.options.filter((option) => option.kind === "allow_once");
|
|
66
|
+
if (params.toolCall.title === "AskUserQuestion" || allowOnce.length > 1) {
|
|
67
|
+
return { outcome: { outcome: "cancelled" } };
|
|
68
|
+
}
|
|
69
|
+
const allowed = allowOnce[0]
|
|
70
|
+
?? (params.options.filter((option) => option.kind === "allow_always").length === 1
|
|
71
|
+
? params.options.find((option) => option.kind === "allow_always")
|
|
72
|
+
: undefined);
|
|
73
|
+
return allowed === undefined
|
|
74
|
+
? { outcome: { outcome: "cancelled" } }
|
|
75
|
+
: { outcome: { outcome: "selected", optionId: allowed.optionId } };
|
|
76
|
+
}
|
|
77
|
+
async function readPrompt() {
|
|
78
|
+
process.stdin.setEncoding("utf8");
|
|
79
|
+
let prompt = "";
|
|
80
|
+
for await (const chunk of process.stdin)
|
|
81
|
+
prompt += String(chunk);
|
|
82
|
+
if (!prompt)
|
|
83
|
+
throw new Error("Kimi ACP prompt is empty");
|
|
84
|
+
return prompt;
|
|
85
|
+
}
|
|
86
|
+
async function stopChild(child) {
|
|
87
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
88
|
+
return;
|
|
89
|
+
const closed = once(child, "close").then(() => undefined);
|
|
90
|
+
child.kill("SIGTERM");
|
|
91
|
+
let timer;
|
|
92
|
+
const graceful = await Promise.race([
|
|
93
|
+
closed.then(() => true),
|
|
94
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(false), 1_000); }),
|
|
95
|
+
]);
|
|
96
|
+
if (timer !== undefined)
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
if (!graceful && child.exitCode === null && child.signalCode === null) {
|
|
99
|
+
child.kill("SIGKILL");
|
|
100
|
+
await closed;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** A help banner is insufficient: Kimi's authenticate RPC re-checks its token without creating a session. */
|
|
104
|
+
export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
105
|
+
const child = spawnProcess(options.bin, ["acp"], {
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
env: process.env,
|
|
108
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
109
|
+
});
|
|
110
|
+
if (child.stdin === null || child.stdout === null || child.stderr === null)
|
|
111
|
+
return false;
|
|
112
|
+
child.stderr.resume();
|
|
113
|
+
const app = client({ name: "nowcrew-daemon-kimi-probe" })
|
|
114
|
+
.onRequest(methods.client.session.requestPermission, () => ({
|
|
115
|
+
outcome: { outcome: "cancelled" },
|
|
116
|
+
}))
|
|
117
|
+
.onNotification(methods.client.session.update, () => undefined);
|
|
118
|
+
let timeout;
|
|
119
|
+
try {
|
|
120
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
121
|
+
const connected = app.connectWith(stream, async (context) => {
|
|
122
|
+
const initialized = await context.request(methods.agent.initialize, {
|
|
123
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
124
|
+
clientCapabilities: {},
|
|
125
|
+
clientInfo: { name: "nowcrew-daemon", version: "1" },
|
|
126
|
+
});
|
|
127
|
+
const authMethod = initialized.authMethods?.[0];
|
|
128
|
+
if (authMethod !== undefined) {
|
|
129
|
+
await context.request(methods.agent.authenticate, { methodId: authMethod.id });
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
});
|
|
133
|
+
const result = await Promise.race([
|
|
134
|
+
connected,
|
|
135
|
+
new Promise((resolve) => {
|
|
136
|
+
timeout = setTimeout(() => {
|
|
137
|
+
void stopChild(child);
|
|
138
|
+
resolve(false);
|
|
139
|
+
}, PROBE_TIMEOUT_MS);
|
|
140
|
+
}),
|
|
141
|
+
]);
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (timeout !== undefined)
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
await stopChild(child);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function runKimiAcp(options) {
|
|
154
|
+
const prompt = await readPrompt();
|
|
155
|
+
const child = spawn(options.bin, ["acp"], {
|
|
156
|
+
cwd: process.cwd(),
|
|
157
|
+
env: process.env,
|
|
158
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
159
|
+
});
|
|
160
|
+
if (child.stdin === null || child.stdout === null || child.stderr === null) {
|
|
161
|
+
throw new Error("Kimi ACP process did not expose stdio");
|
|
162
|
+
}
|
|
163
|
+
child.stderr.pipe(process.stderr, { end: false });
|
|
164
|
+
let context = null;
|
|
165
|
+
let sessionId = null;
|
|
166
|
+
let cancelling = false;
|
|
167
|
+
const cancel = async () => {
|
|
168
|
+
if (cancelling)
|
|
169
|
+
return;
|
|
170
|
+
cancelling = true;
|
|
171
|
+
if (context !== null && sessionId !== null) {
|
|
172
|
+
await context.notify(methods.agent.session.cancel, { sessionId }).catch(() => undefined);
|
|
173
|
+
}
|
|
174
|
+
await stopChild(child);
|
|
175
|
+
};
|
|
176
|
+
const onSignal = () => {
|
|
177
|
+
void cancel().finally(() => process.exit(130));
|
|
178
|
+
};
|
|
179
|
+
process.once("SIGTERM", onSignal);
|
|
180
|
+
process.once("SIGINT", onSignal);
|
|
181
|
+
const app = client({ name: "nowcrew-daemon-kimi" })
|
|
182
|
+
.onRequest(methods.client.session.requestPermission, ({ params }) => selectKimiPermission(params))
|
|
183
|
+
.onNotification(methods.client.session.update, async ({ params }) => {
|
|
184
|
+
for (const event of mapKimiAcpUpdate(params.update))
|
|
185
|
+
await jsonLine(event);
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
189
|
+
const result = await app.connectWith(stream, async (nextContext) => {
|
|
190
|
+
context = nextContext;
|
|
191
|
+
const initialized = await nextContext.request(methods.agent.initialize, {
|
|
192
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
193
|
+
clientCapabilities: {},
|
|
194
|
+
clientInfo: { name: "nowcrew-daemon", version: "1" },
|
|
195
|
+
});
|
|
196
|
+
if (process.env.CREW_KIMI_ACP_DEBUG === "1") {
|
|
197
|
+
process.stderr.write(`Kimi ACP auth methods: ${JSON.stringify(initialized.authMethods ?? [])}\n`);
|
|
198
|
+
}
|
|
199
|
+
const authMethod = initialized.authMethods?.[0];
|
|
200
|
+
if (authMethod !== undefined) {
|
|
201
|
+
await nextContext.request(methods.agent.authenticate, { methodId: authMethod.id });
|
|
202
|
+
}
|
|
203
|
+
const session = await nextContext.request(methods.agent.session.new, {
|
|
204
|
+
cwd: process.cwd(),
|
|
205
|
+
mcpServers: [],
|
|
206
|
+
});
|
|
207
|
+
sessionId = session.sessionId;
|
|
208
|
+
if (options.model) {
|
|
209
|
+
await nextContext.request(methods.agent.session.setConfigOption, {
|
|
210
|
+
sessionId,
|
|
211
|
+
configId: "model",
|
|
212
|
+
value: options.model,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return nextContext.request(methods.agent.session.prompt, {
|
|
216
|
+
sessionId,
|
|
217
|
+
prompt: [{ type: "text", text: prompt }],
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
const usage = result.usage;
|
|
221
|
+
await jsonLine({
|
|
222
|
+
type: "turn.completed",
|
|
223
|
+
stop_reason: result.stopReason,
|
|
224
|
+
...(usage === undefined || usage === null ? {} : {
|
|
225
|
+
usage: {
|
|
226
|
+
input_tokens: usage.inputTokens,
|
|
227
|
+
output_tokens: usage.outputTokens,
|
|
228
|
+
cache_read_input_tokens: usage.cachedReadTokens ?? 0,
|
|
229
|
+
cache_creation_input_tokens: usage.cachedWriteTokens ?? 0,
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
return result.stopReason === "end_turn" ? 0 : result.stopReason === "cancelled" ? 130 : 1;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
process.stderr.write(`Kimi ACP execution failed: ${safeErrorMessage(error, prompt)}\n`);
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
process.off("SIGTERM", onSignal);
|
|
241
|
+
process.off("SIGINT", onSignal);
|
|
242
|
+
await stopChild(child);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function optionsFromArgv(argv) {
|
|
246
|
+
const { values } = parseArgs({
|
|
247
|
+
args: [...argv],
|
|
248
|
+
options: {
|
|
249
|
+
bin: { type: "string" },
|
|
250
|
+
model: { type: "string" },
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
if (!values.bin)
|
|
254
|
+
throw new Error("--bin is required");
|
|
255
|
+
return { bin: values.bin, ...(values.model ? { model: values.model } : {}) };
|
|
256
|
+
}
|
|
257
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
258
|
+
runKimiAcp(optionsFromArgv(process.argv.slice(2)))
|
|
259
|
+
.then((code) => { process.exitCode = code; })
|
|
260
|
+
.catch((error) => {
|
|
261
|
+
process.stderr.write(`Kimi ACP runner failed: ${safeErrorMessage(error, "")}\n`);
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
});
|
|
264
|
+
}
|
package/dist/runtimes/kimi.js
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
// cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
|
|
12
12
|
import spawn from "cross-spawn";
|
|
13
|
+
// Windows CreateProcess receives one UTF-16 command line. Reserve room for the executable, flags,
|
|
14
|
+
// model and cmd shim quoting instead of relying on the theoretical 32767-character ceiling.
|
|
15
|
+
export const KIMI_LEGACY_PROMPT_MAX_UTF16 = 28_000;
|
|
16
|
+
export function assertKimiLegacyPromptFits(prompt, platform = process.platform) {
|
|
17
|
+
if (platform !== "win32" || prompt.length <= KIMI_LEGACY_PROMPT_MAX_UTF16)
|
|
18
|
+
return;
|
|
19
|
+
throw new Error(`Kimi legacy prompt exceeds the Windows argv limit (${prompt.length}/${KIMI_LEGACY_PROMPT_MAX_UTF16}); `
|
|
20
|
+
+ "shorten the prompt (protocol-v1 remains disabled on Windows until Job Object ownership is available)");
|
|
21
|
+
}
|
|
13
22
|
// Kimi Code 思考强度档位(kimi-code 0.23.0 实测+源码):无 CLI 参数,
|
|
14
23
|
// 由 runner 经 KIMI_MODEL_THINKING_EFFORT env 注入;白名单外的值不注。
|
|
15
24
|
export const KIMI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
@@ -24,6 +33,7 @@ export function buildKimiArgs(input) {
|
|
|
24
33
|
return args;
|
|
25
34
|
}
|
|
26
35
|
export function spawnKimi(input) {
|
|
36
|
+
assertKimiLegacyPromptFits(input.wakePrompt);
|
|
27
37
|
// stdio 固定 ignore/pipe/pipe,stdout/stderr 必为 Readable;cross-spawn 类型不带该细化,断言之
|
|
28
38
|
return spawn(input.bin, buildKimiArgs(input), {
|
|
29
39
|
cwd: input.cwd,
|
package/dist/scheduled-report.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
export function normalizeScheduledPolicy(value) {
|
|
2
2
|
return value === "always_report" ? "always_report" : "silent_unless_report";
|
|
3
3
|
}
|
|
4
|
+
export function normalizeExternalNotificationPolicy(value) {
|
|
5
|
+
return value === "agent_decides" ? "agent_decides" : "disabled";
|
|
6
|
+
}
|
|
4
7
|
export function normalizeScheduledContext(input) {
|
|
5
8
|
return {
|
|
6
9
|
jobId: input.jobId,
|
|
7
10
|
runId: input.runId,
|
|
8
11
|
title: input.title?.trim() || "Scheduled job",
|
|
9
12
|
outputPolicy: normalizeScheduledPolicy(input.outputPolicy),
|
|
13
|
+
externalNotificationPolicy: normalizeExternalNotificationPolicy(input.externalNotificationPolicy),
|
|
10
14
|
};
|
|
11
15
|
}
|
|
12
16
|
export async function deliverScheduledReport(input) {
|
|
@@ -27,6 +27,7 @@ export function reportAgentRunComplete(socket, input, log) {
|
|
|
27
27
|
exitCode: input.exitCode,
|
|
28
28
|
...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
|
|
29
29
|
...(input.originDecision ? { originDecision: input.originDecision } : {}),
|
|
30
|
+
...(input.contextUpToSeq !== undefined ? { contextUpToSeq: input.contextUpToSeq } : {}),
|
|
30
31
|
...(input.runtime ? { runtime: input.runtime } : {}),
|
|
31
32
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
32
33
|
...(input.resumed !== undefined ? { resumed: input.resumed } : {}),
|
package/dist/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import { initSlog, 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, DAEMON_CAPABILITIES, EXECUTION_PROTOCOL } from "./machine-info.js";
|
|
11
|
+
import { collectMachineHello, DAEMON_CAPABILITIES, EXECUTION_PROTOCOL, } from "./machine-info.js";
|
|
12
12
|
import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
|
|
13
13
|
import { listSkills } from "./skills.js";
|
|
14
14
|
import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
|
|
@@ -18,8 +18,10 @@ import { formatDaemonLogLine } from "./log-format.js";
|
|
|
18
18
|
import { reportAgentRunComplete } from "./scheduled-run-report.js";
|
|
19
19
|
import { runWithOriginDecisionGuard } from "./origin-decision.js";
|
|
20
20
|
import { createExecutionJournal } from "./execution-journal.js";
|
|
21
|
+
import { createExecutionTelemetryJournal } from "./execution-telemetry-journal.js";
|
|
21
22
|
import { ExecutionRejectedSchema, ExecutionSnapshotSchema, LegacyAgentStartSchema, ServerToDaemonExecutionFrameSchema, } from "./execution-protocol.js";
|
|
22
23
|
import { hashExecutionSpec, runExecution, } from "./execution-runner.js";
|
|
24
|
+
import { executionBackendCapability } from "./execution-backend.js";
|
|
23
25
|
// normalize.ts 的活动种类 → activity 枚举
|
|
24
26
|
const ACTIVITY_MAP = {
|
|
25
27
|
init: "working", text: "thinking", reading: "reading", sending: "sending",
|
|
@@ -28,7 +30,7 @@ const ACTIVITY_MAP = {
|
|
|
28
30
|
};
|
|
29
31
|
export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform) {
|
|
30
32
|
const query = new URLSearchParams({ key: machineToken });
|
|
31
|
-
if (runtimePlatform
|
|
33
|
+
if (executionBackendCapability(runtimePlatform).supported) {
|
|
32
34
|
query.set("execution_min", String(EXECUTION_PROTOCOL.min));
|
|
33
35
|
query.set("execution_max", String(EXECUTION_PROTOCOL.max));
|
|
34
36
|
}
|
|
@@ -43,8 +45,9 @@ export function serve(config, opts = {}) {
|
|
|
43
45
|
let backoff = 1000;
|
|
44
46
|
const maxBackoff = opts.maxBackoffMs ?? 30_000;
|
|
45
47
|
const executionJournal = opts.execution?.journal ?? createExecutionJournal(config.agentsRoot);
|
|
48
|
+
const executionTelemetry = createExecutionTelemetryJournal(config.agentsRoot);
|
|
46
49
|
const executeProtocol = opts.execution?.runExecution ?? runExecution;
|
|
47
|
-
let
|
|
50
|
+
let detectedExecutionRuntimes = [];
|
|
48
51
|
let runtimeFacts = null;
|
|
49
52
|
const executionReady = executionJournal.reconcileAfterRestart().then(() => true, (error) => {
|
|
50
53
|
dslog("execution.recovery_failed", "execution journal 恢复失败", {
|
|
@@ -66,6 +69,26 @@ export function serve(config, opts = {}) {
|
|
|
66
69
|
}
|
|
67
70
|
catch { /* reconnect/sync replays durable lifecycle */ }
|
|
68
71
|
};
|
|
72
|
+
const reportExecutionFrame = async (frame) => {
|
|
73
|
+
if (frame.type === "execution:activity" || frame.type === "execution:console") {
|
|
74
|
+
try {
|
|
75
|
+
await executionTelemetry.append(frame);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
dslog("execution.telemetry_journal_failed", "execution telemetry 持久化失败", {
|
|
79
|
+
level: "ERROR", execution_id: frame.executionId,
|
|
80
|
+
error_message: error.message,
|
|
81
|
+
});
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
safeExecutionSend(frame);
|
|
86
|
+
};
|
|
87
|
+
const replayExecutionTelemetry = async () => {
|
|
88
|
+
const frames = await executionTelemetry.replay();
|
|
89
|
+
for (const frame of frames)
|
|
90
|
+
safeExecutionSend(frame);
|
|
91
|
+
};
|
|
69
92
|
const snapshotEntry = (entry) => {
|
|
70
93
|
if ((entry.state === "completed" || entry.state === "interrupted") && entry.completion !== null) {
|
|
71
94
|
return { executionId: entry.executionId, state: entry.state, completion: entry.completion, updatedAt: entry.updatedAt };
|
|
@@ -238,7 +261,7 @@ export function serve(config, opts = {}) {
|
|
|
238
261
|
void drainSpool();
|
|
239
262
|
// 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
|
|
240
263
|
const helloPromise = collectMachineHello(config.agentsRoot, config.executionLimits);
|
|
241
|
-
runtimeFacts = helloPromise.then((hello) => hello.
|
|
264
|
+
runtimeFacts = helloPromise.then((hello) => hello.executionRuntimes, (error) => {
|
|
242
265
|
dslog("execution.runtime_detection_failed", "runtime 探测失败", {
|
|
243
266
|
level: "ERROR", error_message: error.message,
|
|
244
267
|
});
|
|
@@ -246,16 +269,21 @@ export function serve(config, opts = {}) {
|
|
|
246
269
|
});
|
|
247
270
|
void helloPromise
|
|
248
271
|
.then((hello) => {
|
|
249
|
-
|
|
272
|
+
detectedExecutionRuntimes = hello.executionRuntimes;
|
|
250
273
|
try {
|
|
251
274
|
ws?.send(JSON.stringify(hello));
|
|
252
|
-
log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} ·
|
|
275
|
+
log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`);
|
|
253
276
|
}
|
|
254
277
|
catch { /* 非 OPEN,忽略 */ }
|
|
255
278
|
})
|
|
256
279
|
.catch(() => { });
|
|
257
280
|
opts.onOpen?.(ws);
|
|
258
281
|
void executionReady.then((ready) => ready ? sendSnapshot(`reconnect-${randomUUID()}`) : undefined);
|
|
282
|
+
void replayExecutionTelemetry().catch((error) => {
|
|
283
|
+
dslog("execution.telemetry_replay_failed", "execution telemetry 重放失败", {
|
|
284
|
+
level: "ERROR", error_message: error.message,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
259
287
|
});
|
|
260
288
|
ws.on("message", async (data) => {
|
|
261
289
|
let decoded;
|
|
@@ -292,6 +320,15 @@ export function serve(config, opts = {}) {
|
|
|
292
320
|
await executionJournal.acknowledgeCompletion(frame.executionId).catch(() => { });
|
|
293
321
|
return;
|
|
294
322
|
}
|
|
323
|
+
if (frame.type === "execution:event-ack") {
|
|
324
|
+
await executionTelemetry.acknowledge(frame.executionId, frame.kind, frame.seq).catch((error) => {
|
|
325
|
+
dslog("execution.telemetry_ack_failed", "execution telemetry ACK 处理失败", {
|
|
326
|
+
level: "ERROR", execution_id: frame.executionId,
|
|
327
|
+
error_message: error.message,
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
295
332
|
if (frame.type === "execution:cancel") {
|
|
296
333
|
if (!knownExecutionHashes.has(frame.executionId)
|
|
297
334
|
&& !executionRuns.has(frame.executionId)
|
|
@@ -352,7 +389,7 @@ export function serve(config, opts = {}) {
|
|
|
352
389
|
let availableRuntimes;
|
|
353
390
|
try {
|
|
354
391
|
availableRuntimes = opts.execution?.availableRuntimes?.()
|
|
355
|
-
?? await (runtimeFacts ?? Promise.resolve(
|
|
392
|
+
?? await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes));
|
|
356
393
|
}
|
|
357
394
|
catch (error) {
|
|
358
395
|
reservation.release();
|
|
@@ -373,7 +410,7 @@ export function serve(config, opts = {}) {
|
|
|
373
410
|
availableRuntimes,
|
|
374
411
|
...reservation.facts,
|
|
375
412
|
},
|
|
376
|
-
report:
|
|
413
|
+
report: reportExecutionFrame,
|
|
377
414
|
...(reservation.state === undefined ? {} : {
|
|
378
415
|
slot: { state: reservation.state, ready: reservation.ready },
|
|
379
416
|
}),
|
|
@@ -478,6 +515,9 @@ export function serve(config, opts = {}) {
|
|
|
478
515
|
...(msg.scheduledRun.outputPolicy !== undefined
|
|
479
516
|
? { outputPolicy: msg.scheduledRun.outputPolicy }
|
|
480
517
|
: {}),
|
|
518
|
+
...(msg.scheduledRun.externalNotificationPolicy !== undefined
|
|
519
|
+
? { externalNotificationPolicy: msg.scheduledRun.externalNotificationPolicy }
|
|
520
|
+
: {}),
|
|
481
521
|
})
|
|
482
522
|
: null;
|
|
483
523
|
const threadId = msg.wake?.threadId;
|
|
@@ -582,7 +622,7 @@ export function serve(config, opts = {}) {
|
|
|
582
622
|
? `crew thread read`
|
|
583
623
|
: `crew message read --channel ${msg.channelId}`;
|
|
584
624
|
const wakeText = scheduled
|
|
585
|
-
? buildScheduledPrompt(msg.channelId, msg.wake?.content ?? "", scheduled.outputPolicy)
|
|
625
|
+
? buildScheduledPrompt(msg.channelId, msg.wake?.content ?? "", scheduled.outputPolicy, scheduled.externalNotificationPolicy)
|
|
586
626
|
: (msg.wake?.content
|
|
587
627
|
? `你被唤醒(${msg.reason}): ${msg.wake.content}\n用 ${readCmd} 读${threadId ? "本线程" : "频道"}后按需处理。${reasonHint}${ackHint}${threadHint}${attHint}`
|
|
588
628
|
: undefined);
|
|
@@ -602,7 +642,11 @@ export function serve(config, opts = {}) {
|
|
|
602
642
|
taskKey, // 每任务隔离 cwd + work-log(并行不冲突)
|
|
603
643
|
runId, // 贯穿 SLS 日志的单轮关联键
|
|
604
644
|
...(scheduled ? {
|
|
605
|
-
scheduled: {
|
|
645
|
+
scheduled: {
|
|
646
|
+
title: scheduled.title,
|
|
647
|
+
outputPolicy: scheduled.outputPolicy,
|
|
648
|
+
externalNotificationPolicy: scheduled.externalNotificationPolicy,
|
|
649
|
+
},
|
|
606
650
|
} : {}),
|
|
607
651
|
...(wakeOrigin ? { wakeOrigin, originDecisionAttempt: attempt } : {}),
|
|
608
652
|
// 唤醒锚点是具体消息(非纯频道唤醒)时,把它透传下去,供 `crew task create` 锚定到该消息。
|
|
@@ -648,6 +692,7 @@ export function serve(config, opts = {}) {
|
|
|
648
692
|
...(wakeOrigin ? {
|
|
649
693
|
wakeOrigin,
|
|
650
694
|
originDecision: result.originDecision ?? "missing",
|
|
695
|
+
...(msg.wake?.seq !== undefined ? { contextUpToSeq: msg.wake.seq } : {}),
|
|
651
696
|
} : {}),
|
|
652
697
|
...(scheduled ? { scheduledRunId: scheduled.runId } : {}),
|
|
653
698
|
...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@agentclientprotocol/sdk": "1.2.1",
|
|
20
21
|
"cross-spawn": "^7.0.6",
|
|
21
22
|
"ws": "^8",
|
|
22
23
|
"zod": "^3.23.0",
|
|
23
|
-
"@nowcrew/cli": "^0.4.
|
|
24
|
+
"@nowcrew/cli": "^0.4.8"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@types/cross-spawn": "^6.0.6",
|