u-foo 1.0.6 → 1.2.0

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 (149) hide show
  1. package/README.md +247 -23
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +168 -28
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +157 -0
  64. package/src/chat/index.js +938 -2910
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +133 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1587 -0
  98. package/src/config.js +50 -2
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +662 -489
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
@@ -1,96 +1,53 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
- const net = require("net");
4
3
  const { spawnSync } = require("child_process");
5
4
  const { runUfooAgent } = require("../agent/ufooAgent");
6
- const { launchAgent, closeAgent, resumeAgents } = require("./ops");
5
+ const { launchAgent, closeAgent, getRecoverableAgents, resumeAgents } = require("./ops");
7
6
  const { buildStatus } = require("./status");
8
7
  const EventBus = require("../bus");
8
+ const { AgentProcessManager } = require("./agentProcessManager");
9
9
  const NicknameManager = require("../bus/nickname");
10
10
  const { generateInstanceId, subscriberToSafeName } = require("../bus/utils");
11
+ const { createDaemonIpcServer } = require("./ipcServer");
12
+ const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../shared/eventContract");
11
13
  const { getUfooPaths } = require("../ufoo/paths");
12
14
  const { scheduleProviderSessionProbe, loadProviderSessionCache } = require("./providerSessions");
13
- const { loadConfig } = require("../config");
14
-
15
- /**
16
- * Agent 进程管理器 - daemon 作为父进程监控所有 internal agents
17
- */
18
- class AgentProcessManager {
19
- constructor(projectRoot) {
20
- this.projectRoot = projectRoot;
21
- this.processes = new Map(); // subscriber_id -> child_process
22
- }
23
-
24
- /**
25
- * 注册子进程并监听退出事件
26
- */
27
- register(subscriberId, childProcess) {
28
- if (!subscriberId || !childProcess) return;
29
-
30
- this.processes.set(subscriberId, childProcess);
31
-
32
- childProcess.on("exit", (code, signal) => {
33
- this.processes.delete(subscriberId);
15
+ const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
16
+ const { createDaemonCronController } = require("./cronOps");
17
+ const { runAssistantTask } = require("../assistant/bridge");
18
+ const { runPromptWithAssistant } = require("./promptLoop");
19
+ const { handlePromptRequest } = require("./promptRequest");
20
+ const { recordAgentReport } = require("./reporting");
34
21
 
35
- // 自动清理 bus 状态
36
- try {
37
- const eventBus = new EventBus(this.projectRoot);
38
- eventBus.loadBusData();
39
- if (eventBus.busData.agents?.[subscriberId]) {
40
- eventBus.busData.agents[subscriberId].status = "inactive";
41
- eventBus.busData.agents[subscriberId].last_seen = new Date().toISOString();
42
- eventBus.saveBusData();
43
- console.log(`[daemon] Agent ${subscriberId} exited (code=${code}, signal=${signal}), marked inactive`);
44
- }
45
- } catch (err) {
46
- console.error(`[daemon] Failed to cleanup ${subscriberId}:`, err.message);
47
- }
48
- });
49
-
50
- childProcess.on("error", (err) => {
51
- console.error(`[daemon] Agent ${subscriberId} error:`, err.message);
52
- this.processes.delete(subscriberId);
53
- });
54
- }
55
-
56
- /**
57
- * 获取运行中的进程
58
- */
59
- get(subscriberId) {
60
- return this.processes.get(subscriberId);
61
- }
22
+ let providerSessions = null;
23
+ let probeHandles = new Map();
24
+ let daemonCronController = null;
62
25
 
63
- /**
64
- * 获取所有进程数量
65
- */
66
- count() {
67
- return this.processes.size;
68
- }
26
+ function sleep(ms) {
27
+ return new Promise((resolve) => setTimeout(resolve, ms));
28
+ }
69
29
 
70
- /**
71
- * 清理所有子进程
72
- */
73
- cleanup() {
74
- for (const [subscriberId, child] of this.processes.entries()) {
75
- try {
76
- child.kill("SIGTERM");
77
- console.log(`[daemon] Killed agent ${subscriberId}`);
78
- } catch {
79
- // ignore
80
- }
81
- }
82
- this.processes.clear();
83
- }
30
+ function normalizeBusAgentType(agentType = "") {
31
+ const value = String(agentType || "").trim().toLowerCase();
32
+ if (!value) return "claude-code";
33
+ if (value === "codex") return "codex";
34
+ if (value === "claude" || value === "claude-code") return "claude-code";
35
+ if (value === "ufoo" || value === "ucode" || value === "ufoo-code") return "ufoo-code";
36
+ return value;
84
37
  }
85
38
 
86
- function sleep(ms) {
87
- return new Promise((resolve) => setTimeout(resolve, ms));
39
+ function normalizeLaunchAgent(agent = "") {
40
+ const value = String(agent || "").trim().toLowerCase();
41
+ if (value === "codex") return "codex";
42
+ if (value === "claude" || value === "claude-code") return "claude";
43
+ if (value === "ufoo" || value === "ucode" || value === "ufoo-code") return "ufoo";
44
+ return "";
88
45
  }
89
46
 
90
47
  async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso) {
91
48
  if (!nickname) return null;
92
49
  const busPath = getUfooPaths(projectRoot).agentsFile;
93
- const targetType = agentType === "codex" ? "codex" : "claude-code";
50
+ const targetType = normalizeBusAgentType(agentType);
94
51
  const deadline = Date.now() + 10000;
95
52
  const eventBus = new EventBus(projectRoot);
96
53
  let lastError = null;
@@ -149,19 +106,84 @@ function readPid(projectRoot) {
149
106
  }
150
107
  }
151
108
 
109
+ function checkPid(pid) {
110
+ if (!Number.isFinite(pid) || pid <= 0) {
111
+ return { alive: false, uncertain: false };
112
+ }
113
+ try {
114
+ process.kill(pid, 0);
115
+ return { alive: true, uncertain: false };
116
+ } catch (err) {
117
+ if (err && err.code === "EPERM") {
118
+ return { alive: true, uncertain: true };
119
+ }
120
+ return { alive: false, uncertain: false };
121
+ }
122
+ }
123
+
124
+ function readProcessArgs(pid) {
125
+ if (!Number.isFinite(pid) || pid <= 0) return "";
126
+ try {
127
+ const res = spawnSync("ps", ["-p", String(pid), "-o", "args="], {
128
+ encoding: "utf8",
129
+ stdio: ["ignore", "pipe", "ignore"],
130
+ });
131
+ if (res && res.error) {
132
+ if (res.error.code === "EPERM") return "__EPERM__";
133
+ return "";
134
+ }
135
+ if (res && res.status === 0) {
136
+ return String(res.stdout || "").trim();
137
+ }
138
+ } catch {
139
+ // ignore
140
+ }
141
+ return "";
142
+ }
143
+
144
+ function isLikelyDaemonProcess(pid) {
145
+ const args = readProcessArgs(pid);
146
+ if (!args || args === "__EPERM__") return null;
147
+ const text = args.toLowerCase();
148
+ const hasCliPattern = /\bufoo\s+daemon\s+(--start|start)\b/.test(text);
149
+ const hasNodePattern = /\bufoo\.js\s+daemon\s+(--start|start)\b/.test(text);
150
+ if (hasCliPattern || hasNodePattern) return true;
151
+ if (text.includes("/src/daemon/run.js")) return true;
152
+ return false;
153
+ }
154
+
155
+ function looksLikeRunningDaemon(projectRoot, pid) {
156
+ const state = checkPid(pid);
157
+ if (!state.alive) return false;
158
+ const sock = socketPath(projectRoot);
159
+ if (!fs.existsSync(sock)) return false;
160
+ try {
161
+ const stat = fs.statSync(sock);
162
+ if (!stat.isSocket()) return false;
163
+ } catch {
164
+ return false;
165
+ }
166
+ const procMatch = isLikelyDaemonProcess(pid);
167
+ if (procMatch === true) return true;
168
+ if (procMatch === false) return false;
169
+ if (!state.uncertain) return true;
170
+ const recordedPid = readPid(projectRoot);
171
+ return recordedPid === pid && fs.existsSync(sock);
172
+ }
173
+
152
174
  function isRunning(projectRoot) {
153
175
  const pid = readPid(projectRoot);
154
176
  if (!pid) return false;
155
- try {
156
- process.kill(pid, 0);
157
- // PID 存活即认为 daemon 正在运行
158
- // 不调用 isDaemonProcess() — ps 命令可能有瞬态失败导致误判
159
- // 不删除 PID/socket — 破坏性操作会导致竞争条件
177
+ if (looksLikeRunningDaemon(projectRoot, pid)) {
160
178
  return true;
179
+ }
180
+ try {
181
+ fs.unlinkSync(pidPath(projectRoot));
161
182
  } catch {
162
- // 进程已死
163
- return false;
183
+ // ignore
164
184
  }
185
+ removeSocket(projectRoot);
186
+ return false;
165
187
  }
166
188
 
167
189
  function removeSocket(projectRoot) {
@@ -240,12 +262,32 @@ function checkAndCleanupNickname(projectRoot, nickname) {
240
262
  }
241
263
  }
242
264
 
265
+ function resolveSubscriberNickname(projectRoot, subscriberId) {
266
+ if (!subscriberId) return "";
267
+ try {
268
+ const busPath = getUfooPaths(projectRoot).agentsFile;
269
+ const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
270
+ return bus.agents?.[subscriberId]?.nickname || "";
271
+ } catch {
272
+ return "";
273
+ }
274
+ }
275
+
243
276
  async function handleOps(projectRoot, ops = [], processManager = null) {
244
277
  const results = [];
245
278
  for (const op of ops) {
246
279
  if (op.action === "launch") {
247
280
  const count = op.count || 1;
248
- const agent = op.agent === "codex" ? "codex" : "claude";
281
+ const agent = normalizeLaunchAgent(op.agent);
282
+ if (!agent) {
283
+ results.push({
284
+ action: "launch",
285
+ ok: false,
286
+ count,
287
+ error: `unsupported launch agent: ${op.agent || "unknown"}`,
288
+ });
289
+ continue;
290
+ }
249
291
  const nickname = op.nickname || "";
250
292
  const startTime = new Date(Date.now() - 1000);
251
293
  const startIso = startTime.toISOString();
@@ -278,6 +320,34 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
278
320
  }
279
321
  // eslint-disable-next-line no-await-in-loop
280
322
  const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager);
323
+ if (launchResult.mode === "internal" && launchResult.subscriberIds && launchResult.subscriberIds.length > 0) {
324
+ const probeAgentType = agent === "codex"
325
+ ? "codex"
326
+ : (agent === "claude" ? "claude-code" : "");
327
+ for (const subscriberId of launchResult.subscriberIds) {
328
+ if (!probeAgentType) continue;
329
+ const resolvedNickname = resolveSubscriberNickname(projectRoot, subscriberId) || nickname;
330
+ const probeHandle = scheduleProviderSessionProbe({
331
+ projectRoot,
332
+ subscriberId,
333
+ agentType: probeAgentType,
334
+ nickname: resolvedNickname,
335
+ onResolved: (id, resolved) => {
336
+ if (providerSessions) {
337
+ providerSessions.set(id, {
338
+ sessionId: resolved.sessionId,
339
+ source: resolved.source || "",
340
+ updated_at: new Date().toISOString(),
341
+ });
342
+ }
343
+ probeHandles.delete(id);
344
+ },
345
+ });
346
+ if (probeHandle) {
347
+ probeHandles.set(subscriberId, probeHandle);
348
+ }
349
+ }
350
+ }
281
351
  results.push({
282
352
  action: "launch",
283
353
  mode: launchResult.mode,
@@ -349,6 +419,25 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
349
419
  error: err && err.message ? err.message : String(err || "rename failed"),
350
420
  });
351
421
  }
422
+ } else if (op.action === "cron") {
423
+ if (!daemonCronController) {
424
+ results.push({
425
+ action: "cron",
426
+ ok: false,
427
+ error: "cron controller unavailable",
428
+ });
429
+ continue;
430
+ }
431
+ try {
432
+ const result = daemonCronController.handleCronOp(op);
433
+ results.push(result);
434
+ } catch (err) {
435
+ results.push({
436
+ action: "cron",
437
+ ok: false,
438
+ error: err && err.message ? err.message : String(err || "cron failed"),
439
+ });
440
+ }
352
441
  }
353
442
  }
