@nowcrew/daemon 0.5.18 → 0.5.20
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 +23 -0
- package/dist/attachments.js +196 -0
- package/dist/computer-cli.js +72 -12
- package/dist/computer-profile-lock.js +395 -0
- package/dist/computer-profile.js +189 -20
- package/dist/config.js +2 -1
- package/dist/console.js +175 -9
- package/dist/execution-event-limit.js +1 -1
- package/dist/execution-journal-lock.js +199 -40
- package/dist/execution-journal.js +42 -4
- package/dist/execution-protocol.js +21 -1
- package/dist/execution-recovery.js +71 -0
- package/dist/execution-runner.js +68 -77
- package/dist/execution-supervisor.js +79 -31
- package/dist/external-output.js +114 -0
- package/dist/i18n.js +5 -5
- package/dist/list-models.js +41 -5
- package/dist/local-executor.js +103 -14
- package/dist/machine-info.js +6 -1
- package/dist/main.js +23 -8
- package/dist/origin-decision.js +3 -1
- package/dist/prompt.js +4 -1
- package/dist/runner.js +14 -9
- package/dist/runtime-cancellation.js +74 -0
- package/dist/runtime-capabilities.js +38 -0
- package/dist/runtime-path.js +60 -0
- package/dist/runtimes/claude.js +9 -4
- package/dist/runtimes/codex-app-server-runner.js +340 -0
- package/dist/runtimes/codex.js +10 -4
- package/dist/runtimes/kimi-acp-runner.js +117 -17
- package/dist/runtimes/kimi.js +2 -0
- package/dist/runtimes/progress-watchdog.js +26 -0
- package/dist/serve-lifecycle.js +82 -0
- package/dist/serve.js +212 -212
- package/dist/session.js +1 -1
- package/dist/shared-execution-slots.js +68 -0
- package/dist/shutdown-deadline.js +32 -0
- package/dist/slog.js +34 -20
- package/dist/supervised-runtime.js +104 -0
- package/dist/websocket-shutdown.js +53 -0
- package/package.json +3 -3
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const EXTERNAL_ANSWER_OPEN = "<nowwork_external_answer>";
|
|
2
|
+
export const EXTERNAL_ANSWER_CLOSE = "</nowwork_external_answer>";
|
|
3
|
+
function retainedMarkerPrefix(value, marker) {
|
|
4
|
+
const maximum = Math.min(value.length, marker.length - 1);
|
|
5
|
+
for (let length = maximum; length > 0; length -= 1) {
|
|
6
|
+
if (marker.startsWith(value.slice(-length)))
|
|
7
|
+
return length;
|
|
8
|
+
}
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
export class ExternalAnswerDecoder {
|
|
12
|
+
state = "outside";
|
|
13
|
+
pending = "";
|
|
14
|
+
push(text) {
|
|
15
|
+
if (!text)
|
|
16
|
+
return [];
|
|
17
|
+
const output = [];
|
|
18
|
+
this.consume(this.pending + text, output);
|
|
19
|
+
return output;
|
|
20
|
+
}
|
|
21
|
+
finish() {
|
|
22
|
+
this.pending = "";
|
|
23
|
+
this.state = "outside";
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
consume(value, output) {
|
|
27
|
+
this.pending = "";
|
|
28
|
+
const marker = this.state === "outside" ? EXTERNAL_ANSWER_OPEN : EXTERNAL_ANSWER_CLOSE;
|
|
29
|
+
const markerAt = value.indexOf(marker);
|
|
30
|
+
if (markerAt >= 0) {
|
|
31
|
+
if (this.state === "inside" && markerAt > 0)
|
|
32
|
+
output.push(value.slice(0, markerAt));
|
|
33
|
+
this.state = this.state === "outside" ? "inside" : "outside";
|
|
34
|
+
const remaining = value.slice(markerAt + marker.length);
|
|
35
|
+
if (remaining)
|
|
36
|
+
this.consume(remaining, output);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const retained = retainedMarkerPrefix(value, marker);
|
|
40
|
+
const safe = retained > 0 ? value.slice(0, -retained) : value;
|
|
41
|
+
this.pending = retained > 0 ? value.slice(-retained) : "";
|
|
42
|
+
if (this.state === "inside" && safe)
|
|
43
|
+
output.push(safe);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function decodeExternalOutputEvent(runtime, event, decoder) {
|
|
47
|
+
if (runtime === "claude") {
|
|
48
|
+
const candidate = (event ?? {});
|
|
49
|
+
const delta = candidate.event?.delta;
|
|
50
|
+
if (candidate.type !== "stream_event"
|
|
51
|
+
|| candidate.event?.type !== "content_block_delta"
|
|
52
|
+
|| delta?.type !== "text_delta"
|
|
53
|
+
|| typeof delta.text !== "string")
|
|
54
|
+
return [];
|
|
55
|
+
return decoder.push(delta.text);
|
|
56
|
+
}
|
|
57
|
+
const candidate = (event ?? {});
|
|
58
|
+
if (runtime === "codex") {
|
|
59
|
+
return candidate.type === "item.completed"
|
|
60
|
+
&& candidate.item?.type === "agent_message"
|
|
61
|
+
&& typeof candidate.item.text === "string"
|
|
62
|
+
? decoder.push(candidate.item.text)
|
|
63
|
+
: [];
|
|
64
|
+
}
|
|
65
|
+
return candidate.type === undefined
|
|
66
|
+
&& candidate.role === "assistant"
|
|
67
|
+
&& typeof candidate.content === "string"
|
|
68
|
+
? decoder.push(candidate.content)
|
|
69
|
+
: [];
|
|
70
|
+
}
|
|
71
|
+
export function stripExternalAnswerMarkers(value) {
|
|
72
|
+
const sections = [];
|
|
73
|
+
let cursor = 0;
|
|
74
|
+
while (cursor < value.length) {
|
|
75
|
+
const openAt = value.indexOf(EXTERNAL_ANSWER_OPEN, cursor);
|
|
76
|
+
if (openAt < 0)
|
|
77
|
+
break;
|
|
78
|
+
const contentAt = openAt + EXTERNAL_ANSWER_OPEN.length;
|
|
79
|
+
const closeAt = value.indexOf(EXTERNAL_ANSWER_CLOSE, contentAt);
|
|
80
|
+
if (closeAt < 0)
|
|
81
|
+
return value.slice(contentAt).trim();
|
|
82
|
+
sections.push(value.slice(contentAt, closeAt));
|
|
83
|
+
cursor = closeAt + EXTERNAL_ANSWER_CLOSE.length;
|
|
84
|
+
}
|
|
85
|
+
return (sections.length > 0 ? sections.join("") : value).trim();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 提取 marker 内容:有 marker 返回拼接内容(trim,空→null),无 marker 返回 null。
|
|
89
|
+
* 与 stripExternalAnswerMarkers 的区别:strip 在无 marker 时回退整段原文(finalText 展示用),
|
|
90
|
+
* 本函数用于判定"agent 是否给出了频道直接回复"——必须能区分有无 marker。
|
|
91
|
+
*/
|
|
92
|
+
export function extractExternalAnswer(value) {
|
|
93
|
+
const parts = [];
|
|
94
|
+
let found = false;
|
|
95
|
+
let rest = value;
|
|
96
|
+
for (;;) {
|
|
97
|
+
const open = rest.indexOf(EXTERNAL_ANSWER_OPEN);
|
|
98
|
+
if (open < 0)
|
|
99
|
+
break;
|
|
100
|
+
found = true;
|
|
101
|
+
const afterOpen = rest.slice(open + EXTERNAL_ANSWER_OPEN.length);
|
|
102
|
+
const close = afterOpen.indexOf(EXTERNAL_ANSWER_CLOSE);
|
|
103
|
+
if (close < 0) {
|
|
104
|
+
parts.push(afterOpen);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
parts.push(afterOpen.slice(0, close));
|
|
108
|
+
rest = afterOpen.slice(close + EXTERNAL_ANSWER_CLOSE.length);
|
|
109
|
+
}
|
|
110
|
+
if (!found)
|
|
111
|
+
return null;
|
|
112
|
+
const answer = parts.join("").trim();
|
|
113
|
+
return answer.length > 0 ? answer : null;
|
|
114
|
+
}
|
package/dist/i18n.js
CHANGED
|
@@ -17,6 +17,8 @@ export function detectDaemonLang(env = process.env) {
|
|
|
17
17
|
}
|
|
18
18
|
const zh = {
|
|
19
19
|
"Claude session started": "Claude 会话启动",
|
|
20
|
+
"Codex session started": "Codex 会话启动",
|
|
21
|
+
"Files changed": "文件变更",
|
|
20
22
|
"Run failed": "运行出错",
|
|
21
23
|
"Run finished": "本轮结束",
|
|
22
24
|
"Missing CREW_MACHINE_TOKEN (sk_machine_*, printed by seed)": "缺少 CREW_MACHINE_TOKEN(sk_machine_*,由 seed 打印)",
|
|
@@ -40,6 +42,7 @@ const zh = {
|
|
|
40
42
|
"Saved profile '{{name}}' with private credentials.": "已保存配置 '{{name}}',凭证仅私有可读。",
|
|
41
43
|
"Service '{{id}}' is not installed": "服务 '{{id}}' 尚未安装",
|
|
42
44
|
"Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.": "daemon 已升级,并已请求重启 '{{name}}';请用 status 确认。",
|
|
45
|
+
"Upgraded daemon but skipped restart for '{{name}}': {{reason}}": "daemon 已升级,但已跳过 '{{name}}' 的重启:{{reason}}",
|
|
43
46
|
"Upgraded daemon. Installed services were not restarted; pass --profile to restart one.": "daemon 已升级;已安装服务尚未重启,可传入 --profile 重启指定服务。",
|
|
44
47
|
"Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry": "服务生命周期必须使用已构建的 daemon 入口(.js),不能使用 TypeScript 开发入口",
|
|
45
48
|
"Installed '{{id}}'. Use status to confirm runtime state.": "已安装 '{{id}}';请用 status 确认运行状态。",
|
|
@@ -47,6 +50,7 @@ const zh = {
|
|
|
47
50
|
"{{action}} request accepted for '{{id}}'. Verify with status.": "已接受对 '{{id}}' 的 {{action}} 请求;请用 status 确认。",
|
|
48
51
|
"--token-stdin requires a token on standard input": "--token-stdin 需要从标准输入读取令牌",
|
|
49
52
|
"Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@latest": "服务生命周期需要全局安装 @nowcrew/daemon;请运行 npm install --global @nowcrew/daemon@latest",
|
|
53
|
+
"Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}": "配置 '{{profile}}' 与配置 '{{conflict}}' 解析到了同一个 agents root '{{agentsRoot}}'。请保存为唯一目录,例如:{{command}}",
|
|
50
54
|
};
|
|
51
55
|
export function translateDaemon(lang, message) {
|
|
52
56
|
if (lang === "zh")
|
|
@@ -54,9 +58,5 @@ export function translateDaemon(lang, message) {
|
|
|
54
58
|
return message;
|
|
55
59
|
}
|
|
56
60
|
export function formatDaemonText(lang, message, values = {}) {
|
|
57
|
-
|
|
58
|
-
for (const [key, value] of Object.entries(values)) {
|
|
59
|
-
rendered = rendered.replaceAll(`{{${key}}}`, String(value));
|
|
60
|
-
}
|
|
61
|
-
return rendered;
|
|
61
|
+
return translateDaemon(lang, message).replace(/\{\{([^{}]+)\}\}/g, (token, key) => Object.hasOwn(values, key) ? String(values[key]) : token);
|
|
62
62
|
}
|
package/dist/list-models.js
CHANGED
|
@@ -2,8 +2,15 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { isWin } from "./platform.js";
|
|
4
4
|
const execFileRaw = promisify(execFile);
|
|
5
|
+
const MODEL_PROBE_TIMEOUT_MS = 8_000;
|
|
6
|
+
const MODEL_PROBE_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
5
7
|
// win32 上 npm CLI 是 .cmd shim,execFile 需 shell 才能执行;参数全是固定字面量,无注入面。
|
|
6
|
-
const execFileP = (bin, args) => execFileRaw(bin, args, {
|
|
8
|
+
const execFileP = (bin, args) => execFileRaw(bin, args, {
|
|
9
|
+
shell: isWin(),
|
|
10
|
+
timeout: MODEL_PROBE_TIMEOUT_MS,
|
|
11
|
+
killSignal: "SIGKILL",
|
|
12
|
+
maxBuffer: MODEL_PROBE_MAX_BUFFER_BYTES,
|
|
13
|
+
});
|
|
7
14
|
export async function listRuntimeModels(runtime) {
|
|
8
15
|
switch (runtime) {
|
|
9
16
|
case "codex":
|
|
@@ -21,15 +28,44 @@ export async function listRuntimeModels(runtime) {
|
|
|
21
28
|
function parseCodexModels(stdout) {
|
|
22
29
|
try {
|
|
23
30
|
const parsed = JSON.parse(stdout);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.models)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
return normalizeCodexModels(parsed.models);
|
|
28
35
|
}
|
|
29
36
|
catch {
|
|
30
37
|
return [];
|
|
31
38
|
}
|
|
32
39
|
}
|
|
40
|
+
function normalizeCodexModels(rows) {
|
|
41
|
+
const normalized = rows.flatMap((value) => {
|
|
42
|
+
if (typeof value !== "object" || value === null)
|
|
43
|
+
return [];
|
|
44
|
+
const row = value;
|
|
45
|
+
if (row.visibility != null && row.visibility !== "list")
|
|
46
|
+
return [];
|
|
47
|
+
const id = nonEmptyString(row.slug) ?? nonEmptyString(row.id);
|
|
48
|
+
if (id === null)
|
|
49
|
+
return [];
|
|
50
|
+
return [{
|
|
51
|
+
id,
|
|
52
|
+
label: nonEmptyString(row.display_name) ?? nonEmptyString(row.name) ?? id,
|
|
53
|
+
explicitDefault: row.default === true,
|
|
54
|
+
}];
|
|
55
|
+
});
|
|
56
|
+
const hasExplicitDefault = normalized.some((model) => model.explicitDefault);
|
|
57
|
+
return normalized.map((model, index) => ({
|
|
58
|
+
id: model.id,
|
|
59
|
+
label: model.label,
|
|
60
|
+
...((model.explicitDefault || (!hasExplicitDefault && index === 0)) ? { default: true } : {}),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
function nonEmptyString(value) {
|
|
64
|
+
if (typeof value !== "string")
|
|
65
|
+
return null;
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
68
|
+
}
|
|
33
69
|
function parseCursorModels(stdout) {
|
|
34
70
|
return stdout
|
|
35
71
|
.split(/\r?\n/)
|
package/dist/local-executor.js
CHANGED
|
@@ -6,10 +6,15 @@ import { spawnClaude } from "./runtimes/claude.js";
|
|
|
6
6
|
import { spawnCodex } from "./runtimes/codex.js";
|
|
7
7
|
import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
8
8
|
import { applyProviderEnv, providerFingerprint } from "./provider-env.js";
|
|
9
|
+
import { augmentedPath } from "./runtime-path.js";
|
|
9
10
|
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
10
11
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
11
12
|
import { toConsoleLines } from "./console.js";
|
|
12
13
|
import { capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
14
|
+
import { decodeExternalOutputEvent, extractExternalAnswer, ExternalAnswerDecoder, stripExternalAnswerMarkers, } from "./external-output.js";
|
|
15
|
+
import { cleanupMaterializedAttachments as cleanupAttachments, executionAttachmentDirectory, materializeAttachments, } from "./attachments.js";
|
|
16
|
+
import { routeRuntimeAttachments, runtimeCapability, } from "./runtime-capabilities.js";
|
|
17
|
+
import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
|
|
13
18
|
function truncateUtf8(value, maxBytes) {
|
|
14
19
|
if (maxBytes <= 0)
|
|
15
20
|
return "";
|
|
@@ -118,6 +123,7 @@ async function launchLegacyRuntime(request) {
|
|
|
118
123
|
return wrapChild(spawnCodex({
|
|
119
124
|
...common,
|
|
120
125
|
wakePrompt: `${request.systemPrompt}\n\n${request.wakePrompt}`,
|
|
126
|
+
...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
|
|
121
127
|
}));
|
|
122
128
|
}
|
|
123
129
|
return wrapChild(spawnKimi({
|
|
@@ -133,24 +139,29 @@ function resolvePrompt(prompt, context) {
|
|
|
133
139
|
return typeof prompt === "string" ? prompt : prompt(context);
|
|
134
140
|
}
|
|
135
141
|
const nativeSessionLeaseTails = new Map();
|
|
136
|
-
async function withKeyedLease(key, operation) {
|
|
142
|
+
async function withKeyedLease(key, operation, cancellation) {
|
|
137
143
|
const predecessor = nativeSessionLeaseTails.get(key) ?? Promise.resolve();
|
|
138
144
|
let release;
|
|
139
145
|
const current = new Promise((resolve) => { release = resolve; });
|
|
140
146
|
const tail = predecessor.then(() => current);
|
|
141
147
|
nativeSessionLeaseTails.set(key, tail);
|
|
142
|
-
await predecessor;
|
|
143
148
|
try {
|
|
149
|
+
await awaitWithCancellation(predecessor, cancellation);
|
|
144
150
|
return await operation();
|
|
145
151
|
}
|
|
146
152
|
finally {
|
|
147
153
|
release();
|
|
148
|
-
|
|
149
|
-
nativeSessionLeaseTails.
|
|
154
|
+
void tail.then(() => {
|
|
155
|
+
if (nativeSessionLeaseTails.get(key) === tail)
|
|
156
|
+
nativeSessionLeaseTails.delete(key);
|
|
157
|
+
});
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
160
|
export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
153
|
-
|
|
161
|
+
const stableProtocolRuntime = dependencies.launchRuntime !== undefined;
|
|
162
|
+
const supportsNativeResume = input.runtime.name === "claude"
|
|
163
|
+
|| (stableProtocolRuntime && runtimeCapability(input.runtime.name).nativeResume);
|
|
164
|
+
if (!supportsNativeResume || !input.session.enabled) {
|
|
154
165
|
return executeLocalUnlocked(input, callbacks, dependencies);
|
|
155
166
|
}
|
|
156
167
|
const leaseKey = JSON.stringify([
|
|
@@ -159,14 +170,14 @@ export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
|
159
170
|
input.keyMode ?? "legacy",
|
|
160
171
|
input.resumeKey ?? input.taskKey ?? "",
|
|
161
172
|
]);
|
|
162
|
-
return withKeyedLease(leaseKey, () => executeLocalUnlocked(input, callbacks, dependencies));
|
|
173
|
+
return withKeyedLease(leaseKey, () => executeLocalUnlocked(input, callbacks, dependencies), dependencies.cancellation);
|
|
163
174
|
}
|
|
164
175
|
async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
165
176
|
const providerConfig = input.launch.providerConfig ?? {};
|
|
166
177
|
const { runtime } = input;
|
|
167
178
|
const currentModel = runtime.model ?? null;
|
|
168
179
|
const providerFp = providerFingerprint(runtime.name, providerConfig);
|
|
169
|
-
const workspace = await prepareWorkspace({
|
|
180
|
+
const workspace = await awaitWithCancellation(prepareWorkspace({
|
|
170
181
|
agentsRoot: input.launch.agentsRoot,
|
|
171
182
|
handle: input.handle,
|
|
172
183
|
cliPath: input.launch.cliPath,
|
|
@@ -175,9 +186,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
175
186
|
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
176
187
|
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
177
188
|
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
178
|
-
});
|
|
189
|
+
}), dependencies.cancellation);
|
|
190
|
+
let materialized = null;
|
|
191
|
+
let knownAttachmentDirectory = null;
|
|
179
192
|
try {
|
|
180
|
-
const supportsNativeResume = runtime.name === "claude"
|
|
193
|
+
const supportsNativeResume = runtime.name === "claude"
|
|
194
|
+
|| (dependencies.launchRuntime !== undefined && runtimeCapability(runtime.name).nativeResume);
|
|
181
195
|
const prior = input.session.enabled && supportsNativeResume
|
|
182
196
|
? await readSession(workspace.sessionDir)
|
|
183
197
|
: null;
|
|
@@ -197,13 +211,44 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
197
211
|
nearBudget,
|
|
198
212
|
};
|
|
199
213
|
const systemPrompt = resolvePrompt(input.systemPrompt, promptContext);
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
if (input.attachments && input.attachments.length > 0) {
|
|
215
|
+
if (dependencies.cancellation?.isRequested())
|
|
216
|
+
throw new RuntimeCancelledError();
|
|
217
|
+
knownAttachmentDirectory = executionAttachmentDirectory(workspace.runDir, input.executionId);
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const materialization = Promise.resolve().then(() => (dependencies.materializeAttachments ?? materializeAttachments)({
|
|
220
|
+
serverUrl: input.launch.serverUrl,
|
|
221
|
+
token: input.launch.token,
|
|
222
|
+
runDir: workspace.runDir,
|
|
223
|
+
executionId: input.executionId,
|
|
224
|
+
attachments: input.attachments,
|
|
225
|
+
signal: controller.signal,
|
|
226
|
+
}));
|
|
227
|
+
dependencies.cancellation?.register(async () => {
|
|
228
|
+
controller.abort(new RuntimeCancelledError());
|
|
229
|
+
await materialization.then(() => undefined, () => undefined);
|
|
230
|
+
});
|
|
231
|
+
try {
|
|
232
|
+
materialized = await materialization;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (dependencies.cancellation?.isRequested())
|
|
236
|
+
throw new RuntimeCancelledError();
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
if (dependencies.cancellation?.isRequested()) {
|
|
240
|
+
await dependencies.cancellation.waitForStop();
|
|
241
|
+
throw new RuntimeCancelledError();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
245
|
+
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
246
|
+
await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
|
|
202
247
|
const baseEnv = {
|
|
203
248
|
...process.env,
|
|
204
249
|
...sanitizeEnvVars(providerConfig.envVars),
|
|
205
250
|
...input.launch.systemEnv,
|
|
206
|
-
PATH: `${workspace.crewDir}${delimiter}${
|
|
251
|
+
PATH: `${workspace.crewDir}${delimiter}${augmentedPath()}`,
|
|
207
252
|
CREW_SERVER_URL: input.launch.serverUrl,
|
|
208
253
|
CREW_TOKEN: input.launch.token,
|
|
209
254
|
CREW_CHANNEL: input.channelId,
|
|
@@ -222,6 +267,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
222
267
|
};
|
|
223
268
|
const childEnv = applyProviderEnv(baseEnv, runtime.name, providerConfig, workspace.homeDir);
|
|
224
269
|
const launchRuntime = dependencies.launchRuntime ?? launchLegacyRuntime;
|
|
270
|
+
if (dependencies.cancellation?.isRequested())
|
|
271
|
+
throw new RuntimeCancelledError();
|
|
225
272
|
const child = await launchRuntime({
|
|
226
273
|
runtime: runtime.name,
|
|
227
274
|
bin: runtime.name,
|
|
@@ -235,13 +282,24 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
235
282
|
...(runtime.reasoning === undefined ? {} : { reasoning: runtime.reasoning }),
|
|
236
283
|
...(launchSessionId === null ? {} : { sessionId: launchSessionId }),
|
|
237
284
|
resume: resuming,
|
|
285
|
+
...(attachmentPlan.nativeImagePaths.length > 0
|
|
286
|
+
? { imagePaths: attachmentPlan.nativeImagePaths }
|
|
287
|
+
: {}),
|
|
238
288
|
});
|
|
289
|
+
if (child.cancel !== undefined) {
|
|
290
|
+
dependencies.cancellation?.register(child.cancel);
|
|
291
|
+
}
|
|
292
|
+
if (dependencies.cancellation?.isRequested()) {
|
|
293
|
+
await dependencies.cancellation.waitForStop();
|
|
294
|
+
throw new RuntimeCancelledError();
|
|
295
|
+
}
|
|
239
296
|
const activities = [];
|
|
240
297
|
let sessionId = launchSessionId;
|
|
241
298
|
let usage;
|
|
242
299
|
let observedModel = currentModel;
|
|
243
300
|
let finalText = null;
|
|
244
301
|
let sentViaCrew = false;
|
|
302
|
+
const externalOutput = new ExternalAnswerDecoder();
|
|
245
303
|
const readline = createInterface({ input: child.stdout });
|
|
246
304
|
readline.on("line", (line) => {
|
|
247
305
|
const event = parseLine(line);
|
|
@@ -266,6 +324,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
266
324
|
&& "type" in event && event.type === "kimi.acp.text_delta";
|
|
267
325
|
finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
|
|
268
326
|
}
|
|
327
|
+
for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
|
|
328
|
+
callbacks.onExternalOutput?.(text);
|
|
329
|
+
}
|
|
269
330
|
for (const chunk of toConsoleLines(event))
|
|
270
331
|
callbacks.onConsole?.(chunk);
|
|
271
332
|
});
|
|
@@ -274,7 +335,17 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
274
335
|
process.stderr.write(data);
|
|
275
336
|
stderrTail = (stderrTail + String(data)).slice(-STDERR_TAIL_CAP);
|
|
276
337
|
});
|
|
277
|
-
|
|
338
|
+
let runtimeExit;
|
|
339
|
+
try {
|
|
340
|
+
runtimeExit = await awaitWithCancellation(child.exit, dependencies.cancellation);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (error instanceof RuntimeCancelledError) {
|
|
344
|
+
await dependencies.cancellation?.waitForStop();
|
|
345
|
+
}
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
const { exitCode, spawnError, terminationSignal } = runtimeExit;
|
|
278
349
|
const errorTail = [
|
|
279
350
|
stderrTail.trim(),
|
|
280
351
|
spawnError,
|
|
@@ -313,11 +384,29 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
313
384
|
resumed: resuming,
|
|
314
385
|
sessionId,
|
|
315
386
|
errorMessage: errorTail || null,
|
|
316
|
-
finalText: input.captureFinal
|
|
387
|
+
finalText: input.captureFinal && finalText !== null
|
|
388
|
+
? stripExternalAnswerMarkers(finalText)
|
|
389
|
+
: null,
|
|
390
|
+
externalAnswer: input.captureFinal && finalText !== null
|
|
391
|
+
? extractExternalAnswer(finalText)
|
|
392
|
+
: null,
|
|
317
393
|
sentViaCrew,
|
|
318
394
|
};
|
|
319
395
|
}
|
|
320
396
|
finally {
|
|
397
|
+
const attachmentDirectories = new Set([
|
|
398
|
+
...(knownAttachmentDirectory === null ? [] : [knownAttachmentDirectory]),
|
|
399
|
+
...(materialized === null ? [] : [materialized.directory]),
|
|
400
|
+
]);
|
|
401
|
+
for (const directory of attachmentDirectories) {
|
|
402
|
+
try {
|
|
403
|
+
await (dependencies.cleanupMaterializedAttachments ?? cleanupAttachments)(directory);
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
407
|
+
process.stderr.write(`[execution] failed to remove attachments ${directory}: ${detail}\n`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
321
410
|
try {
|
|
322
411
|
await rm(workspace.systemPromptPath, { force: true });
|
|
323
412
|
}
|
package/dist/machine-info.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { hostname, arch, platform as osPlatform } from "node:os";
|
|
6
6
|
import { lookupCmd } from "./platform.js";
|
|
7
|
+
import { augmentedPath } from "./runtime-path.js";
|
|
7
8
|
import { execFile } from "node:child_process";
|
|
8
9
|
import { promisify } from "node:util";
|
|
9
10
|
import { readFileSync } from "node:fs";
|
|
@@ -21,6 +22,9 @@ export const DAEMON_CAPABILITIES = [
|
|
|
21
22
|
"reply_origin_v1",
|
|
22
23
|
"origin_decision_v1",
|
|
23
24
|
"execution_telemetry_ack_v1",
|
|
25
|
+
"execution_external_output_v1",
|
|
26
|
+
"execution_attachments_v1",
|
|
27
|
+
"execution_answer_stream_v1",
|
|
24
28
|
];
|
|
25
29
|
export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
|
|
26
30
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|
|
@@ -36,7 +40,8 @@ const RUNTIME_BINS = [
|
|
|
36
40
|
];
|
|
37
41
|
async function isInstalled(bin) {
|
|
38
42
|
try {
|
|
39
|
-
|
|
43
|
+
// 用增强 PATH 探测:daemon 继承的 PATH 可能缺 ~/.local/bin 等用户级目录(Claude 原生安装器落点)。
|
|
44
|
+
await execFileP(lookupCmd(), [bin], { env: { ...process.env, PATH: augmentedPath() } });
|
|
40
45
|
return true;
|
|
41
46
|
}
|
|
42
47
|
catch {
|
package/dist/main.js
CHANGED
|
@@ -6,15 +6,17 @@
|
|
|
6
6
|
* 后续 (M3c):常驻 + 连 server 控制面 WS,由 agent:start 自动唤醒。
|
|
7
7
|
*/
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
|
+
import { homedir } from "node:os";
|
|
9
10
|
import { loadConfig, ConfigError } from "./config.js";
|
|
10
|
-
import { detectDaemonLang, translateDaemon } from "./i18n.js";
|
|
11
|
+
import { detectDaemonLang, formatDaemonText, translateDaemon } from "./i18n.js";
|
|
11
12
|
import { cliVersion, daemonVersion } from "./machine-info.js";
|
|
12
13
|
import { runAgent } from "./runner.js";
|
|
13
14
|
import { serve } from "./serve.js";
|
|
14
15
|
import { initSlog, flushSlog } from "./slog.js";
|
|
15
16
|
import { formatDaemonLogLine } from "./log-format.js";
|
|
16
|
-
import { loadProfile,
|
|
17
|
+
import { applyProfileToEnv, assertProfileAgentsRootUnique, daemonHome, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, } from "./computer-profile.js";
|
|
17
18
|
import { runComputerCommand } from "./computer-cli.js";
|
|
19
|
+
import { runServeLifecycle } from "./serve-lifecycle.js";
|
|
18
20
|
async function main() {
|
|
19
21
|
const computerResult = await runComputerCommand(process.argv.slice(2));
|
|
20
22
|
if (computerResult !== null) {
|
|
@@ -51,8 +53,13 @@ async function main() {
|
|
|
51
53
|
// 不把 machine token 放进 argv / plist / systemd unit / scheduled task。
|
|
52
54
|
if (values["daemon-home"])
|
|
53
55
|
process.env.CREW_DAEMON_HOME = values["daemon-home"];
|
|
54
|
-
if (values.profile)
|
|
55
|
-
|
|
56
|
+
if (values.profile) {
|
|
57
|
+
const home = daemonHome();
|
|
58
|
+
const profile = await loadProfile(values.profile, home);
|
|
59
|
+
if (cmd === "serve")
|
|
60
|
+
await assertProfileAgentsRootUnique(profile, home, homedir());
|
|
61
|
+
applyProfileToEnv(profile, process.env);
|
|
62
|
+
}
|
|
56
63
|
// 命令行参数优先于环境变量,填回 env 供 loadConfig 读取
|
|
57
64
|
if (values["server-url"])
|
|
58
65
|
process.env.CREW_SERVER_URL = values["server-url"];
|
|
@@ -72,8 +79,8 @@ async function main() {
|
|
|
72
79
|
}
|
|
73
80
|
if (cmd === "serve") {
|
|
74
81
|
process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
|
|
75
|
-
serve(config);
|
|
76
|
-
await
|
|
82
|
+
const service = serve(config);
|
|
83
|
+
await runServeLifecycle(service);
|
|
77
84
|
return;
|
|
78
85
|
}
|
|
79
86
|
if (!values.agent || !values.channel) {
|
|
@@ -93,6 +100,14 @@ async function main() {
|
|
|
93
100
|
process.exit(result.exitCode);
|
|
94
101
|
}
|
|
95
102
|
main().catch((e) => {
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
const message = e instanceof ProfileAgentsRootConflictError
|
|
104
|
+
? formatDaemonText(detectDaemonLang(), PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, {
|
|
105
|
+
profile: e.profile,
|
|
106
|
+
conflict: e.conflict,
|
|
107
|
+
agentsRoot: e.agentsRoot,
|
|
108
|
+
command: e.command,
|
|
109
|
+
})
|
|
110
|
+
: e.message;
|
|
111
|
+
process.stderr.write(`crew-daemon: ${message}\n`);
|
|
112
|
+
process.exitCode = 1;
|
|
98
113
|
});
|
package/dist/origin-decision.js
CHANGED
|
@@ -27,7 +27,9 @@ export async function readOriginDecisionFile(path) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
export function shouldRetryOriginDecision(wakeOrigin, decision, attempt) {
|
|
30
|
-
|
|
30
|
+
if (attempt !== 0 || wakeOrigin === undefined)
|
|
31
|
+
return false;
|
|
32
|
+
return wakeOrigin === "wecom" ? decision?.decision !== "reply" : decision === null;
|
|
31
33
|
}
|
|
32
34
|
export async function runWithOriginDecisionGuard(wakeOrigin, runAttempt) {
|
|
33
35
|
const first = await runAttempt(0);
|
package/dist/prompt.js
CHANGED
|
@@ -147,7 +147,10 @@ ${taskAndScheduleCommands}`;
|
|
|
147
147
|
: ctx.wakeOrigin === "wecom"
|
|
148
148
|
? `
|
|
149
149
|
- **本轮来自企微,结束本轮前必须给出一条完整回复**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。最终只用一次 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数)发送有实质内容的完整结果;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定,并在本轮未形成可交付内容时发送统一兜底回复。`
|
|
150
|
-
:
|
|
150
|
+
: ctx.wakeOrigin
|
|
151
|
+
? `
|
|
152
|
+
- **本轮来自${ctx.wakeOrigin === "feishu" ? "飞书" : ctx.wakeOrigin},结束本轮前必须明确选择外部回复决策**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。完整结果确实要回复外部会话时,用 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数);判断无需回复时,用 \`crew message skip-origin --reason "简短原因"\`。两者必须选择一个;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定。`
|
|
153
|
+
: `
|
|
151
154
|
- **本轮是 NowWork 内部唤醒**:普通 \`crew message send\` 只写入 NowWork。内部唤醒不得使用 \`--reply-origin\`,该参数只回答直接触发本轮的企微原消息。
|
|
152
155
|
- **绑定会话主动通知**:用户明确要求同步,或最终结果有实质结论、变更或需群用户行动的阻塞时,才用一次 \`crew message send --notify-bound-im\`(同时带当前 channel/thread/content 参数)请求通知当前频道绑定的外部会话。Server 会校验绑定 owner 授权、绑定 Agent 和单轮边界;不能指定收件人。确认、进度、中间结果、无变化和重复内容一律留在 NowWork。`;
|
|
153
156
|
const interactiveTaskRules = scheduled ? "" : `
|
package/dist/runner.js
CHANGED
|
@@ -4,10 +4,12 @@ import { join } from "node:path";
|
|
|
4
4
|
import { mintAgentToken } from "./token.js";
|
|
5
5
|
import { buildSystemPrompt, buildWakePrompt, capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
6
6
|
import { deliverScheduledReport, } from "./scheduled-report.js";
|
|
7
|
-
import { executeLocal } from "./local-executor.js";
|
|
7
|
+
import { executeLocal, } from "./local-executor.js";
|
|
8
8
|
import { ReasoningSchema } from "./execution-protocol.js";
|
|
9
9
|
import { readOriginDecisionFile, resetOriginDecisionFile } from "./origin-decision.js";
|
|
10
10
|
import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
|
|
11
|
+
import { launchSupervisedRuntime } from "./supervised-runtime.js";
|
|
12
|
+
import { awaitWithCancellation, } from "./runtime-cancellation.js";
|
|
11
13
|
export { awaitExit, exitActivity, sanitizeEnvVars } from "./local-executor.js";
|
|
12
14
|
const ICON = {
|
|
13
15
|
init: "🟢", text: "💬", reading: "📖", sending: "📨", checking: "🔎",
|
|
@@ -18,24 +20,24 @@ function runtimeName(value) {
|
|
|
18
20
|
return value;
|
|
19
21
|
throw new Error(`unsupported runtime: ${value}`);
|
|
20
22
|
}
|
|
21
|
-
export async function runAgent(config, input, onActivity = defaultPrint, onConsole = () => { }) {
|
|
22
|
-
const credential = await mintAgentToken(config.serverUrl, config.machineToken, input.handle, input.displayName, {
|
|
23
|
+
export async function runAgent(config, input, onActivity = defaultPrint, onConsole = () => { }, dependencies = {}) {
|
|
24
|
+
const credential = await awaitWithCancellation((dependencies.mintAgentToken ?? mintAgentToken)(config.serverUrl, config.machineToken, input.handle, input.displayName, {
|
|
23
25
|
...(input.wakeMessageId ? { wakeThreadRoot: input.wakeMessageId } : {}),
|
|
24
26
|
...(input.wakeContextUpToSeq === undefined ? {} : { wakeContextUpToSeq: input.wakeContextUpToSeq }),
|
|
25
27
|
...(input.runId ? { agentRunId: input.runId } : {}),
|
|
26
|
-
});
|
|
28
|
+
}), dependencies.cancellation);
|
|
27
29
|
const providerConfig = credential.config ?? {};
|
|
28
30
|
const runtime = runtimeName(providerConfig.runtime ?? config.runtimeBin);
|
|
29
31
|
const reasoning = ReasoningSchema.safeParse(providerConfig.reasoning);
|
|
30
32
|
const baseWake = input.wake ?? buildWakePrompt(input.channelId);
|
|
31
33
|
const executionId = input.runId ?? `legacy-${randomUUID()}`;
|
|
32
|
-
const originDecisionFileName = input.wakeOrigin
|
|
34
|
+
const originDecisionFileName = input.wakeOrigin
|
|
33
35
|
? `.origin-decision-${executionId}.json`
|
|
34
36
|
: null;
|
|
35
37
|
const boundImDecisionFileName = input.scheduled?.externalNotificationPolicy === "agent_decides"
|
|
36
38
|
? `.bound-im-decision-${executionId}.json`
|
|
37
39
|
: null;
|
|
38
|
-
const local = await executeLocal({
|
|
40
|
+
const local = await (dependencies.executeLocal ?? executeLocal)({
|
|
39
41
|
executionId,
|
|
40
42
|
handle: input.handle,
|
|
41
43
|
channelId: input.channelId,
|
|
@@ -86,7 +88,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
86
88
|
...(providerConfig.fastMode ? { CREW_FAST_MODE: "1" } : {}),
|
|
87
89
|
...(input.scheduled ? { CREW_SCHEDULE_OUTPUT_POLICY: input.scheduled.outputPolicy } : {}),
|
|
88
90
|
...(originDecisionFileName ? {
|
|
89
|
-
CREW_WAKE_ORIGIN:
|
|
91
|
+
CREW_WAKE_ORIGIN: input.wakeOrigin,
|
|
90
92
|
CREW_ORIGIN_DECISION_FILE: originDecisionFileName,
|
|
91
93
|
} : {}),
|
|
92
94
|
...(boundImDecisionFileName ? {
|
|
@@ -101,7 +103,10 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
101
103
|
softTokens: config.sessionSoftTokens,
|
|
102
104
|
maxTurns: config.sessionMaxTurns,
|
|
103
105
|
},
|
|
104
|
-
}, { onActivity, onConsole }
|
|
106
|
+
}, { onActivity, onConsole }, {
|
|
107
|
+
launchRuntime: dependencies.launchRuntime ?? ((request) => launchSupervisedRuntime(request, dependencies.startSupervisor, dependencies.cancellation, dependencies.platform)),
|
|
108
|
+
...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
|
|
109
|
+
});
|
|
105
110
|
const activities = [...local.activities];
|
|
106
111
|
if (!input.scheduled && (runtime === "codex" || runtime === "kimi")
|
|
107
112
|
&& local.exitCode === 0 && !local.sentViaCrew && local.finalText) {
|
|
@@ -169,7 +174,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
169
174
|
...(local.usage ? { usage: local.usage } : {}),
|
|
170
175
|
...(report ? { report } : {}),
|
|
171
176
|
...(boundImDecision ? { boundImDecision } : {}),
|
|
172
|
-
...(input.wakeOrigin
|
|
177
|
+
...(input.wakeOrigin ? {
|
|
173
178
|
originDecision: originDecision?.decision ?? "missing",
|
|
174
179
|
...(originDecision?.decision === "silent" && originDecision.reason
|
|
175
180
|
? { originDecisionReason: originDecision.reason }
|