@nowcrew/daemon 0.5.8 → 0.5.10

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.
@@ -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
+ }
@@ -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(`\n🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...\n`);
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(`\n🚀 ${td("Waking agent")} "${values.agent}" ${td("for channel")} ${values.channel}\n\n`);
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(`\n— ${td("agent exited")} (code ${result.exitCode}), ${td("activities")}: ${result.activities.length} —\n`);
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,18 +19,29 @@ 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 scheduled = ctx.scheduled ?? false;
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 = scheduled
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
- : `## 启动序列
33
- 1. 若本轮已带具体来信,先判断是否需要立即确认/提问/声明接手;需要就先用 \`crew message send\` 发出,再去深挖上下文。
43
+ : `## 启动序列
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。
36
47
  4. 收到消息就处理,并用 \`crew message send\` 回复。
@@ -39,17 +50,81 @@ export function buildSystemPrompt(ctx) {
39
50
  // 消息、没有 task,这条规则字面上无法执行,省略(2026-07-13 复审 Minor 5)。
40
51
  const claimRule = scheduled
41
52
  ? ""
42
- : "\n- **动手干活前必须先 `crew task claim`**;claim 失败就转做别的任务。";
53
+ : "\n- **除定时任务创建与管理外,动手干活前必须先 `crew task claim`**;claim 失败就转做别的任务。";
43
54
  // "一线程一 task 且必须带 --thread"与"进度/产出回本线程,严禁发到顶层"都建立在"本轮有一条
44
55
  // 触发消息、线程根即为该消息 id"这个前提上——scheduled run 没有触发消息、没有 thread 锚点,
45
56
  // 唯一输出路径就是频道顶层 `--send-draft`,与这两条正面矛盾且 --thread 参数根本填不出来,
46
57
  // scheduled 换成静默版表述(2026-07-13 复审 Minor 5)。
47
58
  const threadTaskRule = scheduled
48
59
  ? "\n- **静默任务无线程锚点**:只有存在需要人跟进的具体事项时才用 `crew task create --title \"…\"`(无需 `--thread`——本轮没有触发消息可绑)。"