354
443
  return results;
@@ -405,7 +494,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
405
494
  try {
406
495
  fs.writeFileSync(debugFile, `Attempting join at ${new Date().toISOString()}\n`, { flag: "a" });
407
496
  // Determine agent type based on provider configuration
408
- const agentType = provider === "codex-cli" ? "codex" : "claude-code";
497
+ const agentType = provider === "codex-cli" ? "codex" : (provider === "ucode" ? "ufoo-code" : "claude-code");
409
498
  // Use fixed ID "ufoo-agent" for daemon's bus identity with explicit nickname
410
499
  const sub = await eventBus.join("ufoo-agent", agentType, "ufoo-agent");
411
500
  if (!sub) {
@@ -480,7 +569,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
480
569
  state.pending.delete(evt.publisher);
481
570
  if (onStatus) {
482
571
  const displayName = getAgentNickname(evt.publisher);
483
- onStatus({ phase: "done", text: `${displayName} done`, key: evt.publisher });
572
+ onStatus({ phase: BUS_STATUS_PHASES.DONE, text: `${displayName} done`, key: evt.publisher });
484
573
  }
485
574
  }
486
575
  }
@@ -493,7 +582,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
493
582
  state.pending.add(target);
494
583
  if (onStatus) {
495
584
  const displayName = getAgentNickname(target);
496
- onStatus({ phase: "start", text: `${displayName} processing`, key: target });
585
+ onStatus({ phase: BUS_STATUS_PHASES.START, text: `${displayName} processing`, key: target });
497
586
  }
