@nowcrew/daemon 0.4.4 → 0.5.6
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/config.js +5 -1
- package/dist/list-models.js +4 -1
- package/dist/machine-info.js +3 -2
- package/dist/normalize.js +13 -2
- package/dist/platform.js +8 -0
- package/dist/prompt.js +88 -27
- package/dist/provider-env.js +66 -0
- package/dist/runner.js +106 -33
- package/dist/runtimes/claude.js +9 -1
- package/dist/runtimes/codex.js +13 -2
- package/dist/runtimes/kimi.js +6 -1
- package/dist/serve.js +60 -10
- package/dist/session.js +23 -1
- package/dist/workspace.js +2 -0
- package/package.json +4 -2
package/dist/config.js
CHANGED
|
@@ -33,6 +33,10 @@ export function loadConfig(env = process.env) {
|
|
|
33
33
|
dangerous: env.CREW_RUNTIME_SAFE !== "1", // 默认开启 (headless agent 在自有 workspace 内运行)
|
|
34
34
|
resume: env.CREW_RESUME !== "off" && env.CREW_RESUME !== "0", // 默认开启;一键回退现状用 CREW_RESUME=off
|
|
35
35
|
resumeWarmMs: env.CREW_RESUME_WARM_MS != null ? Number(env.CREW_RESUME_WARM_MS) : 3_600_000, // 默认 1h
|
|
36
|
-
|
|
36
|
+
// 会话预算轮换:hard 120k 触发冷启动轮换;soft 90k 先提醒蒸馏 work-log;30 轮为 usage 缺失的兜底
|
|
37
|
+
sessionBudgetTokens: env.CREW_SESSION_BUDGET_TOKENS != null ? Number(env.CREW_SESSION_BUDGET_TOKENS) : 120_000,
|
|
38
|
+
sessionSoftTokens: env.CREW_SESSION_SOFT_TOKENS != null ? Number(env.CREW_SESSION_SOFT_TOKENS) : 90_000,
|
|
39
|
+
sessionMaxTurns: env.CREW_SESSION_MAX_TURNS != null ? Number(env.CREW_SESSION_MAX_TURNS) : 30,
|
|
40
|
+
productName: env.CREW_PRODUCT_NAME ?? "nowwork",
|
|
37
41
|
};
|
|
38
42
|
}
|
package/dist/list-models.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
|
|
3
|
+
import { isWin } from "./platform.js";
|
|
4
|
+
const execFileRaw = promisify(execFile);
|
|
5
|
+
// win32 上 npm CLI 是 .cmd shim,execFile 需 shell 才能执行;参数全是固定字面量,无注入面。
|
|
6
|
+
const execFileP = (bin, args) => execFileRaw(bin, args, { shell: isWin() });
|
|
4
7
|
export async function listRuntimeModels(runtime) {
|
|
5
8
|
switch (runtime) {
|
|
6
9
|
case "codex":
|
package/dist/machine-info.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 采集本机信息上报给控制面 (machine:hello):hostname / os / daemon 版本 / 已装 runtimes。
|
|
3
|
-
* runtimes 探测靠 `which <bin
|
|
3
|
+
* runtimes 探测靠 `which <bin>`(win32 用 `where`),只报真实可执行的 CLI(用于展示 Detected Runtimes)。
|
|
4
4
|
*/
|
|
5
5
|
import { hostname, arch, platform } from "node:os";
|
|
6
|
+
import { lookupCmd } from "./platform.js";
|
|
6
7
|
import { execFile } from "node:child_process";
|
|
7
8
|
import { promisify } from "node:util";
|
|
8
9
|
import { readFileSync } from "node:fs";
|
|
@@ -24,7 +25,7 @@ const RUNTIME_BINS = [
|
|
|
24
25
|
];
|
|
25
26
|
async function isInstalled(bin) {
|
|
26
27
|
try {
|
|
27
|
-
await execFileP(
|
|
28
|
+
await execFileP(lookupCmd(), [bin]);
|
|
28
29
|
return true;
|
|
29
30
|
}
|
|
30
31
|
catch {
|
package/dist/normalize.js
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* 这是 daemon 的关键职责:前端/server 看到的是"在读历史/领任务/发消息",而非裸 Bash。
|
|
5
5
|
* 纯函数,无 IO,完整单测。
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Strip /bin/zsh -lc, /bin/sh -c, or /bin/bash -lc shell wrappers (single OR double quotes),
|
|
9
|
+
* including multi-line heredoc forms. [\s\S]+ matches across newlines. Backreference \1 ensures
|
|
10
|
+
* opening and closing quotes match. classifyCommand requires the result to START with the crew
|
|
11
|
+
* prefix, keeping false-positive risk low (e.g. echo "crew message send" stays as tool).
|
|
12
|
+
* Codex runtime uses zsh/bash -lc and switches between single/double quotes depending on context
|
|
13
|
+
* (e.g. heredoc <<'CREWMSG' triggers double-quote wrapper: /bin/zsh -lc "cmd").
|
|
14
|
+
*/
|
|
15
|
+
function unwrapShellCommand(command) {
|
|
16
|
+
const m = command.match(/^\/bin\/(?:zsh\s+-lc|sh\s+-c|bash\s+-lc)\s+(['"])([\s\S]+)\1$/);
|
|
17
|
+
return m?.[2] ?? command;
|
|
18
|
+
}
|
|
7
19
|
/** 把一条 Bash 命令归类为语义活动 (仅看 crew 子命令)。 */
|
|
8
20
|
export function classifyCommand(command) {
|
|
9
21
|
const c = command.trim();
|
|
@@ -45,8 +57,7 @@ export function normalizeEvent(event) {
|
|
|
45
57
|
}
|
|
46
58
|
if (e.type === "item.completed" && e.item?.type === "command_execution" && e.item.command?.trim()) {
|
|
47
59
|
const command = e.item.command.trim();
|
|
48
|
-
|
|
49
|
-
return [classifyCommand(shell?.[1] ?? command)];
|
|
60
|
+
return [classifyCommand(unwrapShellCommand(command))];
|
|
50
61
|
}
|
|
51
62
|
if (e.type === "turn.completed") {
|
|
52
63
|
return [{ kind: "done", label: "本轮结束" }];
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 平台差异集中点(win32 vs unix)。
|
|
3
|
+
* Windows 三个坑:没有 `which`(用 `where`);npm 全局 CLI 是 .cmd shim,
|
|
4
|
+
* spawn/execFile 不带 shell 无法执行(Node 18.20+ 直接拒绝);shebang 脚本不可执行。
|
|
5
|
+
*/
|
|
6
|
+
export const isWin = (p = process.platform) => p === "win32";
|
|
7
|
+
/** 探测可执行文件用的命令:win32 = where,其余 = which。 */
|
|
8
|
+
export const lookupCmd = (p = process.platform) => (isWin(p) ? "where" : "which");
|
package/dist/prompt.js
CHANGED
|
@@ -1,12 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 系统提示词与唤醒提示词构造(
|
|
2
|
+
* 系统提示词与唤醒提示词构造(nowwork 自有设计)。
|
|
3
3
|
*
|
|
4
|
-
* 描述的是
|
|
4
|
+
* 描述的是 nowwork 自身的 crew CLI 命令面与运行约定:crew-only 通信、一命令一调用、
|
|
5
5
|
* claim-before-work、freshness/draft、"做完所有事再停"、inbox notice 语义、
|
|
6
6
|
* 分层记忆与压缩安全、协作礼仪。措辞为本项目原创。
|
|
7
7
|
*/
|
|
8
|
+
/** work-log 注入上限(字符):防冷启动注入随文件膨胀。超限保头(目标/结论)+尾(最近进展),
|
|
9
|
+
* 截中段并提示读全文——全文仍在盘上,只限"默认推送量"不限"可获取量"。纯函数。 */
|
|
10
|
+
export const WORKLOG_INJECT_CAP = 8000;
|
|
11
|
+
const WORKLOG_HEAD = 4800;
|
|
12
|
+
const WORKLOG_TAIL = 3000;
|
|
13
|
+
export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
|
|
14
|
+
if (workLog.length <= cap)
|
|
15
|
+
return workLog;
|
|
16
|
+
const head = workLog.slice(0, WORKLOG_HEAD);
|
|
17
|
+
const tail = workLog.slice(-WORKLOG_TAIL);
|
|
18
|
+
return `${head}\n…(work-log 过长已截去中段,全文用 Read 读 $CREW_TASK_LOG)\n${tail}`;
|
|
19
|
+
}
|
|
8
20
|
export function buildSystemPrompt(ctx) {
|
|
9
|
-
const product = ctx.productName ?? "
|
|
21
|
+
const product = ctx.productName ?? "nowwork";
|
|
22
|
+
const scheduled = ctx.scheduled ?? false;
|
|
23
|
+
// 启动序列:非 scheduled 保持原文(含"先确认接手"/"收到消息就处理并回复"——这两条是
|
|
24
|
+
// 协作场景的核心礼仪);scheduled 换成静默版,不出现任何"先确认/先回复"的措辞。
|
|
25
|
+
const startupSequence = scheduled
|
|
26
|
+
? `## 启动序列(静默定时任务)
|
|
27
|
+
1. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
28
|
+
2. 执行唤醒提示词里给出的定时指令。**默认零输出**:不判断"是否需要先确认/声明接手",本轮没有人在等你回复。
|
|
29
|
+
3. 只有发现异常、需要人处理、或指令明确要求汇报时,才用 \`crew message send --send-draft\` 输出到频道(用法见上面"通信"一节)。
|
|
30
|
+
4. 只有存在需要后续跟进的具体事项时,才用 \`crew task create\` 创建任务。
|
|
31
|
+
5. 无异常时直接结束,不需要任何输出,不必读频道历史(除非指令要求)。`
|
|
32
|
+
: `## 启动序列
|
|
33
|
+
1. 若本轮已带具体来信,先判断是否需要立即确认/提问/声明接手;需要就先用 \`crew message send\` 发出,再去深挖上下文。
|
|
34
|
+
2. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
35
|
+
3. 若本轮只有"有未读"的 inbox notice、没有正文:notice 表示存在你尚未看到的消息(正文被暂时省略以免刷屏,不是没有内容)。是否读、何时读由你判断,可用 \`crew message check\` / \`crew message read\` 拉取。**绝不能仅凭一条 content-free notice 就断定"没有工作"**;若选择暂不读,要诚实当作 defer。
|
|
36
|
+
4. 收到消息就处理,并用 \`crew message send\` 回复。
|
|
37
|
+
5. **做完你负责的所有事再停。** 多步任务(调研/改码/测试)要全部完成、汇报结果后再停。新消息会在你存活期间自动送达,无需轮询等待。`;
|
|
38
|
+
// "动手干活前必须先 claim"是协作场景的强约束(有 task 可 claim);scheduled run 没有触发
|
|
39
|
+
// 消息、没有 task,这条规则字面上无法执行,省略(2026-07-13 复审 Minor 5)。
|
|
40
|
+
const claimRule = scheduled
|
|
41
|
+
? ""
|
|
42
|
+
: "\n- **动手干活前必须先 `crew task claim`**;claim 失败就转做别的任务。";
|
|
43
|
+
// "一线程一 task 且必须带 --thread"与"进度/产出回本线程,严禁发到顶层"都建立在"本轮有一条
|
|
44
|
+
// 触发消息、线程根即为该消息 id"这个前提上——scheduled run 没有触发消息、没有 thread 锚点,
|
|
45
|
+
// 唯一输出路径就是频道顶层 `--send-draft`,与这两条正面矛盾且 --thread 参数根本填不出来,
|
|
46
|
+
// scheduled 换成静默版表述(2026-07-13 复审 Minor 5)。
|
|
47
|
+
const threadTaskRule = scheduled
|
|
48
|
+
? "\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)。**严禁把进度发到别的线程或频道顶层。**";
|
|
53
|
+
// 协作礼仪:整节都在讲"如何对人主动发声/插话/回报"——scheduled run 没有对话对象,
|
|
54
|
+
// 这节整体不适用,省略(不是"粗暴删",是这节内容本身就是协作场景专属)。
|
|
55
|
+
const collaborationEtiquette = scheduled
|
|
56
|
+
? ""
|
|
57
|
+
: `
|
|
58
|
+
|
|
59
|
+
## 协作礼仪
|
|
60
|
+
- **尊重正在进行的对话**:人类正与他人一来一回时,除非明确 @你或显然在叫你,否则不要插话。
|
|
61
|
+
- **只有真正干活的人来汇报**:别替别人总结或冒领他们的工作。
|
|
62
|
+
- **claim 后再动手**:claim 失败立即停手,换一个任务。
|
|
63
|
+
- **停止前检查你欠的具体阻塞项**:若你还欠某人一个 handoff/review/决定/回复且正卡着对方,先发一条最小可行动消息再停。
|
|
64
|
+
- **少发废话**:只在有可行动内容时发消息,不要播报"我在等/我空闲"。`;
|
|
65
|
+
// 沟通风格:原文"收到任务先确认并简述计划;多步工作发简短进度"同样与静默冲突,
|
|
66
|
+
// scheduled 换成明确的"默认不输出"版本。
|
|
67
|
+
const communicationStyle = scheduled
|
|
68
|
+
? `
|
|
69
|
+
|
|
70
|
+
## 沟通风格(静默定时任务)
|
|
71
|
+
本轮没有人在等你回复,默认不输出、不叙述过程、不播报进度。只有触发上面"启动序列"里列出的汇报条件时才发消息,内容简明陈述结果/异常即可。`
|
|
72
|
+
: `
|
|
73
|
+
|
|
74
|
+
## 沟通风格
|
|
75
|
+
用户看不到你的内部推理,所以:收到任务先确认并简述计划;多步工作发简短进度("正在做 2/3…");完成后总结结果。每条一两句,别刷屏。
|
|
76
|
+
- 完成汇报要直接说“已在当前线程汇报”或“已在任务线程汇报”,并说明“task #N 已置为 in_review”等事实。不要写“通过 ${product} 线程汇报”这类产品名+线程的生硬说法。`;
|
|
10
77
|
return `你是 "${ctx.handle}",${product}(一个让人类与 AI agent 协作的共享工作区)中的 AI 成员。${product} 为可能运行在不同机器上的人与 agent 提供共享的消息服务。
|
|
11
78
|
|
|
12
79
|
## 你是谁
|
|
@@ -51,50 +118,44 @@ CLI 成功时打印人类可读文本到 stdout;失败时 stderr 给出错误,
|
|
|
51
118
|
|
|
52
119
|
CRITICAL 规则:
|
|
53
120
|
- **始终只通过 crew CLI 发声。在 crew 命令之外产生的任何文字都不会送达任何人。**
|
|
54
|
-
- **一个 shell 命令只跑一个 crew 命令**,读完它的输出,再决定下一条。不要把多个 crew
|
|
55
|
-
- **动手干活前必须先 \`crew task claim\`**;claim 失败就转做别的任务。
|
|
121
|
+
- **一个 shell 命令只跑一个 crew 命令**,读完它的输出,再决定下一条。不要把多个 crew 命令串到一行。${claimRule}
|
|
56
122
|
|
|
57
|
-
|
|
58
|
-
1. 若本轮已带具体来信,先判断是否需要立即确认/提问/声明接手;需要就先用 \`crew message send\` 发出,再去深挖上下文。
|
|
59
|
-
2. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
60
|
-
3. 若本轮只有"有未读"的 inbox notice、没有正文:notice 表示存在你尚未看到的消息(正文被暂时省略以免刷屏,不是没有内容)。是否读、何时读由你判断,可用 \`crew message check\` / \`crew message read\` 拉取。**绝不能仅凭一条 content-free notice 就断定"没有工作"**;若选择暂不读,要诚实当作 defer。
|
|
61
|
-
4. 收到消息就处理,并用 \`crew message send\` 回复。
|
|
62
|
-
5. **做完你负责的所有事再停。** 多步任务(调研/改码/测试)要全部完成、汇报结果后再停。新消息会在你存活期间自动送达,无需轮询等待。
|
|
123
|
+
${startupSequence}
|
|
63
124
|
|
|
64
125
|
## 消息与任务
|
|
65
126
|
- 你读到的消息形如 \`#<seq> [<type>] <sender>: <正文>\`,\`type\` 为 \`human\` / \`agent\` / \`system\`。
|
|
66
127
|
- **\`system\` 消息**通报频道状态变化(如新建任务),除非明确要求你行动(如刚给你指派了任务),否则不要回复。
|
|
67
|
-
- **判定规则**:若满足来信需要你"回复之外的动作"(跑工具/改代码/做变更),先 claim;若只是回答问题或闲聊,无需 claim
|
|
68
|
-
- **线程 = 工作单元 / 一个请求一个 task(CRITICAL)**:一条对话(thread)对应一件事,最多绑**一个** task。被唤醒处理来信时,对这件事**只建一个 task,且必须把它绑到当前线程**:\`crew task create --title "…" --thread <触发你的那条消息 id>\`(那条消息就是线程根)。**一定要带 \`--thread\`**——不带会另起一条飘在顶层的新线程,task 就和你的讨论分家了(这正是要避免的)。**别把一个请求拆成多个 task**(如"拉代码"+"读文档"+"写记忆"是同一件事 → 一个 task,用 todo/进度推进,不要建第二个)。当前线程已有 task 时再建会被服务端拒绝(报错会提示你)。
|
|
69
|
-
- **进度/产出回本线程**:这件事的认领/进度/完成汇报都用 \`crew message send --thread <当前线程根>\` 回复在**这个线程里**(线程根 = 触发你的那条消息 id,即 \$CREW_WAKE_MESSAGE_ID)。**严禁把进度发到别的线程或频道顶层。**
|
|
128
|
+
- **判定规则**:若满足来信需要你"回复之外的动作"(跑工具/改代码/做变更),先 claim;若只是回答问题或闲聊,无需 claim。${threadTaskRule}${progressRule}
|
|
70
129
|
- **毫不相关的新任务才另起线程**:只有要处理的事**和当前线程毫不相关**(或用户明确要求新建)时,才用 \`crew task create --new-thread --title "…"\`——系统另起一个子线程(parent=当前线程)绑新 task;之后这件事的回复要发到**这个新子线程**里。能不拆就不拆。
|
|
71
130
|
- 任务状态流:\`todo → in_progress → in_review → done\`。claim 后用 \`crew task update\` 推进:开工→in_progress、完成待验收→in_review、人类确认后→done。只有 assignee 能改自己任务的状态。
|
|
72
131
|
- **交接(handoff)**:当你这一环干完、需要别的角色接手时(如开发完成 → 交给 QA 测试),用 \`crew task assign <taskId> --to <下家handle>\` 把任务交接出去,并在线程里给下家足够背景(分支名 / 改动摘要 / 测试建议)。交接后对方会被自动唤醒。**别让任务停在你手里无人跟进**。
|
|
73
132
|
- **分诊(若你是总管)**:若你收到「【分诊请求】」唤醒,说明频道里有一个无人认领的任务需要你按团队职责分派。唤醒内容里已附上团队成员及其职责:判断谁最合适,用 \`crew task assign <taskId> --to <handle>\` 指派给他(若该你自己做就 \`crew task claim\`);确实没人合适时,在频道里 @发起人 说明并给建议,**不要让任务悬空**。
|
|
74
|
-
- **freshness/draft**:发送若被保存为 draft(kind=held),要么重读后用普通 send 改写,要么用 \`crew message send --send-draft\` 原样发出(不要在改内容时用 --send-draft)
|
|
75
|
-
|
|
76
|
-
## 协作礼仪
|
|
77
|
-
- **尊重正在进行的对话**:人类正与他人一来一回时,除非明确 @你或显然在叫你,否则不要插话。
|
|
78
|
-
- **只有真正干活的人来汇报**:别替别人总结或冒领他们的工作。
|
|
79
|
-
- **claim 后再动手**:claim 失败立即停手,换一个任务。
|
|
80
|
-
- **停止前检查你欠的具体阻塞项**:若你还欠某人一个 handoff/review/决定/回复且正卡着对方,先发一条最小可行动消息再停。
|
|
81
|
-
- **少发废话**:只在有可行动内容时发消息,不要播报"我在等/我空闲"。
|
|
82
|
-
|
|
83
|
-
## 沟通风格
|
|
84
|
-
用户看不到你的内部推理,所以:收到任务先确认并简述计划;多步工作发简短进度("正在做 2/3…");完成后总结结果。每条一两句,别刷屏。
|
|
85
|
-
- 完成汇报要直接说“已在当前线程汇报”或“已在任务线程汇报”,并说明“task #N 已置为 in_review”等事实。不要写“通过 ${product} 线程汇报”这类产品名+线程的生硬说法。
|
|
133
|
+
- **freshness/draft**:发送若被保存为 draft(kind=held),要么重读后用普通 send 改写,要么用 \`crew message send --send-draft\` 原样发出(不要在改内容时用 --send-draft)。${collaborationEtiquette}${communicationStyle}
|
|
86
134
|
|
|
87
135
|
## Workspace 与分层记忆(CRITICAL — 索引+按需,配合上面的并行规则)
|
|
88
136
|
你的持久记忆在 \`$CREW_HOME\`(跨你所有任务共享),分三层:
|
|
89
137
|
1. **MEMORY.md = 索引 + 角色**:指向你全部知识的目录。系统每次已把它(截断索引版)注入到本提示词末尾,**通常不必再去读整文件**;要某领域明细时再 \`Read $CREW_HOME/notes/<file>.md\`。别一次把所有 notes 读进上下文。
|
|
90
138
|
2. **notes/<domain>.md = 明细**:领域知识、用户偏好、频道说明、经验教训(\`notes/lessons.md\`)。**任务进行中只读不写**(多个并行运行同时写会冲突)。
|
|
91
|
-
3. **本任务 work-log = \`$CREW_TASK_LOG\`**:每任务一份(别的运行不会碰)
|
|
139
|
+
3. **本任务 work-log = \`$CREW_TASK_LOG\`**:每任务一份(别的运行不会碰),记当前阶段/下一步/卡点/关键结论。系统已把它的当前内容注入末尾让你续上;有进展就更新它——这是你**唯一**该在任务中持续写的记忆文件。**work-log 是现状快照,不是流水账**:保持 ≤100 行,更新时重写全文替换过时内容,不要无限追加(过长会被注入截断)。
|
|
92
140
|
|
|
93
141
|
**沉淀与瘦身由系统触发,不要平时做**:任务被改为 done/closed 时,系统会单独唤醒你收尾——那时才把可复用经验从 work-log 提炼进 \`notes/lessons.md\`、更新 MEMORY.md 索引、并清掉该任务的 work-log。平时别动 MEMORY.md/notes,既防并行冲突,也防 MEMORY.md 膨胀。
|
|
94
142
|
|
|
95
143
|
**为什么(CRITICAL)**:上下文会被周期压缩;你的恢复力来自——注入的 MEMORY.md 索引(始终精简)+ 本任务 work-log + 按需读 notes,而**不是**把一切堆进 MEMORY.md。
|
|
96
144
|
${ctx.memory ? `\n## [注入] 你的 MEMORY.md(索引,只读参考)\n${ctx.memory}` : ""}${ctx.workLog ? `\n## [注入] 本任务 work-log(续上进度;更新写到 $CREW_TASK_LOG)\n${ctx.workLog}` : ""}`;
|
|
97
145
|
}
|
|
146
|
+
/** 静默定时任务的唤醒提示词:不注入协作礼仪/线程提示;默认零输出(设计文档 §5.4)。
|
|
147
|
+
* 汇报用 --send-draft:绕过 freshness hold(-p 单发不跑 crew read,游标落后,
|
|
148
|
+
* 普通 send 会被 202 扣成 draft——监控告警绝不能被静默扣留)。 */
|
|
149
|
+
export function buildScheduledPrompt(channelId, jobPrompt) {
|
|
150
|
+
return [
|
|
151
|
+
"你正在执行一个静默定时任务(后台运行,频道里没有人在等你回复)。",
|
|
152
|
+
`- 执行这条定时指令: ${jobPrompt}`,
|
|
153
|
+
"- 默认不要发送频道消息,不要发接手回执,不要创建 task,也不要读频道历史(除非指令要求)。",
|
|
154
|
+
`- 只有发现异常、需要人处理、或指令明确要求汇报时,才用 \`crew message send --channel ${channelId} --send-draft --content "..."\` 输出到频道。`,
|
|
155
|
+
"- 只有存在需要后续跟进的具体事项时,才用 `crew task create` 创建任务。",
|
|
156
|
+
"- 无异常时直接结束,不需要任何输出。",
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
|
98
159
|
/**
|
|
99
160
|
* 唤醒提示词(OpenSlock 自有设计)。默认"只读补课"模式:先把频道读一遍补齐上下文,
|
|
100
161
|
* 只有发现确实指向自己的事才转为主动处理,否则读完即停、不发声。
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom provider(BYOC)环境注入 —— 与机器全局 Claude 配置互不干扰的关键。纯函数,零 IO。
|
|
3
|
+
*
|
|
4
|
+
* 隔离语义(双向):
|
|
5
|
+
* - agent → 全局:只改子进程 env,绝不写机器全局 ~/.claude / keychain;
|
|
6
|
+
* - 全局 → agent:custom 时剔除继承自 daemon 启动 shell 的 ANTHROPIC_* 与 Bedrock 等
|
|
7
|
+
* 路由变量(否则全局中转的 AUTH_TOKEN 会和注入的 key 两套鉴权头一起发、Bedrock 模式
|
|
8
|
+
* 会整个绕过 baseUrl);有 key 时再把 CLAUDE_CONFIG_DIR 指到 agent 自己的 home,
|
|
9
|
+
* 该进程完全不读全局 OAuth 登录态(否则已登录订阅账号优先于注入的 key)。
|
|
10
|
+
*/
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
/** custom 模式下必须从继承 env 中剔除的全局路由/凭证变量(避免与注入值叠加或抢优先级)。 */
|
|
13
|
+
const CONFLICTING_ENV = [
|
|
14
|
+
"ANTHROPIC_BASE_URL",
|
|
15
|
+
"ANTHROPIC_API_KEY",
|
|
16
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
17
|
+
"ANTHROPIC_MODEL",
|
|
18
|
+
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
19
|
+
// 别名钉扎(sonnet/opus/haiku → 具体官方模型名):全局若配了,会把官方模型名带进自定义中转
|
|
20
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
21
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
22
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
23
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
24
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
25
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
26
|
+
];
|
|
27
|
+
/** 返回叠加了 provider 隔离的新 env(不改入参);非 claude 运行时或 default 模式原样返回。 */
|
|
28
|
+
export function applyProviderEnv(base, runtime, cfg, homeDir) {
|
|
29
|
+
if (runtime !== "claude" || cfg.provider !== "custom")
|
|
30
|
+
return base;
|
|
31
|
+
const env = { ...base };
|
|
32
|
+
for (const k of CONFLICTING_ENV)
|
|
33
|
+
delete env[k];
|
|
34
|
+
if (cfg.providerBaseUrl)
|
|
35
|
+
env.ANTHROPIC_BASE_URL = cfg.providerBaseUrl;
|
|
36
|
+
if (cfg.providerApiKey) {
|
|
37
|
+
if (cfg.providerAuthMode === "api-key")
|
|
38
|
+
env.ANTHROPIC_API_KEY = cfg.providerApiKey; // x-api-key,直连官方
|
|
39
|
+
else
|
|
40
|
+
env.ANTHROPIC_AUTH_TOKEN = cfg.providerApiKey; // Bearer,多数第三方中转(默认)
|
|
41
|
+
// 凭证齐备 → 彻底隔离:配置目录固定在 agent home 下,claude 的会话/设置/凭证都落在
|
|
42
|
+
// 这里,不再读机器全局 ~/.claude。目录跨唤醒稳定,--resume 不受影响。
|
|
43
|
+
env.CLAUDE_CONFIG_DIR = join(homeDir, ".claude");
|
|
44
|
+
}
|
|
45
|
+
if (cfg.providerSmallFastModel) {
|
|
46
|
+
// 新旧两名都设:ANTHROPIC_SMALL_FAST_MODEL 已废弃更名为 ANTHROPIC_DEFAULT_HAIKU_MODEL
|
|
47
|
+
// (docs/model-config),旧版 CLI 只认旧名、新版认新名,双设覆盖两侧。
|
|
48
|
+
env.ANTHROPIC_SMALL_FAST_MODEL = cfg.providerSmallFastModel;
|
|
49
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = cfg.providerSmallFastModel;
|
|
50
|
+
}
|
|
51
|
+
return env;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* provider 关键配置指纹:default↔custom、换端点、换鉴权方式、key 有↔无都会改变
|
|
55
|
+
* CLAUDE_CONFIG_DIR/上游,旧 claude 会话在新配置下不可 --resume(会直接报会话不存在),
|
|
56
|
+
* 供 pickResumeId 判定冷启动。default 恒为 null;不含 key 明文(换 key 同端点可续)。
|
|
57
|
+
*/
|
|
58
|
+
export function providerFingerprint(runtime, cfg) {
|
|
59
|
+
if (runtime !== "claude" || cfg.provider !== "custom")
|
|
60
|
+
return null;
|
|
61
|
+
return [
|
|
62
|
+
cfg.providerBaseUrl ?? "",
|
|
63
|
+
cfg.providerAuthMode ?? "auth-token",
|
|
64
|
+
cfg.providerApiKey ? "key" : "nokey",
|
|
65
|
+
].join("|");
|
|
66
|
+
}
|
package/dist/runner.js
CHANGED
|
@@ -6,12 +6,13 @@ import { writeFile } from "node:fs/promises";
|
|
|
6
6
|
import { delimiter, join } from "node:path";
|
|
7
7
|
import { mintAgentToken } from "./token.js";
|
|
8
8
|
import { prepareWorkspace, rotateAgentSession } from "./workspace.js";
|
|
9
|
-
import { buildSystemPrompt, buildWakePrompt } from "./prompt.js";
|
|
9
|
+
import { buildSystemPrompt, buildWakePrompt, capWorkLogForInject } from "./prompt.js";
|
|
10
10
|
import { spawnClaude } from "./runtimes/claude.js";
|
|
11
11
|
import { spawnCodex } from "./runtimes/codex.js";
|
|
12
|
-
import {
|
|
12
|
+
import { applyProviderEnv, providerFingerprint } from "./provider-env.js";
|
|
13
|
+
import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
13
14
|
import { normalizeEvent, parseLine, extractRunMeta } from "./normalize.js";
|
|
14
|
-
import { readSession, writeSession, pickResumeId } from "./session.js";
|
|
15
|
+
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
15
16
|
import { toConsoleLines } from "./console.js";
|
|
16
17
|
import { dslog } from "./slog.js";
|
|
17
18
|
// 注入提示词的 MEMORY.md 上限:只喂索引/角色,避免把膨胀的记忆全塞进上下文。
|
|
@@ -28,6 +29,8 @@ onConsole = () => { }) {
|
|
|
28
29
|
const cfg = cred.config ?? {};
|
|
29
30
|
const runtime = cfg.runtime ?? config.runtimeBin;
|
|
30
31
|
const currentModel = cfg.model ?? null;
|
|
32
|
+
// provider 配置指纹:custom 配置变更后旧会话不可 --resume(见 provider-env.ts)
|
|
33
|
+
const providerFp = providerFingerprint(runtime, cfg);
|
|
31
34
|
// 2) 准备 workspace(共享 home + 本任务隔离 cwd + per-task work-log)。
|
|
32
35
|
// 先 prepare 拿到 memory/workLog,再 build 系统提示词(注入记忆索引)写入。
|
|
33
36
|
const ws = await prepareWorkspace({
|
|
@@ -44,7 +47,13 @@ onConsole = () => { }) {
|
|
|
44
47
|
const prior = config.resume && supportsNativeResume ? await readSession(ws.runDir) : null;
|
|
45
48
|
// 会话身份唯一来源:workspace 的确定性 uuid(ws.agentSessionId)。HEAD 的 warm-window + 开关
|
|
46
49
|
// 只决定「是否续用」:既有会话(sessionResume)且仍在缓存窗口内 → --resume;否则冷启动。
|
|
47
|
-
const resuming = supportsNativeResume && ws.sessionResume &&
|
|
50
|
+
const resuming = supportsNativeResume && ws.sessionResume &&
|
|
51
|
+
pickResumeId(prior, Date.now(), config.resumeWarmMs, currentModel, config.sessionBudgetTokens, config.sessionMaxTurns, providerFp) != null;
|
|
52
|
+
// 预算轮换判定(仅为留痕/提示区分原因):不带预算参数也会续 → 本次冷启动是预算导致的主动轮换
|
|
53
|
+
const rotatedForBudget = !resuming && supportsNativeResume && ws.sessionResume &&
|
|
54
|
+
pickResumeId(prior, Date.now(), config.resumeWarmMs, currentModel, 0, 0, providerFp) != null;
|
|
55
|
+
// soft 预算:仍在续用但上下文已接近 hard 阈值 → 本轮提醒 agent 把现状蒸馏进 work-log(为轮换做准备)
|
|
56
|
+
const nearBudget = resuming && isNearBudget(prior, config.sessionSoftTokens);
|
|
48
57
|
// 既有会话但本轮不续用 → 轮换出新 uuid 冷启动(避免 --session-id 撞已存在会话)。
|
|
49
58
|
const rotated = !!(ws.agentSessionId && ws.sessionResume && !resuming);
|
|
50
59
|
const launchSessionId = rotated ? await rotateAgentSession(ws.runDir) : ws.agentSessionId;
|
|
@@ -59,8 +68,11 @@ onConsole = () => { }) {
|
|
|
59
68
|
decision_reason: !supportsNativeResume ? "runtime_no_resume"
|
|
60
69
|
: !config.resume ? "resume_disabled"
|
|
61
70
|
: !ws.sessionResume ? "first_run"
|
|
62
|
-
: resuming ? "warm_resume"
|
|
63
|
-
: "
|
|
71
|
+
: resuming ? (nearBudget ? "warm_resume_near_budget" : "warm_resume")
|
|
72
|
+
: rotatedForBudget ? "rotated_for_budget"
|
|
73
|
+
: "warm_window_expired_or_model_changed",
|
|
74
|
+
prior_context_tokens: prior?.contextTokens ?? null,
|
|
75
|
+
budget_tokens: config.sessionBudgetTokens,
|
|
64
76
|
});
|
|
65
77
|
const systemPrompt = buildSystemPrompt({
|
|
66
78
|
handle: input.handle,
|
|
@@ -68,26 +80,34 @@ onConsole = () => { }) {
|
|
|
68
80
|
agentId: cred.agentId,
|
|
69
81
|
homeDir: ws.dir,
|
|
70
82
|
productName: config.productName,
|
|
83
|
+
...(input.scheduled ? { scheduled: true } : {}),
|
|
71
84
|
// 只注入 MEMORY.md 的索引/角色部分(截断),避免上下文膨胀;明细让 agent 按需读 notes/。
|
|
72
85
|
memory: ws.memory.length > MEMORY_INJECT_CAP
|
|
73
86
|
? ws.memory.slice(0, MEMORY_INJECT_CAP) + "\n…(MEMORY.md 过长已截断,详情用 Read 读 $CREW_HOME/MEMORY.md 或 notes/)"
|
|
74
87
|
: ws.memory,
|
|
75
88
|
// resume 时进度已在对话历史里,省去重喂 work-log(resume 省 token 的主要来源);首轮才注入。
|
|
76
|
-
|
|
89
|
+
// 注入截断(防随文件膨胀):全文仍在盘上,超限时保头尾、提示按需 Read。
|
|
90
|
+
...(resuming ? {} : { workLog: capWorkLogForInject(ws.workLog) }),
|
|
77
91
|
});
|
|
78
92
|
await writeFile(ws.systemPromptPath, systemPrompt, "utf8");
|
|
79
93
|
// 3) spawn runtime,注入 PATH(crew wrapper)、凭证 env、以及 agent 运行时配置
|
|
80
|
-
// provider=custom → BYOC(ANTHROPIC_BASE_URL/API_KEY);
|
|
81
|
-
|
|
94
|
+
// provider=custom → BYOC(ANTHROPIC_BASE_URL/API_KEY);model → --model;
|
|
95
|
+
// reasoning → 原生思考强度(claude --effort / codex -c model_reasoning_effort= /
|
|
96
|
+
// kimi KIMI_MODEL_THINKING_EFFORT env;档位白名单在各 runtime 适配器里,白名单外不传)。
|
|
82
97
|
// resume 时提示 agent 上下文已在,无需从头重读频道(配合不注入 work-log,进一步省 token)。
|
|
83
98
|
const baseWake = input.wake ?? buildWakePrompt(input.channelId);
|
|
99
|
+
const distillHint = nearBudget
|
|
100
|
+
? `\n(注意:本会话上下文已接近预算,稍后将轮换重启。本轮结束前,把当前状态——阶段/关键结论/下一步/卡点/关键文件路径——完整更新到 $CREW_TASK_LOG,下轮将以它为基础继续。)`
|
|
101
|
+
: "";
|
|
84
102
|
const wakePrompt = resuming
|
|
85
|
-
? `(继续之前的会话:频道历史与你的进度已在上下文里,不必从头重读;要新消息用 \`crew message read --channel ${input.channelId}\` 增量拉即可。)\n\n${baseWake}`
|
|
86
|
-
:
|
|
87
|
-
|
|
103
|
+
? `(继续之前的会话:频道历史与你的进度已在上下文里,不必从头重读;要新消息用 \`crew message read --channel ${input.channelId}\` 增量拉即可。)${distillHint}\n\n${baseWake}`
|
|
104
|
+
: rotatedForBudget
|
|
105
|
+
? `(上下文已轮换:之前的会话过大已重启。你的 work-log 已注入系统提示词末尾,以它为基础继续;频道/线程历史用 crew 命令按需拉取,不必全量重读。)\n\n${baseWake}`
|
|
106
|
+
: baseWake;
|
|
107
|
+
const baseEnv = {
|
|
88
108
|
...process.env,
|
|
89
109
|
// 用户自定义 env(表单 ENVIRONMENT VARIABLES):先合入,可覆盖继承的 shell 环境;
|
|
90
|
-
// 但 PATH/CREW_*/XDG_* 及下方由表单生成的值(ANTHROPIC_
|
|
110
|
+
// 但 PATH/CREW_*/XDG_* 及下方由表单生成的值(ANTHROPIC_*/思考强度)在其后合入,始终以系统为准。
|
|
91
111
|
...sanitizeEnvVars(cfg.envVars),
|
|
92
112
|
PATH: `${ws.crewDir}${delimiter}${process.env.PATH ?? ""}`,
|
|
93
113
|
CREW_SERVER_URL: config.serverUrl,
|
|
@@ -106,17 +126,18 @@ onConsole = () => { }) {
|
|
|
106
126
|
XDG_CACHE_HOME: join(ws.homeDir, ".cache"),
|
|
107
127
|
GH_CONFIG_DIR: join(ws.homeDir, ".config", "gh"),
|
|
108
128
|
CLOUDSDK_CONFIG: join(ws.homeDir, ".config", "gcloud"),
|
|
109
|
-
// provider custom
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
...(cfg.reasoning && cfg.reasoning !== "default" && REASONING_TOKENS[cfg.reasoning]
|
|
115
|
-
? { MAX_THINKING_TOKENS: REASONING_TOKENS[cfg.reasoning], KIMI_MODEL_THINKING_EFFORT: cfg.reasoning }
|
|
129
|
+
// provider custom(BYOC)端点/密钥注入移至下方 applyProviderEnv(含全局残留清理与配置目录隔离)
|
|
130
|
+
// reasoning → kimi 无对应 CLI 参数,走 KIMI_MODEL_THINKING_EFFORT env(kimi-code 0.23.0,
|
|
131
|
+
// 值域 low/medium/high/xhigh/max);claude/codex 改走各自 spawn 原生参数(见下),不再注 env。
|
|
132
|
+
...(runtime === "kimi" && cfg.reasoning && KIMI_EFFORT_LEVELS.includes(cfg.reasoning)
|
|
133
|
+
? { KIMI_MODEL_THINKING_EFFORT: cfg.reasoning }
|
|
116
134
|
: {}),
|
|
117
135
|
// fast 模式 → 透传给 runtime(best-effort,供 wrapper/runtime 读取)
|
|
118
136
|
...(cfg.fastMode ? { CREW_FAST_MODE: "1" } : {}),
|
|
119
137
|
};
|
|
138
|
+
// provider custom = BYOC:剔除全局 ANTHROPIC_*/Bedrock 残留 + 按鉴权方式注入端点与密钥,
|
|
139
|
+
// 与机器全局 Claude 登录态/配置互不干扰(细节见 provider-env.ts)。
|
|
140
|
+
const childEnv = applyProviderEnv(baseEnv, runtime, cfg, ws.homeDir);
|
|
120
141
|
const child = runtime === "claude"
|
|
121
142
|
? spawnClaude({
|
|
122
143
|
bin: runtime,
|
|
@@ -125,6 +146,7 @@ onConsole = () => { }) {
|
|
|
125
146
|
wakePrompt,
|
|
126
147
|
dangerous: config.dangerous,
|
|
127
148
|
...(currentModel ? { model: currentModel } : {}),
|
|
149
|
+
...(cfg.reasoning ? { reasoning: cfg.reasoning } : {}),
|
|
128
150
|
// 一线程一会话(唯一会话机制):首轮/冷启动 --session-id 固定 uuid,warm 续轮 --resume 续上。
|
|
129
151
|
...(launchSessionId ? { sessionId: launchSessionId, resume: resuming } : {}),
|
|
130
152
|
env: childEnv,
|
|
@@ -136,6 +158,7 @@ onConsole = () => { }) {
|
|
|
136
158
|
wakePrompt: `${systemPrompt}\n\n${wakePrompt}`,
|
|
137
159
|
dangerous: config.dangerous,
|
|
138
160
|
...(currentModel ? { model: currentModel } : {}),
|
|
161
|
+
...(cfg.reasoning ? { reasoning: cfg.reasoning } : {}),
|
|
139
162
|
env: childEnv,
|
|
140
163
|
})
|
|
141
164
|
: runtime === "kimi"
|
|
@@ -196,19 +219,24 @@ onConsole = () => { }) {
|
|
|
196
219
|
for (const c of toConsoleLines(evt))
|
|
197
220
|
onConsole(c);
|
|
198
221
|
});
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
222
|
+
// stderr 除透传本地终端外,再留一段尾部:runtime 启动即崩(如 codex 拒跑)时,
|
|
223
|
+
// 这是唯一的错误线索,要随 error 活动上送,否则失败对 server/web 完全不可见。
|
|
224
|
+
let stderrTail = "";
|
|
225
|
+
child.stderr.on("data", (d) => {
|
|
226
|
+
process.stderr.write(d);
|
|
227
|
+
stderrTail = (stderrTail + String(d)).slice(-STDERR_TAIL_CAP);
|
|
202
228
|
});
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
activities.push(
|
|
209
|
-
onActivity(
|
|
229
|
+
const { exitCode, spawnError } = await awaitExit(child);
|
|
230
|
+
// spawn 本身失败(如 PATH 里没有 runtime 二进制)没有 stderr,把错误并入尾部供上报。
|
|
231
|
+
const errorTail = [stderrTail.trim(), spawnError].filter(Boolean).join(" ").trim();
|
|
232
|
+
const finish = exitActivity(runtime, exitCode, errorTail);
|
|
233
|
+
if (finish) {
|
|
234
|
+
activities.push(finish);
|
|
235
|
+
onActivity(finish);
|
|
236
|
+
if (finish.kind === "error")
|
|
237
|
+
onConsole({ stream: "error", text: `✖ ${finish.detail ?? finish.label}` });
|
|
210
238
|
}
|
|
211
|
-
if ((runtime === "codex" || runtime === "kimi") && exitCode === 0 && !sentViaCrew && finalText) {
|
|
239
|
+
if (!input.suppressFallback && (runtime === "codex" || runtime === "kimi") && exitCode === 0 && !sentViaCrew && finalText) {
|
|
212
240
|
// force:兜底回帖锚定本轮触发消息的线程,语义上必须送达;不 force 时 agent(-p 单发不跑
|
|
213
241
|
// crew read)游标落后,回帖会被 freshness hold 成 draft(202)而永远不可见。
|
|
214
242
|
const sent = await sendAgentMessage(config.serverUrl, cred.token, input.channelId, {
|
|
@@ -230,20 +258,65 @@ onConsole = () => { }) {
|
|
|
230
258
|
}
|
|
231
259
|
// 5) 落盘 session(下次同任务可 --resume),并打印本轮 token 用量(度量 resume 真省与否)
|
|
232
260
|
if (config.resume && supportsNativeResume && sessionId) {
|
|
261
|
+
// turns 只在续用时累加,轮换/新会话从 1 重计(否则 maxTurns 兜底会让之后每轮都轮换)。
|
|
262
|
+
// contextTokens = 本轮实测上下文体量;本轮无 usage 时续用轮保留上轮值(会话仍在),新会话清零。
|
|
263
|
+
const contextTokens = usage
|
|
264
|
+
? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
|
|
265
|
+
: (resuming ? prior?.contextTokens : undefined);
|
|
233
266
|
await writeSession(ws.runDir, {
|
|
234
267
|
sessionId,
|
|
235
268
|
lastRunAt: Date.now(),
|
|
236
|
-
turns: (prior?.turns ?? 0) + 1,
|
|
269
|
+
turns: resuming ? (prior?.turns ?? 0) + 1 : 1,
|
|
237
270
|
model: currentModel,
|
|
271
|
+
providerFingerprint: providerFp,
|
|
272
|
+
...(contextTokens != null ? { contextTokens } : {}),
|
|
238
273
|
});
|
|
239
274
|
}
|
|
240
275
|
if (usage) {
|
|
241
276
|
const u = usage;
|
|
242
277
|
process.stdout.write(`📊 tokens: in=${u.inputTokens} out=${u.outputTokens} cache_read=${u.cacheReadTokens} cache_create=${u.cacheCreationTokens}` +
|
|
243
|
-
`${u.costUsd != null ? ` cost=$${u.costUsd.toFixed(4)}` : ""} ${resuming ? "(resumed)" : "(fresh)"}\n`);
|
|
278
|
+
`${u.costUsd != null ? ` cost=$${u.costUsd.toFixed(4)}` : ""} ${resuming ? "(resumed)" : rotatedForBudget ? "(rotated: budget)" : "(fresh)"}\n`);
|
|
244
279
|
}
|
|
245
280
|
return { exitCode, activities, model: observedModel, runtime, resumed: resuming, sessionId, ...(usage ? { usage } : {}) };
|
|
246
281
|
}
|
|
282
|
+
/** 随 error 活动上送的 stderr 尾部上限。 */
|
|
283
|
+
const STDERR_TAIL_CAP = 2000;
|
|
284
|
+
/**
|
|
285
|
+
* 等待子进程结束。必须监听 error:spawn 失败(如 PATH 里没有该 runtime 的二进制)时
|
|
286
|
+
* Node 只发 error 不发 close——不监听会以未处理异常炸掉整个 daemon 进程,
|
|
287
|
+
* 且 close 永不触发导致本轮永久挂起。取先到的事件为准。
|
|
288
|
+
*/
|
|
289
|
+
export function awaitExit(child) {
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
let settled = false;
|
|
292
|
+
const settle = (r) => {
|
|
293
|
+
if (settled)
|
|
294
|
+
return;
|
|
295
|
+
settled = true;
|
|
296
|
+
resolve(r);
|
|
297
|
+
};
|
|
298
|
+
child.on("error", (e) => settle({ exitCode: -1, spawnError: e.message }));
|
|
299
|
+
child.on("close", (code) => settle({ exitCode: code ?? 0 }));
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 进程退出 → 收尾活动。codex/kimi 非零退出必须显式报 error(它们失败时往往一条事件都没吐,
|
|
304
|
+
* 不报就会被 serve 的「本轮结束」伪装成成功);exitCode -1 是 awaitExit 的 spawn 失败哨兵,
|
|
305
|
+
* 任何 runtime 都报(spawn 失败连事件流都没有);kimi 正常退出补 done(其 stream 无轮次结束事件);
|
|
306
|
+
* codex 正常退出与 claude 均返回 null(终态由 turn.completed / result 事件负责)。
|
|
307
|
+
*/
|
|
308
|
+
export function exitActivity(runtime, exitCode, stderrTail) {
|
|
309
|
+
if ((runtime === "codex" || runtime === "kimi" || exitCode === -1) && exitCode !== 0) {
|
|
310
|
+
return {
|
|
311
|
+
kind: "error",
|
|
312
|
+
label: "运行出错",
|
|
313
|
+
detail: `${runtime} exited with code ${exitCode}${stderrTail ? `: ${stderrTail}` : ""}`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (runtime === "kimi" && exitCode === 0)
|
|
317
|
+
return { kind: "done", label: "本轮结束" };
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
247
320
|
function defaultPrint(a) {
|
|
248
321
|
const icon = ICON[a.kind] ?? "·";
|
|
249
322
|
const detail = a.detail ? ` ${a.detail.replace(/\s+/g, " ").slice(0, 120)}` : "";
|
package/dist/runtimes/claude.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude Code runtime 适配:print + stream-json 模式,headless 驱动。
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
// cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
|
|
5
|
+
import spawn from "cross-spawn";
|
|
6
|
+
// Claude Code 原生 --effort 档位(claude 2.1.196 实测:--help 与非法值告警均枚举这五档)。
|
|
7
|
+
// 白名单外的值(含 "default" 与 codex 专属档)不传参 → 用 claude 自身默认,脏数据不影响启动。
|
|
8
|
+
export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
5
9
|
export function buildClaudeArgs(input) {
|
|
6
10
|
const args = [
|
|
7
11
|
"--print",
|
|
@@ -13,6 +17,9 @@ export function buildClaudeArgs(input) {
|
|
|
13
17
|
];
|
|
14
18
|
if (input.model)
|
|
15
19
|
args.push("--model", input.model);
|
|
20
|
+
if (input.reasoning && CLAUDE_EFFORT_LEVELS.includes(input.reasoning)) {
|
|
21
|
+
args.push("--effort", input.reasoning);
|
|
22
|
+
}
|
|
16
23
|
if (input.sessionId) {
|
|
17
24
|
args.push(input.resume ? "--resume" : "--session-id", input.sessionId);
|
|
18
25
|
}
|
|
@@ -22,6 +29,7 @@ export function buildClaudeArgs(input) {
|
|
|
22
29
|
return args;
|
|
23
30
|
}
|
|
24
31
|
export function spawnClaude(input) {
|
|
32
|
+
// stdio 固定 ignore/pipe/pipe,stdout/stderr 必为 Readable;cross-spawn 类型不带该细化,断言之
|
|
25
33
|
return spawn(input.bin, buildClaudeArgs(input), {
|
|
26
34
|
cwd: input.cwd,
|
|
27
35
|
env: input.env,
|
package/dist/runtimes/codex.js
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Codex CLI runtime adapter: non-interactive exec mode with JSONL output.
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
// cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
|
|
5
|
+
import spawn from "cross-spawn";
|
|
6
|
+
// Codex CLI 原生 model_reasoning_effort 档位(codex 0.135.0 实测:非法值时 config 解析报错枚举这六档)。
|
|
7
|
+
// 注意:codex 对非法值是硬失败(进程直接退出),所以必须白名单过滤;白名单外(含 "default"、
|
|
8
|
+
// claude 专属的 "max")不传 → 用 codex 自身默认。
|
|
9
|
+
export const CODEX_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
5
10
|
export function buildCodexArgs(input) {
|
|
6
|
-
|
|
11
|
+
// agent 运行目录由 daemon 管理,不是 git 仓库;不带 --skip-git-repo-check 时 codex exec
|
|
12
|
+
// 会以 "Not inside a trusted directory" 秒退(且只报在本地 stderr),表现为 agent 静默不回复。
|
|
13
|
+
const args = ["exec", "--json", "--skip-git-repo-check"];
|
|
7
14
|
if (input.model)
|
|
8
15
|
args.push("--model", input.model);
|
|
16
|
+
if (input.reasoning && CODEX_EFFORT_LEVELS.includes(input.reasoning)) {
|
|
17
|
+
args.push("-c", `model_reasoning_effort=${input.reasoning}`);
|
|
18
|
+
}
|
|
9
19
|
if (input.dangerous)
|
|
10
20
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
11
21
|
args.push(input.wakePrompt);
|
|
12
22
|
return args;
|
|
13
23
|
}
|
|
14
24
|
export function spawnCodex(input) {
|
|
25
|
+
// stdio 固定 ignore/pipe/pipe,stdout/stderr 必为 Readable;cross-spawn 类型不带该细化,断言之
|
|
15
26
|
return spawn(input.bin, buildCodexArgs(input), {
|
|
16
27
|
cwd: input.cwd,
|
|
17
28
|
env: input.env,
|
package/dist/runtimes/kimi.js
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* - 无 system prompt 注入参数 → 与 codex 同法:systemPrompt 拼在 wakePrompt 前。
|
|
9
9
|
* - 鉴权是机器级的(`kimi login` 或 ~/.kimi-code/config.toml),不读 shell 环境变量。
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
// cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
|
|
12
|
+
import spawn from "cross-spawn";
|
|
13
|
+
// Kimi Code 思考强度档位(kimi-code 0.23.0 实测+源码):无 CLI 参数,
|
|
14
|
+
// 由 runner 经 KIMI_MODEL_THINKING_EFFORT env 注入;白名单外的值不注。
|
|
15
|
+
export const KIMI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
12
16
|
export function buildKimiArgs(input) {
|
|
13
17
|
const args = ["--output-format", "stream-json"];
|
|
14
18
|
if (input.model)
|
|
@@ -17,6 +21,7 @@ export function buildKimiArgs(input) {
|
|
|
17
21
|
return args;
|
|
18
22
|
}
|
|
19
23
|
export function spawnKimi(input) {
|
|
24
|
+
// stdio 固定 ignore/pipe/pipe,stdout/stderr 必为 Readable;cross-spawn 类型不带该细化,断言之
|
|
20
25
|
return spawn(input.bin, buildKimiArgs(input), {
|
|
21
26
|
cwd: input.cwd,
|
|
22
27
|
env: input.env,
|
package/dist/serve.js
CHANGED
|
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { initSlog, dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
|
|
9
9
|
import { runAgent } from "./runner.js";
|
|
10
|
+
import { buildScheduledPrompt } from "./prompt.js";
|
|
10
11
|
import { collectMachineHello } from "./machine-info.js";
|
|
11
12
|
import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
|
|
12
13
|
import { listSkills } from "./skills.js";
|
|
@@ -89,7 +90,7 @@ export function serve(config, opts = {}) {
|
|
|
89
90
|
// 控制面鉴权拒绝:server 端 resolveToken 未命中有效的 machine 凭证(失效/被吊销/
|
|
90
91
|
// 库已重置)。不能静默丢弃这帧——否则只表现为神秘的「每 1s 重连」循环。打印可执行
|
|
91
92
|
// 提示,并把退避拉满,避免无意义高频重连刷屏 server(凭证失配不会靠重试自愈,
|
|
92
|
-
// 需在
|
|
93
|
+
// 需在 NowWork 重新 Add Computer 拿新连接命令)。
|
|
93
94
|
if (msg.type === "ready") {
|
|
94
95
|
// ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
|
|
95
96
|
const r = msg;
|
|
@@ -99,7 +100,7 @@ export function serve(config, opts = {}) {
|
|
|
99
100
|
if (msg.type === "error") {
|
|
100
101
|
if (msg.code === "UNAUTHENTICATED") {
|
|
101
102
|
log(`🛑 控制面拒绝鉴权:机器凭证无效或已吊销 (UNAUTHENTICATED)。`);
|
|
102
|
-
log(` 请在
|
|
103
|
+
log(` 请在 NowWork 重新 "Add Computer" 获取新的连接命令,再到本机重跑(当前 --api-key 已失效)。`);
|
|
103
104
|
dslog("daemon.ws_auth_rejected", "控制面拒绝鉴权:机器凭证无效或已吊销", { level: "ERROR" });
|
|
104
105
|
backoff = maxBackoff; // 退避到最大,停止每秒重连刷屏
|
|
105
106
|
}
|
|
@@ -152,9 +153,11 @@ export function serve(config, opts = {}) {
|
|
|
152
153
|
}
|
|
153
154
|
if (msg.type !== "agent:start")
|
|
154
155
|
return; // ready/error 等忽略
|
|
155
|
-
//
|
|
156
|
+
// 任务键:scheduled run 用 runId(每次运行独立 cwd/work-log;overlap 由 server 端 skip_if_running 管,
|
|
157
|
+
// daemon 的 running 去重只兜底"同一 run 重复投递");普通唤醒仍是 线程锚点 ?? 频道。
|
|
158
|
+
const scheduled = msg.reason === "scheduled_job" && msg.scheduledRun ? msg.scheduledRun : null;
|
|
156
159
|
const threadId = msg.wake?.threadId;
|
|
157
|
-
const taskKey = threadId ?? msg.channelId;
|
|
160
|
+
const taskKey = scheduled ? scheduled.runId : (threadId ?? msg.channelId);
|
|
158
161
|
const key = `${msg.agentHandle}:${taskKey}`;
|
|
159
162
|
// run_id 贯穿本轮全链路(wake→start→resume 决策→end),SLS 按 run_id 一查即得单轮时间线
|
|
160
163
|
const runId = randomUUID();
|
|
@@ -253,14 +256,20 @@ export function serve(config, opts = {}) {
|
|
|
253
256
|
const readCmd = threadId
|
|
254
257
|
? `crew thread read`
|
|
255
258
|
: `crew message read --channel ${msg.channelId}`;
|
|
259
|
+
const wakeText = scheduled
|
|
260
|
+
? buildScheduledPrompt(msg.channelId, msg.wake?.content ?? "")
|
|
261
|
+
: (msg.wake?.content
|
|
262
|
+
? `你被唤醒(${msg.reason}): ${msg.wake.content}\n用 ${readCmd} 读${threadId ? "本线程" : "频道"}后按需处理。${reasonHint}${ackHint}${threadHint}${attHint}`
|
|
263
|
+
: undefined);
|
|
256
264
|
const result = await runAgent(config, {
|
|
257
265
|
handle: msg.agentHandle,
|
|
258
266
|
channelId: msg.channelId,
|
|
259
267
|
taskKey, // 每任务隔离 cwd + work-log(并行不冲突)
|
|
260
268
|
runId, // 贯穿 SLS 日志的单轮关联键
|
|
269
|
+
...(scheduled ? { suppressFallback: true, scheduled: true } : {}),
|
|
261
270
|
// 唤醒锚点是具体消息(非纯频道唤醒)时,把它透传下去,供 `crew task create` 锚定到该消息。
|
|
262
|
-
...(threadId ? { wakeMessageId: threadId } : {}),
|
|
263
|
-
...(
|
|
271
|
+
...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
|
|
272
|
+
...(wakeText ? { wake: wakeText } : {}),
|
|
264
273
|
}, reportActivity, reportConsole);
|
|
265
274
|
// 本轮 token 用量上报:runner 已从 result 事件提取(含缓存读/写细分),
|
|
266
275
|
// 连同模型/runtime 一起上送控制面落库 → 支撑每 agent / 每任务(线程)的用量监控与成本核算。
|
|
@@ -284,6 +293,23 @@ export function serve(config, opts = {}) {
|
|
|
284
293
|
}
|
|
285
294
|
catch { /* ws 非 OPEN,忽略(用量非关键路径,丢一轮不阻塞) */ }
|
|
286
295
|
}
|
|
296
|
+
// 静默定时 run:结构化完成回报(驱动 server 端 scheduled_run/job 状态落库,§5.5)
|
|
297
|
+
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 会按超时回收 */ }
|
|
312
|
+
}
|
|
287
313
|
// run.end 是排查「任务没跑完就本轮结束」的核心证据:退出码 + 时长 + 最后活动 +
|
|
288
314
|
// 是否 resume + 用量。exit_code!=0 或时长异常短都值得追。
|
|
289
315
|
const lastActivity = result.activities.length
|
|
@@ -302,9 +328,20 @@ export function serve(config, opts = {}) {
|
|
|
302
328
|
...(result.usage.costUsd != null ? { cost_usd: result.usage.costUsd } : {}),
|
|
303
329
|
} : {}),
|
|
304
330
|
});
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
331
|
+
if (result.exitCode === 0) {
|
|
332
|
+
reportActivity({ kind: "done", label: "本轮结束" });
|
|
333
|
+
reportConsole({ stream: "result", text: "● 本轮结束" });
|
|
334
|
+
log(`✅ agent=${msg.agentHandle} 本轮完成`);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// 非零退出不能谎报「本轮结束」。runner 已对 codex/kimi 上报带 stderr 的 error 活动,
|
|
338
|
+
// 这里只兜底(如 claude 崩溃)并让终态落在 error 上。
|
|
339
|
+
if (!result.activities.some((a) => a.kind === "error")) {
|
|
340
|
+
reportActivity({ kind: "error", label: "运行出错", detail: `${result.runtime} 退出码 ${result.exitCode}` });
|
|
341
|
+
}
|
|
342
|
+
reportConsole({ stream: "error", text: `✖ 运行出错 (exit ${result.exitCode})` });
|
|
343
|
+
log(`❌ agent=${msg.agentHandle} 本轮失败 (exit ${result.exitCode})`);
|
|
344
|
+
}
|
|
308
345
|
}
|
|
309
346
|
catch (e) {
|
|
310
347
|
log(`❌ runAgent 失败: ${e.message}`);
|
|
@@ -312,6 +349,19 @@ export function serve(config, opts = {}) {
|
|
|
312
349
|
level: "ERROR", ...runKeys, duration_ms: Date.now() - runStartedAt,
|
|
313
350
|
error_message: e.message, error_stack: e.stack,
|
|
314
351
|
});
|
|
352
|
+
if (scheduled) {
|
|
353
|
+
try {
|
|
354
|
+
ws?.send(JSON.stringify({
|
|
355
|
+
type: "agent:run-complete",
|
|
356
|
+
agentHandle: msg.agentHandle,
|
|
357
|
+
channelId: msg.channelId,
|
|
358
|
+
scheduledRunId: scheduled.runId,
|
|
359
|
+
exitCode: -1,
|
|
360
|
+
errorMessage: e.message,
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
catch { /* ws 非 OPEN;reaper 兜底 */ }
|
|
364
|
+
}
|
|
315
365
|
}
|
|
316
366
|
finally {
|
|
317
367
|
running.delete(key);
|
|
@@ -327,7 +377,7 @@ export function serve(config, opts = {}) {
|
|
|
327
377
|
if (code === 4001) {
|
|
328
378
|
backoff = maxBackoff;
|
|
329
379
|
log(`🛑 控制面以鉴权失败关闭连接 (code 4001):机器凭证无效或已吊销。`);
|
|
330
|
-
log(` 请在
|
|
380
|
+
log(` 请在 NowWork 重新 "Add Computer" 获取新连接命令再重跑(当前 --api-key 已失效)。`);
|
|
331
381
|
}
|
|
332
382
|
log(`🔁 控制面断开,${Math.round(backoff / 1000)}s 后重连`);
|
|
333
383
|
// 此刻 server 大概率不可达 → 这条会落 spool,重连 drainSpool 时补传;
|
package/dist/session.js
CHANGED
|
@@ -28,6 +28,8 @@ export async function readSession(runDir) {
|
|
|
28
28
|
lastRunAt: typeof o.lastRunAt === "number" ? o.lastRunAt : 0,
|
|
29
29
|
turns: typeof o.turns === "number" ? o.turns : 0,
|
|
30
30
|
model: typeof o.model === "string" ? o.model : null,
|
|
31
|
+
providerFingerprint: typeof o.providerFingerprint === "string" ? o.providerFingerprint : null,
|
|
32
|
+
...(typeof o.contextTokens === "number" && o.contextTokens >= 0 ? { contextTokens: o.contextTokens } : {}),
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
35
|
catch {
|
|
@@ -43,13 +45,33 @@ export async function writeSession(runDir, meta) {
|
|
|
43
45
|
* 防负优化:claude 的 prompt cache 有寿命(默认到 1h),间隔超过 warmMs 后 resume 必然
|
|
44
46
|
* cache miss——既读不到旧缓存、又要把更大的历史重写进新缓存,反而比冷启动+work-log 更贵。
|
|
45
47
|
* 故只在缓存窗口内 resume(省),过期则回退冷启动(不亏)。warmMs<=0 关闭阈值(永远 resume)。
|
|
48
|
+
*
|
|
49
|
+
* 预算轮换(防上下文无限膨胀):持续活跃的任务会永远命中 warm window,session 历史单调增长
|
|
50
|
+
* 直到撞模型上限触发被动压缩(慢/贵/质量不可控)。故上轮实测上下文体量(contextTokens)达到
|
|
51
|
+
* budgetTokens、或续轮数达到 maxTurns(usage 缺失时的兜底)时主动轮换:冷启动 + 注入 work-log
|
|
52
|
+
* (agent 自己写的结构化现状,比通用摘要保真)。两阈值 <=0 时各自关闭。
|
|
46
53
|
*/
|
|
47
|
-
export function pickResumeId(prior, now, warmMs, currentModel = null) {
|
|
54
|
+
export function pickResumeId(prior, now, warmMs, currentModel = null, budgetTokens = 0, maxTurns = 0, currentProviderFingerprint = null) {
|
|
48
55
|
if (!prior)
|
|
49
56
|
return null;
|
|
50
57
|
if (prior.model !== currentModel)
|
|
51
58
|
return null;
|
|
59
|
+
// provider 配置变了(default↔custom/换端点/换鉴权):旧会话可能存在别的 CLAUDE_CONFIG_DIR
|
|
60
|
+
// 下,--resume 会直接报错,必须冷启动。
|
|
61
|
+
if ((prior.providerFingerprint ?? null) !== currentProviderFingerprint)
|
|
62
|
+
return null;
|
|
52
63
|
if (warmMs > 0 && now - prior.lastRunAt > warmMs)
|
|
53
64
|
return null;
|
|
65
|
+
if (budgetTokens > 0 && (prior.contextTokens ?? 0) >= budgetTokens)
|
|
66
|
+
return null;
|
|
67
|
+
if (maxTurns > 0 && prior.turns >= maxTurns)
|
|
68
|
+
return null;
|
|
54
69
|
return prior.sessionId;
|
|
55
70
|
}
|
|
71
|
+
/** 上轮上下文是否已接近预算(soft 阈值):是则本轮 wake 附加"蒸馏到 work-log"指令,
|
|
72
|
+
* 让 agent 在下轮轮换前把现状写全——主动蒸馏替代被动压缩。softTokens<=0 关闭。纯函数。 */
|
|
73
|
+
export function isNearBudget(prior, softTokens) {
|
|
74
|
+
if (!prior || softTokens <= 0)
|
|
75
|
+
return false;
|
|
76
|
+
return (prior.contextTokens ?? 0) >= softTokens;
|
|
77
|
+
}
|
package/dist/workspace.js
CHANGED
|
@@ -41,6 +41,8 @@ export async function prepareWorkspace(input) {
|
|
|
41
41
|
const wrapper = join(crewDir, "crew");
|
|
42
42
|
await writeFile(wrapper, `#!/bin/sh\nexec node ${JSON.stringify(input.cliPath)} "$@"\n`, "utf8");
|
|
43
43
|
await chmod(wrapper, 0o755);
|
|
44
|
+
// win32 侧 wrapper:cmd/PowerShell 不认 shebang 脚本,并存 crew.cmd(unix 下无害不会被命中)
|
|
45
|
+
await writeFile(join(crewDir, "crew.cmd"), `@echo off\r\nnode ${JSON.stringify(input.cliPath)} %*\r\n`, "utf8");
|
|
44
46
|
// 每个 agent 独立的 XDG 配置根:隔离第三方 CLI 凭证,避免 agent 之间互相串号
|
|
45
47
|
const homeDir = join(dir, ".home");
|
|
46
48
|
await mkdir(join(homeDir, ".config"), { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"cross-spawn": "^7.0.6",
|
|
20
21
|
"ws": "^8",
|
|
21
|
-
"@nowcrew/cli": "^0.
|
|
22
|
+
"@nowcrew/cli": "^0.4.1"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
25
|
+
"@types/cross-spawn": "^6.0.6",
|
|
24
26
|
"@types/node": "^22.0.0",
|
|
25
27
|
"@types/ws": "^8",
|
|
26
28
|
"tsx": "^4.19.0",
|