@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.
Files changed (68) hide show
  1. package/README.md +4 -0
  2. package/dist/attachments.js +196 -0
  3. package/dist/bound-im-decision.js +22 -0
  4. package/dist/completion-retransmitter.js +77 -0
  5. package/dist/computer-cli.js +274 -0
  6. package/dist/computer-profile-lock.js +395 -0
  7. package/dist/computer-profile.js +364 -0
  8. package/dist/computer-service.js +358 -0
  9. package/dist/config.js +82 -0
  10. package/dist/console-collapse.js +13 -0
  11. package/dist/console-formatter.js +77 -0
  12. package/dist/console-payload.js +73 -0
  13. package/dist/console.js +329 -0
  14. package/dist/daemon-startup-error.js +30 -0
  15. package/dist/execution-backend.js +44 -0
  16. package/dist/execution-event-limit.js +64 -0
  17. package/dist/execution-journal-lock.js +421 -0
  18. package/dist/execution-journal.js +716 -0
  19. package/dist/execution-protocol.js +342 -0
  20. package/dist/execution-recovery.js +95 -0
  21. package/dist/execution-runner.js +659 -0
  22. package/dist/execution-supervisor-child.js +236 -0
  23. package/dist/execution-supervisor.js +316 -0
  24. package/dist/execution-telemetry-journal.js +71 -0
  25. package/dist/external-output.js +114 -0
  26. package/dist/i18n.js +64 -0
  27. package/dist/json-result.js +27 -0
  28. package/dist/list-models.js +92 -0
  29. package/dist/local-executor.js +439 -0
  30. package/dist/log-format.js +10 -0
  31. package/dist/machine-info.js +124 -0
  32. package/dist/main.js +118 -0
  33. package/dist/normalize.js +170 -0
  34. package/dist/origin-decision.js +44 -0
  35. package/dist/platform.js +8 -0
  36. package/dist/prompt.js +307 -0
  37. package/dist/provider-env.js +90 -0
  38. package/dist/runner.js +234 -0
  39. package/dist/runtime-cancellation.js +74 -0
  40. package/dist/runtime-capabilities.js +43 -0
  41. package/dist/runtime-path.js +60 -0
  42. package/dist/runtimes/claude.js +51 -0
  43. package/dist/runtimes/codex-app-server-runner.js +541 -0
  44. package/dist/runtimes/codex-deepseek-catalog.js +7 -0
  45. package/dist/runtimes/codex-deepseek-config.js +50 -0
  46. package/dist/runtimes/codex.js +53 -0
  47. package/dist/runtimes/kimi-acp-runner.js +364 -0
  48. package/dist/runtimes/kimi.js +45 -0
  49. package/dist/runtimes/progress-watchdog.js +26 -0
  50. package/dist/scheduled-report.js +51 -0
  51. package/dist/scheduled-run-report.js +57 -0
  52. package/dist/serve-lifecycle.js +82 -0
  53. package/dist/serve.js +868 -0
  54. package/dist/session.js +82 -0
  55. package/dist/shared-execution-slots.js +68 -0
  56. package/dist/shutdown-deadline.js +32 -0
  57. package/dist/skill-preview.js +21 -0
  58. package/dist/skills.js +56 -0
  59. package/dist/slog.js +228 -0
  60. package/dist/supervised-runtime.js +104 -0
  61. package/dist/token.js +24 -0
  62. package/dist/unified-diff.js +84 -0
  63. package/dist/websocket-shutdown.js +53 -0
  64. package/dist/win32-job-object.js +193 -0
  65. package/dist/workspace-fs.js +80 -0
  66. package/dist/workspace-import.js +127 -0
  67. package/dist/workspace.js +148 -0
  68. package/package.json +1 -1