498
587
  },
499
588
  getSubscriber() {
@@ -522,34 +611,47 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
522
611
  // 文件锁机制:防止多个 daemon 同时启动
523
612
  const lockFile = path.join(runDir, "daemon.lock");
524
613
  let lockFd;
614
+ let recoveredStaleLock = false;
525
615
  try {
616
+ // 尝试独占方式打开锁文件(如果已存在且被锁定则失败)
526
617
  lockFd = fs.openSync(lockFile, "wx");
527
618
  fs.writeSync(lockFd, `${process.pid}\n`);
528
619
  } catch (err) {
529
620
  if (err.code === "EEXIST") {
530
- // 锁文件已存在,检查持有者是否还活着
531
- let existingPid;
621
+ // 锁文件已存在,检查是否仍有效
622
+ let existingPid = null;
532
623
  try {
533
- existingPid = parseInt(fs.readFileSync(lockFile, "utf8").trim(), 10);
624
+ const raw = fs.readFileSync(lockFile, "utf8").trim();
625
+ const parsed = parseInt(raw, 10);
626
+ if (Number.isFinite(parsed) && parsed > 0) {
627
+ existingPid = parsed;
628
+ }
534
629
  } catch {
535
- existingPid = NaN;
630
+ // ignore malformed lock file and treat as stale
536
631
  }
537
- if (existingPid && Number.isFinite(existingPid)) {
538
- let alive = false;
539
- try {
540
- process.kill(existingPid, 0);
541
- alive = true;
542
- } catch {
543
- // 进程已死
544
- }
545
- if (alive) {
546
- throw new Error(`Daemon already running with PID ${existingPid}`);
547
- }
632
+
633
+ let lockHeld = false;
634
+ if (existingPid) {
635
+ lockHeld = looksLikeRunningDaemon(projectRoot, existingPid);
636
+ }
637
+
638
+ if (lockHeld) {
639
+ throw new Error(`Daemon already running with PID ${existingPid}`);
640
+ }
641
+
642
+ // 进程已死或锁文件损坏,清理旧锁后重试
643
+ try {
644
+ fs.unlinkSync(lockFile);
645
+ recoveredStaleLock = true;
646
+ } catch (unlinkErr) {
647
+ throw new Error(`Failed to remove stale daemon lock: ${unlinkErr.message}`);
648
+ }
649
+ try {
650
+ lockFd = fs.openSync(lockFile, "wx");
651
+ fs.writeSync(lockFd, `${process.pid}\n`);
652
+ } catch (retryErr) {
653
+ throw new Error(`Failed to acquire daemon lock: ${retryErr.message}`);
548
654
  }
549
- // 持有者已死,接管锁
550
- try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
551
- lockFd = fs.openSync(lockFile, "wx");
552
- fs.writeSync(lockFd, `${process.pid}\n`);
553
655
  } else {
554
656
  throw err;
555
657
  }
@@ -568,36 +670,23 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
568
670
  log(`Process manager initialized`);
569
671
 
570
672
  // Provider session cache (in-memory)
571
- const providerSessions = loadProviderSessionCache(projectRoot);
572
-
573
- // Probe handles (用于agent_ready时提前触发probe)
574
- const probeHandles = new Map(); // subscriberId -> { triggerNow }
575
-
576
- const sockets = new Set();
577
- const sendToSockets = (payload) => {
578
- const line = `${JSON.stringify(payload)}\n`;
579
- for (const sock of sockets) {
580
- if (!sock || sock.destroyed) continue;
581
- try {
582
- sock.write(line);
583
- } catch {
584
- // ignore write errors
585
- }
586
- }
587
- };
673
+ providerSessions = loadProviderSessionCache(projectRoot);
674
+ probeHandles = new Map();
675
+ daemonCronController = createDaemonCronController({
676
+ dispatch: async ({ taskId, target, message }) => {
677
+ await dispatchMessages(projectRoot, [{ target, message }]);
678
+ log(`cron:${taskId} -> ${target}`);
679
+ },
680
+ log,
681
+ });
588
682
 
589
- const busBridge = startBusBridge(projectRoot, provider, (evt) => {
590
- sendToSockets({ type: "bus", data: evt });
591
- }, (status) => {
592
- sendToSockets({ type: "status", data: status });
593
- }, () => sockets.size > 0);
683
+ const buildRuntimeStatus = () =>
684
+ buildStatus(projectRoot, {
685
+ cronTasks: daemonCronController ? daemonCronController.listTasks() : [],
686
+ });
594
687
 
595
- // 定期检测状态变化并推送(仅当有变化时)
596
- let lastActiveJson = "";
597
- const statusSyncInterval = setInterval(() => {
598
- if (sockets.size === 0) return; // 没有客户端连接时跳过
688
+ const cleanupInactiveSubscribers = () => {
599
689
  try {
600
- // 先清理不活跃的订阅者,确保状态准确
601
690
  const syncBus = new EventBus(projectRoot);
602
691
  syncBus.ensureBus();
603
692
  syncBus.loadBusData();
@@ -606,373 +695,450 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
606
695
  } catch {
607
696
  // ignore cleanup errors
608
697
  }
609
- try {
610
- const status = buildStatus(projectRoot);
611
- const currentActiveJson = JSON.stringify(status.active);
612
- if (currentActiveJson !== lastActiveJson) {
613
- lastActiveJson = currentActiveJson;
614
- sendToSockets({ type: "status", data: status });
615
- log(`status sync: active agents changed to ${status.active.length}`);
616
- }
617
- } catch {
618
- // ignore status check errors
698
+ };
699
+
700
+ let handleIpcRequest = async () => {};
701
+ const ipcServer = createDaemonIpcServer({
702
+ projectRoot,
703
+ parseJsonLines,
704
+ handleRequest: async (req, socket) => handleIpcRequest(req, socket),
705
+ buildStatus: () => buildRuntimeStatus(),
706
+ cleanupInactive: cleanupInactiveSubscribers,
707
+ log,
708
+ });
709
+
710
+ const busBridge = startBusBridge(projectRoot, provider, (evt) => {
711
+ ipcServer.sendToSockets({ type: IPC_RESPONSE_TYPES.BUS, data: evt });
712
+ }, (status) => {
713
+ ipcServer.sendToSockets({ type: IPC_RESPONSE_TYPES.STATUS, data: status });
714
+ }, () => ipcServer.hasClients());
715
+
716
+ handleIpcRequest = async (req, socket) => {
717
+ if (!req || typeof req !== "object") return;
718
+ if (req.type === IPC_REQUEST_TYPES.STATUS) {
719
+ cleanupInactiveSubscribers();
720
+ const status = buildRuntimeStatus();
721
+ socket.write(`${JSON.stringify({ type: IPC_RESPONSE_TYPES.STATUS, data: status })}
722
+ `);
723
+ return;
619
724
  }
620
- }, 3000); // 每3秒检测一次
621
-
622
- const server = net.createServer((socket) => {
623
- sockets.add(socket);
624
- socket.on("close", () => sockets.delete(socket));
625
- let buffer = "";
626
- socket.on("data", async (data) => {
627
- buffer += data.toString("utf8");
628
- const lines = buffer.split(/\r?\n/);
629
- buffer = lines.pop() || "";
630
- const complete = lines.filter((l) => l.trim());
631
- for (const line of complete) {
632
- const items = parseJsonLines(line);
633
- for (const req of items) {
634
- if (!req || typeof req !== "object") continue;
635
- if (req.type === "status") {
636
- // 先清理不活跃的订阅者,确保状态准确
637
- try {
638
- const eventBus = new EventBus(projectRoot);
639
- eventBus.ensureBus();
640
- eventBus.loadBusData();
641
- eventBus.subscriberManager.cleanupInactive();
642
- eventBus.saveBusData();
643
- } catch {
644
- // ignore cleanup errors, proceed with status
645
- }
646
- const status = buildStatus(projectRoot);
647
- socket.write(`${JSON.stringify({ type: "status", data: status })}\n`);
648
- continue;
649
- }
650
- if (req.type === "prompt") {
651
- log(`prompt ${String(req.text || "").slice(0, 200)}`);
652
- let result;
653
- try {
654
- result = await runUfooAgent({
655
- projectRoot,
656
- prompt: req.text || "",
657
- provider,
658
- model,
725
+ if (req.type === IPC_REQUEST_TYPES.PROMPT) {
726
+ await handlePromptRequest({
727
+ projectRoot,
728
+ req,
729
+ socket,
730
+ provider,
731
+ model,
732
+ processManager,
733
+ runPromptWithAssistant,
734
+ runUfooAgent,
735
+ runAssistantTask,
736
+ dispatchMessages,
737
+ handleOps,
738
+ markPending: (target) => busBridge.markPending(target),
739
+ reportTaskStatus: async (report) => {
740
+ await recordAgentReport({
741
+ projectRoot,
742
+ report,
743
+ onStatus: (status) => {
744
+ ipcServer.sendToSockets({
745
+ type: IPC_RESPONSE_TYPES.STATUS,
746
+ data: status,
659
747
  });
660
- } catch (err) {
661
- log(`error ${err.message || String(err)}`);
662
- socket.write(
663
- `${JSON.stringify({
664
- type: "error",
665
- error: err.message || String(err),
666
- })}\n`,
667
- );
668
- continue;
669
- }
670
- if (!result.ok) {
671
- log(`agent-fail ${result.error || "agent failed"}`);
672
- socket.write(
673
- `${JSON.stringify({ type: "error", error: result.error || "agent failed" })}\n`,
674
- );
675
- continue;
676
- }
677
- for (const item of result.payload.dispatch || []) {
678
- if (item && item.target && item.target !== "broadcast") {
679
- busBridge.markPending(item.target);
680
- }
681
- }
682
- await dispatchMessages(projectRoot, result.payload.dispatch || []);
683
- const opsResults = await handleOps(projectRoot, result.payload.ops || [], processManager);
684
- log(`ok reply=${Boolean(result.payload.reply)} dispatch=${(result.payload.dispatch || []).length} ops=${(result.payload.ops || []).length}`);
685
- socket.write(
686
- `${JSON.stringify({
687
- type: "response",
688
- data: result.payload,
689
- opsResults,
690
- })}\n`,
691
- );
692
- continue;
693
- }
694
- if (req.type === "bus_send") {
695
- // Direct bus send request from chat UI
696
- const { target, message } = req;
697
- if (!target || !message) {
698
- socket.write(
699
- `${JSON.stringify({
700
- type: "error",
701
- error: "bus_send requires target and message",
702
- })}\n`,
703
- );
704
- continue;
705
- }
706
- try {
707
- const publisher = busBridge.getSubscriber() || "ufoo-agent";
708
- const eventBus = new EventBus(projectRoot);
709
- await eventBus.send(target, message, publisher);
710
- log(`bus_send target=${target} publisher=${publisher}`);
711
- socket.write(
712
- `${JSON.stringify({
713
- type: "bus_send_ok",
714
- })}\n`,
715
- );
716
- } catch (err) {
717
- log(`bus_send failed: ${err.message}`);
718
- socket.write(
719
- `${JSON.stringify({
720
- type: "error",
721
- error: err.message || "bus_send failed",
722
- })}\n`,
723
- );
724
- }
725
- continue;
726
- }
727
- if (req.type === "launch_agent") {
728
- const { agent, count, nickname } = req;
729
- if (!agent || (agent !== "codex" && agent !== "claude")) {
730
- socket.write(
731
- `${JSON.stringify({
732
- type: "error",
733
- error: "launch_agent requires agent=codex|claude",
734
- })}\n`,
735
- );
736
- continue;
737
- }
738
- const parsedCount = parseInt(count, 10);
739
- const finalCount = Number.isFinite(parsedCount) && parsedCount > 0 ? parsedCount : 1;
740
- const op = {
741
- action: "launch",
742
- agent,
743
- count: finalCount,
744
- nickname: nickname || "",
745
- };
746
- try {
747
- const opsResults = await handleOps(projectRoot, [op], processManager);
748
- const launchResult = opsResults.find((r) => r.action === "launch");
749
- const ok = launchResult ? launchResult.ok !== false : true;
750
- const reply = ok
751
- ? `Launched ${op.count} ${agent} agent(s)`
752
- : `Launch failed: ${launchResult?.error || "unknown error"}`;
753
- socket.write(
754
- `${JSON.stringify({
755
- type: "response",
756
- data: {
757
- reply,
758
- dispatch: [],
759
- ops: [op],
760
- },
761
- opsResults,
762
- })}\n`,
763
- );
764
- } catch (err) {
765
- socket.write(
766
- `${JSON.stringify({
767
- type: "error",
768
- error: err.message || "launch_agent failed",
769
- })}\n`,
770
- );
771
- }
772
- continue;
773
- }
774
- if (req.type === "close_agent") {
775
- const { agentId } = req;
776
- if (!agentId) {
777
- socket.write(
778
- `${JSON.stringify({
779
- type: "error",
780
- error: "close_agent requires agentId",
781
- })}\n`,
782
- );
783
- continue;
784
- }
785
- try {
786
- const ok = await closeAgent(projectRoot, agentId);
787
- // Always cleanup inactive and broadcast — removes dead agents from list
788
- try {
789
- const cleanupBus = new EventBus(projectRoot);
790
- cleanupBus.ensureBus();
791
- cleanupBus.loadBusData();
792
- cleanupBus.subscriberManager.cleanupInactive();
793
- cleanupBus.saveBusData();
794
- } catch { /* ignore */ }
795
- log(`close_agent id=${agentId} ok=${ok}`);
796
- const status = buildStatus(projectRoot);
797
- socket.write(
798
- `${JSON.stringify({ type: "close_agent_ok", ok: true, agentId })}\n`,
799
- );
800
- sendToSockets({ type: "status", data: status });
801
- } catch (err) {
802
- socket.write(
803
- `${JSON.stringify({
804
- type: "error",
805
- error: err.message || "close_agent failed",
806
- })}\n`,
807
- );
808
- }
809
- continue;
748
+ },
749
+ log,
750
+ });
751
+ },
752
+ log,
753
+ });
754
+ return;
755
+ }
756
+ if (req.type === IPC_REQUEST_TYPES.AGENT_REPORT) {
757
+ try {
758
+ const report = req.report && typeof req.report === "object" ? req.report : {};
759
+ const { entry } = await recordAgentReport({
760
+ projectRoot,
761
+ report: {
762
+ ...report,
763
+ source: report.source || "cli",
764
+ },
765
+ onStatus: (status) => {
766
+ ipcServer.sendToSockets({
767
+ type: IPC_RESPONSE_TYPES.STATUS,
768
+ data: status,
769
+ });
770
+ },
771
+ log,
772
+ });
773
+ socket.write(
774
+ `${JSON.stringify({
775
+ type: IPC_RESPONSE_TYPES.RESPONSE,
776
+ data: {
777
+ reply: `Report received (${entry.phase})`,
778
+ report: entry,
779
+ },
780
+ })}
781
+ `,
782
+ );
783
+ ipcServer.sendToSockets({
784
+ type: IPC_RESPONSE_TYPES.STATUS,
785
+ data: buildRuntimeStatus(),
786
+ });
787
+ } catch (err) {
788
+ socket.write(
789
+ `${JSON.stringify({
790
+ type: IPC_RESPONSE_TYPES.ERROR,
791
+ error: err.message || "agent_report failed",
792
+ })}
793
+ `,
794
+ );
795
+ }
796
+ return;
797
+ }
798
+ if (req.type === IPC_REQUEST_TYPES.BUS_SEND) {
799
+ // Direct bus send request from chat UI
800
+ const { target, message } = req;
801
+ if (!target || !message) {
802
+ socket.write(
803
+ `${JSON.stringify({
804
+ type: IPC_RESPONSE_TYPES.ERROR,
805
+ error: "bus_send requires target and message",
806
+ })}
807
+ `,
808
+ );
809
+ return;
810
+ }
811
+ try {
812
+ const publisher = busBridge.getSubscriber() || "ufoo-agent";
813
+ const eventBus = new EventBus(projectRoot);
814
+ await eventBus.send(target, message, publisher);
815
+ busBridge.markPending(target);
816
+ log(`bus_send target=${target} publisher=${publisher}`);
817
+ socket.write(
818
+ `${JSON.stringify({
819
+ type: IPC_RESPONSE_TYPES.BUS_SEND_OK,
820
+ })}
821
+ `,
822
+ );
823
+ } catch (err) {
824
+ log(`bus_send failed: ${err.message}`);
825
+ socket.write(
826
+ `${JSON.stringify({
827
+ type: IPC_RESPONSE_TYPES.ERROR,
828
+ error: err.message || "bus_send failed",
829
+ })}
830
+ `,
831
+ );
832
+ }
833
+ return;
834
+ }
835
+ if (req.type === IPC_REQUEST_TYPES.CLOSE_AGENT) {
836
+ const { agent_id } = req;
837
+ if (!agent_id) {
838
+ socket.write(
839
+ `${JSON.stringify({
840
+ type: IPC_RESPONSE_TYPES.ERROR,
841
+ error: "close_agent requires agent_id",
842
+ })}
843
+ `,
844
+ );
845
+ return;
846
+ }
847
+ try {
848
+ const op = { action: "close", agent_id };
849
+ const opsResults = await handleOps(projectRoot, [op], processManager);
850
+ const closeResult = opsResults.find((r) => r.action === "close");
851
+ const ok = closeResult ? closeResult.ok !== false : true;
852
+ const reply = ok
853
+ ? `Closed ${agent_id}`
854
+ : `Close failed: ${closeResult?.error || "unknown error"}`;
855
+ socket.write(
856
+ `${JSON.stringify({
857
+ type: IPC_RESPONSE_TYPES.RESPONSE,
858
+ data: { reply, dispatch: [], ops: [op] },
859
+ opsResults,
860
+ })}
861
+ `,
862
+ );
863
+ cleanupInactiveSubscribers();
864
+ ipcServer.sendToSockets({
865
+ type: IPC_RESPONSE_TYPES.STATUS,
866
+ data: buildRuntimeStatus(),
867
+ });
868
+ } catch (err) {
869
+ socket.write(
870
+ `${JSON.stringify({
871
+ type: IPC_RESPONSE_TYPES.ERROR,
872
+ error: err.message || "close_agent failed",
873
+ })}
874
+ `,
875
+ );
876
+ }
877
+ return;
878
+ }
879
+ if (req.type === IPC_REQUEST_TYPES.LAUNCH_AGENT) {
880
+ const { agent, count, nickname } = req;
881
+ const normalizedAgent = normalizeLaunchAgent(agent);
882
+ if (!normalizedAgent) {
883
+ socket.write(
884
+ `${JSON.stringify({
885
+ type: IPC_RESPONSE_TYPES.ERROR,
886
+ error: "launch_agent requires agent=codex|claude|ucode",
887
+ })}
888
+ `,
889
+ );
890
+ return;
891
+ }
892
+ const parsedCount = parseInt(count, 10);
893
+ const finalCount = Number.isFinite(parsedCount) && parsedCount > 0 ? parsedCount : 1;
894
+ const op = {
895
+ action: "launch",
896
+ agent: normalizedAgent,
897
+ count: finalCount,
898
+ nickname: nickname || "",
899
+ };
900
+ try {
901
+ const opsResults = await handleOps(projectRoot, [op], processManager);
902
+ const launchResult = opsResults.find((r) => r.action === "launch");
903
+ const ok = launchResult ? launchResult.ok !== false : true;
904
+ const reply = ok
905
+ ? `Launched ${op.count} ${agent} agent(s)`
906
+ : `Launch failed: ${launchResult?.error || "unknown error"}`;
907
+ socket.write(
908
+ `${JSON.stringify({
909
+ type: IPC_RESPONSE_TYPES.RESPONSE,
910
+ data: {
911
+ reply,
912
+ dispatch: [],
913
+ ops: [op],
914
+ },
915
+ opsResults,
916
+ })}
917
+ `,
918
+ );
919
+ cleanupInactiveSubscribers();
920
+ ipcServer.sendToSockets({
921
+ type: IPC_RESPONSE_TYPES.STATUS,
922
+ data: buildRuntimeStatus(),
923
+ });
924
+ } catch (err) {
925
+ socket.write(
926
+ `${JSON.stringify({
927
+ type: IPC_RESPONSE_TYPES.ERROR,
928
+ error: err.message || "launch_agent failed",
929
+ })}
930
+ `,
931
+ );
932
+ }
933
+ return;
934
+ }
935
+ if (req.type === IPC_REQUEST_TYPES.RESUME_AGENTS) {
936
+ const target = req.target || "";
937
+ try {
938
+ const result = await resumeAgents(projectRoot, target, processManager);
939
+ const resumedCount = result.resumed.length;
940
+ const skippedCount = result.skipped.length;
941
+ const reply = resumedCount > 0
942
+ ? `Resumed ${resumedCount} agent(s)` + (skippedCount ? `, skipped ${skippedCount}` : "")
943
+ : (skippedCount ? `No agents resumed (skipped ${skippedCount})` : "No agents resumed");
944
+ socket.write(
945
+ `${JSON.stringify({
946
+ type: IPC_RESPONSE_TYPES.RESPONSE,
947
+ data: {
948
+ reply,
949
+ resume: result,
950
+ },
951
+ })}
952
+ `,
953
+ );
954
+ } catch (err) {
955
+ socket.write(
956
+ `${JSON.stringify({
957
+ type: IPC_RESPONSE_TYPES.ERROR,
958
+ error: err.message || "resume_agents failed",
959
+ })}
960
+ `,
961
+ );
962
+ }
963
+ return;
964
+ }
965
+ if (req.type === IPC_REQUEST_TYPES.LIST_RECOVERABLE_AGENTS) {
966
+ const target = req.target || "";
967
+ try {
968
+ const result = getRecoverableAgents(projectRoot, target);
969
+ const count = result.recoverable.length;
970
+ const reply = count > 0 ? `Found ${count} recoverable agent(s)` : "No recoverable agents";
971
+ socket.write(
972
+ `${JSON.stringify({
973
+ type: IPC_RESPONSE_TYPES.RESPONSE,
974
+ data: {
975
+ reply,
976
+ recoverable: result,
977
+ },
978
+ })}
979
+ `,
980
+ );
981
+ } catch (err) {
982
+ socket.write(
983
+ `${JSON.stringify({
984
+ type: IPC_RESPONSE_TYPES.ERROR,
985
+ error: err.message || "list_recoverable_agents failed",
986
+ })}
987
+ `,
988
+ );
989
+ }
990
+ return;
991
+ }
992
+ if (req.type === IPC_REQUEST_TYPES.REGISTER_AGENT) {
993
+ // Manual agent launch requests daemon to register it
994
+ const { agentType, nickname, parentPid, launchMode, tmuxPane, tty, skipProbe } = req;
995
+ if (!agentType) {
996
+ socket.write(
997
+ `${JSON.stringify({
998
+ type: IPC_RESPONSE_TYPES.ERROR,
999
+ error: "register_agent requires agentType",
1000
+ })}
1001
+ `,
1002
+ );
1003
+ return;
1004
+ }
1005
+ try {
1006
+ const crypto = require("crypto");
1007
+ const requestedReuse = req.reuseSession && typeof req.reuseSession === "object"
1008
+ ? req.reuseSession
1009
+ : null;
1010
+ const reuseSessionId = typeof requestedReuse?.sessionId === "string"
1011
+ ? requestedReuse.sessionId.trim()
1012
+ : "";
1013
+ const reuseSubscriberId = typeof requestedReuse?.subscriberId === "string"
1014
+ ? requestedReuse.subscriberId.trim()
1015
+ : "";
1016
+ const reuseProviderSessionId = typeof requestedReuse?.providerSessionId === "string"
1017
+ ? requestedReuse.providerSessionId.trim()
1018
+ : "";
1019
+
1020
+ let sessionId = crypto.randomBytes(4).toString("hex");
1021
+ let subscriberId = `${agentType}:${sessionId}`;
1022
+ if (reuseSessionId && reuseSubscriberId === `${agentType}:${reuseSessionId}`) {
1023
+ sessionId = reuseSessionId;
1024
+ subscriberId = reuseSubscriberId;
1025
+ } else if (reuseSessionId || reuseSubscriberId) {
1026
+ log(`register_agent ignored invalid reuseSession for ${agentType}`);
1027
+ }
1028
+
1029
+ // Daemon registers the agent in bus
1030
+ const eventBus = new EventBus(projectRoot);
1031
+ await eventBus.init();
1032
+ eventBus.loadBusData();
1033
+ const parsedParentPid = Number.parseInt(parentPid, 10);
1034
+ if (!Number.isFinite(parsedParentPid) || parsedParentPid <= 0) {
1035
+ throw new Error("register_agent requires valid parentPid");
1036
+ }
1037
+ const joinOptions = {
1038
+ parentPid: Number.isFinite(parsedParentPid) ? parsedParentPid : undefined,
1039
+ launchMode: launchMode || "",
1040
+ tmuxPane: tmuxPane || "",
1041
+ tty: tty || "",
1042
+ reuseSessionId,
1043
+ reuseProviderSessionId,
1044
+ };
1045
+ if (skipProbe) joinOptions.skipProbe = true;
1046
+
1047
+ let finalNickname = nickname || "";
1048
+ if (finalNickname) {
1049
+ const nickCheck = checkAndCleanupNickname(projectRoot, finalNickname);
1050
+ if (nickCheck.existing) {
1051
+ finalNickname = "";
810
1052
  }
811
- if (req.type === "resume_agents") {
812
- const target = req.target || "";
813
- try {
814
- const result = await resumeAgents(projectRoot, target, processManager);
815
- const resumedCount = result.resumed.length;
816
- const skippedCount = result.skipped.length;
817
- const reply = resumedCount > 0
818
- ? `Resumed ${resumedCount} agent(s)` + (skippedCount ? `, skipped ${skippedCount}` : "")
819
- : (skippedCount ? `No agents resumed (skipped ${skippedCount})` : "No agents resumed");
820
- socket.write(
821
- `${JSON.stringify({
822
- type: "response",
823
- data: {
824
- reply,
825
- resume: result,
826
- },
827
- })}\n`,
828
- );
829
- } catch (err) {
830
- socket.write(
831
- `${JSON.stringify({
832
- type: "error",
833
- error: err.message || "resume_agents failed",
834
- })}\n`,
835
- );
836
- }
837
- continue;
1053
+ }
1054
+ await eventBus.join(
1055
+ sessionId,
1056
+ normalizeBusAgentType(agentType),
1057
+ finalNickname,
1058
+ joinOptions,
1059
+ );
1060
+ if (finalNickname) {
1061
+ eventBus.rename(subscriberId, finalNickname, "ufoo-agent");
1062
+ }
1063
+ eventBus.saveBusData();
1064
+ const resolvedNickname = resolveSubscriberNickname(projectRoot, subscriberId) || finalNickname || "";
1065
+
1066
+ if (!skipProbe && reuseProviderSessionId) {
1067
+ if (providerSessions) {
1068
+ providerSessions.set(subscriberId, {
1069
+ sessionId: reuseProviderSessionId,
1070
+ source: "reuse",
1071
+ updated_at: new Date().toISOString(),
1072
+ });
838
1073
  }
839
- if (req.type === "register_agent") {
840
- // Manual agent launch requests daemon to register it
841
- const { agentType, nickname, parentPid, launchMode, tmuxPane, tty, skipProbe, reuseSession } = req;
842
- if (!agentType) {
843
- socket.write(
844
- `${JSON.stringify({
845
- type: "error",
846
- error: "register_agent requires agentType",
847
- })}\n`,
848
- );
849
- continue;
850
- }
851
- try {
852
- const crypto = require("crypto");
853
-
854
- // 检查是否复用旧 session
855
- let sessionId;
856
- let subscriberId;
857
- let isReusing = false;
858
-
859
- if (reuseSession && reuseSession.sessionId && reuseSession.subscriberId) {
860
- // 验证旧 session 是否可以复用
861
- sessionId = reuseSession.sessionId;
862
- subscriberId = reuseSession.subscriberId;
863
- isReusing = true;
864
- log(`register_agent reusing session: ${subscriberId}`);
865
- } else {
866
- // 生成新的 session
867
- sessionId = crypto.randomBytes(4).toString("hex");
868
- subscriberId = `${agentType}:${sessionId}`;
869
- }
1074
+ }
870
1075
 
871
- // Daemon registers the agent in bus
872
- const eventBus = new EventBus(projectRoot);
873
- await eventBus.init();
874
- eventBus.loadBusData();
875
- const parsedParentPid = Number.parseInt(parentPid, 10);
876
- if (!Number.isFinite(parsedParentPid) || parsedParentPid <= 0) {
877
- throw new Error("register_agent requires valid parentPid");
878
- }
879
- const joinOptions = {
880
- parentPid: Number.isFinite(parsedParentPid) ? parsedParentPid : undefined,
881
- launchMode: launchMode || "",
882
- tmuxPane: tmuxPane || "",
883
- // 如果复用旧 session,保留 provider session ID
884
- providerSessionId: isReusing ? reuseSession.providerSessionId : undefined,
885
- };
886
- if (Object.prototype.hasOwnProperty.call(req, "tty")) {
887
- const ttyValue = typeof tty === "string" ? tty.trim() : "";
888
- joinOptions.tty = ttyValue;
889
- if (!ttyValue) {
890
- log(`register_agent warning: missing tty for ${subscriberId}`);
891
- }
892
- }
893
- await eventBus.subscriberManager.join(sessionId, agentType, nickname || "", joinOptions);
894
- eventBus.saveBusData();
895
-
896
- const finalNickname = eventBus.busData?.agents?.[subscriberId]?.nickname || "";
897
- const reusedLabel = isReusing ? " (reused)" : "";
898
- log(`register_agent type=${agentType} nickname=${finalNickname || "(none)"} id=${subscriberId}${reusedLabel}`);
899
-
900
- // 如果复用 session 且已有 provider session ID,跳过 probe
901
- const hasProviderSession = isReusing && reuseSession.providerSessionId;
902
- if (!skipProbe && !hasProviderSession && finalNickname) {
903
- const probeHandle = scheduleProviderSessionProbe({
904
- projectRoot,
905
- subscriberId,
906
- agentType,
907
- nickname: finalNickname,
908
- onResolved: (id, resolved) => {
909
- providerSessions.set(id, {
910
- sessionId: resolved.sessionId,
911
- source: resolved.source || "",
912
- updated_at: new Date().toISOString(),
913
- });
914
- // 清理handle
915
- probeHandles.delete(id);
916
- },
1076
+ if (!skipProbe) {
1077
+ const probeHandle = scheduleProviderSessionProbe({
1078
+ projectRoot,
1079
+ subscriberId,
1080
+ agentType,
1081
+ nickname: resolvedNickname,
1082
+ onResolved: (id, resolved) => {
1083
+ if (providerSessions) {
1084
+ providerSessions.set(id, {
1085
+ sessionId: resolved.sessionId,
1086
+ source: resolved.source || "",
1087
+ updated_at: new Date().toISOString(),
917
1088
  });
918
- // 保存handle,用于agent_ready时提前触发
919
- if (probeHandle) {
920
- probeHandles.set(subscriberId, probeHandle);
921
- }
922
1089
  }
923
- socket.write(
924
- `${JSON.stringify({
925
- type: "register_ok",
926
- subscriberId,
927
- nickname: finalNickname || "",
928
- })}\n`,
929
- );
930
- // 广播状态更新给所有连接的客户端
931
- const status = buildStatus(projectRoot);
932
- sendToSockets({ type: "status", data: status });
933
- } catch (err) {
934
- log(`register_agent failed: ${err.message}`);
935
- socket.write(
936
- `${JSON.stringify({
937
- type: "error",
938
- error: err.message || "register_agent failed",
939
- })}\n`,
940
- );
941
- }
942
- continue;
943
- }
944
- if (req.type === "agent_ready") {
945
- // Agent has completed initialization and is ready to receive commands
946
- const { subscriberId } = req;
947
- if (!subscriberId) {
948
- continue; // Silently ignore invalid requests
949
- }
950
-
951
- log(`agent_ready id=${subscriberId} - triggering probe immediately`);
952
-
953
- // 提前触发probe(不再等待8秒延迟)
954
- const probeHandle = probeHandles.get(subscriberId);
955
- if (probeHandle && typeof probeHandle.triggerNow === "function") {
956
- // 异步触发,不阻塞消息处理
957
- probeHandle.triggerNow().catch((err) => {
958
- log(`agent_ready probe trigger failed for ${subscriberId}: ${err.message}`);
959
- });
960
- } else {
961
- log(`agent_ready no probe handle found for ${subscriberId}`);
962
- }
963
-
964
- continue;
1090
+ probeHandles.delete(id);
1091
+ },
1092
+ });
1093
+ if (probeHandle) {
1094
+ probeHandles.set(subscriberId, probeHandle);
965
1095
  }
966
1096
  }
1097
+ socket.write(
1098
+ `${JSON.stringify({
1099
+ type: IPC_RESPONSE_TYPES.REGISTER_OK,
1100
+ subscriberId,
1101
+ nickname: resolvedNickname,
1102
+ })}
1103
+ `,
1104
+ );
1105
+ } catch (err) {
1106
+ log(`register_agent failed: ${err.message}`);
1107
+ socket.write(
1108
+ `${JSON.stringify({
1109
+ type: IPC_RESPONSE_TYPES.ERROR,
1110
+ error: err.message || "register_agent failed",
1111
+ })}
1112
+ `,
1113
+ );
967
1114
  }
968
- });
969
- });
1115
+ return;
1116
+ }
1117
+ if (req.type === IPC_REQUEST_TYPES.AGENT_READY) {
1118
+ const { subscriberId } = req;
1119
+ if (!subscriberId) {
1120
+ return;
1121
+ }
1122
+ log(`agent_ready id=${subscriberId} - triggering probe immediately`);
1123
+ const probeHandle = probeHandles.get(subscriberId);
1124
+ if (probeHandle && typeof probeHandle.triggerNow === "function") {
1125
+ probeHandle.triggerNow().catch((err) => {
1126
+ log(`agent_ready probe trigger failed for ${subscriberId}: ${err.message}`);
1127
+ });
1128
+ } else {
1129
+ log(`agent_ready no probe handle found for ${subscriberId}`);
1130
+ }
1131
+ return;
1132
+ }
1133
+ };
1134
+
1135
+ ipcServer.listen(socketPath(projectRoot));
970
1136
 
971
- server.listen(socketPath(projectRoot));
972
1137
  log(`Started pid=${process.pid}`);
973
1138
 
974
1139
  // 清理旧 daemon 留下的孤儿 internal agent 进程
975
1140
  const EventBus = require("../bus");
1141
+ const { spawnSync } = require("child_process");
976
1142
  const eventBus = new EventBus(projectRoot);
977
1143
  try {
978
1144
  eventBus.ensureBus();
@@ -1037,17 +1203,19 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1037
1203
  }
1038
1204
 
1039
1205
  // 标记对应的 agents 为 inactive
1206
+ const adapterRouter = createTerminalAdapterRouter();
1040
1207
  for (const [subscriberId, meta] of Object.entries(agents)) {
1041
- if (meta.launch_mode && meta.launch_mode.startsWith("internal")) {
1208
+ const launchMode = meta.launch_mode || "";
1209
+ const adapter = adapterRouter.getAdapter({ launchMode, agentId: subscriberId });
1210
+ if (launchMode && adapter.capabilities.supportsInternalQueueLoop) {
1042
1211
  if (meta.pid) {
1043
1212
  try {
1044
1213
  process.kill(meta.pid, 0);
1045
1214
  // 父 daemon 还活着,跳过
1046
1215
  } catch {
1047
1216
  // 父 daemon 已死,标记为 inactive
1048
- // 设置 last_seen 为很久以前,强制立即超时
1217
+ // 注意:不更新 last_seen,保持原有时间戳,这样会自动超时
1049
1218
  meta.status = "inactive";
1050
- meta.last_seen = "2020-01-01T00:00:00.000Z";
1051
1219
  log(`Marked orphan internal agent ${subscriberId} as inactive (parent daemon ${meta.pid} is dead)`);
1052
1220
  }
1053
1221
  }
@@ -1058,10 +1226,10 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1058
1226
  log(`Failed to cleanup orphan agents: ${err.message}`);
1059
1227
  }
1060
1228
 
1061
- const config = loadConfig(projectRoot);
1062
- const autoResumeEnabled = config.autoResume !== false;
1063
- const shouldResume = resumeMode === "force" || (resumeMode === "auto" && autoResumeEnabled);
1229
+ const shouldResume = resumeMode === "force" || (resumeMode === "auto" && recoveredStaleLock);
1064
1230
  if (shouldResume) {
1231
+ const reason = resumeMode === "force" ? "forced by caller" : "stale daemon state detected";
1232
+ log(`Auto-recover enabled: ${reason}`);
1065
1233
  setTimeout(() => {
1066
1234
  resumeAgents(projectRoot, "", processManager).catch((err) => {
1067
1235
  log(`auto resume failed: ${err.message || String(err)}`);
@@ -1072,10 +1240,15 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1072
1240
  const cleanup = () => {
1073
1241
  log(`Shutting down daemon (managed agents: ${processManager.count()})`);
1074
1242
 
1243
+ if (daemonCronController) {
1244
+ daemonCronController.stopAll();
1245
+ daemonCronController = null;
1246
+ }
1247
+
1075
1248
  // 清理所有子进程
1076
1249
  processManager.cleanup();
1077
1250
 
1078
- clearInterval(statusSyncInterval);
1251
+ ipcServer.stop();
1079
1252
  busBridge.stop();
1080
1253
  removeSocket(projectRoot);
1081
1254