@nowcrew/daemon 0.6.102-beta.5 → 0.6.102-beta.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/computer-main.js +0 -0
- package/dist/execution-telemetry-journal.js +14 -26
- package/dist/main.js +0 -0
- package/dist/serve.js +11 -12
- package/package.json +11 -10
- package/dist/execution-telemetry-sender.js +0 -151
package/dist/computer-main.js
CHANGED
|
File without changes
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { access, open, mkdir, readdir, readFile, rename, unlink
|
|
1
|
+
import { access, open, mkdir, readdir, readFile, rename, unlink } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { DaemonToServerExecutionFrameSchema, } from "./execution-protocol.js";
|
|
5
5
|
const MAX_PENDING_FILES = 4_096;
|
|
6
|
-
const MAX_TELEMETRY_FRAME_BYTES = 256 * 1_024;
|
|
7
6
|
const fileName = (frame) => `${frame.executionId}.${frame.type === "execution:activity" ? "activity" : "console"}.${frame.seq}.json`;
|
|
8
7
|
export class ExecutionTelemetryJournal {
|
|
9
8
|
directory;
|
|
@@ -11,10 +10,6 @@ export class ExecutionTelemetryJournal {
|
|
|
11
10
|
this.directory = join(agentsRoot, ".execution-telemetry");
|
|
12
11
|
}
|
|
13
12
|
async append(frame) {
|
|
14
|
-
const serialized = JSON.stringify(frame);
|
|
15
|
-
if (Buffer.byteLength(serialized) > MAX_TELEMETRY_FRAME_BYTES) {
|
|
16
|
-
throw new Error("execution telemetry frame exceeds 256 KiB");
|
|
17
|
-
}
|
|
18
13
|
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
19
14
|
const path = join(this.directory, fileName(frame));
|
|
20
15
|
if (await access(path).then(() => true, () => false))
|
|
@@ -27,7 +22,7 @@ export class ExecutionTelemetryJournal {
|
|
|
27
22
|
let handle;
|
|
28
23
|
try {
|
|
29
24
|
handle = await open(tempPath, "wx", 0o600);
|
|
30
|
-
await handle.writeFile(
|
|
25
|
+
await handle.writeFile(JSON.stringify(frame), "utf8");
|
|
31
26
|
await handle.sync();
|
|
32
27
|
await handle.close();
|
|
33
28
|
handle = undefined;
|
|
@@ -48,36 +43,29 @@ export class ExecutionTelemetryJournal {
|
|
|
48
43
|
throw error;
|
|
49
44
|
});
|
|
50
45
|
}
|
|
51
|
-
async
|
|
46
|
+
async replay() {
|
|
52
47
|
const names = await readdir(this.directory).catch((error) => {
|
|
53
48
|
if (error.code === "ENOENT")
|
|
54
49
|
return [];
|
|
55
50
|
throw error;
|
|
56
51
|
});
|
|
57
|
-
|
|
52
|
+
const frames = [];
|
|
53
|
+
for (const name of names.filter((entry) => entry.endsWith(".json")).sort()) {
|
|
58
54
|
let raw;
|
|
59
55
|
try {
|
|
60
|
-
|
|
61
|
-
if ((await stat(path)).size > MAX_TELEMETRY_FRAME_BYTES) {
|
|
62
|
-
throw new Error("execution telemetry frame exceeds 256 KiB");
|
|
63
|
-
}
|
|
64
|
-
raw = JSON.parse(await readFile(path, "utf8"));
|
|
65
|
-
const parsed = DaemonToServerExecutionFrameSchema.safeParse(raw);
|
|
66
|
-
if (!parsed.success || (parsed.data.type !== "execution:activity" && parsed.data.type !== "execution:console")) {
|
|
67
|
-
throw new Error("invalid execution telemetry frame");
|
|
68
|
-
}
|
|
69
|
-
yield parsed.data;
|
|
56
|
+
raw = JSON.parse(await readFile(join(this.directory, name), "utf8"));
|
|
70
57
|
}
|
|
71
58
|
catch (error) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
throw invalid;
|
|
78
|
-
onInvalid(invalid);
|
|
59
|
+
throw new Error(`invalid execution telemetry journal entry: ${name}`, { cause: error });
|
|
60
|
+
}
|
|
61
|
+
const parsed = DaemonToServerExecutionFrameSchema.safeParse(raw);
|
|
62
|
+
if (!parsed.success || (parsed.data.type !== "execution:activity" && parsed.data.type !== "execution:console")) {
|
|
63
|
+
throw new Error(`invalid execution telemetry journal entry: ${name}`);
|
|
79
64
|
}
|
|
65
|
+
frames.push(parsed.data);
|
|
80
66
|
}
|
|
67
|
+
return frames.sort((left, right) => left.at.localeCompare(right.at)
|
|
68
|
+
|| left.type.localeCompare(right.type) || left.seq - right.seq);
|
|
81
69
|
}
|
|
82
70
|
}
|
|
83
71
|
export const createExecutionTelemetryJournal = (agentsRoot) => new ExecutionTelemetryJournal(agentsRoot);
|
package/dist/main.js
CHANGED
|
File without changes
|
package/dist/serve.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ExecutionTelemetrySender } from "./execution-telemetry-sender.js";
|
|
2
1
|
/** 常驻控制面桥接;按请求拉起本地 runtime。 */
|
|
3
2
|
import { handleAgentEnvironmentFrame } from "./agent-environment-control.js";
|
|
4
3
|
import { handleMachineEnvironmentFrame } from "./machine-environment-control.js";
|
|
@@ -81,11 +80,6 @@ export function serve(config, opts = {}) {
|
|
|
81
80
|
const agentMemory = opts.execution?.dependencies?.agentMemory
|
|
82
81
|
?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
|
|
83
82
|
const executionTelemetry = createExecutionTelemetryJournal(config.agentsRoot);
|
|
84
|
-
const telemetrySender = new ExecutionTelemetrySender(executionTelemetry, (error) => {
|
|
85
|
-
dslog("execution.telemetry_replay_failed", "execution telemetry 重放失败", {
|
|
86
|
-
level: "ERROR", error_message: error.message,
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
83
|
const executeProtocol = opts.execution?.runExecution ?? runExecution;
|
|
90
84
|
const localHome = opts.localHome ?? resolveDaemonLocalStateRootFromEnv();
|
|
91
85
|
const hostCoordinator = opts.execution?.hostCoordinator
|
|
@@ -240,8 +234,6 @@ export function serve(config, opts = {}) {
|
|
|
240
234
|
if (frame.type === "execution:activity" || frame.type === "execution:console") {
|
|
241
235
|
try {
|
|
242
236
|
await executionTelemetry.append(frame);
|
|
243
|
-
telemetrySender.notify();
|
|
244
|
-
return;
|
|
245
237
|
}
|
|
246
238
|
catch (error) {
|
|
247
239
|
dslog("execution.telemetry_journal_failed", "execution telemetry 持久化失败", {
|
|
@@ -262,6 +254,11 @@ export function serve(config, opts = {}) {
|
|
|
262
254
|
completionRetransmitter.track(entry.completion);
|
|
263
255
|
}
|
|
264
256
|
};
|
|
257
|
+
const replayExecutionTelemetry = async () => {
|
|
258
|
+
const frames = await executionTelemetry.replay();
|
|
259
|
+
for (const frame of frames)
|
|
260
|
+
safeExecutionSend(frame);
|
|
261
|
+
};
|
|
265
262
|
const snapshotEntry = (entry) => {
|
|
266
263
|
if ((entry.state === "completed" || entry.state === "interrupted") && entry.completion !== null) {
|
|
267
264
|
return { executionId: entry.executionId, state: entry.state, completion: entry.completion, updatedAt: entry.updatedAt };
|
|
@@ -377,7 +374,11 @@ export function serve(config, opts = {}) {
|
|
|
377
374
|
completionRetransmitter.resume();
|
|
378
375
|
}
|
|
379
376
|
});
|
|
380
|
-
|
|
377
|
+
void replayExecutionTelemetry().catch((error) => {
|
|
378
|
+
dslog("execution.telemetry_replay_failed", "execution telemetry 重放失败", {
|
|
379
|
+
level: "ERROR", error_message: error.message,
|
|
380
|
+
});
|
|
381
|
+
});
|
|
381
382
|
});
|
|
382
383
|
connectionSocket.on("message", async (data) => {
|
|
383
384
|
if (stopped || ws !== connectionSocket)
|
|
@@ -449,7 +450,7 @@ export function serve(config, opts = {}) {
|
|
|
449
450
|
return;
|
|
450
451
|
}
|
|
451
452
|
if (frame.type === "execution:event-ack") {
|
|
452
|
-
await
|
|
453
|
+
await executionTelemetry.acknowledge(frame.executionId, frame.kind, frame.seq).catch((error) => {
|
|
453
454
|
dslog("execution.telemetry_ack_failed", "execution telemetry ACK 处理失败", {
|
|
454
455
|
level: "ERROR", execution_id: frame.executionId,
|
|
455
456
|
error_message: error.message,
|
|
@@ -1262,7 +1263,6 @@ export function serve(config, opts = {}) {
|
|
|
1262
1263
|
// server 能力随连接失效;下个连接的 ready 帧重新声明(防降级重连后沿用过期能力)。
|
|
1263
1264
|
serverCapabilities = new Set();
|
|
1264
1265
|
completionRetransmitter.pause();
|
|
1265
|
-
telemetrySender.stop();
|
|
1266
1266
|
// 4001 = 控制面应用级「鉴权失败」关闭码(见 server control-plane.ts)。即使上面的
|
|
1267
1267
|
// error 帧因 close 抢先而丢失,也能据关闭码识别这是凭证失效——退避拉满,不再每秒热循环。
|
|
1268
1268
|
if (reportDaemonConnectionClose(code, log))
|
|
@@ -1323,7 +1323,6 @@ export function serve(config, opts = {}) {
|
|
|
1323
1323
|
stopped = true;
|
|
1324
1324
|
concurrencyTelemetry.stop();
|
|
1325
1325
|
completionRetransmitter.stop();
|
|
1326
|
-
telemetrySender.stop();
|
|
1327
1326
|
const deadline = createShutdownDeadline(shutdownTimeoutMs);
|
|
1328
1327
|
if (reconnectTimer !== null) {
|
|
1329
1328
|
clearTimeout(reconnectTimer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.6.102-beta.
|
|
3
|
+
"version": "0.6.102-beta.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -18,9 +18,17 @@
|
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:release-critical": "vitest run test/automatic-daemon-installer.test.ts test/automatic-install-target.test.ts test/automatic-service-migration.test.ts test/computer-cli.test.ts test/computer-service.test.ts test/computer-setup-client.test.ts test/computer-setup-orchestration.test.ts test/computer-setup.test.ts test/daemon-release-version.test.ts test/daemon-update-controller.test.ts test/daemon-update-eligibility.test.ts test/daemon-updater.test.ts test/execution-protocol.test.ts test/machine-info.test.ts --maxWorkers=2 --minWorkers=1",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
28
|
+
},
|
|
21
29
|
"dependencies": {
|
|
22
30
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
23
|
-
"@nowcrew/cli": "0.4.
|
|
31
|
+
"@nowcrew/cli": "0.4.17",
|
|
24
32
|
"cross-spawn": "^7.0.6",
|
|
25
33
|
"semver": "7.8.5",
|
|
26
34
|
"smol-toml": "1.8.0",
|
|
@@ -39,12 +47,5 @@
|
|
|
39
47
|
"tsx": "^4.19.0",
|
|
40
48
|
"typescript": "^5.6.0",
|
|
41
49
|
"vitest": "^2.1.0"
|
|
42
|
-
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
45
|
-
"build": "tsc -p tsconfig.json",
|
|
46
|
-
"test": "vitest run",
|
|
47
|
-
"test:release-critical": "vitest run test/automatic-daemon-installer.test.ts test/automatic-install-target.test.ts test/automatic-service-migration.test.ts test/computer-cli.test.ts test/computer-service.test.ts test/computer-setup-client.test.ts test/computer-setup-orchestration.test.ts test/computer-setup.test.ts test/daemon-release-version.test.ts test/daemon-update-controller.test.ts test/daemon-update-eligibility.test.ts test/daemon-updater.test.ts test/execution-protocol.test.ts test/machine-info.test.ts --maxWorkers=2 --minWorkers=1",
|
|
48
|
-
"typecheck": "tsc --noEmit"
|
|
49
50
|
}
|
|
50
|
-
}
|
|
51
|
+
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { dslog } from "./slog.js";
|
|
2
|
-
import {} from "./execution-telemetry-journal.js";
|
|
3
|
-
const MAX_IN_FLIGHT = 16;
|
|
4
|
-
const MAX_IN_FLIGHT_BYTES = 512 * 1_024;
|
|
5
|
-
const ACK_TIMEOUT_MS = 30_000;
|
|
6
|
-
const keyOf = (executionId, kind, seq) => `${executionId}.${kind}.${seq}`;
|
|
7
|
-
/** 复用 journal 与 event-ack。窗口只保留游标,帧内容按需从磁盘读取。 */
|
|
8
|
-
export class ExecutionTelemetrySender {
|
|
9
|
-
journal;
|
|
10
|
-
onError;
|
|
11
|
-
inFlight = new Map();
|
|
12
|
-
generation = 0;
|
|
13
|
-
running = false;
|
|
14
|
-
requested = false;
|
|
15
|
-
windowBlocked = false;
|
|
16
|
-
lastProgress = 0;
|
|
17
|
-
lastReportAt = 0;
|
|
18
|
-
lastErrorAt = -Infinity;
|
|
19
|
-
sentFrames = 0;
|
|
20
|
-
acknowledgedFrames = 0;
|
|
21
|
-
send;
|
|
22
|
-
timer;
|
|
23
|
-
constructor(journal, onError) {
|
|
24
|
-
this.journal = journal;
|
|
25
|
-
this.onError = onError;
|
|
26
|
-
}
|
|
27
|
-
start(send, onTimeout) {
|
|
28
|
-
this.stop();
|
|
29
|
-
this.send = send;
|
|
30
|
-
this.sentFrames = 0;
|
|
31
|
-
this.acknowledgedFrames = 0;
|
|
32
|
-
this.lastReportAt = Date.now();
|
|
33
|
-
this.logState("execution.telemetry_connected", "遥测补传连接已建立");
|
|
34
|
-
this.timer = setInterval(() => {
|
|
35
|
-
// 单个失败帧不能打断仍在正常确认的连接;只有整个窗口停滞才重连。
|
|
36
|
-
if (this.windowBlocked && Date.now() - this.lastProgress >= ACK_TIMEOUT_MS) {
|
|
37
|
-
this.logState("execution.telemetry_stalled", "遥测确认窗口停滞,准备重连", {
|
|
38
|
-
level: "WARN", stalled_ms: Date.now() - this.lastProgress,
|
|
39
|
-
});
|
|
40
|
-
this.stop();
|
|
41
|
-
onTimeout();
|
|
42
|
-
}
|
|
43
|
-
else if (this.inFlight.size > 0 && Date.now() - this.lastReportAt >= ACK_TIMEOUT_MS) {
|
|
44
|
-
this.lastReportAt = Date.now();
|
|
45
|
-
this.logState("execution.telemetry_progress", "遥测确认窗口状态");
|
|
46
|
-
}
|
|
47
|
-
}, 1_000);
|
|
48
|
-
this.timer.unref();
|
|
49
|
-
this.notify();
|
|
50
|
-
}
|
|
51
|
-
stop() {
|
|
52
|
-
if (this.send)
|
|
53
|
-
this.logState("execution.telemetry_disconnected", "遥测发送停止,未确认帧保留在 journal");
|
|
54
|
-
this.generation += 1;
|
|
55
|
-
this.send = undefined;
|
|
56
|
-
this.requested = false;
|
|
57
|
-
this.windowBlocked = false;
|
|
58
|
-
if (this.timer !== undefined)
|
|
59
|
-
clearInterval(this.timer);
|
|
60
|
-
this.timer = undefined;
|
|
61
|
-
this.inFlight.clear();
|
|
62
|
-
}
|
|
63
|
-
async acknowledge(executionId, kind, seq) {
|
|
64
|
-
const key = keyOf(executionId, kind, seq);
|
|
65
|
-
const entry = this.inFlight.get(key);
|
|
66
|
-
if (!entry)
|
|
67
|
-
return;
|
|
68
|
-
await this.journal.acknowledge(executionId, kind, seq);
|
|
69
|
-
// 重连会清空窗口;条目身份也能挡住旧连接和重复 ACK 的异步完成。
|
|
70
|
-
if (this.inFlight.get(key) !== entry)
|
|
71
|
-
return;
|
|
72
|
-
this.inFlight.delete(key);
|
|
73
|
-
this.acknowledgedFrames += 1;
|
|
74
|
-
this.lastProgress = Date.now();
|
|
75
|
-
this.notify();
|
|
76
|
-
}
|
|
77
|
-
notify() {
|
|
78
|
-
if (!this.send)
|
|
79
|
-
return;
|
|
80
|
-
this.requested = true;
|
|
81
|
-
if (!this.running)
|
|
82
|
-
void this.pump();
|
|
83
|
-
}
|
|
84
|
-
get bytes() {
|
|
85
|
-
let bytes = 0;
|
|
86
|
-
for (const entry of this.inFlight.values())
|
|
87
|
-
bytes += entry.bytes;
|
|
88
|
-
return bytes;
|
|
89
|
-
}
|
|
90
|
-
logState(event, message, fields = {}) {
|
|
91
|
-
dslog(event, message, {
|
|
92
|
-
generation: this.generation, in_flight_frames: this.inFlight.size,
|
|
93
|
-
in_flight_bytes: this.bytes, sent_frames: this.sentFrames,
|
|
94
|
-
acknowledged_frames: this.acknowledgedFrames, window_blocked: this.windowBlocked, ...fields,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
reportError(error) {
|
|
98
|
-
// 同一个坏文件会被多次扫描;不要让错误日志形成新的无界积压。
|
|
99
|
-
if (Date.now() - this.lastErrorAt < ACK_TIMEOUT_MS)
|
|
100
|
-
return;
|
|
101
|
-
this.lastErrorAt = Date.now();
|
|
102
|
-
this.onError(error);
|
|
103
|
-
}
|
|
104
|
-
async pump() {
|
|
105
|
-
this.running = true;
|
|
106
|
-
const generation = this.generation;
|
|
107
|
-
try {
|
|
108
|
-
while (this.send && generation === this.generation && this.requested) {
|
|
109
|
-
this.requested = false;
|
|
110
|
-
let blocked = false;
|
|
111
|
-
// 本轮读取中的文件可能刚收到 ACK;不能把已删除游标的旧读取结果再发出去。
|
|
112
|
-
const alreadySent = new Set(this.inFlight.keys());
|
|
113
|
-
for await (const frame of this.journal.replay((error) => this.reportError(error))) {
|
|
114
|
-
if (!this.send || generation !== this.generation)
|
|
115
|
-
return;
|
|
116
|
-
const key = keyOf(frame.executionId, frame.type === "execution:activity" ? "activity" : "console", frame.seq);
|
|
117
|
-
if (alreadySent.has(key) || this.inFlight.has(key))
|
|
118
|
-
continue;
|
|
119
|
-
const bytes = Buffer.byteLength(JSON.stringify(frame));
|
|
120
|
-
if (this.inFlight.size >= MAX_IN_FLIGHT || this.bytes + bytes > MAX_IN_FLIGHT_BYTES) {
|
|
121
|
-
blocked = true;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
if (this.inFlight.size === 0)
|
|
125
|
-
this.lastProgress = Date.now();
|
|
126
|
-
this.inFlight.set(key, { bytes });
|
|
127
|
-
if (!this.send(frame)) {
|
|
128
|
-
this.inFlight.delete(key);
|
|
129
|
-
this.logState("execution.telemetry_send_failed", "遥测发送失败,帧保留在 journal", { level: "WARN" });
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
this.sentFrames += 1;
|
|
133
|
-
}
|
|
134
|
-
if (!this.send || generation !== this.generation)
|
|
135
|
-
return;
|
|
136
|
-
blocked ||= this.inFlight.size >= MAX_IN_FLIGHT || this.bytes >= MAX_IN_FLIGHT_BYTES;
|
|
137
|
-
if (blocked && !this.windowBlocked)
|
|
138
|
-
this.lastProgress = Date.now();
|
|
139
|
-
this.windowBlocked = blocked;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
this.reportError(error);
|
|
144
|
-
}
|
|
145
|
-
finally {
|
|
146
|
-
this.running = false;
|
|
147
|
-
if (this.send && this.requested)
|
|
148
|
-
this.notify();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|