@nowcrew/daemon 0.5.7 → 0.5.9
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/log-format.js +10 -0
- package/dist/machine-info.js +2 -0
- package/dist/main.js +4 -3
- package/dist/normalize.js +13 -0
- package/dist/prompt.js +108 -38
- package/dist/runner.js +64 -18
- package/dist/scheduled-report.js +47 -0
- package/dist/scheduled-run-report.js +48 -0
- package/dist/serve.js +38 -29
- package/dist/session.js +5 -0
- package/package.json +2 -2
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function formatDaemonLogLine(message, date = new Date(), offsetMinutes = date.getTimezoneOffset()) {
|
|
2
|
+
const local = new Date(date.getTime() - offsetMinutes * 60_000);
|
|
3
|
+
const pad = (value, width = 2) => String(value).padStart(width, "0");
|
|
4
|
+
const sign = offsetMinutes <= 0 ? "+" : "-";
|
|
5
|
+
const offset = Math.abs(offsetMinutes);
|
|
6
|
+
const timestamp = `${local.getUTCFullYear()}-${pad(local.getUTCMonth() + 1)}-${pad(local.getUTCDate())}` +
|
|
7
|
+
` ${pad(local.getUTCHours())}:${pad(local.getUTCMinutes())}:${pad(local.getUTCSeconds())}.${pad(local.getUTCMilliseconds(), 3)}` +
|
|
8
|
+
` ${sign}${pad(Math.floor(offset / 60))}:${pad(offset % 60)}`;
|
|
9
|
+
return `[${timestamp}] ${message}`;
|
|
10
|
+
}
|
package/dist/machine-info.js
CHANGED
|
@@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url";
|
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { dirname, resolve } from "node:path";
|
|
14
14
|
const execFileP = promisify(execFile);
|
|
15
|
+
export const DAEMON_CAPABILITIES = ["scheduled_job_v1"];
|
|
15
16
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|
|
16
17
|
const RUNTIME_BINS = [
|
|
17
18
|
["claude", "claude"],
|
|
@@ -82,6 +83,7 @@ export async function collectMachineHello(agentsRoot) {
|
|
|
82
83
|
os: `${platform()} ${arch()}`,
|
|
83
84
|
daemonVersion: daemonVersion(),
|
|
84
85
|
runtimes,
|
|
86
|
+
capabilities: DAEMON_CAPABILITIES,
|
|
85
87
|
agentHandles,
|
|
86
88
|
};
|
|
87
89
|
}
|
package/dist/main.js
CHANGED
|
@@ -12,6 +12,7 @@ import { cliVersion, daemonVersion } from "./machine-info.js";
|
|
|
12
12
|
import { runAgent } from "./runner.js";
|
|
13
13
|
import { serve } from "./serve.js";
|
|
14
14
|
import { initSlog, flushSlog } from "./slog.js";
|
|
15
|
+
import { formatDaemonLogLine } from "./log-format.js";
|
|
15
16
|
async function main() {
|
|
16
17
|
const lang = detectDaemonLang();
|
|
17
18
|
const td = (message) => translateDaemon(lang, message);
|
|
@@ -55,7 +56,7 @@ async function main() {
|
|
|
55
56
|
throw e;
|
|
56
57
|
}
|
|
57
58
|
if (cmd === "serve") {
|
|
58
|
-
process.stdout.write(
|
|
59
|
+
process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
|
|
59
60
|
serve(config);
|
|
60
61
|
await new Promise(() => { }); // 常驻,直到被 kill
|
|
61
62
|
return;
|
|
@@ -64,7 +65,7 @@ async function main() {
|
|
|
64
65
|
process.stderr.write(`${td("Usage:")} crew-daemon run --agent <handle> --channel <id> [--wake ...]\n`);
|
|
65
66
|
process.exit(2);
|
|
66
67
|
}
|
|
67
|
-
process.stdout.write(
|
|
68
|
+
process.stdout.write(formatDaemonLogLine(`🚀 ${td("Waking agent")} "${values.agent}" ${td("for channel")} ${values.channel}`) + "\n");
|
|
68
69
|
initSlog(config.serverUrl, config.machineToken); // 一次性 run 模式也上报 SLS(runner 里的埋点生效)
|
|
69
70
|
const result = await runAgent(config, {
|
|
70
71
|
handle: values.agent,
|
|
@@ -72,7 +73,7 @@ async function main() {
|
|
|
72
73
|
...(values.wake ? { wake: values.wake } : {}),
|
|
73
74
|
...(values.display ? { displayName: values.display } : {}),
|
|
74
75
|
});
|
|
75
|
-
process.stdout.write(
|
|
76
|
+
process.stdout.write(formatDaemonLogLine(`— ${td("agent exited")} (code ${result.exitCode}), ${td("activities")}: ${result.activities.length} —`) + "\n");
|
|
76
77
|
await flushSlog();
|
|
77
78
|
process.exit(result.exitCode);
|
|
78
79
|
}
|
package/dist/normalize.js
CHANGED
|
@@ -31,6 +31,19 @@ export function classifyCommand(command) {
|
|
|
31
31
|
return { kind: "crew", label: "crew 命令", detail: c };
|
|
32
32
|
return { kind: "tool", label: "执行命令", detail: c };
|
|
33
33
|
}
|
|
34
|
+
/** 从各 runtime 的最终事件提取可交付文本。调用方按事件顺序保留最后一个非空值。 */
|
|
35
|
+
export function extractFinalText(event) {
|
|
36
|
+
const e = (event ?? {});
|
|
37
|
+
if (e.type === "result" && !e.is_error && e.result?.trim())
|
|
38
|
+
return e.result.trim();
|
|
39
|
+
if (e.type === "item.completed" && e.item?.type === "agent_message" && e.item.text?.trim()) {
|
|
40
|
+
return e.item.text.trim();
|
|
41
|
+
}
|
|
42
|
+
if (e.role === "assistant" && !e.type && typeof e.content === "string" && e.content.trim()) {
|
|
43
|
+
return e.content.trim();
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
34
47
|
/** kimi 的 Bash 工具 arguments 是 JSON 字符串({"command": "..."}),容错解析出 command。 */
|
|
35
48
|
function parseKimiBashCommand(args) {
|
|
36
49
|
if (!args)
|
package/dist/prompt.js
CHANGED
|
@@ -19,17 +19,28 @@ export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
|
|
|
19
19
|
}
|
|
20
20
|
export function buildSystemPrompt(ctx) {
|
|
21
21
|
const product = ctx.productName ?? "nowwork";
|
|
22
|
-
const
|
|
22
|
+
const scheduledPolicy = ctx.scheduledOutputPolicy
|
|
23
|
+
?? (ctx.scheduled ? "silent_unless_report" : null);
|
|
24
|
+
const scheduled = scheduledPolicy !== null;
|
|
25
|
+
const alwaysReport = scheduledPolicy === "always_report";
|
|
23
26
|
// 启动序列:非 scheduled 保持原文(含"先确认接手"/"收到消息就处理并回复"——这两条是
|
|
24
27
|
// 协作场景的核心礼仪);scheduled 换成静默版,不出现任何"先确认/先回复"的措辞。
|
|
25
|
-
const startupSequence =
|
|
26
|
-
? `## 启动序列(
|
|
28
|
+
const startupSequence = alwaysReport
|
|
29
|
+
? `## 启动序列(每轮汇报定时任务)
|
|
30
|
+
1. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
31
|
+
2. 执行唤醒提示词里给出的定时指令。
|
|
32
|
+
3. Return exactly one self-contained final report as your runtime final response.
|
|
33
|
+
4. Do not call crew message send; the daemon delivers the final response.
|
|
34
|
+
5. Do not send acknowledgements or progress messages.
|
|
35
|
+
6. 只有存在需要后续跟进的具体事项时,才用 \`crew task create\` 创建任务。`
|
|
36
|
+
: scheduled
|
|
37
|
+
? `## 启动序列(静默定时任务)
|
|
27
38
|
1. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
28
39
|
2. 执行唤醒提示词里给出的定时指令。**默认零输出**:不判断"是否需要先确认/声明接手",本轮没有人在等你回复。
|
|
29
40
|
3. 只有发现异常、需要人处理、或指令明确要求汇报时,才用 \`crew message send --send-draft\` 输出到频道(用法见上面"通信"一节)。
|
|
30
41
|
4. 只有存在需要后续跟进的具体事项时,才用 \`crew task create\` 创建任务。
|
|
31
42
|
5. 无异常时直接结束,不需要任何输出,不必读频道历史(除非指令要求)。`
|
|
32
|
-
|
|
43
|
+
: `## 启动序列
|
|
33
44
|
1. 若本轮已带具体来信,先判断是否需要立即确认/提问/声明接手;需要就先用 \`crew message send\` 发出,再去深挖上下文。
|
|
34
45
|
2. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
35
46
|
3. 若本轮只有"有未读"的 inbox notice、没有正文:notice 表示存在你尚未看到的消息(正文被暂时省略以免刷屏,不是没有内容)。是否读、何时读由你判断,可用 \`crew message check\` / \`crew message read\` 拉取。**绝不能仅凭一条 content-free notice 就断定"没有工作"**;若选择暂不读,要诚实当作 defer。
|
|
@@ -39,7 +50,7 @@ export function buildSystemPrompt(ctx) {
|
|
|
39
50
|
// 消息、没有 task,这条规则字面上无法执行,省略(2026-07-13 复审 Minor 5)。
|
|
40
51
|
const claimRule = scheduled
|
|
41
52
|
? ""
|
|
42
|
-
: "\n-
|
|
53
|
+
: "\n- **除定时任务管理外,动手干活前必须先 `crew task claim`**;claim 失败就转做别的任务。";
|
|
43
54
|
// "一线程一 task 且必须带 --thread"与"进度/产出回本线程,严禁发到顶层"都建立在"本轮有一条
|
|
44
55
|
// 触发消息、线程根即为该消息 id"这个前提上——scheduled run 没有触发消息、没有 thread 锚点,
|
|
45
56
|
// 唯一输出路径就是频道顶层 `--send-draft`,与这两条正面矛盾且 --thread 参数根本填不出来,
|
|
@@ -47,9 +58,74 @@ export function buildSystemPrompt(ctx) {
|
|
|
47
58
|
const threadTaskRule = scheduled
|
|
48
59
|
? "\n- **静默任务无线程锚点**:只有存在需要人跟进的具体事项时才用 `crew task create --title \"…\"`(无需 `--thread`——本轮没有触发消息可绑)。"
|
|
49
60
|
: `\n- **线程 = 工作单元 / 一个请求一个 task(CRITICAL)**:一条对话(thread)对应一件事,最多绑**一个** task。被唤醒处理来信时,对这件事**只建一个 task,且必须把它绑到当前线程**:\`crew task create --title "…" --thread <触发你的那条消息 id>\`(那条消息就是线程根)。**一定要带 \`--thread\`**——不带会另起一条飘在顶层的新线程,task 就和你的讨论分家了(这正是要避免的)。**别把一个请求拆成多个 task**(如"拉代码"+"读文档"+"写记忆"是同一件事 → 一个 task,用 todo/进度推进,不要建第二个)。当前线程已有 task 时再建会被服务端拒绝(报错会提示你)。`;
|
|
50
|
-
const progressRule =
|
|
51
|
-
? "\n-
|
|
52
|
-
:
|
|
61
|
+
const progressRule = alwaysReport
|
|
62
|
+
? "\n- **报告交由 daemon 投递**:只返回一个完整最终报告,不要调用 `crew message send`;本轮没有线程锚点。"
|
|
63
|
+
: scheduled
|
|
64
|
+
? "\n- **产出走频道顶层**:静默 run 唯一的输出路径是 `crew message send --channel <id> --send-draft`(顶层、无 thread)——本轮没有触发消息,没有线程锚点可回。"
|
|
65
|
+
: "\n- **进度/产出回本线程**:这件事的认领/进度/完成汇报都用 `crew message send --thread <当前线程根>` 回复在**这个线程里**(线程根 = 触发你的那条消息 id,即 \$CREW_WAKE_MESSAGE_ID)。**严禁把进度发到别的线程或频道顶层。**";
|
|
66
|
+
const scheduleManagementRule = scheduled ? "" : `
|
|
67
|
+
|
|
68
|
+
## 定时任务管理
|
|
69
|
+
当来信要求查看、修改、暂停、恢复、取消或立即执行定时任务时:
|
|
70
|
+
1. 定时任务管理无需 \`crew task claim\`;直接执行下面的查询/管理流程。
|
|
71
|
+
2. 先运行 \`crew schedule list --channel ${ctx.channelId} --agent ${ctx.handle}\` 查询本频道绑定到你自己的候选。
|
|
72
|
+
3. 只有一个明确匹配时,才运行 \`crew schedule update <jobId> ...\` 或对应 pause/resume/cancel/run-now 命令。
|
|
73
|
+
4. 有多个合理候选时,列出候选并先让用户选择;用户选定前不得修改任何任务。
|
|
74
|
+
5. 修改请求不能用 \`crew schedule create\` 代替。完成后回复同一个 job ID 的明确前后变化。`;
|
|
75
|
+
const voiceRule = alwaysReport
|
|
76
|
+
? "- **本轮报告只写在 runtime 最终回复中**:不要调用 `crew message send`;daemon 会把最终回复投递到频道。"
|
|
77
|
+
: "- **始终只通过 crew CLI 发声。在 crew 命令之外产生的任何文字都不会送达任何人。**";
|
|
78
|
+
const actionRule = scheduled
|
|
79
|
+
? `- **判定规则**:直接执行本轮定时指令,执行前不需要 claim。${threadTaskRule}${progressRule}`
|
|
80
|
+
: `- **判定规则**:定时任务管理直接按上节执行,无需 task claim;其它来信若需要你"回复之外的动作"(跑工具/改代码/做变更),先 claim;若只是回答问题或闲聊,无需 claim。${threadTaskRule}${progressRule}`;
|
|
81
|
+
const taskAndScheduleCommands = alwaysReport
|
|
82
|
+
? `4. **\`crew task create --channel <id> --title "<标题>"\`** —— 仅为本轮发现的具体后续事项创建任务;本轮没有线程锚点,不要传 \`--thread\`。
|
|
83
|
+
5. **\`crew schedule list --channel <id> --agent <handle>\`** —— 查询本频道指定 agent 的定时任务。
|
|
84
|
+
6. **\`crew schedule update <jobId> ...\`** —— 修改定时任务标题、指令、时间或输出策略。
|
|
85
|
+
7. **\`crew schedule pause|resume|cancel|run-now <jobId>\`** —— 控制定时任务。`
|
|
86
|
+
: scheduled
|
|
87
|
+
? `5. **\`crew task create --channel <id> --title "<标题>"\`** —— 仅为本轮发现的具体后续事项创建任务;本轮没有线程锚点,不要传 \`--thread\`。
|
|
88
|
+
6. **\`crew schedule list --channel <id> --agent <handle>\`** —— 查询本频道指定 agent 的定时任务。
|
|
89
|
+
7. **\`crew schedule update <jobId> ...\`** —— 修改定时任务标题、指令、时间或输出策略。
|
|
90
|
+
8. **\`crew schedule pause|resume|cancel|run-now <jobId>\`** —— 控制定时任务。`
|
|
91
|
+
: `5. **\`crew task list --channel <id>\`** —— 看任务板。支持 \`--status <s>\` / \`--mine\`。
|
|
92
|
+
6. **\`crew task create --channel <id> --title "<标题>" --thread <当前线程根msgId>\`** —— 新建任务并**绑定到当前线程**。\`--thread\` 传你读到的那条**触发消息 id**(线程根),任务就和讨论同处一个线程。省略 \`--thread\` 会另起新线程,**几乎总是错的——务必带上**。
|
|
93
|
+
7. **\`crew task claim <taskId>\`** —— 除定时任务管理外,执行需要动作的工作前先认领任务。
|
|
94
|
+
8. **\`crew task update <taskId> --status <in_progress|in_review|done>\`** —— 推进任务状态。
|
|
95
|
+
9. **\`crew task unclaim <taskId>\`** —— 释放认领,把任务让给别人。
|
|
96
|
+
10. **\`crew task assign <taskId> --to <handle>\`** —— 把任务指派/交接给另一个 agent(用于交接,见下)。
|
|
97
|
+
11. **\`crew schedule list --channel <id> --agent <handle>\`** —— 查询本频道指定 agent 的定时任务。
|
|
98
|
+
12. **\`crew schedule update <jobId> ...\`** —— 修改定时任务标题、指令、时间或输出策略。
|
|
99
|
+
13. **\`crew schedule pause|resume|cancel|run-now <jobId>\`** —— 控制定时任务。`;
|
|
100
|
+
const communicationSection = alwaysReport
|
|
101
|
+
? `## 控制面工具 —— crew CLI
|
|
102
|
+
需要读取频道或操作任务时使用 crew CLI;最终报告不得通过消息命令发送。可使用:
|
|
103
|
+
1. **\`crew whoami\`** —— 查看你自己的身份。
|
|
104
|
+
2. **\`crew message read --channel <id>\`** —— 读取频道历史。
|
|
105
|
+
3. **\`crew message check --channel <id>\`** —— 非阻塞查看未读数。
|
|
106
|
+
${taskAndScheduleCommands}`
|
|
107
|
+
: `## 通信 —— 只能用 crew CLI
|
|
108
|
+
所有 chat / task 操作必须经 \`crew\` CLI(daemon 已把它注入你的 PATH)。仅可使用以下命令:
|
|
109
|
+
1. **\`crew whoami\`** —— 查看你自己的身份。
|
|
110
|
+
2. **\`crew message read --channel <id>\`** —— 读频道历史(读取即自动推进你的已读/新鲜度游标)。支持 \`--after <seq>\` / \`--limit <n>\`。
|
|
111
|
+
3. **\`crew message check --channel <id>\`** —— 非阻塞查看未读数。工作中可在自然断点随时用。
|
|
112
|
+
4. **\`crew message send --channel <id>\`** —— 发消息。正文从 stdin 读,用 heredoc 避免 shell 解释引号/反引号/代码块:
|
|
113
|
+
\`\`\`bash
|
|
114
|
+
crew message send --channel <id> <<'CREWMSG'
|
|
115
|
+
你的消息正文,可含 "引号"、\\\`反引号\\\`、代码块。
|
|
116
|
+
CREWMSG
|
|
117
|
+
\`\`\`
|
|
118
|
+
也可用 \`--content "<短正文>"\`。线程内回复:加 \`--thread <完整线程根消息 id>\`。被唤醒时优先使用环境变量 \`$CREW_WAKE_MESSAGE_ID\` 或唤醒提示里的完整 id,不要手动截短。
|
|
119
|
+
${taskAndScheduleCommands}`;
|
|
120
|
+
const freshnessRule = alwaysReport
|
|
121
|
+
? ""
|
|
122
|
+
: `
|
|
123
|
+
- **freshness/draft**:发送若被保存为 draft(kind=held),要么重读后用普通 send 改写,要么用 \`crew message send --send-draft\` 原样发出(不要在改内容时用 --send-draft)。`;
|
|
124
|
+
const interactiveTaskRules = scheduled ? "" : `
|
|
125
|
+
- **毫不相关的新任务才另起线程**:只有要处理的事**和当前线程毫不相关**(或用户明确要求新建)时,才用 \`crew task create --new-thread --title "…"\`——系统另起一个子线程(parent=当前线程)绑新 task;之后这件事的回复要发到**这个新子线程**里。能不拆就不拆。
|
|
126
|
+
- 任务状态流:\`todo → in_progress → in_review → done\`。claim 后用 \`crew task update\` 推进:开工→in_progress、完成待验收→in_review、人类确认后→done。只有 assignee 能改自己任务的状态。
|
|
127
|
+
- **交接(handoff)**:当你这一环干完、需要别的角色接手时(如开发完成 → 交给 QA 测试),用 \`crew task assign <taskId> --to <下家handle>\` 把任务交接出去,并在线程里给下家足够背景(分支名 / 改动摘要 / 测试建议)。交接后对方会被自动唤醒。**别让任务停在你手里无人跟进**。
|
|
128
|
+
- **分诊(若你是总管)**:若你收到「【分诊请求】」唤醒,说明频道里有一个无人认领的任务需要你按团队职责分派。唤醒内容里已附上团队成员及其职责:判断谁最合适,用 \`crew task assign <taskId> --to <handle>\` 指派给他(若该你自己做就 \`crew task claim\`);确实没人合适时,在频道里 @发起人 说明并给建议,**不要让任务悬空**。`;
|
|
53
129
|
// 协作礼仪:整节都在讲"如何对人主动发声/插话/回报"——scheduled run 没有对话对象,
|
|
54
130
|
// 这节整体不适用,省略(不是"粗暴删",是这节内容本身就是协作场景专属)。
|
|
55
131
|
const collaborationEtiquette = scheduled
|
|
@@ -59,17 +135,22 @@ export function buildSystemPrompt(ctx) {
|
|
|
59
135
|
## 协作礼仪
|
|
60
136
|
- **尊重正在进行的对话**:人类正与他人一来一回时,除非明确 @你或显然在叫你,否则不要插话。
|
|
61
137
|
- **只有真正干活的人来汇报**:别替别人总结或冒领他们的工作。
|
|
62
|
-
-
|
|
138
|
+
- **除定时任务管理外,claim 后再动手**:claim 失败立即停手,换一个任务。
|
|
63
139
|
- **停止前检查你欠的具体阻塞项**:若你还欠某人一个 handoff/review/决定/回复且正卡着对方,先发一条最小可行动消息再停。
|
|
64
140
|
- **少发废话**:只在有可行动内容时发消息,不要播报"我在等/我空闲"。`;
|
|
65
141
|
// 沟通风格:原文"收到任务先确认并简述计划;多步工作发简短进度"同样与静默冲突,
|
|
66
142
|
// scheduled 换成明确的"默认不输出"版本。
|
|
67
|
-
const communicationStyle =
|
|
143
|
+
const communicationStyle = alwaysReport
|
|
68
144
|
? `
|
|
69
145
|
|
|
146
|
+
## 沟通风格(每轮汇报定时任务)
|
|
147
|
+
不要发送确认或过程消息。只在 runtime 最终回复中给出一次完整、自包含的结果,由 daemon 统一投递。`
|
|
148
|
+
: scheduled
|
|
149
|
+
? `
|
|
150
|
+
|
|
70
151
|
## 沟通风格(静默定时任务)
|
|
71
152
|
本轮没有人在等你回复,默认不输出、不叙述过程、不播报进度。只有触发上面"启动序列"里列出的汇报条件时才发消息,内容简明陈述结果/异常即可。`
|
|
72
|
-
|
|
153
|
+
: `
|
|
73
154
|
|
|
74
155
|
## 沟通风格
|
|
75
156
|
用户看不到你的内部推理,所以:收到任务先确认并简述计划;多步工作发简短进度("正在做 2/3…");完成后总结结果。每条一两句,别刷屏。
|
|
@@ -92,24 +173,7 @@ export function buildSystemPrompt(ctx) {
|
|
|
92
173
|
- **跨任务协调看实时清单**:用 \`crew task list --mine\` 查你当前所有任务及其状态(谁在排队、谁在评审)。例:你一次只能测一个页面时,据此判断"正在测 #1、#2 排队",在 #2 的工作日志里标注"等 #1 完成"。
|
|
93
174
|
- 需要更多背景时再 \`Read $CREW_HOME/notes/<topic>.md\`;不要一次把所有笔记读进上下文。
|
|
94
175
|
|
|
95
|
-
|
|
96
|
-
所有 chat / task 操作必须经 \`crew\` CLI(daemon 已把它注入你的 PATH)。仅可使用以下命令:
|
|
97
|
-
1. **\`crew whoami\`** —— 查看你自己的身份。
|
|
98
|
-
2. **\`crew message read --channel <id>\`** —— 读频道历史(读取即自动推进你的已读/新鲜度游标)。支持 \`--after <seq>\` / \`--limit <n>\`。
|
|
99
|
-
3. **\`crew message check --channel <id>\`** —— 非阻塞查看未读数。工作中可在自然断点随时用。
|
|
100
|
-
4. **\`crew message send --channel <id>\`** —— 发消息。正文从 stdin 读,用 heredoc 避免 shell 解释引号/反引号/代码块:
|
|
101
|
-
\`\`\`bash
|
|
102
|
-
crew message send --channel <id> <<'CREWMSG'
|
|
103
|
-
你的消息正文,可含 "引号"、\\\`反引号\\\`、代码块。
|
|
104
|
-
CREWMSG
|
|
105
|
-
\`\`\`
|
|
106
|
-
也可用 \`--content "<短正文>"\`。线程内回复:加 \`--thread <完整线程根消息 id>\`。被唤醒时优先使用环境变量 \`$CREW_WAKE_MESSAGE_ID\` 或唤醒提示里的完整 id,不要手动截短。
|
|
107
|
-
5. **\`crew task list --channel <id>\`** —— 看任务板。支持 \`--status <s>\` / \`--mine\`。
|
|
108
|
-
6. **\`crew task create --channel <id> --title "<标题>" --thread <当前线程根msgId>\`** —— 新建任务并**绑定到当前线程**。\`--thread\` 传你读到的那条**触发消息 id**(线程根),任务就和讨论同处一个线程。省略 \`--thread\` 会另起新线程,**几乎总是错的——务必带上**。
|
|
109
|
-
7. **\`crew task claim <taskId>\`** —— 认领任务(动手前必做)。
|
|
110
|
-
8. **\`crew task update <taskId> --status <in_progress|in_review|done>\`** —— 推进任务状态。
|
|
111
|
-
9. **\`crew task unclaim <taskId>\`** —— 释放认领,把任务让给别人。
|
|
112
|
-
10. **\`crew task assign <taskId> --to <handle>\`** —— 把任务指派/交接给另一个 agent(用于交接,见下)。
|
|
176
|
+
${communicationSection}
|
|
113
177
|
|
|
114
178
|
CLI 成功时打印人类可读文本到 stdout;失败时 stderr 给出错误,并用**退出码**告诉你下一步:
|
|
115
179
|
- \`4\` = 任务已被他人认领 / 不可认领 → 停手,别抢,转做别的。
|
|
@@ -117,20 +181,16 @@ CLI 成功时打印人类可读文本到 stdout;失败时 stderr 给出错误,
|
|
|
117
181
|
- \`3\` = 鉴权失败;\`5\` = 目标不存在;\`2\` = 参数错误。
|
|
118
182
|
|
|
119
183
|
CRITICAL 规则:
|
|
120
|
-
|
|
184
|
+
${voiceRule}
|
|
121
185
|
- **一个 shell 命令只跑一个 crew 命令**,读完它的输出,再决定下一条。不要把多个 crew 命令串到一行。${claimRule}
|
|
122
186
|
|
|
123
|
-
${startupSequence}
|
|
187
|
+
${startupSequence}${scheduleManagementRule}
|
|
124
188
|
|
|
125
189
|
## 消息与任务
|
|
126
190
|
- 你读到的消息形如 \`#<seq> [<type>] <sender>: <正文>\`,\`type\` 为 \`human\` / \`agent\` / \`system\`。
|
|
127
191
|
- **\`system\` 消息**通报频道状态变化(如新建任务),除非明确要求你行动(如刚给你指派了任务),否则不要回复。
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
- 任务状态流:\`todo → in_progress → in_review → done\`。claim 后用 \`crew task update\` 推进:开工→in_progress、完成待验收→in_review、人类确认后→done。只有 assignee 能改自己任务的状态。
|
|
131
|
-
- **交接(handoff)**:当你这一环干完、需要别的角色接手时(如开发完成 → 交给 QA 测试),用 \`crew task assign <taskId> --to <下家handle>\` 把任务交接出去,并在线程里给下家足够背景(分支名 / 改动摘要 / 测试建议)。交接后对方会被自动唤醒。**别让任务停在你手里无人跟进**。
|
|
132
|
-
- **分诊(若你是总管)**:若你收到「【分诊请求】」唤醒,说明频道里有一个无人认领的任务需要你按团队职责分派。唤醒内容里已附上团队成员及其职责:判断谁最合适,用 \`crew task assign <taskId> --to <handle>\` 指派给他(若该你自己做就 \`crew task claim\`);确实没人合适时,在频道里 @发起人 说明并给建议,**不要让任务悬空**。
|
|
133
|
-
- **freshness/draft**:发送若被保存为 draft(kind=held),要么重读后用普通 send 改写,要么用 \`crew message send --send-draft\` 原样发出(不要在改内容时用 --send-draft)。${collaborationEtiquette}${communicationStyle}
|
|
192
|
+
${actionRule}
|
|
193
|
+
${interactiveTaskRules}${freshnessRule}${collaborationEtiquette}${communicationStyle}
|
|
134
194
|
|
|
135
195
|
## Workspace 与分层记忆(CRITICAL — 索引+按需,配合上面的并行规则)
|
|
136
196
|
你的持久记忆在 \`$CREW_HOME\`(跨你所有任务共享),分三层:
|
|
@@ -146,7 +206,17 @@ ${ctx.memory ? `\n## [注入] 你的 MEMORY.md(索引,只读参考)\n${ctx.memor
|
|
|
146
206
|
/** 静默定时任务的唤醒提示词:不注入协作礼仪/线程提示;默认零输出(设计文档 §5.4)。
|
|
147
207
|
* 汇报用 --send-draft:绕过 freshness hold(-p 单发不跑 crew read,游标落后,
|
|
148
208
|
* 普通 send 会被 202 扣成 draft——监控告警绝不能被静默扣留)。 */
|
|
149
|
-
export function buildScheduledPrompt(channelId, jobPrompt) {
|
|
209
|
+
export function buildScheduledPrompt(channelId, jobPrompt, outputPolicy = "silent_unless_report") {
|
|
210
|
+
if (outputPolicy === "always_report") {
|
|
211
|
+
return [
|
|
212
|
+
"You are executing a scheduled job. No one is waiting for an acknowledgement.",
|
|
213
|
+
`Scheduled instruction: ${jobPrompt}`,
|
|
214
|
+
"Return exactly one self-contained final report as your runtime final response.",
|
|
215
|
+
"Do not call crew message send; the daemon delivers the final response.",
|
|
216
|
+
"Do not send acknowledgements or progress messages.",
|
|
217
|
+
"Create a crew task only when the result contains a concrete follow-up action.",
|
|
218
|
+
].join("\n");
|
|
219
|
+
}
|
|
150
220
|
return [
|
|
151
221
|
"你正在执行一个静默定时任务(后台运行,频道里没有人在等你回复)。",
|
|
152
222
|
`- 执行这条定时指令: ${jobPrompt}`,
|
package/dist/runner.js
CHANGED
|
@@ -11,10 +11,11 @@ import { spawnClaude } from "./runtimes/claude.js";
|
|
|
11
11
|
import { spawnCodex } from "./runtimes/codex.js";
|
|
12
12
|
import { applyProviderEnv, providerFingerprint } from "./provider-env.js";
|
|
13
13
|
import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
14
|
-
import { normalizeEvent, parseLine, extractRunMeta } from "./normalize.js";
|
|
14
|
+
import { normalizeEvent, parseLine, extractFinalText, extractRunMeta } from "./normalize.js";
|
|
15
15
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
16
16
|
import { toConsoleLines } from "./console.js";
|
|
17
17
|
import { dslog } from "./slog.js";
|
|
18
|
+
import { deliverScheduledReport } from "./scheduled-report.js";
|
|
18
19
|
// 注入提示词的 MEMORY.md 上限:只喂索引/角色,避免把膨胀的记忆全塞进上下文。
|
|
19
20
|
const MEMORY_INJECT_CAP = 6000;
|
|
20
21
|
const ICON = {
|
|
@@ -69,9 +70,11 @@ onConsole = () => { }) {
|
|
|
69
70
|
: !config.resume ? "resume_disabled"
|
|
70
71
|
: !ws.sessionResume ? "first_run"
|
|
71
72
|
: resuming ? (nearBudget ? "warm_resume_near_budget" : "warm_resume")
|
|
72
|
-
:
|
|
73
|
-
: "
|
|
73
|
+
: prior?.lastExitOk === false ? "prior_run_crashed"
|
|
74
|
+
: rotatedForBudget ? "rotated_for_budget"
|
|
75
|
+
: "warm_window_expired_or_model_changed",
|
|
74
76
|
prior_context_tokens: prior?.contextTokens ?? null,
|
|
77
|
+
prior_last_exit_ok: prior?.lastExitOk ?? null,
|
|
75
78
|
budget_tokens: config.sessionBudgetTokens,
|
|
76
79
|
});
|
|
77
80
|
const systemPrompt = buildSystemPrompt({
|
|
@@ -80,7 +83,7 @@ onConsole = () => { }) {
|
|
|
80
83
|
agentId: cred.agentId,
|
|
81
84
|
homeDir: ws.dir,
|
|
82
85
|
productName: config.productName,
|
|
83
|
-
...(input.scheduled ? {
|
|
86
|
+
...(input.scheduled ? { scheduledOutputPolicy: input.scheduled.outputPolicy } : {}),
|
|
84
87
|
// 只注入 MEMORY.md 的索引/角色部分(截断),避免上下文膨胀;明细让 agent 按需读 notes/。
|
|
85
88
|
memory: ws.memory.length > MEMORY_INJECT_CAP
|
|
86
89
|
? ws.memory.slice(0, MEMORY_INJECT_CAP) + "\n…(MEMORY.md 过长已截断,详情用 Read 读 $CREW_HOME/MEMORY.md 或 notes/)"
|
|
@@ -134,6 +137,7 @@ onConsole = () => { }) {
|
|
|
134
137
|
: {}),
|
|
135
138
|
// fast 模式 → 透传给 runtime(best-effort,供 wrapper/runtime 读取)
|
|
136
139
|
...(cfg.fastMode ? { CREW_FAST_MODE: "1" } : {}),
|
|
140
|
+
...(input.scheduled ? { CREW_SCHEDULE_OUTPUT_POLICY: input.scheduled.outputPolicy } : {}),
|
|
137
141
|
};
|
|
138
142
|
// provider custom = BYOC:剔除全局 ANTHROPIC_*/Bedrock 残留 + 按鉴权方式注入端点与密钥,
|
|
139
143
|
// 与机器全局 Claude 登录态/配置互不干扰(细节见 provider-env.ts)。
|
|
@@ -206,15 +210,9 @@ onConsole = () => { }) {
|
|
|
206
210
|
activities.push(a);
|
|
207
211
|
onActivity(a);
|
|
208
212
|
}
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
-
finalText =
|
|
212
|
-
}
|
|
213
|
-
// kimi:最后一条带正文的 assistant 行即最终回答(kimi 无 result/turn.completed 事件)
|
|
214
|
-
const kimiMsg = evt;
|
|
215
|
-
if (kimiMsg.role === "assistant" && !kimiMsg.type && typeof kimiMsg.content === "string" && kimiMsg.content.trim()) {
|
|
216
|
-
finalText = kimiMsg.content.trim();
|
|
217
|
-
}
|
|
213
|
+
const extracted = extractFinalText(evt);
|
|
214
|
+
if (extracted)
|
|
215
|
+
finalText = extracted;
|
|
218
216
|
// 同一事件再透传为终端 console 行(独立于状态活动,内容不压缩)。
|
|
219
217
|
for (const c of toConsoleLines(evt))
|
|
220
218
|
onConsole(c);
|
|
@@ -226,9 +224,13 @@ onConsole = () => { }) {
|
|
|
226
224
|
process.stderr.write(d);
|
|
227
225
|
stderrTail = (stderrTail + String(d)).slice(-STDERR_TAIL_CAP);
|
|
228
226
|
});
|
|
229
|
-
const { exitCode, spawnError } = await awaitExit(child);
|
|
227
|
+
const { exitCode, spawnError, terminationSignal } = await awaitExit(child);
|
|
230
228
|
// spawn 本身失败(如 PATH 里没有 runtime 二进制)没有 stderr,把错误并入尾部供上报。
|
|
231
|
-
const errorTail = [
|
|
229
|
+
const errorTail = [
|
|
230
|
+
stderrTail.trim(),
|
|
231
|
+
spawnError,
|
|
232
|
+
terminationSignal ? `terminated by ${terminationSignal}` : undefined,
|
|
233
|
+
].filter(Boolean).join(" ").trim();
|
|
232
234
|
const finish = exitActivity(runtime, exitCode, errorTail);
|
|
233
235
|
if (finish) {
|
|
234
236
|
activities.push(finish);
|
|
@@ -236,7 +238,7 @@ onConsole = () => { }) {
|
|
|
236
238
|
if (finish.kind === "error")
|
|
237
239
|
onConsole({ stream: "error", text: `✖ ${finish.detail ?? finish.label}` });
|
|
238
240
|
}
|
|
239
|
-
if (!input.
|
|
241
|
+
if (!input.scheduled && (runtime === "codex" || runtime === "kimi") && exitCode === 0 && !sentViaCrew && finalText) {
|
|
240
242
|
// force:兜底回帖锚定本轮触发消息的线程,语义上必须送达;不 force 时 agent(-p 单发不跑
|
|
241
243
|
// crew read)游标落后,回帖会被 freshness hold 成 draft(202)而永远不可见。
|
|
242
244
|
const sent = await sendAgentMessage(config.serverUrl, cred.token, input.channelId, {
|
|
@@ -269,6 +271,8 @@ onConsole = () => { }) {
|
|
|
269
271
|
turns: resuming ? (prior?.turns ?? 0) + 1 : 1,
|
|
270
272
|
model: currentModel,
|
|
271
273
|
providerFingerprint: providerFp,
|
|
274
|
+
// 非零退出(崩溃/超窗/被杀)标记本会话不健康 → 下轮 pickResumeId 强制冷启动,不再续这个坏会话。
|
|
275
|
+
lastExitOk: exitCode === 0,
|
|
272
276
|
...(contextTokens != null ? { contextTokens } : {}),
|
|
273
277
|
});
|
|
274
278
|
}
|
|
@@ -277,7 +281,47 @@ onConsole = () => { }) {
|
|
|
277
281
|
process.stdout.write(`📊 tokens: in=${u.inputTokens} out=${u.outputTokens} cache_read=${u.cacheReadTokens} cache_create=${u.cacheCreationTokens}` +
|
|
278
282
|
`${u.costUsd != null ? ` cost=$${u.costUsd.toFixed(4)}` : ""} ${resuming ? "(resumed)" : rotatedForBudget ? "(rotated: budget)" : "(fresh)"}\n`);
|
|
279
283
|
}
|
|
280
|
-
|
|
284
|
+
// 所有可能抛错的本地收尾完成后再投递,避免“报告已发出但随后落盘失败”被 serve catch
|
|
285
|
+
// 当成另一轮失败再次通知。deliverScheduledReport 自身吞掉网络异常并返回可持久化 outcome。
|
|
286
|
+
const report = input.scheduled
|
|
287
|
+
? await deliverScheduledReport({
|
|
288
|
+
policy: input.scheduled.outputPolicy,
|
|
289
|
+
title: input.scheduled.title,
|
|
290
|
+
exitCode,
|
|
291
|
+
finalText,
|
|
292
|
+
errorMessage: errorTail || null,
|
|
293
|
+
send: (content) => sendAgentMessage(config.serverUrl, cred.token, input.channelId, {
|
|
294
|
+
content,
|
|
295
|
+
force: true,
|
|
296
|
+
}),
|
|
297
|
+
})
|
|
298
|
+
: undefined;
|
|
299
|
+
return {
|
|
300
|
+
exitCode,
|
|
301
|
+
activities,
|
|
302
|
+
model: observedModel,
|
|
303
|
+
runtime,
|
|
304
|
+
resumed: resuming,
|
|
305
|
+
sessionId,
|
|
306
|
+
errorMessage: errorTail || null,
|
|
307
|
+
...(usage ? { usage } : {}),
|
|
308
|
+
...(report ? { report } : {}),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** runAgent 在 workspace/spawn 等前置阶段抛错时的可见失败兜底。 */
|
|
312
|
+
export async function reportScheduledStartFailure(config, input) {
|
|
313
|
+
const cred = await mintAgentToken(config.serverUrl, config.machineToken, input.handle);
|
|
314
|
+
return deliverScheduledReport({
|
|
315
|
+
policy: input.scheduled.outputPolicy,
|
|
316
|
+
title: input.scheduled.title,
|
|
317
|
+
exitCode: -1,
|
|
318
|
+
finalText: null,
|
|
319
|
+
errorMessage: input.errorMessage,
|
|
320
|
+
send: (content) => sendAgentMessage(config.serverUrl, cred.token, input.channelId, {
|
|
321
|
+
content,
|
|
322
|
+
force: true,
|
|
323
|
+
}),
|
|
324
|
+
});
|
|
281
325
|
}
|
|
282
326
|
/** 随 error 活动上送的 stderr 尾部上限。 */
|
|
283
327
|
const STDERR_TAIL_CAP = 2000;
|
|
@@ -296,7 +340,9 @@ export function awaitExit(child) {
|
|
|
296
340
|
resolve(r);
|
|
297
341
|
};
|
|
298
342
|
child.on("error", (e) => settle({ exitCode: -1, spawnError: e.message }));
|
|
299
|
-
child.on("close", (code) => settle(
|
|
343
|
+
child.on("close", (code, signal) => settle(code === null
|
|
344
|
+
? { exitCode: 128, terminationSignal: signal ?? "unknown" }
|
|
345
|
+
: { exitCode: code }));
|
|
300
346
|
});
|
|
301
347
|
}
|
|
302
348
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function normalizeScheduledPolicy(value) {
|
|
2
|
+
return value === "always_report" ? "always_report" : "silent_unless_report";
|
|
3
|
+
}
|
|
4
|
+
export function normalizeScheduledContext(input) {
|
|
5
|
+
return {
|
|
6
|
+
jobId: input.jobId,
|
|
7
|
+
runId: input.runId,
|
|
8
|
+
title: input.title?.trim() || "Scheduled job",
|
|
9
|
+
outputPolicy: normalizeScheduledPolicy(input.outputPolicy),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export async function deliverScheduledReport(input) {
|
|
13
|
+
const title = input.title.trim() || "Scheduled job";
|
|
14
|
+
let source = "none";
|
|
15
|
+
let content = null;
|
|
16
|
+
if (input.exitCode !== 0) {
|
|
17
|
+
source = "failure_notice";
|
|
18
|
+
const detail = input.errorMessage?.trim().slice(0, 500);
|
|
19
|
+
content = `Scheduled job "${title}" failed${detail ? `: ${detail}` : ` (exit ${input.exitCode})`}.`;
|
|
20
|
+
}
|
|
21
|
+
else if (input.policy === "always_report") {
|
|
22
|
+
if (input.finalText?.trim()) {
|
|
23
|
+
source = "runtime_final";
|
|
24
|
+
content = input.finalText.trim();
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
source = "empty_notice";
|
|
28
|
+
content = `Scheduled job "${title}" completed without a usable report.`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!content) {
|
|
32
|
+
return { required: false, attempted: false, delivered: false, source: "none" };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const sent = await input.send(content);
|
|
36
|
+
return {
|
|
37
|
+
required: true,
|
|
38
|
+
attempted: true,
|
|
39
|
+
delivered: sent.delivered,
|
|
40
|
+
status: sent.status,
|
|
41
|
+
source,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { required: true, attempted: true, delivered: false, source };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function fields(input) {
|
|
2
|
+
return {
|
|
3
|
+
scheduled_run_id: input.scheduledRunId,
|
|
4
|
+
run_id: input.runId,
|
|
5
|
+
agent_handle: input.agentHandle,
|
|
6
|
+
channel_id: input.channelId,
|
|
7
|
+
exit_code: input.exitCode,
|
|
8
|
+
...(input.runtime ? { runtime: input.runtime } : {}),
|
|
9
|
+
...(input.model ? { model: input.model } : {}),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function reportScheduledRunComplete(socket, input, log) {
|
|
13
|
+
if (!socket) {
|
|
14
|
+
log("scheduled_run.complete_send_failed", "定时任务完成回报未发送:控制面连接不可用", {
|
|
15
|
+
level: "WARN", ...fields(input), error_message: "control socket unavailable",
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const payload = JSON.stringify({
|
|
20
|
+
type: "agent:run-complete",
|
|
21
|
+
agentHandle: input.agentHandle,
|
|
22
|
+
channelId: input.channelId,
|
|
23
|
+
scheduledRunId: input.scheduledRunId,
|
|
24
|
+
exitCode: input.exitCode,
|
|
25
|
+
...(input.runtime ? { runtime: input.runtime } : {}),
|
|
26
|
+
...(input.model !== undefined ? { model: input.model } : {}),
|
|
27
|
+
...(input.resumed !== undefined ? { resumed: input.resumed } : {}),
|
|
28
|
+
...(input.errorMessage ? { errorMessage: input.errorMessage } : {}),
|
|
29
|
+
...(input.usage ? { usage: input.usage } : {}),
|
|
30
|
+
...(input.report ? { report: input.report } : {}),
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
socket.send(payload, (error) => {
|
|
34
|
+
if (error) {
|
|
35
|
+
log("scheduled_run.complete_send_failed", "定时任务完成回报发送失败", {
|
|
36
|
+
level: "WARN", ...fields(input), error_message: error.message,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
log("scheduled_run.complete_sent", "定时任务完成回报已发送", fields(input));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
log("scheduled_run.complete_send_failed", "定时任务完成回报发送失败", {
|
|
45
|
+
level: "WARN", ...fields(input), error_message: error.message,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -6,22 +6,30 @@ import { WebSocket } from "ws";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { initSlog, dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
|
|
9
|
-
import { runAgent } from "./runner.js";
|
|
9
|
+
import { reportScheduledStartFailure, runAgent } from "./runner.js";
|
|
10
10
|
import { buildScheduledPrompt } from "./prompt.js";
|
|
11
|
-
import { collectMachineHello } from "./machine-info.js";
|
|
11
|
+
import { collectMachineHello, DAEMON_CAPABILITIES } 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";
|
|
15
15
|
import { listRuntimeModels } from "./list-models.js";
|
|
16
|
+
import { normalizeScheduledContext } from "./scheduled-report.js";
|
|
17
|
+
import { formatDaemonLogLine } from "./log-format.js";
|
|
18
|
+
import { reportScheduledRunComplete } from "./scheduled-run-report.js";
|
|
16
19
|
// normalize.ts 的活动种类 → activity 枚举
|
|
17
20
|
const ACTIVITY_MAP = {
|
|
18
21
|
init: "working", text: "thinking", reading: "reading", sending: "sending",
|
|
19
22
|
checking: "checking", claiming: "claiming", crew: "working", tool: "working",
|
|
20
23
|
tool_result: "working", done: "done", error: "error",
|
|
21
24
|
};
|
|
25
|
+
export function buildControlPlaneUrl(serverUrl, machineToken) {
|
|
26
|
+
const query = new URLSearchParams({ key: machineToken });
|
|
27
|
+
for (const capability of DAEMON_CAPABILITIES)
|
|
28
|
+
query.append("capability", capability);
|
|
29
|
+
return `${serverUrl.replace(/^http/, "ws").replace(/\/+$/, "")}/daemon/connect?${query.toString()}`;
|
|
30
|
+
}
|
|
22
31
|
export function serve(config, opts = {}) {
|
|
23
|
-
const wsUrl = config.serverUrl.
|
|
24
|
-
`/daemon/connect?key=${encodeURIComponent(config.machineToken)}`;
|
|
32
|
+
const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken);
|
|
25
33
|
let stopped = false;
|
|
26
34
|
let ws = null;
|
|
27
35
|
let backoff = 1000;
|
|
@@ -55,7 +63,7 @@ export function serve(config, opts = {}) {
|
|
|
55
63
|
if (q && q.length)
|
|
56
64
|
(q.shift())();
|
|
57
65
|
};
|
|
58
|
-
const log = (s) => process.stdout.write(s + "\n");
|
|
66
|
+
const log = (s) => process.stdout.write(formatDaemonLogLine(s) + "\n");
|
|
59
67
|
function connect() {
|
|
60
68
|
if (stopped)
|
|
61
69
|
return;
|
|
@@ -155,7 +163,9 @@ export function serve(config, opts = {}) {
|
|
|
155
163
|
return; // ready/error 等忽略
|
|
156
164
|
// 任务键:scheduled run 用 runId(每次运行独立 cwd/work-log;overlap 由 server 端 skip_if_running 管,
|
|
157
165
|
// daemon 的 running 去重只兜底"同一 run 重复投递");普通唤醒仍是 线程锚点 ?? 频道。
|
|
158
|
-
const scheduled = msg.reason === "scheduled_job" && msg.scheduledRun
|
|
166
|
+
const scheduled = msg.reason === "scheduled_job" && msg.scheduledRun
|
|
167
|
+
? normalizeScheduledContext(msg.scheduledRun)
|
|
168
|
+
: null;
|
|
159
169
|
const threadId = msg.wake?.threadId;
|
|
160
170
|
const taskKey = scheduled ? scheduled.runId : (threadId ?? msg.channelId);
|
|
161
171
|
const key = `${msg.agentHandle}:${taskKey}`;
|
|
@@ -257,7 +267,7 @@ export function serve(config, opts = {}) {
|
|
|
257
267
|
? `crew thread read`
|
|
258
268
|
: `crew message read --channel ${msg.channelId}`;
|
|
259
269
|
const wakeText = scheduled
|
|
260
|
-
? buildScheduledPrompt(msg.channelId, msg.wake?.content ?? "")
|
|
270
|
+
? buildScheduledPrompt(msg.channelId, msg.wake?.content ?? "", scheduled.outputPolicy)
|
|
261
271
|
: (msg.wake?.content
|
|
262
272
|
? `你被唤醒(${msg.reason}): ${msg.wake.content}\n用 ${readCmd} 读${threadId ? "本线程" : "频道"}后按需处理。${reasonHint}${ackHint}${threadHint}${attHint}`
|
|
263
273
|
: undefined);
|
|
@@ -266,7 +276,9 @@ export function serve(config, opts = {}) {
|
|
|
266
276
|
channelId: msg.channelId,
|
|
267
277
|
taskKey, // 每任务隔离 cwd + work-log(并行不冲突)
|
|
268
278
|
runId, // 贯穿 SLS 日志的单轮关联键
|
|
269
|
-
...(scheduled ? {
|
|
279
|
+
...(scheduled ? {
|
|
280
|
+
scheduled: { title: scheduled.title, outputPolicy: scheduled.outputPolicy },
|
|
281
|
+
} : {}),
|
|
270
282
|
// 唤醒锚点是具体消息(非纯频道唤醒)时,把它透传下去,供 `crew task create` 锚定到该消息。
|
|
271
283
|
...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
|
|
272
284
|
...(wakeText ? { wake: wakeText } : {}),
|
|
@@ -295,20 +307,13 @@ export function serve(config, opts = {}) {
|
|
|
295
307
|
}
|
|
296
308
|
// 静默定时 run:结构化完成回报(驱动 server 端 scheduled_run/job 状态落库,§5.5)
|
|
297
309
|
if (scheduled) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
runtime: result.runtime,
|
|
306
|
-
model: result.model,
|
|
307
|
-
resumed: result.resumed,
|
|
308
|
-
...(result.usage ? { usage: result.usage } : {}),
|
|
309
|
-
}));
|
|
310
|
-
}
|
|
311
|
-
catch { /* ws 非 OPEN;server 端 reaper 会按超时回收 */ }
|
|
310
|
+
reportScheduledRunComplete(ws?.readyState === WebSocket.OPEN ? ws : null, {
|
|
311
|
+
runId, scheduledRunId: scheduled.runId, agentHandle: msg.agentHandle,
|
|
312
|
+
channelId: msg.channelId, exitCode: result.exitCode, runtime: result.runtime,
|
|
313
|
+
model: result.model, resumed: result.resumed, usage: result.usage,
|
|
314
|
+
...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
|
|
315
|
+
...(result.report ? { report: result.report } : {}),
|
|
316
|
+
}, dslog);
|
|
312
317
|
}
|
|
313
318
|
// run.end 是排查「任务没跑完就本轮结束」的核心证据:退出码 + 时长 + 最后活动 +
|
|
314
319
|
// 是否 resume + 用量。exit_code!=0 或时长异常短都值得追。
|
|
@@ -350,17 +355,21 @@ export function serve(config, opts = {}) {
|
|
|
350
355
|
error_message: e.message, error_stack: e.stack,
|
|
351
356
|
});
|
|
352
357
|
if (scheduled) {
|
|
358
|
+
let report;
|
|
353
359
|
try {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
agentHandle: msg.agentHandle,
|
|
360
|
+
report = await reportScheduledStartFailure(config, {
|
|
361
|
+
handle: msg.agentHandle,
|
|
357
362
|
channelId: msg.channelId,
|
|
358
|
-
|
|
359
|
-
exitCode: -1,
|
|
363
|
+
scheduled,
|
|
360
364
|
errorMessage: e.message,
|
|
361
|
-
})
|
|
365
|
+
});
|
|
362
366
|
}
|
|
363
|
-
catch { /*
|
|
367
|
+
catch { /* token 也不可用时只能让 server 按 runtime failure 终态化 */ }
|
|
368
|
+
reportScheduledRunComplete(ws?.readyState === WebSocket.OPEN ? ws : null, {
|
|
369
|
+
runId, scheduledRunId: scheduled.runId, agentHandle: msg.agentHandle,
|
|
370
|
+
channelId: msg.channelId, exitCode: -1, errorMessage: e.message,
|
|
371
|
+
...(report ? { report } : {}),
|
|
372
|
+
}, dslog);
|
|
364
373
|
}
|
|
365
374
|
}
|
|
366
375
|
finally {
|
package/dist/session.js
CHANGED
|
@@ -30,6 +30,7 @@ export async function readSession(runDir) {
|
|
|
30
30
|
model: typeof o.model === "string" ? o.model : null,
|
|
31
31
|
providerFingerprint: typeof o.providerFingerprint === "string" ? o.providerFingerprint : null,
|
|
32
32
|
...(typeof o.contextTokens === "number" && o.contextTokens >= 0 ? { contextTokens: o.contextTokens } : {}),
|
|
33
|
+
...(typeof o.lastExitOk === "boolean" ? { lastExitOk: o.lastExitOk } : {}),
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
catch {
|
|
@@ -60,6 +61,10 @@ export function pickResumeId(prior, now, warmMs, currentModel = null, budgetToke
|
|
|
60
61
|
// 下,--resume 会直接报错,必须冷启动。
|
|
61
62
|
if ((prior.providerFingerprint ?? null) !== currentProviderFingerprint)
|
|
62
63
|
return null;
|
|
64
|
+
// 上轮崩溃/超窗/被杀:会话大概率已污染(或就是太大),续用几乎必再崩。强制冷启动斩断循环——
|
|
65
|
+
// 这条不依赖 token 记账是否准确(OOM/SIGKILL 时没有 usage,预算判断会漏,此条兜底)。
|
|
66
|
+
if (prior.lastExitOk === false)
|
|
67
|
+
return null;
|
|
63
68
|
if (warmMs > 0 && now - prior.lastRunAt > warmMs)
|
|
64
69
|
return null;
|
|
65
70
|
if (budgetTokens > 0 && (prior.contextTokens ?? 0) >= budgetTokens)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"cross-spawn": "^7.0.6",
|
|
21
21
|
"ws": "^8",
|
|
22
|
-
"@nowcrew/cli": "^0.4.
|
|
22
|
+
"@nowcrew/cli": "^0.4.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/cross-spawn": "^6.0.6",
|