49
- : `\n- **线程 = 工作单元 / 一个请求一个 task(CRITICAL)**:一条对话(thread)对应一件事,最多绑**一个** task。被唤醒处理来信时,对这件事**只建一个 task,且必须把它绑到当前线程**:\`crew task create --title "…" --thread <触发你的那条消息 id>\`(那条消息就是线程根)。**一定要带 \`--thread\`**——不带会另起一条飘在顶层的新线程,task 就和你的讨论分家了(这正是要避免的)。**别把一个请求拆成多个 task**(如"拉代码"+"读文档"+"写记忆"是同一件事 → 一个 task,用 todo/进度推进,不要建第二个)。当前线程已有 task 时再建会被服务端拒绝(报错会提示你)。`;
50
- const progressRule = scheduled
51
- ? "\n- **产出走频道顶层**:静默 run 唯一的输出路径是 `crew message send --channel <id> --send-draft`(顶层、无 thread)——本轮没有触发消息,没有线程锚点可回。"
52
- : "\n- **进度/产出回本线程**:这件事的认领/进度/完成汇报都用 `crew message send --thread <当前线程根>` 回复在**这个线程里**(线程根 = 触发你的那条消息 id,即 \$CREW_WAKE_MESSAGE_ID)。**严禁把进度发到别的线程或频道顶层。**";
60
+ : `\n- **线程 = 工作单元 / 一个请求一个 task(CRITICAL)**:一条对话(thread)对应一件事,最多绑**一个** task。若这件事需要创建 task,只建一个,且必须把它绑到当前线程:\`crew task create --title "…" --thread <触发你的那条消息 id>\`(那条消息就是线程根)。**一定要带 \`--thread\`**——不带会另起一条飘在顶层的新线程,task 就和你的讨论分家了(这正是要避免的)。**别把一个请求拆成多个 task**(如"拉代码"+"读文档"+"写记忆"是同一件事 → 一个 task,用 todo/进度推进,不要建第二个)。当前线程已有 task 时再建会被服务端拒绝(报错会提示你)。`;
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 scheduleIntentRule = scheduled ? "" : `
67
+
68
+ ## 定时任务创建与管理
69
+ 平台不会用关键词预先区分创建或管理意图,由你根据完整来信和上下文判断用户是要创建定时任务,还是查看、修改、暂停、恢复、取消或立即执行已有任务。
70
+ 1. 定时任务创建与管理无需 \`crew task claim\`;不要仅为创建或管理定时任务而创建 task。
71
+ 2. 创建时使用 \`crew schedule create --agent ${ctx.handle} --channel ${ctx.channelId} --prompt '<text>' (--cron '<expr>' | --at '<ISO>')\`,不得编造或替换当前 agent 身份与频道。
72
+ 3. \`crew schedule create\` 和 \`crew schedule update\` 都必须遵守这条规则:\`prompt\`、\`title\`、\`cron\`、\`at\`、\`timezone\` 的值都必须分别作为单个 shell 参数传入,使用单引号 shell 引用;值内若含单引号,必须按标准方式关闭单引号、写入转义后的单引号、再重新开启单引号。不得在创建或更新持久化前展开或执行用户内容中的 \`$\`、反引号或 \`$()\`。
73
+ 4. 创建时默认使用 \`--output-policy always-report\`。创建时只有用户明确要求静默、不发送正常结果或仅在异常时汇报时,才使用 \`--output-policy on-exception\`。
74
+ 5. 更新时若用户未明确要求改变汇报行为,必须省略 \`--output-policy\` 并保留已有策略。只有用户明确要求改变汇报行为时,才按同样规则映射输出策略:正常汇报用 \`always-report\`,静默或仅异常汇报用 \`on-exception\`。创建或更新汇报行为时,把用户原始的汇报条件保留在 \`--prompt\` 中,不要改写或省略。汇报要求确实有歧义时,先向用户确认。
75
+ 6. 执行时间、时区或执行指令信息不足时,先向用户确认,不得猜测。
76
+ 7. 查询现状或执行 create/update/pause/resume/cancel/run-now 任何操作前,先运行 \`crew schedule list --channel ${ctx.channelId} --agent ${ctx.handle} --json\` 查询本频道绑定到你自己的候选;仅查看时也使用这条 JSON 查询,以获取完整的 cron/at/timezone/prompt/output policy。
77
+ 8. 只有一个明确匹配时,才运行 \`crew schedule update <jobId> ...\` 或对应 pause/resume/cancel/run-now 命令。有多个合理候选时,列出候选并先让用户选择;用户选定前不得修改任何任务。
78
+ 9. 修改请求不能用 \`crew schedule create\` 代替。完成后回复 job ID、最终 schedule、timezone 和 output policy;更新还要列出变更字段的前后值。`;
79
+ const voiceRule = alwaysReport
80
+ ? "- **本轮报告只写在 runtime 最终回复中**:不要调用 `crew message send`;daemon 会把最终回复投递到频道。"
81
+ : "- **始终只通过 crew CLI 发声。在 crew 命令之外产生的任何文字都不会送达任何人。**";
82
+ const actionRule = scheduled
83
+ ? `- **判定规则**:直接执行本轮定时指令,执行前不需要 claim。${threadTaskRule}${progressRule}`
84
+ : `- **判定规则**:定时任务创建与管理直接按上节执行,无需 task claim;其它来信若需要你"回复之外的动作"(跑工具/改代码/做变更),先 claim;若只是回答问题或闲聊,无需 claim。${threadTaskRule}${progressRule}`;
85
+ const taskAndScheduleCommands = alwaysReport
86
+ ? `4. **\`crew task create --channel <id> --title "<标题>"\`** —— 仅为本轮发现的具体后续事项创建任务;本轮没有线程锚点,不要传 \`--thread\`。`
87
+ : scheduled
88
+ ? `5. **\`crew task create --channel <id> --title "<标题>"\`** —— 仅为本轮发现的具体后续事项创建任务;本轮没有线程锚点,不要传 \`--thread\`。`
89
+ : `5. **\`crew task list --channel <id>\`** —— 看任务板。支持 \`--status <s>\` / \`--mine\`。
90
+ 6. **\`crew task create --channel <id> --title "<标题>" --thread <当前线程根msgId>\`** —— 新建任务并**绑定到当前线程**。\`--thread\` 传你读到的那条**触发消息 id**(线程根),任务就和讨论同处一个线程。省略 \`--thread\` 会另起新线程,**几乎总是错的——务必带上**。
91
+ 7. **\`crew task claim <taskId>\`** —— 除定时任务创建与管理外,执行需要动作的工作前先认领任务。
92
+ 8. **\`crew task update <taskId> --status <in_progress|in_review|done>\`** —— 推进任务状态。
93
+ 9. **\`crew task unclaim <taskId>\`** —— 释放认领,把任务让给别人。
94
+ 10. **\`crew task assign <taskId> --to <handle>\`** —— 把任务指派/交接给另一个 agent(用于交接,见下)。
95
+ 11. **\`crew schedule create --agent <handle> --channel <id> --prompt <text> [--title <t>] (--cron <expr> | --at <ISO>) [--timezone <iana>] [--output-policy <always-report|on-exception>]\`** —— 创建定时任务。
96
+ 12. **\`crew schedule list --channel <id> --agent <handle> [--json]\`** —— 查询本频道指定 agent 的定时任务,\`--json\` 返回完整字段。
97
+ 13. **\`crew schedule update <jobId> ...\`** —— 修改定时任务标题、指令、时间或输出策略。
98
+ 14. **\`crew schedule pause|resume|cancel|run-now <jobId>\`** —— 控制定时任务。`;
99
+ const communicationSection = alwaysReport
100
+ ? `## 控制面工具 —— crew CLI
101
+ 需要读取频道或操作任务时使用 crew CLI;最终报告不得通过消息命令发送。可使用:
102
+ 1. **\`crew whoami\`** —— 查看你自己的身份。
103
+ 2. **\`crew message read --channel <id>\`** —— 读取频道历史。
104
+ 3. **\`crew message check --channel <id>\`** —— 非阻塞查看未读数。
105
+ ${taskAndScheduleCommands}`
106
+ : `## 通信 —— 只能用 crew CLI
107
+ 所有 chat / task 操作必须经 \`crew\` CLI(daemon 已把它注入你的 PATH)。仅可使用以下命令:
108
+ 1. **\`crew whoami\`** —— 查看你自己的身份。
109
+ 2. **\`crew message read --channel <id>\`** —— 读频道历史(读取即自动推进你的已读/新鲜度游标)。支持 \`--after <seq>\` / \`--limit <n>\`。
110
+ 3. **\`crew message check --channel <id>\`** —— 非阻塞查看未读数。工作中可在自然断点随时用。
111
+ 4. **\`crew message send --channel <id>\`** —— 发消息。正文从 stdin 读,用 heredoc 避免 shell 解释引号/反引号/代码块:
112
+ \`\`\`bash
113
+ crew message send --channel <id> <<'CREWMSG'
114
+ 你的消息正文,可含 "引号"、\\\`反引号\\\`、代码块。
115
+ CREWMSG
116
+ \`\`\`
117
+ 也可用 \`--content "<短正文>"\`。线程内回复:加 \`--thread <完整线程根消息 id>\`。被唤醒时优先使用环境变量 \`$CREW_WAKE_MESSAGE_ID\` 或唤醒提示里的完整 id,不要手动截短。
118
+ ${taskAndScheduleCommands}`;
119
+ const freshnessRule = alwaysReport
120
+ ? ""
121
+ : `
122
+ - **freshness/draft**:发送若被保存为 draft(kind=held),要么重读后用普通 send 改写,要么用 \`crew message send --send-draft\` 原样发出(不要在改内容时用 --send-draft)。`;
123
+ const interactiveTaskRules = scheduled ? "" : `
124
+ - **毫不相关的新任务才另起线程**:只有要处理的事**和当前线程毫不相关**(或用户明确要求新建)时,才用 \`crew task create --new-thread --title "…"\`——系统另起一个子线程(parent=当前线程)绑新 task;之后这件事的回复要发到**这个新子线程**里。能不拆就不拆。
125
+ - 任务状态流:\`todo → in_progress → in_review → done\`。claim 后用 \`crew task update\` 推进:开工→in_progress、完成待验收→in_review、人类确认后→done。只有 assignee 能改自己任务的状态。
126
+ - **交接(handoff)**:当你这一环干完、需要别的角色接手时(如开发完成 → 交给 QA 测试),用 \`crew task assign <taskId> --to <下家handle>\` 把任务交接出去,并在线程里给下家足够背景(分支名 / 改动摘要 / 测试建议)。交接后对方会被自动唤醒。**别让任务停在你手里无人跟进**。
127
+ - **分诊(若你是总管)**:若你收到「【分诊请求】」唤醒,说明频道里有一个无人认领的任务需要你按团队职责分派。唤醒内容里已附上团队成员及其职责:判断谁最合适,用 \`crew task assign <taskId> --to <handle>\` 指派给他(若该你自己做就 \`crew task claim\`);确实没人合适时,在频道里 @发起人 说明并给建议,**不要让任务悬空**。`;
53
128
  // 协作礼仪:整节都在讲"如何对人主动发声/插话/回报"——scheduled run 没有对话对象,
