@nowcrew/daemon 0.5.11 → 0.5.13
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 +77 -0
- package/dist/config.js +38 -0
- package/dist/execution-event-limit.js +59 -0
- package/dist/execution-journal-lock.js +262 -0
- package/dist/execution-journal.js +678 -0
- package/dist/execution-protocol.js +310 -0
- package/dist/execution-runner.js +637 -0
- package/dist/execution-supervisor-child.js +185 -0
- package/dist/execution-supervisor.js +209 -0
- package/dist/local-executor.js +326 -0
- package/dist/machine-info.js +13 -4
- package/dist/origin-decision.js +42 -0
- package/dist/prompt.js +23 -1
- package/dist/runner.js +146 -340
- package/dist/runtimes/claude.js +9 -1
- package/dist/runtimes/codex.js +21 -5
- package/dist/runtimes/kimi.js +3 -0
- package/dist/scheduled-run-report.js +15 -7
- package/dist/serve.js +412 -57
- package/dist/token.js +5 -2
- package/dist/workspace.js +39 -11
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
function fields(input) {
|
|
2
2
|
return {
|
|
3
|
-
scheduled_run_id: input.scheduledRunId,
|
|
3
|
+
...(input.scheduledRunId ? { scheduled_run_id: input.scheduledRunId } : {}),
|
|
4
4
|
run_id: input.runId,
|
|
5
5
|
agent_handle: input.agentHandle,
|
|
6
6
|
channel_id: input.channelId,
|
|
@@ -9,19 +9,24 @@ function fields(input) {
|
|
|
9
9
|
...(input.model ? { model: input.model } : {}),
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
-
export function
|
|
12
|
+
export function reportAgentRunComplete(socket, input, log) {
|
|
13
|
+
const eventPrefix = input.scheduledRunId ? "scheduled_run" : "run";
|
|
13
14
|
if (!socket) {
|
|
14
|
-
log(
|
|
15
|
+
log(`${eventPrefix}.complete_send_failed`, "Agent 完成回报未发送:控制面连接不可用", {
|
|
15
16
|
level: "WARN", ...fields(input), error_message: "control socket unavailable",
|
|
16
17
|
});
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
const payload = JSON.stringify({
|
|
20
21
|
type: "agent:run-complete",
|
|
22
|
+
runId: input.runId,
|
|
21
23
|
agentHandle: input.agentHandle,
|
|
22
24
|
channelId: input.channelId,
|
|
23
|
-
|
|
25
|
+
...(input.threadId !== undefined ? { threadId: input.threadId } : {}),
|
|
26
|
+
...(input.scheduledRunId ? { scheduledRunId: input.scheduledRunId } : {}),
|
|
24
27
|
exitCode: input.exitCode,
|
|
28
|
+
...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
|
|
29
|
+
...(input.originDecision ? { originDecision: input.originDecision } : {}),
|
|
25
30
|
...(input.runtime ? { runtime: input.runtime } : {}),
|
|
26
31
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
27
32
|
...(input.resumed !== undefined ? { resumed: input.resumed } : {}),
|
|
@@ -32,17 +37,20 @@ export function reportScheduledRunComplete(socket, input, log) {
|
|
|
32
37
|
try {
|
|
33
38
|
socket.send(payload, (error) => {
|
|
34
39
|
if (error) {
|
|
35
|
-
log(
|
|
40
|
+
log(`${eventPrefix}.complete_send_failed`, "Agent 完成回报发送失败", {
|
|
36
41
|
level: "WARN", ...fields(input), error_message: error.message,
|
|
37
42
|
});
|
|
38
43
|
return;
|
|
39
44
|
}
|
|
40
|
-
log(
|
|
45
|
+
log(`${eventPrefix}.complete_sent`, "Agent 完成回报已发送", fields(input));
|
|
41
46
|
});
|
|
42
47
|
}
|
|
43
48
|
catch (error) {
|
|
44
|
-
log(
|
|
49
|
+
log(`${eventPrefix}.complete_send_failed`, "Agent 完成回报发送失败", {
|
|
45
50
|
level: "WARN", ...fields(input), error_message: error.message,
|
|
46
51
|
});
|
|
47
52
|
}
|
|
48
53
|
}
|
|
54
|
+
export function reportScheduledRunComplete(socket, input, log) {
|
|
55
|
+
reportAgentRunComplete(socket, input, log);
|
|
56
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -6,24 +6,32 @@ import { WebSocket } from "ws";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { initSlog, dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
|
|
9
|
-
import { reportScheduledStartFailure, runAgent } from "./runner.js";
|
|
10
|
-
import { buildScheduledPrompt } from "./prompt.js";
|
|
11
|
-
import { collectMachineHello, DAEMON_CAPABILITIES } from "./machine-info.js";
|
|
9
|
+
import { mergeRunAgentResults, reportScheduledStartFailure, runAgent } from "./runner.js";
|
|
10
|
+
import { buildOriginDecisionRetryPrompt, buildScheduledPrompt } from "./prompt.js";
|
|
11
|
+
import { collectMachineHello, DAEMON_CAPABILITIES, EXECUTION_PROTOCOL } from "./machine-info.js";
|
|
12
12
|
import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
|
|
13
13
|
import { listSkills } from "./skills.js";
|
|
14
14
|
import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
|
|
15
15
|
import { listRuntimeModels } from "./list-models.js";
|
|
16
16
|
import { normalizeScheduledContext } from "./scheduled-report.js";
|
|
17
17
|
import { formatDaemonLogLine } from "./log-format.js";
|
|
18
|
-
import {
|
|
18
|
+
import { reportAgentRunComplete } from "./scheduled-run-report.js";
|
|
19
|
+
import { runWithOriginDecisionGuard } from "./origin-decision.js";
|
|
20
|
+
import { createExecutionJournal } from "./execution-journal.js";
|
|
21
|
+
import { ExecutionRejectedSchema, ExecutionSnapshotSchema, LegacyAgentStartSchema, ServerToDaemonExecutionFrameSchema, } from "./execution-protocol.js";
|
|
22
|
+
import { hashExecutionSpec, runExecution, } from "./execution-runner.js";
|
|
19
23
|
// normalize.ts 的活动种类 → activity 枚举
|
|
20
24
|
const ACTIVITY_MAP = {
|
|
21
25
|
init: "working", text: "thinking", reading: "reading", sending: "sending",
|
|
22
26
|
checking: "checking", claiming: "claiming", crew: "working", tool: "working",
|
|
23
27
|
tool_result: "working", done: "done", error: "error",
|
|
24
28
|
};
|
|
25
|
-
export function buildControlPlaneUrl(serverUrl, machineToken) {
|
|
29
|
+
export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform) {
|
|
26
30
|
const query = new URLSearchParams({ key: machineToken });
|
|
31
|
+
if (runtimePlatform !== "win32") {
|
|
32
|
+
query.set("execution_min", String(EXECUTION_PROTOCOL.min));
|
|
33
|
+
query.set("execution_max", String(EXECUTION_PROTOCOL.max));
|
|
34
|
+
}
|
|
27
35
|
for (const capability of DAEMON_CAPABILITIES)
|
|
28
36
|
query.append("capability", capability);
|
|
29
37
|
return `${serverUrl.replace(/^http/, "ws").replace(/\/+$/, "")}/daemon/connect?${query.toString()}`;
|
|
@@ -34,34 +42,187 @@ export function serve(config, opts = {}) {
|
|
|
34
42
|
let ws = null;
|
|
35
43
|
let backoff = 1000;
|
|
36
44
|
const maxBackoff = opts.maxBackoffMs ?? 30_000;
|
|
45
|
+
const executionJournal = opts.execution?.journal ?? createExecutionJournal(config.agentsRoot);
|
|
46
|
+
const executeProtocol = opts.execution?.runExecution ?? runExecution;
|
|
47
|
+
let detectedRuntimes = [];
|
|
48
|
+
let runtimeFacts = null;
|
|
49
|
+
const executionReady = executionJournal.reconcileAfterRestart().then(() => true, (error) => {
|
|
50
|
+
dslog("execution.recovery_failed", "execution journal 恢复失败", {
|
|
51
|
+
level: "ERROR", error_message: error.message,
|
|
52
|
+
});
|
|
53
|
+
return false;
|
|
54
|
+
});
|
|
55
|
+
const sharedActive = new Map();
|
|
56
|
+
const sharedQueues = new Map();
|
|
57
|
+
const knownExecutionHashes = new Map();
|
|
58
|
+
const executionReservations = new Map();
|
|
59
|
+
const executionRuns = new Map();
|
|
60
|
+
let executionFrameQueue = Promise.resolve();
|
|
61
|
+
const cancellations = new Map();
|
|
62
|
+
const safeExecutionSend = (frame) => {
|
|
63
|
+
try {
|
|
64
|
+
if (ws?.readyState === WebSocket.OPEN)
|
|
65
|
+
ws.send(JSON.stringify(frame));
|
|
66
|
+
}
|
|
67
|
+
catch { /* reconnect/sync replays durable lifecycle */ }
|
|
68
|
+
};
|
|
69
|
+
const snapshotEntry = (entry) => {
|
|
70
|
+
if ((entry.state === "completed" || entry.state === "interrupted") && entry.completion !== null) {
|
|
71
|
+
return { executionId: entry.executionId, state: entry.state, completion: entry.completion, updatedAt: entry.updatedAt };
|
|
72
|
+
}
|
|
73
|
+
if (entry.state === "accepted" || entry.state === "running") {
|
|
74
|
+
return { executionId: entry.executionId, state: entry.state, updatedAt: entry.updatedAt };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
const sendSnapshot = async (reqId) => {
|
|
79
|
+
const entries = (await executionJournal.replay()).map(snapshotEntry).filter((entry) => entry !== null);
|
|
80
|
+
safeExecutionSend(ExecutionSnapshotSchema.parse({
|
|
81
|
+
type: "execution:snapshot", protocolVersion: 1, reqId, entries,
|
|
82
|
+
}));
|
|
83
|
+
};
|
|
84
|
+
const sendJournalStatus = (entry, acceptanceState = "ready") => {
|
|
85
|
+
if ((entry.state === "completed" || entry.state === "interrupted") && entry.completion !== null) {
|
|
86
|
+
safeExecutionSend(entry.completion);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (entry.state === "accepted" || entry.state === "running") {
|
|
90
|
+
safeExecutionSend({
|
|
91
|
+
type: "execution:accepted", protocolVersion: 1, executionId: entry.executionId,
|
|
92
|
+
state: acceptanceState, effectivePermission: entry.effectivePermission ?? "workspace_write",
|
|
93
|
+
at: entry.acceptedAt,
|
|
94
|
+
});
|
|
95
|
+
if (entry.state === "running" && entry.processStartedAt !== null) {
|
|
96
|
+
safeExecutionSend({
|
|
97
|
+
type: "execution:started", protocolVersion: 1,
|
|
98
|
+
executionId: entry.executionId, at: entry.processStartedAt,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const cancellationFor = (executionId) => {
|
|
104
|
+
const startStop = (state) => {
|
|
105
|
+
if (!state.requested || state.cancel === null)
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
state.stopPromise ??= Promise.resolve().then(state.cancel);
|
|
108
|
+
return state.stopPromise;
|
|
109
|
+
};
|
|
110
|
+
const existing = cancellations.get(executionId);
|
|
111
|
+
if (existing !== undefined) {
|
|
112
|
+
return {
|
|
113
|
+
isRequested: () => existing.requested,
|
|
114
|
+
requested: existing.promise,
|
|
115
|
+
register: (cancel) => {
|
|
116
|
+
existing.cancel = cancel;
|
|
117
|
+
void startStop(existing).catch(() => { });
|
|
118
|
+
},
|
|
119
|
+
waitForStop: () => startStop(existing),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
let resolve;
|
|
123
|
+
const promise = new Promise((done) => { resolve = done; });
|
|
124
|
+
const state = {
|
|
125
|
+
requested: false, resolve, promise,
|
|
126
|
+
cancel: null,
|
|
127
|
+
stopPromise: null,
|
|
128
|
+
};
|
|
129
|
+
cancellations.set(executionId, state);
|
|
130
|
+
return {
|
|
131
|
+
isRequested: () => state.requested,
|
|
132
|
+
requested: promise,
|
|
133
|
+
register: (cancel) => {
|
|
134
|
+
state.cancel = cancel;
|
|
135
|
+
void startStop(state).catch(() => { });
|
|
136
|
+
},
|
|
137
|
+
waitForStop: () => startStop(state),
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const requestCancellation = (executionId) => {
|
|
141
|
+
cancellationFor(executionId);
|
|
142
|
+
const state = cancellations.get(executionId);
|
|
143
|
+
if (state.requested)
|
|
144
|
+
return;
|
|
145
|
+
state.requested = true;
|
|
146
|
+
state.resolve();
|
|
147
|
+
const reservation = executionReservations.get(executionId);
|
|
148
|
+
if (reservation?.isQueued()) {
|
|
149
|
+
reservation.release();
|
|
150
|
+
}
|
|
151
|
+
if (state.cancel !== null && state.stopPromise === null) {
|
|
152
|
+
state.stopPromise = Promise.resolve().then(state.cancel);
|
|
153
|
+
void state.stopPromise.catch(() => { });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const promoteNext = (handle) => {
|
|
157
|
+
const queue = sharedQueues.get(handle) ?? [];
|
|
158
|
+
while ((sharedActive.get(handle) ?? 0) < config.executionLimits.maxParallelPerAgent && queue.length > 0) {
|
|
159
|
+
const next = queue.shift();
|
|
160
|
+
if (next.released)
|
|
161
|
+
continue;
|
|
162
|
+
next.promoted = true;
|
|
163
|
+
sharedActive.set(handle, (sharedActive.get(handle) ?? 0) + 1);
|
|
164
|
+
next.resolve();
|
|
165
|
+
}
|
|
166
|
+
if (queue.length === 0)
|
|
167
|
+
sharedQueues.delete(handle);
|
|
168
|
+
};
|
|
169
|
+
const reserveSharedSlot = (handle, kind) => {
|
|
170
|
+
const active = sharedActive.get(handle) ?? 0;
|
|
171
|
+
const queued = sharedQueues.get(handle)?.length ?? 0;
|
|
172
|
+
const facts = { activeForAgent: active, queuedForAgent: queued };
|
|
173
|
+
if (kind === "execution" && active >= config.executionLimits.maxParallelPerAgent
|
|
174
|
+
&& queued >= config.executionLimits.maxQueuedPerAgent) {
|
|
175
|
+
return { facts, ready: Promise.resolve(), isQueued: () => false, release: () => { } };
|
|
176
|
+
}
|
|
177
|
+
let resolve;
|
|
178
|
+
const ready = new Promise((done) => { resolve = done; });
|
|
179
|
+
const entry = { kind, released: false, promoted: active < config.executionLimits.maxParallelPerAgent, resolve };
|
|
180
|
+
if (entry.promoted) {
|
|
181
|
+
sharedActive.set(handle, active + 1);
|
|
182
|
+
resolve();
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const queue = sharedQueues.get(handle) ?? [];
|
|
186
|
+
queue.push(entry);
|
|
187
|
+
sharedQueues.set(handle, queue);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
facts,
|
|
191
|
+
state: entry.promoted ? "ready" : "queued",
|
|
192
|
+
ready,
|
|
193
|
+
isQueued: () => !entry.promoted && !entry.released,
|
|
194
|
+
release: () => {
|
|
195
|
+
if (entry.released)
|
|
196
|
+
return;
|
|
197
|
+
entry.released = true;
|
|
198
|
+
if (entry.promoted) {
|
|
199
|
+
sharedActive.set(handle, Math.max(0, (sharedActive.get(handle) ?? 1) - 1));
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const queue = sharedQueues.get(handle);
|
|
203
|
+
const index = queue?.indexOf(entry) ?? -1;
|
|
204
|
+
if (queue !== undefined && index >= 0)
|
|
205
|
+
queue.splice(index, 1);
|
|
206
|
+
if (queue?.length === 0)
|
|
207
|
+
sharedQueues.delete(handle);
|
|
208
|
+
}
|
|
209
|
+
promoteNext(handle);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
};
|
|
37
213
|
let connectedAt = 0; // 本次 WS 连接建立时刻(断开日志算在线时长用)
|
|
38
214
|
initSlog(config.serverUrl, config.machineToken);
|
|
39
215
|
dslog("daemon.start", "daemon 常驻模式启动", { server_url: config.serverUrl, runtime: config.runtimeBin });
|
|
40
216
|
// 并行调度:同一 agent 可并行处理多个【不同任务】(线程/频道),每任务隔离 cwd+work-log。
|
|
41
217
|
// - running:正在跑的「agent:任务」去重键(同一任务重复唤醒才跳过)。
|
|
42
|
-
// - agentSlots:每 agent
|
|
43
|
-
const MAX_PARALLEL = Number(process.env.CREW_MAX_PARALLEL) || 4;
|
|
218
|
+
// - agentSlots:每 agent 当前并行数;超过配置上限的进 FIFO 队列(不丢)。
|
|
44
219
|
const running = new Set();
|
|
45
|
-
const agentSlots = new Map();
|
|
46
|
-
const waitQueue = new Map();
|
|
47
220
|
const acquireSlot = async (handle) => {
|
|
48
|
-
|
|
49
|
-
if (n < MAX_PARALLEL) {
|
|
50
|
-
agentSlots.set(handle, n + 1);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
await new Promise((resolve) => {
|
|
54
|
-
const q = waitQueue.get(handle) ?? [];
|
|
55
|
-
q.push(resolve);
|
|
56
|
-
waitQueue.set(handle, q);
|
|
57
|
-
});
|
|
58
|
-
agentSlots.set(handle, (agentSlots.get(handle) ?? 0) + 1);
|
|
221
|
+
await reserveSharedSlot(handle, "legacy").ready;
|
|
59
222
|
};
|
|
60
223
|
const releaseSlot = (handle) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (q && q.length)
|
|
64
|
-
(q.shift())();
|
|
224
|
+
sharedActive.set(handle, Math.max(0, (sharedActive.get(handle) ?? 1) - 1));
|
|
225
|
+
promoteNext(handle);
|
|
65
226
|
};
|
|
66
227
|
const log = (s) => process.stdout.write(formatDaemonLogLine(s) + "\n");
|
|
67
228
|
function connect() {
|
|
@@ -76,8 +237,16 @@ export function serve(config, opts = {}) {
|
|
|
76
237
|
// 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
|
|
77
238
|
void drainSpool();
|
|
78
239
|
// 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
|
|
79
|
-
|
|
240
|
+
const helloPromise = collectMachineHello(config.agentsRoot, config.executionLimits);
|
|
241
|
+
runtimeFacts = helloPromise.then((hello) => hello.runtimes, (error) => {
|
|
242
|
+
dslog("execution.runtime_detection_failed", "runtime 探测失败", {
|
|
243
|
+
level: "ERROR", error_message: error.message,
|
|
244
|
+
});
|
|
245
|
+
return [];
|
|
246
|
+
});
|
|
247
|
+
void helloPromise
|
|
80
248
|
.then((hello) => {
|
|
249
|
+
detectedRuntimes = hello.runtimes;
|
|
81
250
|
try {
|
|
82
251
|
ws?.send(JSON.stringify(hello));
|
|
83
252
|
log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · runtimes=[${hello.runtimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`);
|
|
@@ -86,15 +255,153 @@ export function serve(config, opts = {}) {
|
|
|
86
255
|
})
|
|
87
256
|
.catch(() => { });
|
|
88
257
|
opts.onOpen?.(ws);
|
|
258
|
+
void executionReady.then((ready) => ready ? sendSnapshot(`reconnect-${randomUUID()}`) : undefined);
|
|
89
259
|
});
|
|
90
260
|
ws.on("message", async (data) => {
|
|
91
|
-
let
|
|
261
|
+
let decoded;
|
|
92
262
|
try {
|
|
93
|
-
|
|
263
|
+
decoded = JSON.parse(data.toString());
|
|
94
264
|
}
|
|
95
265
|
catch {
|
|
96
266
|
return;
|
|
97
267
|
}
|
|
268
|
+
if (typeof decoded !== "object" || decoded === null)
|
|
269
|
+
return;
|
|
270
|
+
const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
|
|
271
|
+
if (rawType.startsWith("execution:")) {
|
|
272
|
+
const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
|
|
273
|
+
if (!parsedExecution.success) {
|
|
274
|
+
const executionId = "executionId" in decoded && typeof decoded.executionId === "string"
|
|
275
|
+
? decoded.executionId
|
|
276
|
+
: null;
|
|
277
|
+
if (executionId !== null) {
|
|
278
|
+
const rejected = ExecutionRejectedSchema.safeParse({
|
|
279
|
+
type: "execution:rejected", protocolVersion: 1, executionId,
|
|
280
|
+
reason: "invalid_spec", message: parsedExecution.error.issues[0]?.message,
|
|
281
|
+
at: new Date().toISOString(),
|
|
282
|
+
});
|
|
283
|
+
if (rejected.success)
|
|
284
|
+
safeExecutionSend(rejected.data);
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const frame = parsedExecution.data;
|
|
289
|
+
executionFrameQueue = executionFrameQueue.then(async () => {
|
|
290
|
+
const recovered = await executionReady;
|
|
291
|
+
if (frame.type === "execution:completion-ack") {
|
|
292
|
+
await executionJournal.acknowledgeCompletion(frame.executionId).catch(() => { });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (frame.type === "execution:cancel") {
|
|
296
|
+
if (!knownExecutionHashes.has(frame.executionId)
|
|
297
|
+
&& !executionRuns.has(frame.executionId)
|
|
298
|
+
&& await executionJournal.get(frame.executionId) === null)
|
|
299
|
+
return;
|
|
300
|
+
requestCancellation(frame.executionId);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (frame.type === "execution:sync") {
|
|
304
|
+
await sendSnapshot(frame.reqId);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const spec = frame;
|
|
308
|
+
if (!recovered) {
|
|
309
|
+
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
310
|
+
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
311
|
+
reason: "resource_limit", message: "Local execution journal recovery failed",
|
|
312
|
+
at: new Date().toISOString(),
|
|
313
|
+
}));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const hash = hashExecutionSpec(spec);
|
|
317
|
+
const knownHash = knownExecutionHashes.get(spec.executionId);
|
|
318
|
+
if (knownHash !== undefined) {
|
|
319
|
+
if (knownHash !== hash) {
|
|
320
|
+
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
321
|
+
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
322
|
+
reason: "invalid_spec", message: "executionId already has a different spec",
|
|
323
|
+
at: new Date().toISOString(),
|
|
324
|
+
}));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const existing = await executionJournal.get(spec.executionId);
|
|
328
|
+
if (existing !== null) {
|
|
329
|
+
const state = executionReservations.get(spec.executionId)?.isQueued() ? "queued" : "ready";
|
|
330
|
+
sendJournalStatus(existing, state);
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const durableExisting = await executionJournal.get(spec.executionId);
|
|
335
|
+
if (durableExisting !== null) {
|
|
336
|
+
if (durableExisting.specHash !== hash) {
|
|
337
|
+
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
338
|
+
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
339
|
+
reason: "invalid_spec", message: "executionId already has a different spec",
|
|
340
|
+
at: new Date().toISOString(),
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
sendJournalStatus(durableExisting);
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
knownExecutionHashes.set(spec.executionId, hash);
|
|
349
|
+
const reservation = reserveSharedSlot(spec.agent.handle, "execution");
|
|
350
|
+
executionReservations.set(spec.executionId, reservation);
|
|
351
|
+
const cancellation = cancellationFor(spec.executionId);
|
|
352
|
+
let availableRuntimes;
|
|
353
|
+
try {
|
|
354
|
+
availableRuntimes = opts.execution?.availableRuntimes?.()
|
|
355
|
+
?? await (runtimeFacts ?? Promise.resolve(detectedRuntimes));
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
reservation.release();
|
|
359
|
+
cancellations.delete(spec.executionId);
|
|
360
|
+
executionReservations.delete(spec.executionId);
|
|
361
|
+
knownExecutionHashes.delete(spec.executionId);
|
|
362
|
+
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
363
|
+
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
364
|
+
reason: "resource_limit", message: `Runtime detection failed: ${error.message}`,
|
|
365
|
+
at: new Date().toISOString(),
|
|
366
|
+
}));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const execution = executeProtocol(config, spec, {
|
|
370
|
+
...opts.execution?.dependencies,
|
|
371
|
+
journal: executionJournal,
|
|
372
|
+
facts: {
|
|
373
|
+
availableRuntimes,
|
|
374
|
+
...reservation.facts,
|
|
375
|
+
},
|
|
376
|
+
report: safeExecutionSend,
|
|
377
|
+
...(reservation.state === undefined ? {} : {
|
|
378
|
+
slot: { state: reservation.state, ready: reservation.ready },
|
|
379
|
+
}),
|
|
380
|
+
cancellation,
|
|
381
|
+
}).finally(() => {
|
|
382
|
+
reservation.release();
|
|
383
|
+
cancellations.delete(spec.executionId);
|
|
384
|
+
executionReservations.delete(spec.executionId);
|
|
385
|
+
executionRuns.delete(spec.executionId);
|
|
386
|
+
knownExecutionHashes.delete(spec.executionId);
|
|
387
|
+
});
|
|
388
|
+
executionRuns.set(spec.executionId, execution);
|
|
389
|
+
void execution.catch(() => { });
|
|
390
|
+
return;
|
|
391
|
+
}).catch((error) => {
|
|
392
|
+
dslog("execution.frame_failed", "execution 控制帧处理失败", {
|
|
393
|
+
level: "ERROR", error_message: error.message,
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const legacy = typeof decoded === "object" && decoded !== null
|
|
399
|
+
&& "type" in decoded && decoded.type === "agent:start"
|
|
400
|
+
? LegacyAgentStartSchema.safeParse(decoded)
|
|
401
|
+
: null;
|
|
402
|
+
if (legacy !== null && !legacy.success)
|
|
403
|
+
return;
|
|
404
|
+
const msg = (legacy?.success ? legacy.data : decoded);
|
|
98
405
|
// 控制面鉴权拒绝:server 端 resolveToken 未命中有效的 machine 凭证(失效/被吊销/
|
|
99
406
|
// 库已重置)。不能静默丢弃这帧——否则只表现为神秘的「每 1s 重连」循环。打印可执行
|
|
100
407
|
// 提示,并把退避拉满,避免无意义高频重连刷屏 server(凭证失配不会靠重试自愈,
|
|
@@ -164,7 +471,14 @@ export function serve(config, opts = {}) {
|
|
|
164
471
|
// 任务键:scheduled run 用 runId(每次运行独立 cwd/work-log;overlap 由 server 端 skip_if_running 管,
|
|
165
472
|
// daemon 的 running 去重只兜底"同一 run 重复投递");普通唤醒仍是 线程锚点 ?? 频道。
|
|
166
473
|
const scheduled = msg.reason === "scheduled_job" && msg.scheduledRun
|
|
167
|
-
? normalizeScheduledContext(
|
|
474
|
+
? normalizeScheduledContext({
|
|
475
|
+
jobId: msg.scheduledRun.jobId,
|
|
476
|
+
runId: msg.scheduledRun.runId,
|
|
477
|
+
...(typeof msg.scheduledRun.title === "string" ? { title: msg.scheduledRun.title } : {}),
|
|
478
|
+
...(msg.scheduledRun.outputPolicy !== undefined
|
|
479
|
+
? { outputPolicy: msg.scheduledRun.outputPolicy }
|
|
480
|
+
: {}),
|
|
481
|
+
})
|
|
168
482
|
: null;
|
|
169
483
|
const threadId = msg.wake?.threadId;
|
|
170
484
|
const taskKey = scheduled ? scheduled.runId : (threadId ?? msg.channelId);
|
|
@@ -177,6 +491,7 @@ export function serve(config, opts = {}) {
|
|
|
177
491
|
};
|
|
178
492
|
dslog("run.wake_received", `收到唤醒 ${msg.agentHandle}`, {
|
|
179
493
|
...runKeys, reason: msg.reason ?? "", sender: msg.wake?.senderHandle,
|
|
494
|
+
wake_origin: msg.wake?.origin ?? null,
|
|
180
495
|
content_preview: (msg.wake?.content ?? "").replace(/\s+/g, " ").slice(0, 120),
|
|
181
496
|
});
|
|
182
497
|
if (running.has(key)) {
|
|
@@ -185,7 +500,7 @@ export function serve(config, opts = {}) {
|
|
|
185
500
|
return;
|
|
186
501
|
}
|
|
187
502
|
running.add(key);
|
|
188
|
-
// 并行槽:同 agent
|
|
503
|
+
// 并行槽:同 agent 超过配置上限的任务在此排队(不丢),有空位再跑。
|
|
189
504
|
const slotWaitStart = Date.now();
|
|
190
505
|
await acquireSlot(msg.agentHandle);
|
|
191
506
|
const queueMs = Date.now() - slotWaitStart;
|
|
@@ -271,18 +586,32 @@ export function serve(config, opts = {}) {
|
|
|
271
586
|
: (msg.wake?.content
|
|
272
587
|
? `你被唤醒(${msg.reason}): ${msg.wake.content}\n用 ${readCmd} 读${threadId ? "本线程" : "频道"}后按需处理。${reasonHint}${ackHint}${threadHint}${attHint}`
|
|
273
588
|
: undefined);
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
589
|
+
const wakeOrigin = scheduled ? undefined : msg.wake?.origin;
|
|
590
|
+
const guarded = await runWithOriginDecisionGuard(wakeOrigin, async (attempt) => {
|
|
591
|
+
if (attempt === 1) {
|
|
592
|
+
dslog("run.origin_decision_retry", "Agent 未完成企微回复决策,补跑一次", {
|
|
593
|
+
level: "WARN", ...runKeys, wake_origin: wakeOrigin,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
const attemptWake = attempt === 1
|
|
597
|
+
? buildOriginDecisionRetryPrompt(msg.channelId, threadId)
|
|
598
|
+
: wakeText;
|
|
599
|
+
return runAgent(config, {
|
|
600
|
+
handle: msg.agentHandle,
|
|
601
|
+
channelId: msg.channelId,
|
|
602
|
+
taskKey, // 每任务隔离 cwd + work-log(并行不冲突)
|
|
603
|
+
runId, // 贯穿 SLS 日志的单轮关联键
|
|
604
|
+
...(scheduled ? {
|
|
605
|
+
scheduled: { title: scheduled.title, outputPolicy: scheduled.outputPolicy },
|
|
606
|
+
} : {}),
|
|
607
|
+
...(wakeOrigin ? { wakeOrigin, originDecisionAttempt: attempt } : {}),
|
|
608
|
+
// 唤醒锚点是具体消息(非纯频道唤醒)时,把它透传下去,供 `crew task create` 锚定到该消息。
|
|
609
|
+
...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
|
|
610
|
+
...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
|
|
611
|
+
...(attemptWake ? { wake: attemptWake } : {}),
|
|
612
|
+
}, reportActivity, reportConsole);
|
|
613
|
+
});
|
|
614
|
+
const result = mergeRunAgentResults(guarded.results);
|
|
286
615
|
// 本轮 token 用量上报:runner 已从 result 事件提取(含缓存读/写细分),
|
|
287
616
|
// 连同模型/runtime 一起上送控制面落库 → 支撑每 agent / 每任务(线程)的用量监控与成本核算。
|
|
288
617
|
if (result.usage) {
|
|
@@ -305,16 +634,25 @@ export function serve(config, opts = {}) {
|
|
|
305
634
|
}
|
|
306
635
|
catch { /* ws 非 OPEN,忽略(用量非关键路径,丢一轮不阻塞) */ }
|
|
307
636
|
}
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
637
|
+
// 所有 run 都上报完成边界:普通交互用于企微单轮聚合;scheduled 继续驱动 run/job 落库。
|
|
638
|
+
reportAgentRunComplete(ws?.readyState === WebSocket.OPEN ? ws : null, {
|
|
639
|
+
runId,
|
|
640
|
+
agentHandle: msg.agentHandle,
|
|
641
|
+
channelId: msg.channelId,
|
|
642
|
+
threadId: threadId ?? null,
|
|
643
|
+
exitCode: result.exitCode,
|
|
644
|
+
runtime: result.runtime,
|
|
645
|
+
model: result.model,
|
|
646
|
+
resumed: result.resumed,
|
|
647
|
+
usage: result.usage,
|
|
648
|
+
...(wakeOrigin ? {
|
|
649
|
+
wakeOrigin,
|
|
650
|
+
originDecision: result.originDecision ?? "missing",
|
|
651
|
+
} : {}),
|
|
652
|
+
...(scheduled ? { scheduledRunId: scheduled.runId } : {}),
|
|
653
|
+
...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
|
|
654
|
+
...(result.report ? { report: result.report } : {}),
|
|
655
|
+
}, dslog);
|
|
318
656
|
// run.end 是排查「任务没跑完就本轮结束」的核心证据:退出码 + 时长 + 最后活动 +
|
|
319
657
|
// 是否 resume + 用量。exit_code!=0 或时长异常短都值得追。
|
|
320
658
|
const lastActivity = result.activities.length
|
|
@@ -327,6 +665,8 @@ export function serve(config, opts = {}) {
|
|
|
327
665
|
runtime: result.runtime, model: result.model, resumed: result.resumed,
|
|
328
666
|
session_id: result.sessionId,
|
|
329
667
|
activity_count: result.activities.length, last_activity: lastActivity,
|
|
668
|
+
wake_origin: wakeOrigin ?? null,
|
|
669
|
+
origin_decision: result.originDecision ?? null,
|
|
330
670
|
...(result.usage ? {
|
|
331
671
|
tokens_input: result.usage.inputTokens, tokens_output: result.usage.outputTokens,
|
|
332
672
|
cache_read: result.usage.cacheReadTokens, cache_creation: result.usage.cacheCreationTokens,
|
|
@@ -354,8 +694,8 @@ export function serve(config, opts = {}) {
|
|
|
354
694
|
level: "ERROR", ...runKeys, duration_ms: Date.now() - runStartedAt,
|
|
355
695
|
error_message: e.message, error_stack: e.stack,
|
|
356
696
|
});
|
|
697
|
+
let report;
|
|
357
698
|
if (scheduled) {
|
|
358
|
-
let report;
|
|
359
699
|
try {
|
|
360
700
|
report = await reportScheduledStartFailure(config, {
|
|
361
701
|
handle: msg.agentHandle,
|
|
@@ -365,12 +705,21 @@ export function serve(config, opts = {}) {
|
|
|
365
705
|
});
|
|
366
706
|
}
|
|
367
707
|
catch { /* token 也不可用时只能让 server 按 runtime failure 终态化 */ }
|
|
368
|
-
reportScheduledRunComplete(ws?.readyState === WebSocket.OPEN ? ws : null, {
|
|
369
|
-
runId, scheduledRunId: scheduled.runId, agentHandle: msg.agentHandle,
|
|
370
|
-
channelId: msg.channelId, exitCode: -1, errorMessage: e.message,
|
|
371
|
-
...(report ? { report } : {}),
|
|
372
|
-
}, dslog);
|
|
373
708
|
}
|
|
709
|
+
reportAgentRunComplete(ws?.readyState === WebSocket.OPEN ? ws : null, {
|
|
710
|
+
runId,
|
|
711
|
+
agentHandle: msg.agentHandle,
|
|
712
|
+
channelId: msg.channelId,
|
|
713
|
+
threadId: threadId ?? null,
|
|
714
|
+
exitCode: -1,
|
|
715
|
+
errorMessage: e.message,
|
|
716
|
+
...(!scheduled && msg.wake?.origin ? {
|
|
717
|
+
wakeOrigin: msg.wake.origin,
|
|
718
|
+
originDecision: "missing",
|
|
719
|
+
} : {}),
|
|
720
|
+
...(scheduled ? { scheduledRunId: scheduled.runId } : {}),
|
|
721
|
+
...(report ? { report } : {}),
|
|
722
|
+
}, dslog);
|
|
374
723
|
}
|
|
375
724
|
finally {
|
|
376
725
|
running.delete(key);
|
|
@@ -407,6 +756,12 @@ export function serve(config, opts = {}) {
|
|
|
407
756
|
stop: () => {
|
|
408
757
|
stopped = true;
|
|
409
758
|
ws?.close();
|
|
759
|
+
for (const executionId of executionRuns.keys())
|
|
760
|
+
requestCancellation(executionId);
|
|
761
|
+
void Promise.race([
|
|
762
|
+
Promise.allSettled([...executionRuns.values()]).then(() => true),
|
|
763
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 10_000)),
|
|
764
|
+
]).then((settled) => settled ? executionJournal.close() : undefined);
|
|
410
765
|
},
|
|
411
766
|
};
|
|
412
767
|
}
|