package/dist/serve.js ADDED
@@ -0,0 +1,868 @@
1
+ /**
2
+ * 常驻模式 (M3c):连接 server 控制面 WS,注册机器,监听 agent:start 自动唤醒 agent。
3
+ * 断线指数退避重连。一个 agent:start 进来就跑一次现有的 runAgent。
4
+ */
5
+ import { WebSocket } from "ws";
6
+ import { join } from "node:path";
7
+ import { randomUUID } from "node:crypto";
8
+ import { initSlog, dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.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
+ import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
13
+ import { listSkills } from "./skills.js";
14
+ import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
15
+ import { listRuntimeModels } from "./list-models.js";
16
+ import { normalizeScheduledContext } from "./scheduled-report.js";
17
+ import { formatDaemonLogLine } from "./log-format.js";
18
+ import { reportAgentRunComplete } from "./scheduled-run-report.js";
19
+ import { runWithOriginDecisionGuard } from "./origin-decision.js";
20
+ import { createExecutionJournal } from "./execution-journal.js";
21
+ import { createExecutionTelemetryJournal } from "./execution-telemetry-journal.js";
22
+ import { ExecutionRejectedSchema, ExecutionSnapshotSchema, LegacyAgentStartSchema, ServerToDaemonExecutionFrameSchema, } from "./execution-protocol.js";
23
+ import { hashExecutionSpec, runExecution, } from "./execution-runner.js";
24
+ import { executionBackendCapability } from "./execution-backend.js";
25
+ import { awaitWithCancellation, createRuntimeCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
26
+ import { createShutdownDeadline, readTestShutdownConfiguration } from "./shutdown-deadline.js";
27
+ import { closeWebSocketWithinDeadline } from "./websocket-shutdown.js";
28
+ import { reconcileExecutionJournal } from "./execution-recovery.js";
29
+ import { createSharedSlotManager } from "./shared-execution-slots.js";
30
+ import { createCompletionRetransmitter } from "./completion-retransmitter.js";
31
+ // normalize.ts 的活动种类 → activity 枚举
32
+ const ACTIVITY_MAP = {
33
+ init: "working", text: "thinking", reading: "reading", sending: "sending",
34
+ checking: "checking", claiming: "claiming", crew: "working", tool: "working",
35
+ tool_result: "working", done: "done", error: "error",
36
+ };
37
+ export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe) {
38
+ const query = new URLSearchParams({ key: machineToken });
39
+ if (executionBackendCapability(runtimePlatform, jobObjectProbe).supported) {
40
+ query.set("execution_min", String(EXECUTION_PROTOCOL.min));
41
+ query.set("execution_max", String(EXECUTION_PROTOCOL.max));
42
+ }
43
+ for (const capability of DAEMON_CAPABILITIES)
44
+ query.append("capability", capability);
45
+ return `${serverUrl.replace(/^http/, "ws").replace(/\/+$/, "")}/daemon/connect?${query.toString()}`;
46
+ }
47
+ export function serve(config, opts = {}) {
48
+ const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken);
49
+ let stopped = false;
50
+ let ws = null;
51
+ let backoff = 1000;
52
+ const maxBackoff = opts.maxBackoffMs ?? 30_000;
53
+ const testShutdown = readTestShutdownConfiguration(process.env);
54
+ const shutdownTimeoutMs = opts.shutdownTimeoutMs ?? testShutdown.timeoutMs ?? 30_000;
55
+ const createWebSocket = opts.createWebSocket ?? ((url) => new WebSocket(url));
56
+ let reconnectTimer = null;
57
+ let stopPromise = null;
58
+ const executionJournal = opts.execution?.journal ?? createExecutionJournal(config.agentsRoot);
59
+ const executionTelemetry = createExecutionTelemetryJournal(config.agentsRoot);
60
+ const executeProtocol = opts.execution?.runExecution ?? runExecution;
61
+ let detectedExecutionRuntimes = [];
62
+ let runtimeFacts = null;
63
+ const sharedSlots = createSharedSlotManager(config.executionLimits);
64
+ const knownExecutionHashes = new Map();
65
+ const executionReservations = new Map();
66
+ const executionRuns = new Map();
67
+ let executionFrameQueue = Promise.resolve();
68
+ const cancellations = new Map();
69
+ const legacyRuns = new Map();
70
+ const safeExecutionSend = (frame) => {
71
+ try {
72
+ if (ws?.readyState !== WebSocket.OPEN)
73
+ return false;
74
+ ws.send(JSON.stringify(frame));
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ };
81
+ const completionRetransmitter = createCompletionRetransmitter({
82
+ send: safeExecutionSend,
83
+ ...(opts.execution?.completionRetryDelaysMs === undefined ? {} : {
84
+ retryDelaysMs: opts.execution.completionRetryDelaysMs,
85
+ }),
86
+ onAttempt: ({ kind, executionId, attempt, nextDelayMs }) => {
87
+ dslog(kind === "sent" ? "execution.completion_sent" : "execution.completion_retried", kind === "sent" ? "execution completion 已发送" : "execution completion 未确认,已重传", { execution_id: executionId, attempt, next_delay_ms: nextDelayMs });
88
+ },
89
+ });
90
+ const reportExecutionFrame = async (frame) => {
91
+ if (frame.type === "execution:activity" || frame.type === "execution:console") {
92
+ try {
93
+ await executionTelemetry.append(frame);
94
+ }
95
+ catch (error) {
96
+ dslog("execution.telemetry_journal_failed", "execution telemetry 持久化失败", {
97
+ level: "ERROR", execution_id: frame.executionId,
98
+ error_message: error.message,
99
+ });
100
+ throw error;
101
+ }
102
+ }
103
+ if (frame.type === "execution:completed") {
104
+ completionRetransmitter.track(frame);
105
+ return;
106
+ }
107
+ safeExecutionSend(frame);
108
+ };
109
+ const replayExecutionTelemetry = async () => {
110
+ const frames = await executionTelemetry.replay();
111
+ for (const frame of frames)
112
+ safeExecutionSend(frame);
113
+ };
114
+ const snapshotEntry = (entry) => {
115
+ if ((entry.state === "completed" || entry.state === "interrupted") && entry.completion !== null) {
116
+ return { executionId: entry.executionId, state: entry.state, completion: entry.completion, updatedAt: entry.updatedAt };
117
+ }
118
+ if (entry.state === "accepted" || entry.state === "running") {
119
+ return { executionId: entry.executionId, state: entry.state, updatedAt: entry.updatedAt };
120
+ }
121
+ return null;
122
+ };
123
+ const sendSnapshot = async (reqId) => {
124
+ const entries = (await executionJournal.replay()).map(snapshotEntry).filter((entry) => entry !== null);
125
+ safeExecutionSend(ExecutionSnapshotSchema.parse({
126
+ type: "execution:snapshot", protocolVersion: 1, reqId, entries,
127
+ }));
128
+ };
129
+ const sendJournalStatus = (entry, acceptanceState = "ready") => {
130
+ if ((entry.state === "completed" || entry.state === "interrupted") && entry.completion !== null) {
131
+ completionRetransmitter.track(entry.completion);
132
+ return;
133
+ }
134
+ if (entry.state === "accepted" || entry.state === "running") {
135
+ safeExecutionSend({
136
+ type: "execution:accepted", protocolVersion: 1, executionId: entry.executionId,
137
+ state: acceptanceState, effectivePermission: entry.effectivePermission ?? "workspace_write",
138
+ at: entry.acceptedAt,
139
+ });
140
+ if (entry.state === "running" && entry.processStartedAt !== null) {
141
+ safeExecutionSend({
142
+ type: "execution:started", protocolVersion: 1,
143
+ executionId: entry.executionId, at: entry.processStartedAt,
144
+ });
145
+ }
146
+ }
147
+ };
148
+ const cancellationFor = (executionId) => {
149
+ const existing = cancellations.get(executionId);
150
+ if (existing !== undefined)
151
+ return existing.cancellation;
152
+ const controller = createRuntimeCancellation();
153
+ cancellations.set(executionId, controller);
154
+ return controller.cancellation;
155
+ };
156
+ const requestCancellation = (executionId) => {
157
+ cancellationFor(executionId);
158
+ cancellations.get(executionId).request();
159
+ const reservation = executionReservations.get(executionId);
160
+ if (reservation?.isQueued())
161
+ reservation.release();
162
+ };
163
+ let connectedAt = 0; // 本次 WS 连接建立时刻(断开日志算在线时长用)
164
+ initSlog(config.serverUrl, config.machineToken);
165
+ dslog("daemon.start", "daemon 常驻模式启动", { server_url: config.serverUrl, runtime: config.runtimeBin });
166
+ // 并行调度:同一 agent 可并行处理多个【不同任务】(线程/频道),每任务隔离 cwd+work-log。
167
+ // scheduled 重复 run 仍由 running 去重;普通同线程 wake 用 legacyTaskTails 串成 FIFO。
168
+ // 每 agent 超过并行上限的任务继续进入 sharedQueues(不丢)。
169
+ const running = new Set();
170
+ const legacyTaskTails = new Map();
171
+ const log = (s) => process.stdout.write(formatDaemonLogLine(s) + "\n");
172
+ function connect() {
173
+ if (stopped)
174
+ return;
175
+ ws = createWebSocket(wsUrl);
176
+ ws.on("open", () => {
177
+ const openedSocket = ws;
178
+ backoff = 1000;
179
+ connectedAt = Date.now();
180
+ log(`🔌 已连接控制面 ${config.serverUrl}`);
181
+ dslog("daemon.ws_open", "已连接控制面", { server_url: config.serverUrl });
182
+ // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
183
+ void drainSpool();
184
+ // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
185
+ const helloPromise = collectMachineHello(config.agentsRoot, config.executionLimits);
186
+ runtimeFacts = helloPromise.then((hello) => hello.executionRuntimes, (error) => {
187
+ dslog("execution.runtime_detection_failed", "runtime 探测失败", {
188
+ level: "ERROR", error_message: error.message,
189
+ });
190
+ return [];
191
+ });
192
+ void helloPromise
193
+ .then((hello) => {
194
+ detectedExecutionRuntimes = hello.executionRuntimes;
195
+ try {
196
+ ws?.send(JSON.stringify(hello));
197
+ log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`);
198
+ }
199
+ catch { /* 非 OPEN,忽略 */ }
200
+ })
201
+ .catch(() => { });
202
+ opts.onOpen?.(ws);
203
+ void sendSnapshot(`reconnect-${randomUUID()}`);
204
+ void executionJournal.replay()
205
+ .then((entries) => {
206
+ for (const entry of entries) {
207
+ if ((entry.state === "completed" || entry.state === "interrupted")
208
+ && !entry.completionAcknowledged
209
+ && entry.completion !== null) {
210
+ completionRetransmitter.track(entry.completion);
211
+ }
212
+ }
213
+ })
214
+ .catch((error) => {
215
+ dslog("execution.completion_replay_failed", "execution completion 重放失败", {
216
+ level: "ERROR", error_message: error.message,
217
+ });
218
+ })
219
+ .finally(() => {
220
+ if (ws === openedSocket && openedSocket?.readyState === WebSocket.OPEN) {
221
+ completionRetransmitter.resume();
222
+ }
223
+ });
224
+ void replayExecutionTelemetry().catch((error) => {
225
+ dslog("execution.telemetry_replay_failed", "execution telemetry 重放失败", {
226
+ level: "ERROR", error_message: error.message,
227
+ });
228
+ });
229
+ });
230
+ ws.on("message", async (data) => {
231
+ if (stopped)
232
+ return;
233
+ let decoded;
234
+ try {
235
+ decoded = JSON.parse(data.toString());
236
+ }
237
+ catch {
238
+ return;
239
+ }
240
+ if (typeof decoded !== "object" || decoded === null)
241
+ return;
242
+ const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
243
+ if (rawType.startsWith("execution:")) {
244
+ const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
245
+ if (!parsedExecution.success) {
246
+ const executionId = "executionId" in decoded && typeof decoded.executionId === "string"
247
+ ? decoded.executionId
248
+ : null;
249
+ if (executionId !== null) {
250
+ const rejected = ExecutionRejectedSchema.safeParse({
251
+ type: "execution:rejected", protocolVersion: 1, executionId,
252
+ reason: "invalid_spec", message: parsedExecution.error.issues[0]?.message,
253
+ at: new Date().toISOString(),
254
+ });
255
+ if (rejected.success)
256
+ safeExecutionSend(rejected.data);
257
+ }
258
+ return;
259
+ }
260
+ const frame = parsedExecution.data;
261
+ executionFrameQueue = executionFrameQueue.then(async () => {
262
+ if (stopped)
263
+ return;
264
+ if (frame.type === "execution:completion-ack") {
265
+ try {
266
+ await executionJournal.acknowledgeCompletion(frame.executionId);
267
+ const attempts = completionRetransmitter.acknowledge(frame.executionId);
268
+ dslog("execution.completion_acknowledged", "execution completion ACK 已持久化", {
269
+ execution_id: frame.executionId,
270
+ attempts,
271
+ });
272
+ }
273
+ catch (error) {
274
+ dslog("execution.completion_ack_failed", "execution completion ACK 持久化失败", {
275
+ level: "ERROR",
276
+ execution_id: frame.executionId,
277
+ error_message: error.message,
278
+ });
279
+ }
280
+ return;
281
+ }
282
+ if (frame.type === "execution:event-ack") {
283
+ await executionTelemetry.acknowledge(frame.executionId, frame.kind, frame.seq).catch((error) => {
284
+ dslog("execution.telemetry_ack_failed", "execution telemetry ACK 处理失败", {
285
+ level: "ERROR", execution_id: frame.executionId,
286
+ error_message: error.message,
287
+ });
288
+ });
289
+ return;
290
+ }
291
+ if (frame.type === "execution:cancel") {
292
+ if (!knownExecutionHashes.has(frame.executionId)
293
+ && !executionRuns.has(frame.executionId)
294
+ && await executionJournal.get(frame.executionId) === null)
295
+ return;
296
+ requestCancellation(frame.executionId);
297
+ return;
298
+ }
299
+ if (frame.type === "execution:sync") {
300
+ await sendSnapshot(frame.reqId);
301
+ return;
302
+ }
303
+ const spec = frame;
304
+ const hash = hashExecutionSpec(spec);
305
+ const knownHash = knownExecutionHashes.get(spec.executionId);
306
+ if (knownHash !== undefined) {
307
+ if (knownHash !== hash) {
308
+ safeExecutionSend(ExecutionRejectedSchema.parse({
309
+ type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
310
+ reason: "invalid_spec", message: "executionId already has a different spec",
311
+ at: new Date().toISOString(),
312
+ }));
313
+ return;
314
+ }
315
+ const existing = await executionJournal.get(spec.executionId);
316
+ if (existing !== null) {
317
+ const state = executionReservations.get(spec.executionId)?.isQueued() ? "queued" : "ready";
318
+ sendJournalStatus(existing, state);
319
+ }
320
+ return;
321
+ }
322
+ const durableExisting = await executionJournal.get(spec.executionId);
323
+ if (stopped)
324
+ return;
325
+ if (durableExisting !== null) {
326
+ if (durableExisting.specHash !== hash) {
327
+ safeExecutionSend(ExecutionRejectedSchema.parse({
328
+ type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
329
+ reason: "invalid_spec", message: "executionId already has a different spec",
330
+ at: new Date().toISOString(),
331
+ }));
332
+ }
333
+ else {
334
+ sendJournalStatus(durableExisting);
335
+ }
336
+ return;
337
+ }
338
+ knownExecutionHashes.set(spec.executionId, hash);
339
+ const reservation = sharedSlots.reserve(spec.agent.handle, "execution");
340
+ executionReservations.set(spec.executionId, reservation);
341
+ const cancellation = cancellationFor(spec.executionId);
342
+ const cleanupExecutionReservation = () => {
343
+ reservation.release();
344
+ cancellations.delete(spec.executionId);
345
+ executionReservations.delete(spec.executionId);
346
+ knownExecutionHashes.delete(spec.executionId);
347
+ };
348
+ let availableRuntimes;
349
+ try {
350
+ availableRuntimes = opts.execution?.availableRuntimes?.()
351
+ ?? await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes));
352
+ }
353
+ catch (error) {
354
+ cleanupExecutionReservation();
355
+ safeExecutionSend(ExecutionRejectedSchema.parse({
356
+ type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
357
+ reason: "resource_limit", message: `Runtime detection failed: ${error.message}`,
358
+ at: new Date().toISOString(),
359
+ }));
360
+ return;
361
+ }
362
+ if (stopped) {
363
+ cleanupExecutionReservation();
364
+ return;
365
+ }
366
+ const execution = executeProtocol(config, spec, {
367
+ ...opts.execution?.dependencies,
368
+ journal: executionJournal,
369
+ facts: {
370
+ availableRuntimes,
371
+ ...reservation.facts,
372
+ },
373
+ report: reportExecutionFrame,
374
+ ...(reservation.state === undefined ? {} : {
375
+ slot: { state: reservation.state, ready: reservation.ready },
376
+ }),
377
+ cancellation,
378
+ }).finally(() => {
379
+ cleanupExecutionReservation();
380
+ executionRuns.delete(spec.executionId);
381
+ });
382
+ executionRuns.set(spec.executionId, execution);
383
+ void execution.catch(() => { });
384
+ return;
385
+ }).catch((error) => {
386
+ dslog("execution.frame_failed", "execution 控制帧处理失败", {
387
+ level: "ERROR", error_message: error.message,
388
+ });
389
+ });
390
+ return;
391
+ }
392
+ const legacy = typeof decoded === "object" && decoded !== null
393
+ && "type" in decoded && decoded.type === "agent:start"
394
+ ? LegacyAgentStartSchema.safeParse(decoded)
395
+ : null;
396
+ if (legacy !== null && !legacy.success) {
397
+ // 帧结构不合法(如 server 端已升级到 execution v1 但本 daemon 仍按 legacy 校验)。
398
+ // 曾经静默 return——只表现为"@了 agent 没反应",且完全没有排查线索。记录字段路径和
399
+ // zod 错误码,不记录消息正文/附件等可能敏感的字段值。
400
+ const raw = decoded;
401
+ dslog("run.legacy_frame_rejected", "agent:start 帧未通过 legacy schema 校验,已丢弃", {
402
+ level: "WARN",
403
+ agent_handle: typeof raw.agentHandle === "string" ? raw.agentHandle : undefined,
404
+ channel_id: typeof raw.channelId === "string" ? raw.channelId : undefined,
405
+ reason: typeof raw.reason === "string" ? raw.reason : undefined,
406
+ issue_count: legacy.error.issues.length,
407
+ issue_paths: legacy.error.issues
408
+ .map((issue) => `${issue.path.join(".")}:${issue.code}`)
409
+ .join(","),
410
+ });
411
+ return;
412
+ }
413
+ const msg = (legacy?.success ? legacy.data : decoded);
414
+ // 控制面鉴权拒绝:server 端 resolveToken 未命中有效的 machine 凭证(失效/被吊销/
415
+ // 库已重置)。不能静默丢弃这帧——否则只表现为神秘的「每 1s 重连」循环。打印可执行
416
+ // 提示,并把退避拉满,避免无意义高频重连刷屏 server(凭证失配不会靠重试自愈,
417
+ // 需在 NowWork 重新 Add Computer 拿新连接命令)。
418
+ if (msg.type === "ready") {
419
+ // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
420
+ const r = msg;
421
+ setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
422
+ return;
423
+ }
424
+ if (msg.type === "error") {
425
+ if (msg.code === "UNAUTHENTICATED") {
426
+ log(`🛑 控制面拒绝鉴权:机器凭证无效或已吊销 (UNAUTHENTICATED)。`);
427
+ log(` 请在 NowWork 重新 "Add Computer" 获取新的连接命令,再到本机重跑(当前 --api-key 已失效)。`);
428
+ dslog("daemon.ws_auth_rejected", "控制面拒绝鉴权:机器凭证无效或已吊销", { level: "ERROR" });
429
+ backoff = maxBackoff; // 退避到最大,停止每秒重连刷屏
430
+ }
431
+ return;
432
+ }
433
+ // 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
434
+ if (msg.type === "raft:inspect" || msg.type === "raft:import") {
435
+ const req = msg;
436
+ const reply = (r) => {
437
+ try {
438
+ ws?.send(JSON.stringify({ type: "fs:result", reqId: req.reqId, ...r }));
439
+ }
440
+ catch { /* 忽略 */ }
441
+ };
442
+ try {
443
+ const data2 = req.type === "raft:inspect"
444
+ ? await inspectRaftWorkspace(req.path)
445
+ : await importRaftWorkspace(req.path, join(config.agentsRoot, req.handle));
446
+ if (req.type === "raft:import") {
447
+ log(`📦 已导入 raft 工作区 → ${req.handle}: 复制 ${data2.copied.length} 项`);
448
+ }
449
+ reply({ ok: true, data: data2 });
450
+ }
451
+ catch (e) {
452
+ reply({ ok: false, error: e.message });
453
+ }
454
+ return;
455
+ }
456
+ // workspace 文件浏览 / skills 枚举请求 (只读、沙箱;见 workspace-fs.ts / skills.ts)
457
+ if (msg.type === "fs:list" || msg.type === "fs:read" || msg.type === "skills:list" || msg.type === "probe-models") {
458
+ const req = msg;
459
+ const root = join(config.agentsRoot, req.handle);
460
+ const reply = (r) => {
461
+ try {
462
+ ws?.send(JSON.stringify({ type: "fs:result", reqId: req.reqId, ...r }));
463
+ }
464
+ catch { /* 忽略 */ }
465
+ };
466
+ try {
467
+ const data2 = req.type === "fs:list" ? await listWorkspace(root, req.path)
468
+ : req.type === "fs:read" ? await readWorkspaceFile(root, req.path)
469
+ : req.type === "skills:list" ? await listSkills(config.agentsRoot, req.handle)
470
+ : { models: await listRuntimeModels(req.type === "probe-models" ? (req.runtime ?? "") : "") };
471
+ reply({ ok: true, data: data2 });
472
+ }
473
+ catch (e) {
474
+ reply({ ok: false, error: e.message });
475
+ }
476
+ return;
477
+ }
478
+ if (msg.type !== "agent:start")
479
+ return; // ready/error 等忽略
480
+ // 任务键:scheduled run 用 runId(每次运行独立 cwd/work-log;overlap 由 server 端 skip_if_running 管,
481
+ // daemon 的 running 去重只兜底"同一 run 重复投递");普通唤醒仍是 线程锚点 ?? 频道。
482
+ const scheduled = msg.reason === "scheduled_job" && msg.scheduledRun
483
+ ? normalizeScheduledContext({
484
+ jobId: msg.scheduledRun.jobId,
485
+ runId: msg.scheduledRun.runId,
486
+ ...(typeof msg.scheduledRun.title === "string" ? { title: msg.scheduledRun.title } : {}),
487
+ ...(msg.scheduledRun.outputPolicy !== undefined
488
+ ? { outputPolicy: msg.scheduledRun.outputPolicy }
489
+ : {}),
490
+ ...(msg.scheduledRun.externalNotificationPolicy !== undefined
491
+ ? { externalNotificationPolicy: msg.scheduledRun.externalNotificationPolicy }
492
+ : {}),
493
+ })
494
+ : null;
495
+ const threadId = msg.wake?.threadId;
496
+ const taskKey = scheduled ? scheduled.runId : (threadId ?? msg.channelId);
497
+ const key = `${msg.agentHandle}:${taskKey}`;
498
+ // run_id 贯穿本轮全链路(wake→start→resume 决策→end),SLS 按 run_id 一查即得单轮时间线
499
+ const runId = randomUUID();
500
+ const runKeys = {
501
+ run_id: runId, agent_handle: msg.agentHandle, channel_id: msg.channelId,
502
+ thread_id: threadId ?? null, task_key: taskKey,
503
+ };
504
+ dslog("run.wake_received", `收到唤醒 ${msg.agentHandle}`, {
505
+ ...runKeys, reason: msg.reason ?? "", sender: msg.wake?.senderHandle,
506
+ wake_origin: msg.wake?.origin ?? null,
507
+ content_preview: (msg.wake?.content ?? "").replace(/\s+/g, " ").slice(0, 120),
508
+ });
509
+ if (scheduled && running.has(key)) {
510
+ log(`↩︎ 跳过(该任务已在运行): ${key}`);
511
+ dslog("run.dedupe_skip", "跳过唤醒:该任务已在运行", { level: "WARN", ...runKeys });
512
+ return;
513
+ }
514
+ const queueWaitStart = Date.now();
515
+ const runStartedAt = Date.now();
516
+ const controller = createRuntimeCancellation();
517
+ let finishLegacyRun;
518
+ let failLegacyRun;
519
+ const legacyDone = new Promise((resolveDone, rejectDone) => {
520
+ finishLegacyRun = resolveDone;
521
+ failLegacyRun = rejectDone;
522
+ });
523
+ void legacyDone.catch(() => undefined);
524
+ legacyRuns.set(runId, { controller, done: legacyDone });
525
+ let legacyStopError;
526
+ let legacyReservation = null;
527
+ let legacyQueueDone = null;
528
+ let finishLegacyQueue = () => { };
529
+ try {
530
+ if (!scheduled) {
531
+ const previous = legacyTaskTails.get(key);
532
+ legacyQueueDone = new Promise((resolve) => { finishLegacyQueue = resolve; });
533
+ legacyTaskTails.set(key, legacyQueueDone);
534
+ if (previous) {
535
+ log(`⏳ 排队(该任务已在运行): ${key}`);
536
+ dslog("run.wake_queued", "唤醒已排队:该任务正在运行", { ...runKeys });
537
+ await awaitWithCancellation(previous, controller.cancellation);
538
+ }
539
+ }
540
+ running.add(key);
541
+ legacyReservation = sharedSlots.reserve(msg.agentHandle, "legacy");
542
+ await awaitWithCancellation(legacyReservation.ready, controller.cancellation);
543
+ const queueMs = Date.now() - queueWaitStart;
544
+ const threadLabel = threadId ?? null;
545
+ const from = msg.wake?.senderHandle ?? "?";
546
+ const incoming = msg.wake?.content ?? "";
547
+ dslog("run.start", `开始运行 ${msg.agentHandle}`, { ...runKeys, queue_ms: queueMs });
548
+ log(`\n${"─".repeat(56)}`);
549
+ log(`🔔 唤醒 agent=${msg.agentHandle} reason=${msg.reason ?? "?"}`);
550
+ log(` channel = ${msg.channelId}`);
551
+ log(` thread = ${threadLabel ? `${threadLabel} (要求线程内回复)` : "(无,顶层回复)"}`);
552
+ if (incoming)
553
+ log(`📥 来信 @${from}: ${incoming.replace(/\s+/g, " ").slice(0, 200)}`);
554
+ let actSeq = 0;
555
+ const reportActivity = (a) => {
556
+ const det = a.detail ? a.detail.replace(/\s+/g, " ").trim() : "";
557
+ // 发消息时尽量打印回复正文/目标(--content "..." 或 heredoc 首行)
558
+ let line = ` · ${a.label}`;
559
+ if (a.kind === "sending") {
560
+ const m = det.match(/--content\s+"([^"]*)"/) || det.match(/<<'?\w+'?\s*(.*)/);
561
+ line = ` 💬 回复${threadLabel ? `(thread ${threadLabel})` : ""}: ${m ? m[1].slice(0, 160) : det.slice(0, 120)}`;
562
+ }
563
+ else if (det) {
564
+ line += ` ${det.slice(0, 80)}`;
565
+ }
566
+ log(line);
567
+ try {
568
+ ws?.send(JSON.stringify({
569
+ type: "agent:activity",
570
+ agentHandle: msg.agentHandle,
571
+ channelId: msg.channelId,
572
+ activity: ACTIVITY_MAP[a.kind] ?? "working",
573
+ detail: a.detail || a.label,
574
+ seq: actSeq++,
575
+ }));
576
+ }
577
+ catch { /* ws 非 OPEN,忽略 */ }
578
+ };
579
+ // 终端透传:把底层 claude 的每条 console 行按线程上送给 server(独立 seq,落库+广播给 web 终端窗口)。
580
+ let conSeq = 0;
581
+ const reportConsole = (c) => {
582
+ try {
583
+ ws?.send(JSON.stringify({
584
+ type: "agent:console",
585
+ agentHandle: msg.agentHandle,
586
+ channelId: msg.channelId,
587
+ threadId: threadId ?? null,
588
+ stream: c.stream,
589
+ text: c.text,
590
+ // 结构化负载(diff/命令/todo…):前端富渲染用;缺省 = 纯文本行
591
+ ...(c.payload !== undefined ? { payload: c.payload } : {}),
592
+ seq: conSeq++,
593
+ }));
594
+ }
595
+ catch { /* ws 非 OPEN,忽略 */ }
596
+ };
597
+ // 线程聚合:触发消息即任务线程根,你的确认+后续所有回复都要发到它的线程里,
598
+ // 不要发顶层——这样 task 讨论全部聚合在该 thread 下。
599
+ const threadHint = threadId
600
+ ? `\n**所有回复都必须发到这条消息的线程里**(任务线程):用 crew message send --channel ${msg.channelId} --thread ${threadId} 发送,不要发频道顶层。`
601
+ : "";
602
+ // channel(广播投递):你是频道成员之一,自己判断是否与你职责相关——相关才行动(回复 /
603
+ // crew task create / claim / 交接给下一棒),不相关就不回(频道沉默不算失败,避免人人都答)。
604
+ const reasonHint = msg.reason === "channel"
605
+ ? `\n这是频道里的新消息(广播给频道成员)。**先判断是否属于你的职责**:与你无关就直接结束、不要回复(频道沉默不算失败);相关才接手。`
606
+ : "";
607
+ // 关键协作礼仪:一旦决定接手,先在线程回应一声,别让频道空着干等;但确认措辞要与
608
+ // 系统提示词「对人说话/沟通风格」一致——日常语言带理解/方向,不写 task #N 等协议词,
609
+ // 写不出实质内容就并进第一条进展(此处若强制"收到+task #N"会把系统提示词顶掉)。
610
+ const sendCmd = threadId
611
+ ? `crew message send --channel ${msg.channelId} --thread ${threadId}`
612
+ : `crew message send --channel ${msg.channelId}`;
613
+ const ackHint = `\n**协作礼仪:决定接手后,先用 \`${sendCmd}\` 在该任务线程说一声你来跟进**——用日常语言带上你对问题的理解或打算先查什么,不要写"我接 task #N"这类内部编号;写不出实质内容就不单发,并进第一条实质进展,但不要长时间闷头干活把线程空着。`;
614
+ // 图片/文件附件:crew message read 会在消息下列出附件及其 id;图片需下载后用 Read 工具查看,才能真正"看到"内容。
615
+ const attHint = `\n若消息带图片/文件附件(read 会列出 id),用 \`crew attachment get <id>\` 下载到本地,图片再用 Read 工具打开查看后再处理。`;
616
+ // 线程隔离:有 threadId 时用 `crew thread read` 只读本线程(避免被其他线程消息干扰);
617
+ // 顶层消息(无 threadId)用 `crew message read` 读整个频道。
618
+ const readCmd = threadId
619
+ ? `crew thread read`
620
+ : `crew message read --channel ${msg.channelId}`;
621
+ const wakeText = scheduled
622
+ ? buildScheduledPrompt(msg.channelId, msg.wake?.content ?? "", scheduled.outputPolicy, scheduled.externalNotificationPolicy)
623
+ : (msg.wake?.content
624
+ ? `你被唤醒(${msg.reason}): ${msg.wake.content}\n用 ${readCmd} 读${threadId ? "本线程" : "频道"}后按需处理。${reasonHint}${ackHint}${threadHint}${attHint}`
625
+ : undefined);
626
+ const wakeOrigin = scheduled ? undefined : msg.wake?.origin;
627
+ const guarded = await runWithOriginDecisionGuard(wakeOrigin, async (attempt) => {
628
+ if (attempt === 1) {
629
+ dslog("run.origin_decision_retry", "Agent 未完成企微回复决策,补跑一次", {
630
+ level: "WARN", ...runKeys, wake_origin: wakeOrigin,
631
+ });
632
+ }
633
+ const attemptWake = attempt === 1
634
+ ? buildOriginDecisionRetryPrompt(msg.channelId, threadId)
635
+ : wakeText;
636
+ return runAgent(config, {
637
+ handle: msg.agentHandle,
638
+ channelId: msg.channelId,
639
+ taskKey, // 每任务隔离 cwd + work-log(并行不冲突)
640
+ runId, // 贯穿 SLS 日志的单轮关联键
641
+ ...(scheduled ? {
642
+ scheduled: {
643
+ title: scheduled.title,
644
+ outputPolicy: scheduled.outputPolicy,
645
+ externalNotificationPolicy: scheduled.externalNotificationPolicy,
646
+ },
647
+ } : {}),
648
+ ...(wakeOrigin ? { wakeOrigin, originDecisionAttempt: attempt } : {}),
649
+ // 唤醒锚点是具体消息(非纯频道唤醒)时,把它透传下去,供 `crew task create` 锚定到该消息。
650
+ ...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
651
+ ...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
652
+ ...(attemptWake ? { wake: attemptWake } : {}),
653
+ }, reportActivity, reportConsole, { cancellation: controller.cancellation });
654
+ });
655
+ const result = mergeRunAgentResults(guarded.results);
656
+ // 本轮 token 用量上报:runner 已从 result 事件提取(含缓存读/写细分),
657
+ // 连同模型/runtime 一起上送控制面落库 → 支撑每 agent / 每任务(线程)的用量监控与成本核算。
658
+ if (result.usage) {
659
+ const u = result.usage;
660
+ try {
661
+ ws?.send(JSON.stringify({
662
+ type: "agent:usage",
663
+ agentHandle: msg.agentHandle,
664
+ channelId: msg.channelId,
665
+ threadId: threadId ?? null, // 线程根消息 id(= 任务锚点);null = 频道级唤醒
666
+ runtime: result.runtime,
667
+ model: result.model,
668
+ resumed: result.resumed,
669
+ inputTokens: u.inputTokens,
670
+ outputTokens: u.outputTokens,
671
+ cacheReadTokens: u.cacheReadTokens,
672
+ cacheCreationTokens: u.cacheCreationTokens,
673
+ ...(u.costUsd != null ? { costUsd: u.costUsd } : {}),
674
+ }));
675
+ }
676
+ catch { /* ws 非 OPEN,忽略(用量非关键路径,丢一轮不阻塞) */ }
677
+ }
678
+ // 所有 run 都上报完成边界:普通交互用于企微单轮聚合;scheduled 继续驱动 run/job 落库。
679
+ reportAgentRunComplete(ws?.readyState === WebSocket.OPEN ? ws : null, {
680
+ runId,
681
+ agentHandle: msg.agentHandle,
682
+ channelId: msg.channelId,
683
+ threadId: threadId ?? null,
684
+ exitCode: result.exitCode,
685
+ runtime: result.runtime,
686
+ model: result.model,
687
+ resumed: result.resumed,
688
+ usage: result.usage,
689
+ ...(wakeOrigin ? {
690
+ wakeOrigin,
691
+ originDecision: result.originDecision ?? "missing",
692
+ ...(msg.wake?.seq !== undefined ? { contextUpToSeq: msg.wake.seq } : {}),
693
+ } : {}),
694
+ ...(scheduled ? { scheduledRunId: scheduled.runId } : {}),
695
+ ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
696
+ ...(result.report ? { report: result.report } : {}),
697
+ }, dslog);
698
+ // run.end 是排查「任务没跑完就本轮结束」的核心证据:退出码 + 时长 + 最后活动 +
699
+ // 是否 resume + 用量。exit_code!=0 或时长异常短都值得追。
700
+ const lastActivity = result.activities.length
701
+ ? result.activities[result.activities.length - 1].kind
702
+ : null;
703
+ dslog("run.end", `本轮结束 ${msg.agentHandle} (exit=${result.exitCode})`, {
704
+ ...runKeys,
705
+ level: result.exitCode === 0 ? "INFO" : "ERROR",
706
+ exit_code: result.exitCode, duration_ms: Date.now() - runStartedAt,
707
+ runtime: result.runtime, model: result.model, resumed: result.resumed,
708
+ session_id: result.sessionId,
709
+ activity_count: result.activities.length, last_activity: lastActivity,
710
+ wake_origin: wakeOrigin ?? null,
711
+ origin_decision: result.originDecision ?? null,
712
+ ...(result.usage ? {
713
+ tokens_input: result.usage.inputTokens, tokens_output: result.usage.outputTokens,
714
+ cache_read: result.usage.cacheReadTokens, cache_creation: result.usage.cacheCreationTokens,
715
+ ...(result.usage.costUsd != null ? { cost_usd: result.usage.costUsd } : {}),
716
+ } : {}),
717
+ });
718
+ if (result.exitCode === 0) {
719
+ reportActivity({ kind: "done", label: "本轮结束" });
720
+ reportConsole({ stream: "result", text: "● 本轮结束" });
721
+ log(`✅ agent=${msg.agentHandle} 本轮完成`);
722
+ }
723
+ else {
724
+ // 非零退出不能谎报「本轮结束」。runner 已对 codex/kimi 上报带 stderr 的 error 活动,
725
+ // 这里只兜底(如 claude 崩溃)并让终态落在 error 上。
726
+ if (!result.activities.some((a) => a.kind === "error")) {
727
+ reportActivity({ kind: "error", label: "运行出错", detail: `${result.runtime} 退出码 ${result.exitCode}` });
728
+ }
729
+ reportConsole({ stream: "error", text: `✖ 运行出错 (exit ${result.exitCode})` });
730
+ log(`❌ agent=${msg.agentHandle} 本轮失败 (exit ${result.exitCode})`);
731
+ }
732
+ }
733
+ catch (e) {
734
+ if (controller.cancellation.isRequested() && !(e instanceof RuntimeCancelledError)) {
735
+ legacyStopError = e;
736
+ }
737
+ log(`❌ runAgent 失败: ${e.message}`);
738
+ dslog("run.error", `runAgent 失败: ${e.message}`, {
739
+ level: "ERROR", ...runKeys, duration_ms: Date.now() - runStartedAt,
740
+ error_message: e.message, error_stack: e.stack,
741
+ });
742
+ let report;
743
+ if (scheduled) {
744
+ try {
745
+ report = await reportScheduledStartFailure(config, {
746
+ handle: msg.agentHandle,
747
+ channelId: msg.channelId,
748
+ scheduled,
749
+ errorMessage: e.message,
750
+ });
751
+ }
752
+ catch { /* token 也不可用时只能让 server 按 runtime failure 终态化 */ }
753
+ }
754
+ reportAgentRunComplete(ws?.readyState === WebSocket.OPEN ? ws : null, {
755
+ runId,
756
+ agentHandle: msg.agentHandle,
757
+ channelId: msg.channelId,
758
+ threadId: threadId ?? null,
759
+ exitCode: -1,
760
+ errorMessage: e.message,
761
+ ...(!scheduled && msg.wake?.origin ? {
762
+ wakeOrigin: msg.wake.origin,
763
+ originDecision: "missing",
764
+ } : {}),
765
+ ...(scheduled ? { scheduledRunId: scheduled.runId } : {}),
766
+ ...(report ? { report } : {}),
767
+ }, dslog);
768
+ }
769
+ finally {
770
+ running.delete(key);
771
+ legacyReservation?.release();
772
+ finishLegacyQueue();
773
+ if (legacyQueueDone && legacyTaskTails.get(key) === legacyQueueDone) {
774
+ legacyTaskTails.delete(key);
775
+ }
776
+ legacyRuns.delete(runId);
777
+ if (legacyStopError === undefined)
778
+ finishLegacyRun();
779
+ else
780
+ failLegacyRun(legacyStopError);
781
+ void flushSlog(); // 每轮收尾冲一次,保证 run.end 尽快可查
782
+ }
783
+ });
784
+ ws.on("close", (code) => {
785
+ if (stopped)
786
+ return;
787
+ completionRetransmitter.pause();
788
+ // 4001 = 控制面应用级「鉴权失败」关闭码(见 server control-plane.ts)。即使上面的
789
+ // error 帧因 close 抢先而丢失,也能据关闭码识别这是凭证失效——退避拉满,不再每秒热循环。
790
+ if (code === 4001) {
791
+ backoff = maxBackoff;
792
+ log(`🛑 控制面以鉴权失败关闭连接 (code 4001):机器凭证无效或已吊销。`);
793
+ log(` 请在 NowWork 重新 "Add Computer" 获取新连接命令再重跑(当前 --api-key 已失效)。`);
794
+ }
795
+ log(`🔁 控制面断开,${Math.round(backoff / 1000)}s 后重连`);
796
+ // 此刻 server 大概率不可达 → 这条会落 spool,重连 drainSpool 时补传;
797
+ // ts 是现在(断开时刻),排查「daemon 为什么断」以它对齐 server 侧 machine_disconnected。
798
+ dslog("daemon.ws_close", `控制面断开 (code ${code})`, {
799
+ level: "WARN", close_code: code, backoff_ms: backoff,
800
+ online_ms: connectedAt ? Date.now() - connectedAt : null,
801
+ running_tasks: [...running].join(","),
802
+ });
803
+ void flushSlog();
804
+ reconnectTimer = setTimeout(() => {
805
+ reconnectTimer = null;
806
+ connect();
807
+ }, backoff);
808
+ backoff = Math.min(backoff * 2, maxBackoff);
809
+ });
810
+ ws.on("error", () => {
811
+ if (!stopped)
812
+ ws?.close();
813
+ });
814
+ }
815
+ const ready = (async () => {
816
+ await reconcileExecutionJournal(executionJournal, {
817
+ agentsRoot: config.agentsRoot,
818
+ serverUrl: config.serverUrl,
819
+ ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
820
+ log: dslog,
821
+ flush: flushSlog,
822
+ writeStderr: (line) => process.stderr.write(line),
823
+ });
824
+ connect();
825
+ })();
826
+ const stop = () => {
827
+ if (stopPromise !== null)
828
+ return stopPromise;
829
+ stopPromise = (async () => {
830
+ stopped = true;
831
+ completionRetransmitter.stop();
832
+ const deadline = createShutdownDeadline(shutdownTimeoutMs);
833
+ if (reconnectTimer !== null) {
834
+ clearTimeout(reconnectTimer);
835
+ reconnectTimer = null;
836
+ }
837
+ const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
838
+ const pending = [
839
+ webSocketClosed,
840
+ executionFrameQueue,
841
+ ...executionRuns.values(),
842
+ ...[...legacyRuns.values()].map((run) => run.done),
843
+ ...(testShutdown.barrier === null ? [] : [testShutdown.barrier]),
844
+ ];
845
+ try {
846
+ for (const executionId of executionRuns.keys())
847
+ requestCancellation(executionId);
848
+ for (const run of legacyRuns.values())
849
+ run.controller.request();
850
+ await deadline.waitFor(Promise.all(pending));
851
+ await executionJournal.close({ signal: deadline.signal });
852
+ }
853
+ finally {
854
+ deadline.dispose();
855
+ }
856
+ })();
857
+ return stopPromise;
858
+ };
859
+ return {
860
+ ready,
861
+ stop,
862
+ shutdownSnapshot: () => ({
863
+ activeExecutionCount: executionRuns.size,
864
+ activeLegacyCount: legacyRuns.size,
865
+ deadlineMs: shutdownTimeoutMs,
866
+ }),
867
+ };
868
+ }