54
129
  // 这节整体不适用,省略(不是"粗暴删",是这节内容本身就是协作场景专属)。
55
130
  const collaborationEtiquette = scheduled
@@ -59,21 +134,52 @@ export function buildSystemPrompt(ctx) {
59
134
  ## 协作礼仪
60
135
  - **尊重正在进行的对话**:人类正与他人一来一回时,除非明确 @你或显然在叫你,否则不要插话。
61
136
  - **只有真正干活的人来汇报**:别替别人总结或冒领他们的工作。
62
- - **claim 后再动手**:claim 失败立即停手,换一个任务。
137
+ - **除定时任务创建与管理外,claim 后再动手**:claim 失败立即停手,换一个任务。
63
138
  - **停止前检查你欠的具体阻塞项**:若你还欠某人一个 handoff/review/决定/回复且正卡着对方,先发一条最小可行动消息再停。
64
139
  - **少发废话**:只在有可行动内容时发消息,不要播报"我在等/我空闲"。`;
140
+ // 对人说话:约束的是"表达方式",不是"执行规则"——claim/thread/freshness 等操作协议照常执行,
141
+ // 只是这些协议词不进入发给人的消息正文。scheduled/always-report 的产出(异常报告/最终报告)
142
+ // 同样是给人看的,注入精简版(无 task/thread 语境,只留可读性分层规则)。
143
+ const humanVoiceCore = `
144
+ - 消息开头用**不了解内部系统的同事也能看懂的话**给出结论和影响;证据与技术细节放在后面。英文枚举/状态码/字段名/函数名不要求翻译成中文,但首次出现要附一句中文说明,如 "job_not_found(在岗位下拉里没找到目标岗位)"——技术标识符是证据,保留它,给它加注。
145
+ - 内部系统/工具名词(日志平台、网关、监控面板等)首次出现时用半句话说明它是什么;同一线程只解释一次,后续直接用。
146
+ - sid、conversationId、环境标识等溯源 ID 有价值,要保留,但统一放在消息**末尾单独一行**(如 "溯源: sid=469676 / conv=894578…/ prod"),别塞进第一句的括号里。`;
147
+ const humanLanguageSection = scheduled
148
+ ? `
149
+
150
+ ## 对人说话(报告可读性)
151
+ 本节只约束表达方式,不改变上面的任何执行与输出规则:${humanVoiceCore}`
152
+ : `
153
+
154
+ ## 对人说话(CRITICAL — 协议语言 ≠ 人话)
155
+ 本节只约束表达方式,**不改变上面的任何执行规则**(该 claim 照 claim、该带 --thread 照带、该推进状态照推进):
156
+ - task / claim / thread / in_review / draft 这些词是你和平台之间的操作协议,用命令执行即可,**不要写进发给人的消息正文**。接手说"这个问题我来跟进",不说"我接 task #163"或"已 claim";完成待确认说"已查完/已修好,等你确认",不说"task 已置为 in_review";会话中断后恢复,直接接着说进展,不写"(断连恢复)"之类的内部事件。${humanVoiceCore}
157
+ - 发出前自查:一位不了解内部系统的同事只读前三行,能否知道**结论是什么、影响谁、需要他做什么**?不能就重写前三行。`;
65
158
  // 沟通风格:原文"收到任务先确认并简述计划;多步工作发简短进度"同样与静默冲突,
66
159
  // scheduled 换成明确的"默认不输出"版本。
67
- const communicationStyle = scheduled
160
+ const communicationStyle = alwaysReport
68
161
  ? `
69
162
 
163
+ ## 沟通风格(每轮汇报定时任务)
164
+ 不要发送确认或过程消息。只在 runtime 最终回复中给出一次完整、自包含的结果,由 daemon 统一投递。`
165
+ : scheduled
166
+ ? `
167
+
70
168
  ## 沟通风格(静默定时任务)
71
169
  本轮没有人在等你回复,默认不输出、不叙述过程、不播报进度。只有触发上面"启动序列"里列出的汇报条件时才发消息,内容简明陈述结果/异常即可。`
72
- : `
170
+ : `
73
171
 
74
172
  ## 沟通风格
75
- 用户看不到你的内部推理,所以:收到任务先确认并简述计划;多步工作发简短进度("正在做 2/3…");完成后总结结果。每条一两句,别刷屏。
76
- - 完成汇报要直接说“已在当前线程汇报”或“已在任务线程汇报”,并说明“task #N 已置为 in_review”等事实。不要写“通过 ${product} 线程汇报”这类产品名+线程的生硬说法。`;
173
+ 用户看不到你的内部推理,所以:多步工作发简短进度("正在做 2/3…");完成后总结结果和验证方式。每条一两句,别刷屏。
174
+ - **确认消息要有信息量**:接手时用一两句说清你对问题的理解、打算从哪查起或预计耗时;写不出比"收到,我去查"更多的内容就不单发确认,直接开工,把接手一句并进第一条实质进展。别每次都套同一个句式。
175
+ - 任务状态(claim / in_progress / in_review / done)用 crew 命令推进即可,**不要在消息正文里播报这些字段**;对人只说事实:"我来跟进""已完成,等你确认"。也不要写"通过 ${product} 线程汇报"这类产品名+机制的生硬说法。
176
+ - 同类背景说明(如某数据源在当前环境不可达)在一个线程里说一次就够,后续消息不必逐条重复。`;
177
+ const skillIntentRule = scheduled
178
+ ? ""
179
+ : `
180
+
181
+ ## Skill 指定
182
+ - 如果用户输入 \`/xxxx\`、或消息开头/正文中有形如 \`/skill-name\` 的片段,优先判断用户是否在精准指定某个 skill。若该 skill 可用且适合当前任务,按该 skill 的工作流处理;若名称不明确或不可用,先说明不确定并按最接近的可用能力处理。`;
77
183
  return `你是 "${ctx.handle}",${product}(一个让人类与 AI agent 协作的共享工作区)中的 AI 成员。${product} 为可能运行在不同机器上的人与 agent 提供共享的消息服务。
78
184
 
79
185
  ## 你是谁
@@ -92,24 +198,7 @@ export function buildSystemPrompt(ctx) {
92
198
  - **跨任务协调看实时清单**:用 \`crew task list --mine\` 查你当前所有任务及其状态(谁在排队、谁在评审)。例:你一次只能测一个页面时,据此判断"正在测 #1、#2 排队",在 #2 的工作日志里标注"等 #1 完成"。
93
199
  - 需要更多背景时再 \`Read $CREW_HOME/notes/<topic>.md\`;不要一次把所有笔记读进上下文。
94
200
 
95
- ## 通信 —— 只能用 crew CLI
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(用于交接,见下)。
201
+ ${communicationSection}
113
202
 
114
203
  CLI 成功时打印人类可读文本到 stdout;失败时 stderr 给出错误,并用**退出码**告诉你下一步:
115
204
  - \`4\` = 任务已被他人认领 / 不可认领 → 停手,别抢,转做别的。
@@ -117,20 +206,16 @@ CLI 成功时打印人类可读文本到 stdout;失败时 stderr 给出错误,
117
206
  - \`3\` = 鉴权失败;\`5\` = 目标不存在;\`2\` = 参数错误。
118
207
 
119
208
  CRITICAL 规则:
120
- - **始终只通过 crew CLI 发声。在 crew 命令之外产生的任何文字都不会送达任何人。**
209
+ ${voiceRule}
121
210
  - **一个 shell 命令只跑一个 crew 命令**,读完它的输出,再决定下一条。不要把多个 crew 命令串到一行。${claimRule}
122
211
 
123
- ${startupSequence}
212
+ ${startupSequence}${scheduleIntentRule}
124
213
 
125
214
  ## 消息与任务
126
215
  - 你读到的消息形如 \`#<seq> [<type>] <sender>: <正文>\`,\`type\` 为 \`human\` / \`agent\` / \`system\`。
127
216
  - **\`system\` 消息**通报频道状态变化(如新建任务),除非明确要求你行动(如刚给你指派了任务),否则不要回复。
128
- - **判定规则**:若满足来信需要你"回复之外的动作"(跑工具/改代码/做变更),先 claim;若只是回答问题或闲聊,无需 claim。${threadTaskRule}${progressRule}
129
- - **毫不相关的新任务才另起线程**:只有要处理的事**和当前线程毫不相关**(或用户明确要求新建)时,才用 \`crew task create --new-thread --title "…"\`——系统另起一个子线程(parent=当前线程)绑新 task;之后这件事的回复要发到**这个新子线程**里。能不拆就不拆。
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}
217
+ ${actionRule}
218
+ ${interactiveTaskRules}${freshnessRule}${collaborationEtiquette}${humanLanguageSection}${communicationStyle}${skillIntentRule}
134
219
 
135
220
  ## Workspace 与分层记忆(CRITICAL — 索引+按需,配合上面的并行规则)
136
221
  你的持久记忆在 \`$CREW_HOME\`(跨你所有任务共享),分三层:
@@ -146,7 +231,17 @@ ${ctx.memory ? `\n## [注入] 你的 MEMORY.md(索引,只读参考)\n${ctx.memor
146
231
  /** 静默定时任务的唤醒提示词:不注入协作礼仪/线程提示;默认零输出(设计文档 §5.4)。
147
232
  * 汇报用 --send-draft:绕过 freshness hold(-p 单发不跑 crew read,游标落后,
148
233
  * 普通 send 会被 202 扣成 draft——监控告警绝不能被静默扣留)。 */
149
- export function buildScheduledPrompt(channelId, jobPrompt) {
234
+ export function buildScheduledPrompt(channelId, jobPrompt, outputPolicy = "silent_unless_report") {
235
+ if (outputPolicy === "always_report") {
236
+ return [
237
+ "You are executing a scheduled job. No one is waiting for an acknowledgement.",
238
+ `Scheduled instruction: ${jobPrompt}`,
239
+ "Return exactly one self-contained final report as your runtime final response.",
240
+ "Do not call crew message send; the daemon delivers the final response.",
241
+ "Do not send acknowledgements or progress messages.",
242
+ "Create a crew task only when the result contains a concrete follow-up action.",
243
+ ].join("\n");
244
+ }
150
245
  return [
151
246
  "你正在执行一个静默定时任务(后台运行,频道里没有人在等你回复)。",
152
247
  `- 执行这条定时指令: ${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 = {
@@ -82,7 +83,7 @@ onConsole = () => { }) {
82
83
  agentId: cred.agentId,
83
84
  homeDir: ws.dir,
84
85
  productName: config.productName,
85
- ...(input.scheduled ? { scheduled: true } : {}),
86
+ ...(input.scheduled ? { scheduledOutputPolicy: input.scheduled.outputPolicy } : {}),
86
87
  // 只注入 MEMORY.md 的索引/角色部分(截断),避免上下文膨胀;明细让 agent 按需读 notes/。
87
88
  memory: ws.memory.length > MEMORY_INJECT_CAP
88
89
  ? ws.memory.slice(0, MEMORY_INJECT_CAP) + "\n…(MEMORY.md 过长已截断,详情用 Read 读 $CREW_HOME/MEMORY.md 或 notes/)"
@@ -136,6 +137,7 @@ onConsole = () => { }) {
136
137
  : {}),
137
138
  // fast 模式 → 透传给 runtime(best-effort,供 wrapper/runtime 读取)
138
139
  ...(cfg.fastMode ? { CREW_FAST_MODE: "1" } : {}),
140
+ ...(input.scheduled ? { CREW_SCHEDULE_OUTPUT_POLICY: input.scheduled.outputPolicy } : {}),
139
141
  };
140
142
  // provider custom = BYOC:剔除全局 ANTHROPIC_*/Bedrock 残留 + 按鉴权方式注入端点与密钥,
141
143
  // 与机器全局 Claude 登录态/配置互不干扰(细节见 provider-env.ts)。
@@ -208,15 +210,9 @@ onConsole = () => { }) {
208
210
  activities.push(a);
209
211
  onActivity(a);
210
212
  }
211
- const item = evt.item;
212
- if (evt.type === "item.completed" && item?.type === "agent_message" && item.text?.trim()) {
213
- finalText = item.text.trim();
214
- }
215
- // kimi:最后一条带正文的 assistant 行即最终回答(kimi 无 result/turn.completed 事件)
216
- const kimiMsg = evt;
217
- if (kimiMsg.role === "assistant" && !kimiMsg.type && typeof kimiMsg.content === "string" && kimiMsg.content.trim()) {
218
- finalText = kimiMsg.content.trim();
219
- }
213
+ const extracted = extractFinalText(evt);
214
+ if (extracted)
215
+ finalText = extracted;
220
216
  // 同一事件再透传为终端 console 行(独立于状态活动,内容不压缩)。
221
217
  for (const c of toConsoleLines(evt))
222
218
  onConsole(c);
@@ -228,9 +224,13 @@ onConsole = () => { }) {
228
224
  process.stderr.write(d);
229
225
  stderrTail = (stderrTail + String(d)).slice(-STDERR_TAIL_CAP);
230
226
  });
231
- const { exitCode, spawnError } = await awaitExit(child);
227
+ const { exitCode, spawnError, terminationSignal } = await awaitExit(child);
232
228
  // spawn 本身失败(如 PATH 里没有 runtime 二进制)没有 stderr,把错误并入尾部供上报。
233
- const errorTail = [stderrTail.trim(), spawnError].filter(Boolean).join(" ").trim();
229
+ const errorTail = [
230
+ stderrTail.trim(),
231
+ spawnError,
232
+ terminationSignal ? `terminated by ${terminationSignal}` : undefined,
233
+ ].filter(Boolean).join(" ").trim();
234
234
  const finish = exitActivity(runtime, exitCode, errorTail);
235
235
  if (finish) {
236
236
  activities.push(finish);
@@ -238,7 +238,7 @@ onConsole = () => { }) {
238
238
  if (finish.kind === "error")
239
239
  onConsole({ stream: "error", text: `✖ ${finish.detail ?? finish.label}` });
240
240
  }
241
- if (!input.suppressFallback && (runtime === "codex" || runtime === "kimi") && exitCode === 0 && !sentViaCrew && finalText) {
241
+ if (!input.scheduled && (runtime === "codex" || runtime === "kimi") && exitCode === 0 && !sentViaCrew && finalText) {
242
242
  // force:兜底回帖锚定本轮触发消息的线程,语义上必须送达;不 force 时 agent(-p 单发不跑
243
243
  // crew read)游标落后,回帖会被 freshness hold 成 draft(202)而永远不可见。
244
244
  const sent = await sendAgentMessage(config.serverUrl, cred.token, input.channelId, {
@@ -281,7 +281,47 @@ onConsole = () => { }) {
281
281
  process.stdout.write(`📊 tokens: in=${u.inputTokens} out=${u.outputTokens} cache_read=${u.cacheReadTokens} cache_create=${u.cacheCreationTokens}` +
282
282
  `${u.costUsd != null ? ` cost=$${u.costUsd.toFixed(4)}` : ""} ${resuming ? "(resumed)" : rotatedForBudget ? "(rotated: budget)" : "(fresh)"}\n`);
283
283
  }
284
- return { exitCode, activities, model: observedModel, runtime, resumed: resuming, sessionId, ...(usage ? { usage } : {}) };
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
+ });
285
325
  }
286
326
  /** 随 error 活动上送的 stderr 尾部上限。 */
287
327
  const STDERR_TAIL_CAP = 2000;
@@ -300,7 +340,9 @@ export function awaitExit(child) {
300
340
  resolve(r);
301
341
  };
302
342
  child.on("error", (e) => settle({ exitCode: -1, spawnError: e.message }));
303
- child.on("close", (code) => settle({ exitCode: code ?? 0 }));
343
+ child.on("close", (code, signal) => settle(code === null
344
+ ? { exitCode: 128, terminationSignal: signal ?? "unknown" }
345
+ : { exitCode: code }));
304
346
  });
305
347
  }
306
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.replace(/^http/, "ws") +
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 ? msg.scheduledRun : null;
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 ? { suppressFallback: true, scheduled: true } : {}),
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
- try {
299
- ws?.send(JSON.stringify({
300
- type: "agent:run-complete",
301
- agentHandle: msg.agentHandle,
302
- channelId: msg.channelId,
303
- scheduledRunId: scheduled.runId,
304
- exitCode: result.exitCode,
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
- ws?.send(JSON.stringify({
355
- type: "agent:run-complete",
356
- agentHandle: msg.agentHandle,
360
+ report = await reportScheduledStartFailure(config, {
361
+ handle: msg.agentHandle,
357
362
  channelId: msg.channelId,
358
- scheduledRunId: scheduled.runId,
359
- exitCode: -1,
363
+ scheduled,
360
364
  errorMessage: e.message,
361
- }));
365
+ });
362
366
  }
363
- catch { /* ws OPEN;reaper 兜底 */ }
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
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.1"
22
+ "@nowcrew/cli": "^0.4.3"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/cross-spawn": "^6.0.6",
package/LICENSE DELETED
@@ -1,201 +0,0 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the purposes
44
- of this License, Derivative Works shall not include works that remain
45
- separable from, or merely link (or bind by name) to the interfaces of,
46
- the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including
49
- the original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright owner
52
- or by an individual or Legal Entity authorized to submit on behalf of
53
- the copyright owner. For the purposes of this definition, "submitted"
54
- means any form of electronic, verbal, or written communication sent
55
- to the Licensor or its representatives, including but not limited to
56
- communication on electronic mailing lists, source code control systems,
57
- and issue tracking systems that are managed by, or on behalf of, the
58
- Licensor for the purpose of discussing and improving the Work, but
59
- excluding communication that is conspicuously marked or otherwise
60
- designated in writing by the copyright owner as "Not a Contribution."
61
-
62
- "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by Licensor and
64
- subsequently incorporated within the Work.
65
-
66
- 2. Grant of Copyright License. Subject to the terms and conditions of
67
- this License, each Contributor hereby grants to You a perpetual,
68
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
- copyright license to reproduce, prepare Derivative Works of,
70
- publicly display, publicly perform, sublicense, and distribute the
71
- Work and such Derivative Works in Source or Object form.
72
-
73
- 3. Grant of Patent License. Subject to the terms and conditions of
74
- this License, each Contributor hereby grants to You a perpetual,
75
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
- (except as stated in this section) patent license to make, have made,
77
- use, offer to sell, sell, import, and otherwise transfer the Work,
78
- where such license applies only to those patent claims licensable
79
- by such Contributor that are necessarily infringed by their
80
- Contribution(s) alone or by combination of their Contribution(s)
81
- with the Work to which such Contribution(s) was submitted. If You
82
- institute patent litigation against any entity (including a
83
- cross-claim or counterclaim in a lawsuit) alleging that the Work
84
- or a Contribution incorporated within the Work constitutes direct
85
- or contributory patent infringement, then any patent licenses
86
- granted to You under this License for that Work shall terminate
87
- as of the date such litigation is filed.
88
-
89
- 4. Redistribution. You may reproduce and distribute copies of the
90
- Work or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You
92
- meet the following conditions:
93
-
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
96
-
97
- (b) You must cause any modified files to carry prominent notices
98
- stating that You changed the files; and
99
-
100
- (c) You must retain, in the Source form of any Derivative Works
101
- that You distribute, all copyright, patent, trademark, and
102
- attribution notices from the Source form of the Work,
103
- excluding those notices that do not pertain to any part of
104
- the Derivative Works; and
105
-
106
- (d) If the Work includes a "NOTICE" text file as part of its
107
- distribution, then any Derivative Works that You distribute must
108
- include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding those notices that do not
110
- pertain to any part of the Derivative Works, in at least one
111
- of the following places: within a NOTICE text file distributed
112
- as part of the Derivative Works; within the Source form or
113
- documentation, if provided along with the Derivative Works; or,
114
- within a display generated by the Derivative Works, if and
115
- wherever such third-party notices normally appear. The contents
116
- of the NOTICE file are for informational purposes only and
117
- do not modify the License. You may add Your own attribution
118
- notices within Derivative Works that You distribute, alongside
119
- or as an addendum to the NOTICE text from the Work, provided
120
- that such additional attribution notices cannot be construed
121
- as modifying the License.
122
-
123
- You may add Your own copyright statement to Your modifications and
124
- may provide additional or different license terms and conditions
125
- for use, reproduction, or distribution of Your modifications, or
126
- for any such Derivative Works as a whole, provided Your use,
127
- reproduction, and distribution of the Work otherwise complies with
128
- the conditions stated in this License.
129
-
130
- 5. Submission of Contributions. Unless You explicitly state otherwise,
131
- any Contribution intentionally submitted for inclusion in the Work
132
- by You to the Licensor shall be under the terms and conditions of
133
- this License, without any additional terms or conditions.
134
- Notwithstanding the above, nothing herein shall supersede or modify
135
- the terms of any separate license agreement you may have executed
136
- with Licensor regarding such Contributions.
137
-
138
- 6. Trademarks. This License does not grant permission to use the trade
139
- names, trademarks, service marks, or product names of the Licensor,
140
- except as required for reasonable and customary use in describing the
141
- origin of the Work and reproducing the content of the NOTICE file.
142
-
143
- 7. Disclaimer of Warranty. Unless required by applicable law or
144
- agreed to in writing, Licensor provides the Work (and each
145
- Contributor provides its Contributions) on an "AS IS" BASIS,
146
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
- implied, including, without limitation, any warranties or conditions
148
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
- PARTICULAR PURPOSE. You are solely responsible for determining the
150
- appropriateness of using or redistributing the Work and assume any
151
- risks associated with Your exercise of permissions under this License.
152
-
153
- 8. Limitation of Liability. In no event and under no legal theory,
154
- whether in tort (including negligence), contract, or otherwise,
155
- unless required by applicable law (such as deliberate and grossly
156
- negligent acts) or agreed to in writing, shall any Contributor be
157
- liable to You for damages, including any direct, indirect, special,
158
- incidental, or consequential damages of any character arising as a
159
- result of this License or out of the use or inability to use the
160
- Work (including but not limited to damages for loss of goodwill,
161
- work stoppage, computer failure or malfunction, or any and all
162
- other commercial damages or losses), even if such Contributor
163
- has been advised of the possibility of such damages.
164
-
165
- 9. Accepting Warranty or Additional Liability. While redistributing
166
- the Work or Derivative Works thereof, You may choose to offer,
167
- and charge a fee for, acceptance of support, warranty, indemnity,
168
- or other liability obligations and/or rights consistent with this
169
- License. However, in accepting such obligations, You may act only
170
- on Your own behalf and on Your sole responsibility, not on behalf
171
- of any other Contributor, and only if You agree to indemnify,
172
- defend, and hold each Contributor harmless for any liability
173
- incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or additional liability.
175
-
176
- END OF TERMS AND CONDITIONS
177
-
178
- APPENDIX: How to apply the Apache License to your work.
179
-
180
- To apply the Apache License to your work, attach the following
181
- boilerplate notice, with the fields enclosed by brackets "[]"
182
- replaced with your own identifying information. (Don't include
183
- the brackets!) The text should be enclosed in the appropriate
184
- comment syntax for the file format. We also recommend that a
185
- file or class name and description of purpose be included on the
186
- same "printed page" as the copyright notice for easier
187
- identification within third-party archives.
188
-
189
- Copyright 2026 OpenSlock contributors
190
-
191
- Licensed under the Apache License, Version 2.0 (the "License");
192
- you may not use this file except in compliance with the License.
193
- You may obtain a copy of the License at
194
-
195
- http://www.apache.org/licenses/LICENSE-2.0
196
-
197
- Unless required by applicable law or agreed to in writing, software
198
- distributed under the License is distributed on an "AS IS" BASIS,
199
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
- See the License for the specific language governing permissions and
201
- limitations under the License.