@nowcrew/daemon 0.5.11 → 0.5.13
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 +77 -0
- package/dist/config.js +38 -0
- package/dist/execution-event-limit.js +59 -0
- package/dist/execution-journal-lock.js +262 -0
- package/dist/execution-journal.js +678 -0
- package/dist/execution-protocol.js +310 -0
- package/dist/execution-runner.js +637 -0
- package/dist/execution-supervisor-child.js +185 -0
- package/dist/execution-supervisor.js +209 -0
- package/dist/local-executor.js +326 -0
- package/dist/machine-info.js +13 -4
- package/dist/origin-decision.js +42 -0
- package/dist/prompt.js +23 -1
- package/dist/runner.js +146 -340
- package/dist/runtimes/claude.js +9 -1
- package/dist/runtimes/codex.js +21 -5
- package/dist/runtimes/kimi.js +3 -0
- package/dist/scheduled-run-report.js +15 -7
- package/dist/serve.js +412 -57
- package/dist/token.js +5 -2
- package/dist/workspace.js +39 -11
- package/package.json +3 -2
package/dist/runner.js
CHANGED
|
@@ -1,255 +1,108 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { createInterface } from "node:readline";
|
|
5
|
-
import { writeFile } from "node:fs/promises";
|
|
6
|
-
import { delimiter, join } from "node:path";
|
|
1
|
+
/** Legacy agent orchestration around the business-neutral local executor. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
7
4
|
import { mintAgentToken } from "./token.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import { normalizeEvent, parseLine, extractFinalText, extractRunMeta } from "./normalize.js";
|
|
15
|
-
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
16
|
-
import { toConsoleLines } from "./console.js";
|
|
17
|
-
import { dslog } from "./slog.js";
|
|
18
|
-
import { deliverScheduledReport } from "./scheduled-report.js";
|
|
19
|
-
// 注入提示词的 MEMORY.md 上限:只喂索引/角色,避免把膨胀的记忆全塞进上下文。
|
|
20
|
-
const MEMORY_INJECT_CAP = 6000;
|
|
5
|
+
import { buildSystemPrompt, buildWakePrompt, capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
6
|
+
import { deliverScheduledReport, } from "./scheduled-report.js";
|
|
7
|
+
import { executeLocal } from "./local-executor.js";
|
|
8
|
+
import { ReasoningSchema } from "./execution-protocol.js";
|
|
9
|
+
import { readOriginDecisionFile, resetOriginDecisionFile } from "./origin-decision.js";
|
|
10
|
+
export { awaitExit, exitActivity, sanitizeEnvVars } from "./local-executor.js";
|
|
21
11
|
const ICON = {
|
|
22
12
|
init: "🟢", text: "💬", reading: "📖", sending: "📨", checking: "🔎",
|
|
23
13
|
claiming: "📌", crew: "⚙️", tool: "🛠️", tool_result: "↩️", done: "✅", error: "❌",
|
|
24
14
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// 2) 准备 workspace(共享 home + 本任务隔离 cwd + per-task work-log)。
|
|
36
|
-
// 先 prepare 拿到 memory/workLog,再 build 系统提示词(注入记忆索引)写入。
|
|
37
|
-
const ws = await prepareWorkspace({
|
|
38
|
-
agentsRoot: config.agentsRoot,
|
|
39
|
-
handle: input.handle,
|
|
40
|
-
cliPath: config.cliPath,
|
|
41
|
-
...(input.taskKey ? { taskKey: input.taskKey } : {}),
|
|
42
|
-
// 仅在首次创建 MEMORY.md 时,用 agent 的 description 种子化 ## Role
|
|
43
|
-
...(cred.config?.description ? { description: cred.config.description } : {}),
|
|
44
|
-
});
|
|
45
|
-
// session resume:同任务有历史会话且仍在缓存窗口内 → spawn 时 --resume 复用上下文(省 token)。
|
|
46
|
-
// 关:CREW_RESUME=off;超 warm 窗口 → 冷启动(避免 cache miss 重写更贵);读取容错(损坏 → 当首轮)。
|
|
47
|
-
const supportsNativeResume = runtime === "claude";
|
|
48
|
-
const prior = config.resume && supportsNativeResume ? await readSession(ws.runDir) : null;
|
|
49
|
-
// 会话身份唯一来源:workspace 的确定性 uuid(ws.agentSessionId)。HEAD 的 warm-window + 开关
|
|
50
|
-
// 只决定「是否续用」:既有会话(sessionResume)且仍在缓存窗口内 → --resume;否则冷启动。
|
|
51
|
-
const resuming = supportsNativeResume && ws.sessionResume &&
|
|
52
|
-
pickResumeId(prior, Date.now(), config.resumeWarmMs, currentModel, config.sessionBudgetTokens, config.sessionMaxTurns, providerFp) != null;
|
|
53
|
-
// 预算轮换判定(仅为留痕/提示区分原因):不带预算参数也会续 → 本次冷启动是预算导致的主动轮换
|
|
54
|
-
const rotatedForBudget = !resuming && supportsNativeResume && ws.sessionResume &&
|
|
55
|
-
pickResumeId(prior, Date.now(), config.resumeWarmMs, currentModel, 0, 0, providerFp) != null;
|
|
56
|
-
// soft 预算:仍在续用但上下文已接近 hard 阈值 → 本轮提醒 agent 把现状蒸馏进 work-log(为轮换做准备)
|
|
57
|
-
const nearBudget = resuming && isNearBudget(prior, config.sessionSoftTokens);
|
|
58
|
-
// 既有会话但本轮不续用 → 轮换出新 uuid 冷启动(避免 --session-id 撞已存在会话)。
|
|
59
|
-
const rotated = !!(ws.agentSessionId && ws.sessionResume && !resuming);
|
|
60
|
-
const launchSessionId = rotated ? await rotateAgentSession(ws.runDir) : ws.agentSessionId;
|
|
61
|
-
// resume 决策是「会话为什么没续上/为什么重开」的直接证据,把判定依据全量留痕
|
|
62
|
-
dslog("session.resume_decision", resuming ? "续用上轮会话 (--resume)" : "冷启动新会话", {
|
|
63
|
-
run_id: input.runId, agent_handle: input.handle, channel_id: input.channelId,
|
|
64
|
-
task_key: input.taskKey, runtime, model: currentModel,
|
|
65
|
-
resuming, rotated, session_id: launchSessionId,
|
|
66
|
-
prior_session_id: prior?.sessionId ?? null,
|
|
67
|
-
prior_age_ms: prior ? Date.now() - prior.lastRunAt : null,
|
|
68
|
-
warm_ms: config.resumeWarmMs,
|
|
69
|
-
decision_reason: !supportsNativeResume ? "runtime_no_resume"
|
|
70
|
-
: !config.resume ? "resume_disabled"
|
|
71
|
-
: !ws.sessionResume ? "first_run"
|
|
72
|
-
: resuming ? (nearBudget ? "warm_resume_near_budget" : "warm_resume")
|
|
73
|
-
: prior?.lastExitOk === false ? "prior_run_crashed"
|
|
74
|
-
: rotatedForBudget ? "rotated_for_budget"
|
|
75
|
-
: "warm_window_expired_or_model_changed",
|
|
76
|
-
prior_context_tokens: prior?.contextTokens ?? null,
|
|
77
|
-
prior_last_exit_ok: prior?.lastExitOk ?? null,
|
|
78
|
-
budget_tokens: config.sessionBudgetTokens,
|
|
15
|
+
function runtimeName(value) {
|
|
16
|
+
if (value === "claude" || value === "codex" || value === "kimi")
|
|
17
|
+
return value;
|
|
18
|
+
throw new Error(`unsupported runtime: ${value}`);
|
|
19
|
+
}
|
|
20
|
+
export async function runAgent(config, input, onActivity = defaultPrint, onConsole = () => { }) {
|
|
21
|
+
const credential = await mintAgentToken(config.serverUrl, config.machineToken, input.handle, input.displayName, {
|
|
22
|
+
...(input.wakeMessageId ? { wakeThreadRoot: input.wakeMessageId } : {}),
|
|
23
|
+
...(input.wakeContextUpToSeq === undefined ? {} : { wakeContextUpToSeq: input.wakeContextUpToSeq }),
|
|
24
|
+
...(input.runId ? { agentRunId: input.runId } : {}),
|
|
79
25
|
});
|
|
80
|
-
const
|
|
26
|
+
const providerConfig = credential.config ?? {};
|
|
27
|
+
const runtime = runtimeName(providerConfig.runtime ?? config.runtimeBin);
|
|
28
|
+
const reasoning = ReasoningSchema.safeParse(providerConfig.reasoning);
|
|
29
|
+
const baseWake = input.wake ?? buildWakePrompt(input.channelId);
|
|
30
|
+
const executionId = input.runId ?? `legacy-${randomUUID()}`;
|
|
31
|
+
const originDecisionFileName = input.wakeOrigin === "wecom"
|
|
32
|
+
? `.origin-decision-${executionId}.json`
|
|
33
|
+
: null;
|
|
34
|
+
const local = await executeLocal({
|
|
35
|
+
executionId,
|
|
81
36
|
handle: input.handle,
|
|
82
37
|
channelId: input.channelId,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
:
|
|
108
|
-
|
|
109
|
-
:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const childEnv = applyProviderEnv(baseEnv, runtime, cfg, ws.homeDir);
|
|
145
|
-
const child = runtime === "claude"
|
|
146
|
-
? spawnClaude({
|
|
147
|
-
bin: runtime,
|
|
148
|
-
cwd: ws.runDir, // 本任务隔离工作目录(并行运行互不干扰)
|
|
149
|
-
systemPromptPath: ws.systemPromptPath,
|
|
150
|
-
wakePrompt,
|
|
151
|
-
dangerous: config.dangerous,
|
|
152
|
-
...(currentModel ? { model: currentModel } : {}),
|
|
153
|
-
...(cfg.reasoning ? { reasoning: cfg.reasoning } : {}),
|
|
154
|
-
// 一线程一会话(唯一会话机制):首轮/冷启动 --session-id 固定 uuid,warm 续轮 --resume 续上。
|
|
155
|
-
...(launchSessionId ? { sessionId: launchSessionId, resume: resuming } : {}),
|
|
156
|
-
env: childEnv,
|
|
157
|
-
})
|
|
158
|
-
: runtime === "codex"
|
|
159
|
-
? spawnCodex({
|
|
160
|
-
bin: runtime,
|
|
161
|
-
cwd: ws.runDir,
|
|
162
|
-
wakePrompt: `${systemPrompt}\n\n${wakePrompt}`,
|
|
163
|
-
dangerous: config.dangerous,
|
|
164
|
-
...(currentModel ? { model: currentModel } : {}),
|
|
165
|
-
...(cfg.reasoning ? { reasoning: cfg.reasoning } : {}),
|
|
166
|
-
env: childEnv,
|
|
167
|
-
})
|
|
168
|
-
: runtime === "kimi"
|
|
169
|
-
? spawnKimi({
|
|
170
|
-
bin: runtime,
|
|
171
|
-
cwd: ws.runDir,
|
|
172
|
-
// kimi 与 codex 一样没有 system prompt 参数,拼在 wake prompt 前;
|
|
173
|
-
// -p 模式固定 auto 权限,dangerous 无对应 flag(见 runtimes/kimi.ts)。
|
|
174
|
-
wakePrompt: `${systemPrompt}\n\n${wakePrompt}`,
|
|
175
|
-
...(currentModel ? { model: currentModel } : {}),
|
|
176
|
-
env: childEnv,
|
|
177
|
-
})
|
|
178
|
-
: (() => {
|
|
179
|
-
throw new Error(`unsupported runtime: ${runtime}`);
|
|
180
|
-
})();
|
|
181
|
-
// 记下 OS 进程号:排查「进程被谁杀的/是否 OOM」时可与系统日志对齐
|
|
182
|
-
dslog("run.spawned", `${runtime} 进程已拉起 (pid ${child.pid})`, {
|
|
183
|
-
run_id: input.runId, agent_handle: input.handle, channel_id: input.channelId,
|
|
184
|
-
task_key: input.taskKey, runtime, model: currentModel,
|
|
185
|
-
os_pid: child.pid ?? null, session_id: launchSessionId, resume: resuming,
|
|
186
|
-
});
|
|
187
|
-
// 4) 逐行解析 stdout → 归一化 → 回调;顺带抓 session_id(记 lastRunAt 供 warm-window)+ token usage(度量)
|
|
188
|
-
const activities = [];
|
|
189
|
-
// 身份是本轮下发的 launchSessionId;仍兜底采 claude 自报的 session_id(理应一致)。
|
|
190
|
-
let sessionId = launchSessionId;
|
|
191
|
-
let usage;
|
|
192
|
-
let observedModel = currentModel;
|
|
193
|
-
let finalText = null;
|
|
194
|
-
let sentViaCrew = false;
|
|
195
|
-
const rl = createInterface({ input: child.stdout });
|
|
196
|
-
rl.on("line", (line) => {
|
|
197
|
-
const evt = parseLine(line);
|
|
198
|
-
if (!evt)
|
|
199
|
-
return;
|
|
200
|
-
const meta = extractRunMeta(evt);
|
|
201
|
-
if (meta.sessionId)
|
|
202
|
-
sessionId = meta.sessionId;
|
|
203
|
-
if (meta.usage)
|
|
204
|
-
usage = meta.usage;
|
|
205
|
-
if (meta.model)
|
|
206
|
-
observedModel = meta.model;
|
|
207
|
-
for (const a of normalizeEvent(evt)) {
|
|
208
|
-
if (a.kind === "sending")
|
|
209
|
-
sentViaCrew = true;
|
|
210
|
-
activities.push(a);
|
|
211
|
-
onActivity(a);
|
|
212
|
-
}
|
|
213
|
-
const extracted = extractFinalText(evt);
|
|
214
|
-
if (extracted)
|
|
215
|
-
finalText = extracted;
|
|
216
|
-
// 同一事件再透传为终端 console 行(独立于状态活动,内容不压缩)。
|
|
217
|
-
for (const c of toConsoleLines(evt))
|
|
218
|
-
onConsole(c);
|
|
219
|
-
});
|
|
220
|
-
// stderr 除透传本地终端外,再留一段尾部:runtime 启动即崩(如 codex 拒跑)时,
|
|
221
|
-
// 这是唯一的错误线索,要随 error 活动上送,否则失败对 server/web 完全不可见。
|
|
222
|
-
let stderrTail = "";
|
|
223
|
-
child.stderr.on("data", (d) => {
|
|
224
|
-
process.stderr.write(d);
|
|
225
|
-
stderrTail = (stderrTail + String(d)).slice(-STDERR_TAIL_CAP);
|
|
226
|
-
});
|
|
227
|
-
const { exitCode, spawnError, terminationSignal } = await awaitExit(child);
|
|
228
|
-
// spawn 本身失败(如 PATH 里没有 runtime 二进制)没有 stderr,把错误并入尾部供上报。
|
|
229
|
-
const errorTail = [
|
|
230
|
-
stderrTail.trim(),
|
|
231
|
-
spawnError,
|
|
232
|
-
terminationSignal ? `terminated by ${terminationSignal}` : undefined,
|
|
233
|
-
].filter(Boolean).join(" ").trim();
|
|
234
|
-
const finish = exitActivity(runtime, exitCode, errorTail);
|
|
235
|
-
if (finish) {
|
|
236
|
-
activities.push(finish);
|
|
237
|
-
onActivity(finish);
|
|
238
|
-
if (finish.kind === "error")
|
|
239
|
-
onConsole({ stream: "error", text: `✖ ${finish.detail ?? finish.label}` });
|
|
240
|
-
}
|
|
241
|
-
if (!input.scheduled && (runtime === "codex" || runtime === "kimi") && exitCode === 0 && !sentViaCrew && finalText) {
|
|
242
|
-
// force:兜底回帖锚定本轮触发消息的线程,语义上必须送达;不 force 时 agent(-p 单发不跑
|
|
243
|
-
// crew read)游标落后,回帖会被 freshness hold 成 draft(202)而永远不可见。
|
|
244
|
-
const sent = await sendAgentMessage(config.serverUrl, cred.token, input.channelId, {
|
|
245
|
-
content: finalText,
|
|
38
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
39
|
+
...(input.wakeMessageId === undefined ? {} : { wakeMessageId: input.wakeMessageId }),
|
|
40
|
+
systemPrompt: ({ workspace, resuming }) => buildSystemPrompt({
|
|
41
|
+
handle: input.handle,
|
|
42
|
+
channelId: input.channelId,
|
|
43
|
+
agentId: credential.agentId,
|
|
44
|
+
homeDir: workspace.dir,
|
|
45
|
+
productName: config.productName,
|
|
46
|
+
...(input.scheduled ? { scheduledOutputPolicy: input.scheduled.outputPolicy } : {}),
|
|
47
|
+
...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
|
|
48
|
+
memory: capMemoryForInject(workspace.memory),
|
|
49
|
+
...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
|
|
50
|
+
}),
|
|
51
|
+
wakePrompt: ({ resuming, rotatedForBudget, nearBudget }) => {
|
|
52
|
+
const distillHint = nearBudget
|
|
53
|
+
? "\n(注意:本会话上下文已接近预算,稍后将轮换重启。本轮结束前,把当前状态——阶段/关键结论/下一步/卡点/关键文件路径——完整更新到 $CREW_TASK_LOG,下轮将以它为基础继续。)"
|
|
54
|
+
: "";
|
|
55
|
+
if (resuming) {
|
|
56
|
+
return `(继续之前的会话:频道历史与你的进度已在上下文里,不必从头重读;要新消息用 \`crew message read --channel ${input.channelId}\` 增量拉即可。)${distillHint}\n\n${baseWake}`;
|
|
57
|
+
}
|
|
58
|
+
return rotatedForBudget
|
|
59
|
+
? `(上下文已轮换:之前的会话过大已重启。你的 work-log 已注入系统提示词末尾,以它为基础继续;频道/线程历史用 crew 命令按需拉取,不必全量重读。)\n\n${baseWake}`
|
|
60
|
+
: baseWake;
|
|
61
|
+
},
|
|
62
|
+
runtime: {
|
|
63
|
+
name: runtime,
|
|
64
|
+
...(providerConfig.model ? { model: providerConfig.model } : {}),
|
|
65
|
+
...(reasoning.success ? { reasoning: reasoning.data } : {}),
|
|
66
|
+
},
|
|
67
|
+
effectivePermission: config.dangerous ? "full_access" : "workspace_write",
|
|
68
|
+
captureFinal: true,
|
|
69
|
+
launch: {
|
|
70
|
+
serverUrl: config.serverUrl,
|
|
71
|
+
token: credential.token,
|
|
72
|
+
agentId: credential.agentId,
|
|
73
|
+
agentsRoot: config.agentsRoot,
|
|
74
|
+
cliPath: config.cliPath,
|
|
75
|
+
providerConfig,
|
|
76
|
+
...(providerConfig.description ? { description: providerConfig.description } : {}),
|
|
77
|
+
systemEnv: {
|
|
78
|
+
...(providerConfig.fastMode ? { CREW_FAST_MODE: "1" } : {}),
|
|
79
|
+
...(input.scheduled ? { CREW_SCHEDULE_OUTPUT_POLICY: input.scheduled.outputPolicy } : {}),
|
|
80
|
+
...(originDecisionFileName ? {
|
|
81
|
+
CREW_WAKE_ORIGIN: "wecom",
|
|
82
|
+
CREW_ORIGIN_DECISION_FILE: originDecisionFileName,
|
|
83
|
+
} : {}),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
session: {
|
|
87
|
+
enabled: config.resume,
|
|
88
|
+
warmMs: config.resumeWarmMs,
|
|
89
|
+
budgetTokens: config.sessionBudgetTokens,
|
|
90
|
+
softTokens: config.sessionSoftTokens,
|
|
91
|
+
maxTurns: config.sessionMaxTurns,
|
|
92
|
+
},
|
|
93
|
+
}, { onActivity, onConsole });
|
|
94
|
+
const activities = [...local.activities];
|
|
95
|
+
if (!input.scheduled && (runtime === "codex" || runtime === "kimi")
|
|
96
|
+
&& local.exitCode === 0 && !local.sentViaCrew && local.finalText) {
|
|
97
|
+
const sent = await sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
98
|
+
content: local.finalText,
|
|
246
99
|
force: true,
|
|
247
100
|
...(input.wakeMessageId ? { thread: input.wakeMessageId } : {}),
|
|
248
101
|
});
|
|
249
102
|
if (sent.delivered) {
|
|
250
|
-
const
|
|
251
|
-
activities.push(
|
|
252
|
-
onActivity(
|
|
103
|
+
const activity = { kind: "sending", label: "发消息", detail: `${runtime} final answer fallback` };
|
|
104
|
+
activities.push(activity);
|
|
105
|
+
onActivity(activity);
|
|
253
106
|
}
|
|
254
107
|
else if (sent.status === 202) {
|
|
255
108
|
process.stderr.write(`${runtime} fallback reply was held as draft (HTTP 202) — not visible in channel\n`);
|
|
@@ -258,134 +111,89 @@ onConsole = () => { }) {
|
|
|
258
111
|
process.stderr.write(`${runtime} fallback send failed (HTTP ${sent.status})\n`);
|
|
259
112
|
}
|
|
260
113
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const contextTokens = usage
|
|
266
|
-
? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
|
|
267
|
-
: (resuming ? prior?.contextTokens : undefined);
|
|
268
|
-
await writeSession(ws.runDir, {
|
|
269
|
-
sessionId,
|
|
270
|
-
lastRunAt: Date.now(),
|
|
271
|
-
turns: resuming ? (prior?.turns ?? 0) + 1 : 1,
|
|
272
|
-
model: currentModel,
|
|
273
|
-
providerFingerprint: providerFp,
|
|
274
|
-
// 非零退出(崩溃/超窗/被杀)标记本会话不健康 → 下轮 pickResumeId 强制冷启动,不再续这个坏会话。
|
|
275
|
-
lastExitOk: exitCode === 0,
|
|
276
|
-
...(contextTokens != null ? { contextTokens } : {}),
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
if (usage) {
|
|
280
|
-
const u = usage;
|
|
281
|
-
process.stdout.write(`📊 tokens: in=${u.inputTokens} out=${u.outputTokens} cache_read=${u.cacheReadTokens} cache_create=${u.cacheCreationTokens}` +
|
|
282
|
-
`${u.costUsd != null ? ` cost=$${u.costUsd.toFixed(4)}` : ""} ${resuming ? "(resumed)" : rotatedForBudget ? "(rotated: budget)" : "(fresh)"}\n`);
|
|
114
|
+
if (local.usage) {
|
|
115
|
+
const usage = local.usage;
|
|
116
|
+
process.stdout.write(`📊 tokens: in=${usage.inputTokens} out=${usage.outputTokens} cache_read=${usage.cacheReadTokens} cache_create=${usage.cacheCreationTokens}`
|
|
117
|
+
+ `${usage.costUsd != null ? ` cost=$${usage.costUsd.toFixed(4)}` : ""} ${local.resumed ? "(resumed)" : "(fresh)"}\n`);
|
|
283
118
|
}
|
|
284
|
-
// 所有可能抛错的本地收尾完成后再投递,避免“报告已发出但随后落盘失败”被 serve catch
|
|
285
|
-
// 当成另一轮失败再次通知。deliverScheduledReport 自身吞掉网络异常并返回可持久化 outcome。
|
|
286
119
|
const report = input.scheduled
|
|
287
120
|
? await deliverScheduledReport({
|
|
288
121
|
policy: input.scheduled.outputPolicy,
|
|
289
122
|
title: input.scheduled.title,
|
|
290
|
-
exitCode,
|
|
291
|
-
finalText,
|
|
292
|
-
errorMessage:
|
|
293
|
-
send: (content) => sendAgentMessage(config.serverUrl,
|
|
123
|
+
exitCode: local.exitCode,
|
|
124
|
+
finalText: local.finalText,
|
|
125
|
+
errorMessage: local.errorMessage,
|
|
126
|
+
send: (content) => sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
294
127
|
content,
|
|
295
128
|
force: true,
|
|
296
129
|
}),
|
|
297
130
|
})
|
|
298
131
|
: undefined;
|
|
132
|
+
const originDecisionPath = originDecisionFileName
|
|
133
|
+
? join(local.workspaceRunDir, originDecisionFileName)
|
|
134
|
+
: null;
|
|
135
|
+
const originDecision = originDecisionPath ? await readOriginDecisionFile(originDecisionPath) : null;
|
|
136
|
+
if (originDecisionPath)
|
|
137
|
+
await resetOriginDecisionFile(originDecisionPath);
|
|
299
138
|
return {
|
|
300
|
-
exitCode,
|
|
139
|
+
exitCode: local.exitCode,
|
|
301
140
|
activities,
|
|
302
|
-
model:
|
|
141
|
+
model: local.model,
|
|
303
142
|
runtime,
|
|
304
|
-
resumed:
|
|
305
|
-
sessionId,
|
|
306
|
-
errorMessage:
|
|
307
|
-
...(usage ? { usage } : {}),
|
|
143
|
+
resumed: local.resumed,
|
|
144
|
+
sessionId: local.sessionId,
|
|
145
|
+
errorMessage: local.errorMessage,
|
|
146
|
+
...(local.usage ? { usage: local.usage } : {}),
|
|
308
147
|
...(report ? { report } : {}),
|
|
148
|
+
...(input.wakeOrigin === "wecom" ? {
|
|
149
|
+
originDecision: originDecision?.decision ?? "missing",
|
|
150
|
+
...(originDecision?.decision === "silent" && originDecision.reason
|
|
151
|
+
? { originDecisionReason: originDecision.reason }
|
|
152
|
+
: {}),
|
|
153
|
+
} : {}),
|
|
309
154
|
};
|
|
310
155
|
}
|
|
311
|
-
/** runAgent 在 workspace/spawn 等前置阶段抛错时的可见失败兜底。 */
|
|
312
156
|
export async function reportScheduledStartFailure(config, input) {
|
|
313
|
-
const
|
|
157
|
+
const credential = await mintAgentToken(config.serverUrl, config.machineToken, input.handle);
|
|
314
158
|
return deliverScheduledReport({
|
|
315
159
|
policy: input.scheduled.outputPolicy,
|
|
316
160
|
title: input.scheduled.title,
|
|
317
161
|
exitCode: -1,
|
|
318
162
|
finalText: null,
|
|
319
163
|
errorMessage: input.errorMessage,
|
|
320
|
-
send: (content) => sendAgentMessage(config.serverUrl,
|
|
164
|
+
send: (content) => sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
321
165
|
content,
|
|
322
166
|
force: true,
|
|
323
167
|
}),
|
|
324
168
|
});
|
|
325
169
|
}
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
: { exitCode: code }));
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* 进程退出 → 收尾活动。codex/kimi 非零退出必须显式报 error(它们失败时往往一条事件都没吐,
|
|
350
|
-
* 不报就会被 serve 的「本轮结束」伪装成成功);exitCode -1 是 awaitExit 的 spawn 失败哨兵,
|
|
351
|
-
* 任何 runtime 都报(spawn 失败连事件流都没有);kimi 正常退出补 done(其 stream 无轮次结束事件);
|
|
352
|
-
* codex 正常退出与 claude 均返回 null(终态由 turn.completed / result 事件负责)。
|
|
353
|
-
*/
|
|
354
|
-
export function exitActivity(runtime, exitCode, stderrTail) {
|
|
355
|
-
if ((runtime === "codex" || runtime === "kimi" || exitCode === -1) && exitCode !== 0) {
|
|
356
|
-
return {
|
|
357
|
-
kind: "error",
|
|
358
|
-
label: "运行出错",
|
|
359
|
-
detail: `${runtime} exited with code ${exitCode}${stderrTail ? `: ${stderrTail}` : ""}`,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
if (runtime === "kimi" && exitCode === 0)
|
|
363
|
-
return { kind: "done", label: "本轮结束" };
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
366
|
-
function defaultPrint(a) {
|
|
367
|
-
const icon = ICON[a.kind] ?? "·";
|
|
368
|
-
const detail = a.detail ? ` ${a.detail.replace(/\s+/g, " ").slice(0, 120)}` : "";
|
|
369
|
-
process.stdout.write(`${icon} ${a.label}${detail}\n`);
|
|
170
|
+
export function mergeRunAgentResults(results) {
|
|
171
|
+
const final = results[results.length - 1];
|
|
172
|
+
if (!final)
|
|
173
|
+
throw new Error("cannot merge empty run results");
|
|
174
|
+
const usages = results.flatMap((result) => result.usage ? [result.usage] : []);
|
|
175
|
+
const usage = usages.length > 0 ? {
|
|
176
|
+
inputTokens: usages.reduce((sum, item) => sum + item.inputTokens, 0),
|
|
177
|
+
outputTokens: usages.reduce((sum, item) => sum + item.outputTokens, 0),
|
|
178
|
+
cacheReadTokens: usages.reduce((sum, item) => sum + item.cacheReadTokens, 0),
|
|
179
|
+
cacheCreationTokens: usages.reduce((sum, item) => sum + item.cacheCreationTokens, 0),
|
|
180
|
+
...(usages.some((item) => item.costUsd != null)
|
|
181
|
+
? { costUsd: usages.reduce((sum, item) => sum + (item.costUsd ?? 0), 0) }
|
|
182
|
+
: {}),
|
|
183
|
+
} : undefined;
|
|
184
|
+
return {
|
|
185
|
+
...final,
|
|
186
|
+
activities: results.flatMap((result) => result.activities),
|
|
187
|
+
...(usage ? { usage } : {}),
|
|
188
|
+
};
|
|
370
189
|
}
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return {};
|
|
376
|
-
const out = {};
|
|
377
|
-
for (const [k, v] of Object.entries(raw)) {
|
|
378
|
-
if (typeof v !== "string")
|
|
379
|
-
continue;
|
|
380
|
-
if (!ENV_KEY_RE.test(k) || k.toUpperCase().startsWith("CREW_"))
|
|
381
|
-
continue;
|
|
382
|
-
out[k] = v;
|
|
383
|
-
}
|
|
384
|
-
return out;
|
|
190
|
+
function defaultPrint(activity) {
|
|
191
|
+
const icon = ICON[activity.kind] ?? "·";
|
|
192
|
+
const detail = activity.detail ? ` ${activity.detail.replace(/\s+/g, " ").slice(0, 120)}` : "";
|
|
193
|
+
process.stdout.write(`${icon} ${activity.label}${detail}\n`);
|
|
385
194
|
}
|
|
386
|
-
/** exported for tests */
|
|
387
195
|
export async function sendAgentMessage(serverUrl, token, channelId, body) {
|
|
388
|
-
const
|
|
196
|
+
const response = await fetch(`${serverUrl}/agent/channels/${encodeURIComponent(channelId)}/messages`, {
|
|
389
197
|
method: "POST",
|
|
390
198
|
headers: {
|
|
391
199
|
authorization: `Bearer ${token}`,
|
|
@@ -393,7 +201,5 @@ export async function sendAgentMessage(serverUrl, token, channelId, body) {
|
|
|
393
201
|
},
|
|
394
202
|
body: JSON.stringify(body),
|
|
395
203
|
});
|
|
396
|
-
|
|
397
|
-
// 不能当成功(把 202 当成功正是 kimi/codex fallback 回帖丢失的根因)。
|
|
398
|
-
return { delivered: res.status === 201, status: res.status };
|
|
204
|
+
return { delivered: response.status === 201, status: response.status };
|
|
399
205
|
}
|
package/dist/runtimes/claude.js
CHANGED
|
@@ -23,8 +23,16 @@ export function buildClaudeArgs(input) {
|
|
|
23
23
|
if (input.sessionId) {
|
|
24
24
|
args.push(input.resume ? "--resume" : "--session-id", input.sessionId);
|
|
25
25
|
}
|
|
26
|
-
if (input.
|
|
26
|
+
if (input.effectivePermission === undefined) {
|
|
27
|
+
if (input.dangerous)
|
|
28
|
+
args.push("--dangerously-skip-permissions");
|
|
29
|
+
}
|
|
30
|
+
else if (input.effectivePermission === "full_access") {
|
|
27
31
|
args.push("--dangerously-skip-permissions");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
args.push("--permission-mode", input.effectivePermission === "sandboxed" ? "plan" : "acceptEdits");
|
|
35
|
+
}
|
|
28
36
|
args.push(input.wakePrompt);
|
|
29
37
|
return args;
|
|
30
38
|
}
|
package/dist/runtimes/codex.js
CHANGED
|
@@ -16,16 +16,32 @@ export function buildCodexArgs(input) {
|
|
|
16
16
|
if (input.reasoning && CODEX_EFFORT_LEVELS.includes(input.reasoning)) {
|
|
17
17
|
args.push("-c", `model_reasoning_effort=${input.reasoning}`);
|
|
18
18
|
}
|
|
19
|
-
if (input.
|
|
19
|
+
if (input.effectivePermission === "sandboxed")
|
|
20
|
+
args.push("--sandbox", "read-only");
|
|
21
|
+
else if (input.effectivePermission === "workspace_write")
|
|
22
|
+
args.push("--sandbox", "workspace-write");
|
|
23
|
+
else if (input.effectivePermission === "full_access" || (input.effectivePermission === undefined && input.dangerous)) {
|
|
20
24
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
21
|
-
|
|
25
|
+
}
|
|
26
|
+
// `-` instructs codex exec to read the prompt from stdin. Keeping the complete prompt out of argv
|
|
27
|
+
// avoids Windows' command-line length limit when a thread carries a large wake context.
|
|
28
|
+
args.push("-");
|
|
22
29
|
return args;
|
|
23
30
|
}
|
|
24
31
|
export function spawnCodex(input) {
|
|
25
|
-
// stdio 固定
|
|
26
|
-
|
|
32
|
+
// stdio 固定 pipe/pipe/pipe;cross-spawn 类型不带该细化,断言之。
|
|
33
|
+
const child = spawn(input.bin, buildCodexArgs(input), {
|
|
27
34
|
cwd: input.cwd,
|
|
28
35
|
env: input.env,
|
|
29
|
-
stdio: ["
|
|
36
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
30
37
|
});
|
|
38
|
+
// Decode at the pipe boundary so split multi-byte characters are buffered correctly before
|
|
39
|
+
// readline, stderr forwarding, activity reporting, and websocket JSON serialization consume them.
|
|
40
|
+
child.stdout.setEncoding("utf8");
|
|
41
|
+
child.stderr.setEncoding("utf8");
|
|
42
|
+
// Codex may exit before consuming a large prompt (for example on config/auth failure). In that
|
|
43
|
+
// case the pipe can emit EPIPE; the child exit code and stderr remain the authoritative failure.
|
|
44
|
+
child.stdin.on("error", () => undefined);
|
|
45
|
+
child.stdin.end(input.wakePrompt);
|
|
46
|
+
return child;
|
|
31
47
|
}
|
package/dist/runtimes/kimi.js
CHANGED
|
@@ -14,6 +14,9 @@ import spawn from "cross-spawn";
|
|
|
14
14
|
// 由 runner 经 KIMI_MODEL_THINKING_EFFORT env 注入;白名单外的值不注。
|
|
15
15
|
export const KIMI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
16
16
|
export function buildKimiArgs(input) {
|
|
17
|
+
if (input.effectivePermission !== undefined && input.effectivePermission !== "full_access") {
|
|
18
|
+
throw new Error(`Kimi prompt mode cannot enforce ${input.effectivePermission} permission`);
|
|
19
|
+
}
|
|
17
20
|
const args = ["--output-format", "stream-json"];
|
|
18
21
|
if (input.model)
|
|
19
22
|
args.push("--model", input.model);
|