@nowcrew/daemon 0.5.28 → 0.5.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/attachments.js +196 -0
- package/dist/bound-im-decision.js +22 -0
- package/dist/completion-retransmitter.js +77 -0
- package/dist/computer-cli.js +274 -0
- package/dist/computer-profile-lock.js +395 -0
- package/dist/computer-profile.js +364 -0
- package/dist/computer-service.js +358 -0
- package/dist/config.js +82 -0
- package/dist/console-collapse.js +13 -0
- package/dist/console-formatter.js +77 -0
- package/dist/console-payload.js +73 -0
- package/dist/console.js +329 -0
- package/dist/daemon-startup-error.js +30 -0
- package/dist/execution-backend.js +44 -0
- package/dist/execution-event-limit.js +64 -0
- package/dist/execution-journal-lock.js +421 -0
- package/dist/execution-journal.js +716 -0
- package/dist/execution-protocol.js +342 -0
- package/dist/execution-recovery.js +95 -0
- package/dist/execution-runner.js +659 -0
- package/dist/execution-supervisor-child.js +236 -0
- package/dist/execution-supervisor.js +316 -0
- package/dist/execution-telemetry-journal.js +71 -0
- package/dist/external-output.js +114 -0
- package/dist/i18n.js +64 -0
- package/dist/json-result.js +27 -0
- package/dist/list-models.js +92 -0
- package/dist/local-executor.js +439 -0
- package/dist/log-format.js +10 -0
- package/dist/machine-info.js +124 -0
- package/dist/main.js +118 -0
- package/dist/normalize.js +170 -0
- package/dist/origin-decision.js +44 -0
- package/dist/platform.js +8 -0
- package/dist/prompt.js +307 -0
- package/dist/provider-env.js +90 -0
- package/dist/runner.js +234 -0
- package/dist/runtime-cancellation.js +74 -0
- package/dist/runtime-capabilities.js +43 -0
- package/dist/runtime-path.js +60 -0
- package/dist/runtimes/claude.js +51 -0
- package/dist/runtimes/codex-app-server-runner.js +541 -0
- package/dist/runtimes/codex-deepseek-catalog.js +7 -0
- package/dist/runtimes/codex-deepseek-config.js +50 -0
- package/dist/runtimes/codex.js +53 -0
- package/dist/runtimes/kimi-acp-runner.js +364 -0
- package/dist/runtimes/kimi.js +45 -0
- package/dist/runtimes/progress-watchdog.js +26 -0
- package/dist/scheduled-report.js +51 -0
- package/dist/scheduled-run-report.js +57 -0
- package/dist/serve-lifecycle.js +82 -0
- package/dist/serve.js +868 -0
- package/dist/session.js +82 -0
- package/dist/shared-execution-slots.js +68 -0
- package/dist/shutdown-deadline.js +32 -0
- package/dist/skill-preview.js +21 -0
- package/dist/skills.js +56 -0
- package/dist/slog.js +228 -0
- package/dist/supervised-runtime.js +104 -0
- package/dist/token.js +24 -0
- package/dist/unified-diff.js +84 -0
- package/dist/websocket-shutdown.js +53 -0
- package/dist/win32-job-object.js +193 -0
- package/dist/workspace-fs.js +80 -0
- package/dist/workspace-import.js +127 -0
- package/dist/workspace.js +148 -0
- package/package.json +1 -1
package/dist/session.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* per-task runtime 会话元数据 —— 让同一任务的重复唤醒复用原生上下文(省 token)。
|
|
3
|
+
*
|
|
4
|
+
* 存在本任务隔离运行目录下(<runDir>/.crew-session.json):天然按 taskKey 隔离、落盘防 daemon
|
|
5
|
+
* 重启丢失。读取一律容错 → 不存在/损坏/字段非法都回退 null(= 当作首轮冷启动),绝不阻断运行。
|
|
6
|
+
*/
|
|
7
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
const FILE = ".crew-session.json";
|
|
10
|
+
export function sessionPath(runDir) {
|
|
11
|
+
return join(runDir, FILE);
|
|
12
|
+
}
|
|
13
|
+
/** 读会话元数据;不存在/坏 json/缺 sessionId → null(回退冷启动,绝不抛)。 */
|
|
14
|
+
export async function readSession(runDir) {
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
raw = await readFile(sessionPath(runDir), "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null; // 文件不存在 = 首轮
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const o = JSON.parse(raw);
|
|
24
|
+
if (typeof o.sessionId !== "string" || !o.sessionId)
|
|
25
|
+
return null;
|
|
26
|
+
return {
|
|
27
|
+
sessionId: o.sessionId,
|
|
28
|
+
lastRunAt: typeof o.lastRunAt === "number" ? o.lastRunAt : 0,
|
|
29
|
+
turns: typeof o.turns === "number" ? o.turns : 0,
|
|
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 } : {}),
|
|
33
|
+
...(typeof o.lastExitOk === "boolean" ? { lastExitOk: o.lastExitOk } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null; // 坏 json → 当首轮,不阻断运行
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function writeSession(runDir, meta) {
|
|
41
|
+
await writeFile(sessionPath(runDir), JSON.stringify(meta, null, 2), "utf8");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 决定本轮是否 --resume 复用会话:返回 sessionId(resume)或 null(冷启动)。纯函数。
|
|
45
|
+
*
|
|
46
|
+
* 防负优化:claude 的 prompt cache 有寿命(默认到 1h),间隔超过 warmMs 后 resume 必然
|
|
47
|
+
* cache miss——既读不到旧缓存、又要把更大的历史重写进新缓存,反而比冷启动+work-log 更贵。
|
|
48
|
+
* 故只在缓存窗口内 resume(省),过期则回退冷启动(不亏)。warmMs<=0 关闭阈值(永远 resume)。
|
|
49
|
+
*
|
|
50
|
+
* 预算轮换(防上下文无限膨胀):持续活跃的任务会永远命中 warm window,session 历史单调增长
|
|
51
|
+
* 直到撞模型上限触发被动压缩(慢/贵/质量不可控)。故上轮实测上下文体量(contextTokens)达到
|
|
52
|
+
* budgetTokens、或续轮数达到 maxTurns(usage 缺失时的兜底)时主动轮换:冷启动 + 注入 work-log
|
|
53
|
+
* (agent 自己写的结构化现状,比通用摘要保真)。两阈值 <=0 时各自关闭。
|
|
54
|
+
*/
|
|
55
|
+
export function pickResumeId(prior, now, warmMs, currentModel = null, budgetTokens = 0, maxTurns = 0, currentProviderFingerprint = null) {
|
|
56
|
+
if (!prior)
|
|
57
|
+
return null;
|
|
58
|
+
if (prior.model !== currentModel)
|
|
59
|
+
return null;
|
|
60
|
+
// provider 配置变了(default↔custom/换端点/换鉴权):旧会话可能存在别的 CLAUDE_CONFIG_DIR
|
|
61
|
+
// 下,--resume 会直接报错,必须冷启动。
|
|
62
|
+
if ((prior.providerFingerprint ?? null) !== currentProviderFingerprint)
|
|
63
|
+
return null;
|
|
64
|
+
// 上轮崩溃/超窗/被杀:会话大概率已污染(或就是太大),续用几乎必再崩。强制冷启动斩断循环——
|
|
65
|
+
// 这条不依赖 token 记账是否准确(OOM/SIGKILL 时没有 usage,预算判断会漏,此条兜底)。
|
|
66
|
+
if (prior.lastExitOk === false)
|
|
67
|
+
return null;
|
|
68
|
+
if (warmMs > 0 && now - prior.lastRunAt > warmMs)
|
|
69
|
+
return null;
|
|
70
|
+
if (budgetTokens > 0 && (prior.contextTokens ?? 0) >= budgetTokens)
|
|
71
|
+
return null;
|
|
72
|
+
if (maxTurns > 0 && prior.turns >= maxTurns)
|
|
73
|
+
return null;
|
|
74
|
+
return prior.sessionId;
|
|
75
|
+
}
|
|
76
|
+
/** 上轮上下文是否已接近预算(soft 阈值):是则本轮 wake 附加"蒸馏到 work-log"指令,
|
|
77
|
+
* 让 agent 在下轮轮换前把现状写全——主动蒸馏替代被动压缩。softTokens<=0 关闭。纯函数。 */
|
|
78
|
+
export function isNearBudget(prior, softTokens) {
|
|
79
|
+
if (!prior || softTokens <= 0)
|
|
80
|
+
return false;
|
|
81
|
+
return (prior.contextTokens ?? 0) >= softTokens;
|
|
82
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function createSharedSlotManager(limits) {
|
|
2
|
+
const activeByHandle = new Map();
|
|
3
|
+
const queuesByHandle = new Map();
|
|
4
|
+
const promoteNext = (handle) => {
|
|
5
|
+
const queue = queuesByHandle.get(handle) ?? [];
|
|
6
|
+
while ((activeByHandle.get(handle) ?? 0) < limits.maxParallelPerAgent && queue.length > 0) {
|
|
7
|
+
const next = queue.shift();
|
|
8
|
+
if (next.released)
|
|
9
|
+
continue;
|
|
10
|
+
next.promoted = true;
|
|
11
|
+
activeByHandle.set(handle, (activeByHandle.get(handle) ?? 0) + 1);
|
|
12
|
+
next.resolve();
|
|
13
|
+
}
|
|
14
|
+
if (queue.length === 0)
|
|
15
|
+
queuesByHandle.delete(handle);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
reserve: (handle, kind) => {
|
|
19
|
+
const active = activeByHandle.get(handle) ?? 0;
|
|
20
|
+
const queued = queuesByHandle.get(handle)?.length ?? 0;
|
|
21
|
+
const facts = { activeForAgent: active, queuedForAgent: queued };
|
|
22
|
+
if (kind === "execution" && active >= limits.maxParallelPerAgent
|
|
23
|
+
&& queued >= limits.maxQueuedPerAgent) {
|
|
24
|
+
return { facts, ready: Promise.resolve(), isQueued: () => false, release: () => { } };
|
|
25
|
+
}
|
|
26
|
+
let resolveReady;
|
|
27
|
+
const ready = new Promise((resolve) => { resolveReady = resolve; });
|
|
28
|
+
const entry = {
|
|
29
|
+
kind,
|
|
30
|
+
released: false,
|
|
31
|
+
promoted: active < limits.maxParallelPerAgent,
|
|
32
|
+
resolve: resolveReady,
|
|
33
|
+
};
|
|
34
|
+
if (entry.promoted) {
|
|
35
|
+
activeByHandle.set(handle, active + 1);
|
|
36
|
+
resolveReady();
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const queue = queuesByHandle.get(handle) ?? [];
|
|
40
|
+
queue.push(entry);
|
|
41
|
+
queuesByHandle.set(handle, queue);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
facts,
|
|
45
|
+
state: entry.promoted ? "ready" : "queued",
|
|
46
|
+
ready,
|
|
47
|
+
isQueued: () => !entry.promoted && !entry.released,
|
|
48
|
+
release: () => {
|
|
49
|
+
if (entry.released)
|
|
50
|
+
return;
|
|
51
|
+
entry.released = true;
|
|
52
|
+
if (entry.promoted) {
|
|
53
|
+
activeByHandle.set(handle, Math.max(0, (activeByHandle.get(handle) ?? 1) - 1));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const queue = queuesByHandle.get(handle);
|
|
57
|
+
const index = queue?.indexOf(entry) ?? -1;
|
|
58
|
+
if (queue !== undefined && index >= 0)
|
|
59
|
+
queue.splice(index, 1);
|
|
60
|
+
if (queue?.length === 0)
|
|
61
|
+
queuesByHandle.delete(handle);
|
|
62
|
+
}
|
|
63
|
+
promoteNext(handle);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function readTestShutdownConfiguration(env) {
|
|
2
|
+
if (env.NODE_ENV !== "test")
|
|
3
|
+
return { barrier: null };
|
|
4
|
+
const timeoutMs = env.CREW_DAEMON_TEST_SHUTDOWN_TIMEOUT_MS === undefined
|
|
5
|
+
? undefined
|
|
6
|
+
: Number(env.CREW_DAEMON_TEST_SHUTDOWN_TIMEOUT_MS);
|
|
7
|
+
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
|
|
8
|
+
throw new RangeError("CREW_DAEMON_TEST_SHUTDOWN_TIMEOUT_MS must be a positive number");
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
12
|
+
barrier: env.CREW_DAEMON_TEST_SHUTDOWN_BARRIER === "1" ? new Promise(() => { }) : null,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function createShutdownDeadline(timeoutMs) {
|
|
16
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
17
|
+
throw new RangeError("shutdown timeout must be a positive finite number");
|
|
18
|
+
}
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timeoutError = new Error(`Timed out waiting for daemon shutdown after ${timeoutMs}ms`);
|
|
21
|
+
let rejectExpired;
|
|
22
|
+
const expired = new Promise((_resolve, reject) => { rejectExpired = reject; });
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
controller.abort(timeoutError);
|
|
25
|
+
rejectExpired(timeoutError);
|
|
26
|
+
}, timeoutMs);
|
|
27
|
+
return {
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
waitFor: async (operation) => Promise.race([operation, expired]),
|
|
30
|
+
dispose: () => clearTimeout(timer),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const FRONTMATTER = /(?:^|\n)---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
2
|
+
export function buildSkillPreview(output) {
|
|
3
|
+
const frontmatter = output.match(FRONTMATTER)?.[1];
|
|
4
|
+
if (!frontmatter)
|
|
5
|
+
return null;
|
|
6
|
+
const name = scalar(frontmatter, "name");
|
|
7
|
+
const description = scalar(frontmatter, "description");
|
|
8
|
+
if (!name && !description)
|
|
9
|
+
return null;
|
|
10
|
+
return {
|
|
11
|
+
kind: "skill_preview",
|
|
12
|
+
name: name || "skill",
|
|
13
|
+
description,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function scalar(frontmatter, key) {
|
|
17
|
+
const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
18
|
+
if (!match)
|
|
19
|
+
return "";
|
|
20
|
+
return match[1].trim().replace(/^['"]|['"]$/g, "").slice(0, 500);
|
|
21
|
+
}
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 枚举某 agent 可用的 skill,供 Profile 的 SKILLS 区展示。两个来源:
|
|
3
|
+
* - workspace 私有:<agentsRoot>/<handle>/skills/<name>/SKILL.md
|
|
4
|
+
* - 全局共享: ~/.claude/skills/<name>/SKILL.md (可经 CREW_GLOBAL_SKILLS_DIR 覆盖)
|
|
5
|
+
*
|
|
6
|
+
* 每个 skill = 一个目录,内含 SKILL.md;从其 YAML frontmatter 取 name/description,
|
|
7
|
+
* 缺失则回退到目录名。只读、容错(目录不存在/无 frontmatter 都不报错)。
|
|
8
|
+
*/
|
|
9
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
const globalSkillsDir = () => process.env.CREW_GLOBAL_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
|
|
13
|
+
/** 从 SKILL.md 顶部 YAML frontmatter 取 name / description (简易解析,够用)。 */
|
|
14
|
+
function parseFrontmatter(md) {
|
|
15
|
+
const m = md.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
16
|
+
if (!m)
|
|
17
|
+
return {};
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const line of m[1].split("\n")) {
|
|
20
|
+
const kv = line.match(/^(name|description)\s*:\s*(.+?)\s*$/);
|
|
21
|
+
if (kv) {
|
|
22
|
+
const val = kv[2].replace(/^["']|["']$/g, "");
|
|
23
|
+
if (kv[1] === "name")
|
|
24
|
+
out.name = val;
|
|
25
|
+
else
|
|
26
|
+
out.description = val;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
async function readSkillsFrom(dir, scope) {
|
|
32
|
+
const names = await readdir(dir).catch(() => []);
|
|
33
|
+
const skills = [];
|
|
34
|
+
for (const name of names) {
|
|
35
|
+
if (name.startsWith("."))
|
|
36
|
+
continue;
|
|
37
|
+
const skillMd = join(dir, name, "SKILL.md");
|
|
38
|
+
const s = await stat(skillMd).catch(() => null);
|
|
39
|
+
if (!s || !s.isFile())
|
|
40
|
+
continue;
|
|
41
|
+
const md = await readFile(skillMd, "utf8").catch(() => "");
|
|
42
|
+
const fm = parseFrontmatter(md);
|
|
43
|
+
skills.push({ scope, name: fm.name || name, description: fm.description ?? "" });
|
|
44
|
+
}
|
|
45
|
+
return skills;
|
|
46
|
+
}
|
|
47
|
+
/** 列出 agent 的 workspace 私有 + 全局 skill,按 name 排序。 */
|
|
48
|
+
export async function listSkills(agentsRoot, handle) {
|
|
49
|
+
const [ws, global] = await Promise.all([
|
|
50
|
+
readSkillsFrom(join(agentsRoot, handle, "skills"), "workspace"),
|
|
51
|
+
readSkillsFrom(globalSkillsDir(), "global"),
|
|
52
|
+
]);
|
|
53
|
+
const all = [...ws, ...global];
|
|
54
|
+
all.sort((a, b) => (a.scope !== b.scope ? (a.scope === "workspace" ? -1 : 1) : a.name.localeCompare(b.name)));
|
|
55
|
+
return all;
|
|
56
|
+
}
|
package/dist/slog.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon 侧 SLS 日志客户端 —— 透过 server 的 POST /ingest/logs 上报(daemon 不直连 SLS)。
|
|
3
|
+
*
|
|
4
|
+
* 断线补传(关键设计):
|
|
5
|
+
* - 每条日志的 ts = 事件**实际发生时间**(epoch ms)。server 把它写成 SLS __time__,
|
|
6
|
+
* 所以补传的日志仍落在正确时间点,时间线不乱;补传条目带 spooled=true + reported_at 归因。
|
|
7
|
+
* - 上报失败(server 不可达/断网)→ 落本地 spool(~/.crew/logs/sls-spool/*.jsonl);
|
|
8
|
+
* 下次 WS 重连成功(drainSpool)时补传。「daemon 为什么断开」的现场就靠这批日志还原。
|
|
9
|
+
* - 进程退出(exit)→ 残余队列同步落 spool,下次启动补传。异步信号由 main.ts 统一收口。
|
|
10
|
+
*
|
|
11
|
+
* 永不抛错、永不阻塞业务;未 init 时所有调用是 no-op(兼容单测/一次性 run 模式无凭证场景)。
|
|
12
|
+
*/
|
|
13
|
+
import { hostname } from "node:os";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
17
|
+
const FLUSH_INTERVAL_MS = 1500;
|
|
18
|
+
const BATCH_SIZE = 50;
|
|
19
|
+
const SPOOL_MAX_FILES = 50; // spool 目录文件数上限,超出丢最旧(日志非关键数据,防无限膨胀)
|
|
20
|
+
const SPOOL_DRAIN_CAP = 1000; // 单次补传条数上限
|
|
21
|
+
let cfg = null;
|
|
22
|
+
let defaults = {};
|
|
23
|
+
let queue = [];
|
|
24
|
+
let timer = null;
|
|
25
|
+
let flushPromise = null;
|
|
26
|
+
let inFlightBatch = [];
|
|
27
|
+
let lastErrorAt = 0;
|
|
28
|
+
let exitHookInstalled = false;
|
|
29
|
+
function spoolDir() {
|
|
30
|
+
return process.env.CREW_SLS_SPOOL_DIR ?? join(homedir(), ".crew", "logs", "sls-spool");
|
|
31
|
+
}
|
|
32
|
+
/** serve/run 启动时调用一次;disabled(CREW_SLS_LOG=off)则保持 no-op。 */
|
|
33
|
+
export function initSlog(serverUrl, machineToken) {
|
|
34
|
+
if (process.env.CREW_SLS_LOG === "off" || process.env.CREW_SLS_LOG === "0")
|
|
35
|
+
return;
|
|
36
|
+
cfg = { serverUrl: serverUrl.replace(/\/+$/, ""), token: machineToken };
|
|
37
|
+
defaults = { host: hostname(), pid: process.pid };
|
|
38
|
+
// 退出兜底:残余队列同步落 spool(exit 回调只能做同步工作,append 正合适)
|
|
39
|
+
if (!exitHookInstalled) {
|
|
40
|
+
exitHookInstalled = true;
|
|
41
|
+
process.once("exit", () => spoolRemainingSync());
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** 追加默认关联字段(如 ready 帧下发的 machine_id),之后每条日志自动带上。 */
|
|
45
|
+
export function setSlogDefaults(fields) {
|
|
46
|
+
defaults = { ...defaults, ...fields };
|
|
47
|
+
}
|
|
48
|
+
/** 记一条结构化日志(异步批量上报;失败自动落 spool)。 */
|
|
49
|
+
export function dslog(eventType, message, fields = {}) {
|
|
50
|
+
if (!cfg)
|
|
51
|
+
return;
|
|
52
|
+
const { level, ...rest } = fields;
|
|
53
|
+
queue.push({
|
|
54
|
+
ts: Date.now(),
|
|
55
|
+
level: level ?? (eventType.includes("error") || eventType.includes("failed") ? "ERROR" : "INFO"),
|
|
56
|
+
event_type: eventType,
|
|
57
|
+
message,
|
|
58
|
+
fields: compact({ ...defaults, ...rest }),
|
|
59
|
+
});
|
|
60
|
+
if (queue.length >= BATCH_SIZE) {
|
|
61
|
+
void flushSlog();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!timer) {
|
|
65
|
+
timer = setTimeout(() => void flushSlog(), FLUSH_INTERVAL_MS);
|
|
66
|
+
timer.unref?.();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** 把内存队列冲到 server;失败整批落 spool(保 ts 不丢现场)。 */
|
|
70
|
+
export async function flushSlog() {
|
|
71
|
+
if (timer) {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
timer = null;
|
|
74
|
+
}
|
|
75
|
+
if (!cfg)
|
|
76
|
+
return;
|
|
77
|
+
if (flushPromise !== null)
|
|
78
|
+
return flushPromise;
|
|
79
|
+
const current = (async () => {
|
|
80
|
+
while (queue.length > 0) {
|
|
81
|
+
const batch = queue;
|
|
82
|
+
queue = [];
|
|
83
|
+
inFlightBatch = batch;
|
|
84
|
+
try {
|
|
85
|
+
await post(batch);
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
appendSpool(batch);
|
|
89
|
+
warnThrottled(`SLS 日志上报失败,已落本地 spool(${batch.length} 条): ${e.message}`);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
inFlightBatch = [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
flushPromise = current;
|
|
97
|
+
try {
|
|
98
|
+
await current;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (flushPromise === current)
|
|
102
|
+
flushPromise = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 补传本地 spool(WS 重连成功后调用):上次断连/退出前落盘的现场日志,
|
|
107
|
+
* 此刻才有机会送出。条目加 spooled=true,ts 仍是当时的发生时间。
|
|
108
|
+
*/
|
|
109
|
+
export async function drainSpool() {
|
|
110
|
+
if (!cfg)
|
|
111
|
+
return;
|
|
112
|
+
const dir = spoolDir();
|
|
113
|
+
let files;
|
|
114
|
+
try {
|
|
115
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return; // 目录不存在 = 无积压
|
|
119
|
+
}
|
|
120
|
+
if (files.length === 0)
|
|
121
|
+
return;
|
|
122
|
+
const entries = [];
|
|
123
|
+
const consumed = [];
|
|
124
|
+
for (const f of files) {
|
|
125
|
+
if (entries.length >= SPOOL_DRAIN_CAP)
|
|
126
|
+
break;
|
|
127
|
+
const path = join(dir, f);
|
|
128
|
+
try {
|
|
129
|
+
const lines = readFileSync(path, "utf8").split("\n").filter(Boolean);
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
try {
|
|
132
|
+
const e = JSON.parse(line);
|
|
133
|
+
if (typeof e.ts === "number" && typeof e.event_type === "string") {
|
|
134
|
+
entries.push({ ...e, fields: { ...(e.fields ?? {}), spooled: true } });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch { /* 坏行跳过 */ }
|
|
138
|
+
}
|
|
139
|
+
consumed.push(path);
|
|
140
|
+
}
|
|
141
|
+
catch { /* 读失败跳过该文件 */ }
|
|
142
|
+
}
|
|
143
|
+
if (entries.length === 0) {
|
|
144
|
+
for (const p of consumed)
|
|
145
|
+
try {
|
|
146
|
+
unlinkSync(p);
|
|
147
|
+
}
|
|
148
|
+
catch { /* 忽略 */ }
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
for (let i = 0; i < entries.length; i += 100) {
|
|
153
|
+
await post(entries.slice(i, i + 100));
|
|
154
|
+
}
|
|
155
|
+
for (const p of consumed)
|
|
156
|
+
try {
|
|
157
|
+
unlinkSync(p);
|
|
158
|
+
}
|
|
159
|
+
catch { /* 忽略 */ }
|
|
160
|
+
process.stdout.write(`📮 已补传离线期间的 ${entries.length} 条 SLS 日志\n`);
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
warnThrottled(`spool 补传失败,留待下次重连: ${e.message}`); // 文件保留,下次再试
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ── 内部 ─────────────────────────────────────────────────────────
|
|
167
|
+
async function post(entries) {
|
|
168
|
+
const res = await fetch(`${cfg.serverUrl}/ingest/logs`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { authorization: `Bearer ${cfg.token}`, "content-type": "application/json" },
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
entries: entries.map((e) => ({
|
|
173
|
+
ts: e.ts, level: e.level, event_type: e.event_type, message: e.message, fields: e.fields,
|
|
174
|
+
})),
|
|
175
|
+
}),
|
|
176
|
+
signal: AbortSignal.timeout(8000),
|
|
177
|
+
});
|
|
178
|
+
if (!res.ok)
|
|
179
|
+
throw new Error(`HTTP ${res.status}`);
|
|
180
|
+
}
|
|
181
|
+
function appendSpool(entries) {
|
|
182
|
+
try {
|
|
183
|
+
const dir = spoolDir();
|
|
184
|
+
if (!existsSync(dir))
|
|
185
|
+
mkdirSync(dir, { recursive: true });
|
|
186
|
+
rotateSpool(dir);
|
|
187
|
+
const file = join(dir, `daemon-${Date.now()}-${process.pid}.jsonl`);
|
|
188
|
+
appendFileSync(file, entries.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
|
|
189
|
+
}
|
|
190
|
+
catch { /* spool 也失败(磁盘只读等):放弃这批,不能影响业务 */ }
|
|
191
|
+
}
|
|
192
|
+
/** 目录内文件超上限时删最旧,防止长期断网把磁盘写满。 */
|
|
193
|
+
function rotateSpool(dir) {
|
|
194
|
+
try {
|
|
195
|
+
const files = readdirSync(dir)
|
|
196
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
197
|
+
.map((f) => ({ f, mtime: statSync(join(dir, f)).mtimeMs }))
|
|
198
|
+
.sort((a, b) => a.mtime - b.mtime);
|
|
199
|
+
for (const { f } of files.slice(0, Math.max(0, files.length - SPOOL_MAX_FILES + 1))) {
|
|
200
|
+
unlinkSync(join(dir, f));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch { /* 忽略 */ }
|
|
204
|
+
}
|
|
205
|
+
function spoolRemainingSync() {
|
|
206
|
+
if (inFlightBatch.length === 0 && queue.length === 0)
|
|
207
|
+
return;
|
|
208
|
+
const batch = [...inFlightBatch, ...queue];
|
|
209
|
+
inFlightBatch = [];
|
|
210
|
+
queue = [];
|
|
211
|
+
appendSpool(batch);
|
|
212
|
+
}
|
|
213
|
+
function warnThrottled(msg) {
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
if (now - lastErrorAt > 60_000) {
|
|
216
|
+
lastErrorAt = now;
|
|
217
|
+
process.stderr.write(`[slog] ${msg}\n`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function compact(fields) {
|
|
221
|
+
const out = {};
|
|
222
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
223
|
+
if (v === undefined)
|
|
224
|
+
continue;
|
|
225
|
+
out[k] = typeof v === "string" && v.length > 4096 ? `${v.slice(0, 4096)}…(truncated)` : v;
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { startDormantSupervisor, } from "./execution-supervisor.js";
|
|
3
|
+
import { buildClaudeArgs } from "./runtimes/claude.js";
|
|
4
|
+
import { RuntimeCancelledError } from "./runtime-cancellation.js";
|
|
5
|
+
export function supervisorLaunch(request) {
|
|
6
|
+
const common = {
|
|
7
|
+
wakePrompt: request.wakePrompt,
|
|
8
|
+
dangerous: request.effectivePermission === "full_access",
|
|
9
|
+
effectivePermission: request.effectivePermission,
|
|
10
|
+
...(request.model === undefined ? {} : { model: request.model }),
|
|
11
|
+
...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
|
|
12
|
+
};
|
|
13
|
+
if (request.runtime === "claude") {
|
|
14
|
+
return {
|
|
15
|
+
command: request.bin,
|
|
16
|
+
args: buildClaudeArgs({
|
|
17
|
+
...common,
|
|
18
|
+
bin: request.bin,
|
|
19
|
+
cwd: request.cwd,
|
|
20
|
+
env: request.env,
|
|
21
|
+
systemPromptPath: request.systemPromptPath,
|
|
22
|
+
...(request.sessionId === undefined ? {} : {
|
|
23
|
+
sessionId: request.sessionId,
|
|
24
|
+
resume: request.resume,
|
|
25
|
+
}),
|
|
26
|
+
}),
|
|
27
|
+
cwd: request.cwd,
|
|
28
|
+
env: request.env,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (request.runtime === "codex") {
|
|
32
|
+
return {
|
|
33
|
+
command: process.execPath,
|
|
34
|
+
args: [
|
|
35
|
+
fileURLToPath(new URL("./runtimes/codex-app-server-runner.js", import.meta.url)),
|
|
36
|
+
"--bin", request.bin,
|
|
37
|
+
],
|
|
38
|
+
cwd: request.cwd,
|
|
39
|
+
env: request.env,
|
|
40
|
+
stdinText: JSON.stringify({
|
|
41
|
+
systemPrompt: request.systemPrompt,
|
|
42
|
+
wakePrompt: request.wakePrompt,
|
|
43
|
+
effectivePermission: request.effectivePermission,
|
|
44
|
+
...(request.model === undefined ? {} : { model: request.model }),
|
|
45
|
+
...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
|
|
46
|
+
...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
|
|
47
|
+
...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
|
|
48
|
+
resume: request.resume,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (request.effectivePermission !== "full_access") {
|
|
53
|
+
throw new Error(`Kimi ACP cannot enforce ${request.effectivePermission} permission`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
command: process.execPath,
|
|
57
|
+
args: [
|
|
58
|
+
fileURLToPath(new URL("./runtimes/kimi-acp-runner.js", import.meta.url)),
|
|
59
|
+
"--bin", request.bin,
|
|
60
|
+
...(request.model === undefined ? [] : ["--model", request.model]),
|
|
61
|
+
...(request.sessionId === undefined ? [] : ["--session", request.sessionId]),
|
|
62
|
+
...(request.resume ? ["--resume"] : []),
|
|
63
|
+
],
|
|
64
|
+
cwd: request.cwd,
|
|
65
|
+
env: request.env,
|
|
66
|
+
stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export async function launchSupervisedRuntime(request, startSupervisor = startDormantSupervisor, cancellation, platform) {
|
|
70
|
+
if (cancellation?.isRequested())
|
|
71
|
+
throw new RuntimeCancelledError();
|
|
72
|
+
const supervisor = await startSupervisor(supervisorLaunch(request), {
|
|
73
|
+
ownershipMode: "process-lifetime",
|
|
74
|
+
...(platform === undefined ? {} : { platform }),
|
|
75
|
+
});
|
|
76
|
+
let stopPromise = null;
|
|
77
|
+
const cancelOnce = () => {
|
|
78
|
+
stopPromise ??= Promise.resolve().then(supervisor.cancel);
|
|
79
|
+
return stopPromise;
|
|
80
|
+
};
|
|
81
|
+
cancellation?.register(cancelOnce);
|
|
82
|
+
if (cancellation?.isRequested()) {
|
|
83
|
+
await cancellation.waitForStop();
|
|
84
|
+
throw new RuntimeCancelledError();
|
|
85
|
+
}
|
|
86
|
+
const release = supervisor.release();
|
|
87
|
+
if (cancellation === undefined) {
|
|
88
|
+
await release;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
await Promise.race([
|
|
92
|
+
release,
|
|
93
|
+
cancellation.requested.then(async () => {
|
|
94
|
+
await cancellation.waitForStop();
|
|
95
|
+
throw new RuntimeCancelledError();
|
|
96
|
+
}),
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
if (cancellation?.isRequested()) {
|
|
100
|
+
await cancellation.waitForStop();
|
|
101
|
+
throw new RuntimeCancelledError();
|
|
102
|
+
}
|
|
103
|
+
return { ...supervisor, cancel: cancelOnce };
|
|
104
|
+
}
|
package/dist/token.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** 用机器令牌为某 agent 换取 per-launch 的 sk_agent_*。 */
|
|
2
|
+
export async function mintAgentToken(serverUrl, machineToken, handle, displayName, options = {}, fetchImpl = fetch) {
|
|
3
|
+
const res = await fetchImpl(`${serverUrl}/daemon/agents/token`, {
|
|
4
|
+
method: "POST",
|
|
5
|
+
headers: {
|
|
6
|
+
authorization: `Bearer ${machineToken}`,
|
|
7
|
+
"content-type": "application/json",
|
|
8
|
+
},
|
|
9
|
+
// 把本轮唤醒的线程根钉到 per-launch 令牌:服务端据此让 task/消息默认绑/回当前线程。
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
handle,
|
|
12
|
+
...(displayName ? { displayName } : {}),
|
|
13
|
+
...(options.wakeThreadRoot ? { wakeThreadRoot: options.wakeThreadRoot } : {}),
|
|
14
|
+
...(options.wakeContextUpToSeq !== undefined ? { wakeContextUpToSeq: options.wakeContextUpToSeq } : {}),
|
|
15
|
+
...(options.agentRunId ? { agentRunId: options.agentRunId } : {}),
|
|
16
|
+
...(options.executionId ? { executionId: options.executionId } : {}),
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
const body = (await res.json().catch(() => null));
|
|
20
|
+
if (!res.ok || !body?.success || !body.data) {
|
|
21
|
+
throw new Error(`换取 agent 令牌失败 (HTTP ${res.status}): ${body?.error?.message ?? ""}`);
|
|
22
|
+
}
|
|
23
|
+
return body.data;
|
|
24
|
+
}
|