u-foo 1.0.3 → 1.1.9

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 (179) hide show
  1. package/README.md +110 -11
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +132 -0
  4. package/SKILLS/uinit/SKILL.md +78 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucode-core.js +15 -0
  8. package/bin/ucode.js +125 -0
  9. package/bin/ucodex.js +13 -0
  10. package/bin/ufoo +9 -31
  11. package/bin/ufoo-assistant-agent.js +5 -0
  12. package/bin/ufoo-engine.js +25 -0
  13. package/bin/ufoo.js +17 -0
  14. package/modules/AGENTS.template.md +29 -11
  15. package/modules/bus/README.md +33 -25
  16. package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
  17. package/modules/context/README.md +18 -40
  18. package/modules/context/SKILLS/uctx/SKILL.md +63 -1
  19. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  20. package/package.json +25 -4
  21. package/scripts/import-pi-mono.js +124 -0
  22. package/scripts/postinstall.js +30 -0
  23. package/scripts/sync-claude-skills.sh +21 -0
  24. package/src/agent/cliRunner.js +554 -33
  25. package/src/agent/internalRunner.js +150 -56
  26. package/src/agent/launcher.js +754 -0
  27. package/src/agent/normalizeOutput.js +1 -1
  28. package/src/agent/notifier.js +340 -0
  29. package/src/agent/ptyRunner.js +847 -0
  30. package/src/agent/ptyWrapper.js +379 -0
  31. package/src/agent/readyDetector.js +175 -0
  32. package/src/agent/ucode.js +443 -0
  33. package/src/agent/ucodeBootstrap.js +113 -0
  34. package/src/agent/ucodeBuild.js +67 -0
  35. package/src/agent/ucodeDoctor.js +184 -0
  36. package/src/agent/ucodeRuntimeConfig.js +129 -0
  37. package/src/agent/ufooAgent.js +46 -42
  38. package/src/assistant/agent.js +260 -0
  39. package/src/assistant/bridge.js +172 -0
  40. package/src/assistant/engine.js +252 -0
  41. package/src/assistant/stdio.js +58 -0
  42. package/src/assistant/ufooEngineCli.js +306 -0
  43. package/src/bus/activate.js +172 -0
  44. package/src/bus/daemon.js +436 -0
  45. package/src/bus/index.js +842 -0
  46. package/src/bus/inject.js +315 -0
  47. package/src/bus/message.js +430 -0
  48. package/src/bus/nickname.js +88 -0
  49. package/src/bus/queue.js +136 -0
  50. package/src/bus/shake.js +26 -0
  51. package/src/bus/store.js +189 -0
  52. package/src/bus/subscriber.js +312 -0
  53. package/src/bus/utils.js +363 -0
  54. package/src/chat/agentBar.js +117 -0
  55. package/src/chat/agentDirectory.js +88 -0
  56. package/src/chat/agentSockets.js +225 -0
  57. package/src/chat/agentViewController.js +298 -0
  58. package/src/chat/chatLogController.js +115 -0
  59. package/src/chat/commandExecutor.js +700 -0
  60. package/src/chat/commands.js +132 -0
  61. package/src/chat/completionController.js +414 -0
  62. package/src/chat/cronScheduler.js +160 -0
  63. package/src/chat/daemonConnection.js +166 -0
  64. package/src/chat/daemonCoordinator.js +64 -0
  65. package/src/chat/daemonMessageRouter.js +257 -0
  66. package/src/chat/daemonReconnect.js +41 -0
  67. package/src/chat/daemonTransport.js +36 -0
  68. package/src/chat/daemonTransportDefaults.js +10 -0
  69. package/src/chat/dashboardKeyController.js +480 -0
  70. package/src/chat/dashboardView.js +154 -0
  71. package/src/chat/index.js +1011 -1392
  72. package/src/chat/inputHistoryController.js +105 -0
  73. package/src/chat/inputListenerController.js +304 -0
  74. package/src/chat/inputMath.js +104 -0
  75. package/src/chat/inputSubmitHandler.js +171 -0
  76. package/src/chat/layout.js +165 -0
  77. package/src/chat/pasteController.js +81 -0
  78. package/src/chat/rawKeyMap.js +42 -0
  79. package/src/chat/settingsController.js +132 -0
  80. package/src/chat/statusLineController.js +177 -0
  81. package/src/chat/streamTracker.js +138 -0
  82. package/src/chat/text.js +70 -0
  83. package/src/chat/transport.js +61 -0
  84. package/src/cli/busCoreCommands.js +59 -0
  85. package/src/cli/ctxCoreCommands.js +199 -0
  86. package/src/cli/onlineCoreCommands.js +379 -0
  87. package/src/cli.js +1162 -96
  88. package/src/code/README.md +29 -0
  89. package/src/code/UCODE_PROMPT.md +32 -0
  90. package/src/code/agent.js +1651 -0
  91. package/src/code/cli.js +158 -0
  92. package/src/code/config +0 -0
  93. package/src/code/dispatch.js +42 -0
  94. package/src/code/index.js +70 -0
  95. package/src/code/nativeRunner.js +1213 -0
  96. package/src/code/runtime.js +154 -0
  97. package/src/code/sessionStore.js +162 -0
  98. package/src/code/taskDecomposer.js +269 -0
  99. package/src/code/tools/bash.js +53 -0
  100. package/src/code/tools/common.js +42 -0
  101. package/src/code/tools/edit.js +70 -0
  102. package/src/code/tools/read.js +44 -0
  103. package/src/code/tools/write.js +35 -0
  104. package/src/code/tui.js +1580 -0
  105. package/src/config.js +56 -3
  106. package/src/context/decisions.js +324 -0
  107. package/src/context/doctor.js +183 -0
  108. package/src/context/index.js +55 -0
  109. package/src/context/sync.js +127 -0
  110. package/src/daemon/agentProcessManager.js +74 -0
  111. package/src/daemon/cronOps.js +241 -0
  112. package/src/daemon/index.js +998 -170
  113. package/src/daemon/ipcServer.js +99 -0
  114. package/src/daemon/ops.js +630 -48
  115. package/src/daemon/promptLoop.js +319 -0
  116. package/src/daemon/promptRequest.js +101 -0
  117. package/src/daemon/providerSessions.js +306 -0
  118. package/src/daemon/reporting.js +90 -0
  119. package/src/daemon/run.js +31 -1
  120. package/src/daemon/status.js +48 -8
  121. package/src/doctor/index.js +50 -0
  122. package/src/init/index.js +318 -0
  123. package/src/online/bridge.js +663 -0
  124. package/src/online/client.js +245 -0
  125. package/src/online/runner.js +253 -0
  126. package/src/online/server.js +992 -0
  127. package/src/online/tokens.js +103 -0
  128. package/src/report/store.js +331 -0
  129. package/src/shared/eventContract.js +35 -0
  130. package/src/shared/ptySocketContract.js +21 -0
  131. package/src/skills/index.js +159 -0
  132. package/src/status/index.js +285 -0
  133. package/src/terminal/adapterContract.js +87 -0
  134. package/src/terminal/adapterRouter.js +84 -0
  135. package/src/terminal/adapters/externalAdapter.js +14 -0
  136. package/src/terminal/adapters/internalAdapter.js +13 -0
  137. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  138. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  139. package/src/terminal/adapters/terminalAdapter.js +31 -0
  140. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  141. package/src/terminal/detect.js +64 -0
  142. package/src/terminal/index.js +8 -0
  143. package/src/terminal/iterm2.js +126 -0
  144. package/src/ufoo/agentsStore.js +107 -0
  145. package/src/ufoo/paths.js +46 -0
  146. package/src/utils/banner.js +76 -0
  147. package/bin/uclaude +0 -65
  148. package/bin/ucodex +0 -65
  149. package/modules/bus/scripts/bus-alert.sh +0 -185
  150. package/modules/bus/scripts/bus-listen.sh +0 -117
  151. package/modules/context/ASSUMPTIONS.md +0 -7
  152. package/modules/context/CONSTRAINTS.md +0 -7
  153. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  154. package/modules/context/DECISION-PROTOCOL.md +0 -62
  155. package/modules/context/HANDOFF.md +0 -33
  156. package/modules/context/RULES.md +0 -15
  157. package/modules/context/SKILLS/README.md +0 -14
  158. package/modules/context/SYSTEM.md +0 -18
  159. package/modules/context/TEMPLATES/assumptions.md +0 -4
  160. package/modules/context/TEMPLATES/constraints.md +0 -4
  161. package/modules/context/TEMPLATES/decision.md +0 -16
  162. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  163. package/modules/context/TEMPLATES/system.md +0 -3
  164. package/modules/context/TEMPLATES/terminology.md +0 -4
  165. package/modules/context/TERMINOLOGY.md +0 -10
  166. package/scripts/banner.sh +0 -89
  167. package/scripts/bus-alert.sh +0 -6
  168. package/scripts/bus-autotrigger.sh +0 -6
  169. package/scripts/bus-daemon.sh +0 -231
  170. package/scripts/bus-inject.sh +0 -144
  171. package/scripts/bus-listen.sh +0 -6
  172. package/scripts/bus.sh +0 -984
  173. package/scripts/context-decisions.sh +0 -167
  174. package/scripts/context-doctor.sh +0 -72
  175. package/scripts/context-lint.sh +0 -110
  176. package/scripts/doctor.sh +0 -22
  177. package/scripts/init.sh +0 -247
  178. package/scripts/skills.sh +0 -113
  179. package/scripts/status.sh +0 -125
@@ -1,25 +1,60 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
- const net = require("net");
3
+ const { spawnSync } = require("child_process");
4
4
  const { runUfooAgent } = require("../agent/ufooAgent");
5
- const { spawnAgent, closeAgent } = require("./ops");
5
+ const { launchAgent, closeAgent, getRecoverableAgents, resumeAgents } = require("./ops");
6
6
  const { buildStatus } = require("./status");
7
- const { spawnSync } = require("child_process");
7
+ const EventBus = require("../bus");
8
+ const { AgentProcessManager } = require("./agentProcessManager");
9
+ const NicknameManager = require("../bus/nickname");
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");
13
+ const { getUfooPaths } = require("../ufoo/paths");
14
+ const { scheduleProviderSessionProbe, loadProviderSessionCache } = require("./providerSessions");
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");
21
+
22
+ let providerSessions = null;
23
+ let probeHandles = new Map();
24
+ let daemonCronController = null;
8
25
 
9
26
  function sleep(ms) {
10
27
  return new Promise((resolve) => setTimeout(resolve, ms));
11
28
  }
12
29
 
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;
37
+ }
38
+
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 "";
45
+ }
46
+
13
47
  async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso) {
14
48
  if (!nickname) return null;
15
- const busPath = path.join(projectRoot, ".ufoo", "bus", "bus.json");
16
- const script = path.join(projectRoot, "scripts", "bus.sh");
17
- const targetType = agentType === "codex" ? "codex" : "claude-code";
49
+ const busPath = getUfooPaths(projectRoot).agentsFile;
50
+ const targetType = normalizeBusAgentType(agentType);
18
51
  const deadline = Date.now() + 10000;
52
+ const eventBus = new EventBus(projectRoot);
53
+ let lastError = null;
19
54
  while (Date.now() < deadline) {
20
55
  try {
21
56
  const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
22
- let entries = Object.entries(bus.subscribers || {})
57
+ let entries = Object.entries(bus.agents || {})
23
58
  .filter(([, meta]) => meta && meta.agent_type === targetType && meta.status === "active");
24
59
  if (startIso) {
25
60
  entries = entries.filter(([, meta]) => (meta.joined_at || "") >= startIso);
@@ -32,16 +67,15 @@ async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso) {
32
67
  if (candidates.length === 0) candidates = entries;
33
68
  candidates.sort((a, b) => (a[1].joined_at || "").localeCompare(b[1].joined_at || ""));
34
69
  const [agentId] = candidates[candidates.length - 1];
35
- const res = spawnSync("bash", [script, "rename", agentId, nickname], { cwd: projectRoot });
36
- if (res.status === 0) return { ok: true, agent_id: agentId, nickname };
37
- const err = (res.stderr || res.stdout || "").toString("utf8").trim();
38
- return { ok: false, agent_id: agentId, nickname, error: err || "rename failed" };
39
- } catch {
70
+ await eventBus.rename(agentId, nickname, "ufoo-agent");
71
+ return { ok: true, agent_id: agentId, nickname };
72
+ } catch (err) {
73
+ lastError = err && err.message ? err.message : String(err || "rename failed");
40
74
  // ignore and retry
41
75
  }
42
76
  await sleep(200);
43
77
  }
44
- return { ok: false, nickname, error: "rename timeout" };
78
+ return { ok: false, nickname, error: lastError || "rename timeout" };
45
79
  }
46
80
 
47
81
  function ensureDir(dir) {
@@ -49,15 +83,15 @@ function ensureDir(dir) {
49
83
  }
50
84
 
51
85
  function socketPath(projectRoot) {
52
- return path.join(projectRoot, ".ufoo", "run", "ufoo.sock");
86
+ return getUfooPaths(projectRoot).ufooSock;
53
87
  }
54
88
 
55
89
  function pidPath(projectRoot) {
56
- return path.join(projectRoot, ".ufoo", "run", "ufoo-daemon.pid");
90
+ return getUfooPaths(projectRoot).ufooDaemonPid;
57
91
  }
58
92
 
59
93
  function logPath(projectRoot) {
60
- return path.join(projectRoot, ".ufoo", "run", "ufoo-daemon.log");
94
+ return getUfooPaths(projectRoot).ufooDaemonLog;
61
95
  }
62
96
 
63
97
  function writePid(projectRoot) {
@@ -72,21 +106,84 @@ function readPid(projectRoot) {
72
106
  }
73
107
  }
74
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
+
75
174
  function isRunning(projectRoot) {
76
175
  const pid = readPid(projectRoot);
77
176
  if (!pid) return false;
78
- try {
79
- process.kill(pid, 0);
177
+ if (looksLikeRunningDaemon(projectRoot, pid)) {
80
178
  return true;
179
+ }
180
+ try {
181
+ fs.unlinkSync(pidPath(projectRoot));
81
182
  } catch {
82
- try {
83
- fs.unlinkSync(pidPath(projectRoot));
84
- } catch {
85
- // ignore
86
- }
87
- removeSocket(projectRoot);
88
- return false;
183
+ // ignore
89
184
  }
185
+ removeSocket(projectRoot);
186
+ return false;
90
187
  }
91
188
 
92
189
  function removeSocket(projectRoot) {
@@ -108,7 +205,7 @@ function parseJsonLines(buffer) {
108
205
  }
109
206
 
110
207
  function readBus(projectRoot) {
111
- const busPath = path.join(projectRoot, ".ufoo", "bus", "bus.json");
208
+ const busPath = getUfooPaths(projectRoot).agentsFile;
112
209
  try {
113
210
  return JSON.parse(fs.readFileSync(busPath, "utf8"));
114
211
  } catch {
@@ -119,7 +216,7 @@ function readBus(projectRoot) {
119
216
  function listSubscribers(projectRoot, agentType) {
120
217
  const bus = readBus(projectRoot);
121
218
  if (!bus) return [];
122
- return Object.entries(bus.subscribers || {})
219
+ return Object.entries(bus.agents || {})
123
220
  .filter(([, meta]) => meta && meta.agent_type === agentType)
124
221
  .map(([id]) => id);
125
222
  }
@@ -136,18 +233,12 @@ async function waitForNewSubscriber(projectRoot, agentType, existing, timeoutMs
136
233
  return null;
137
234
  }
138
235
 
139
- function renameSubscriber(projectRoot, subscriberId, nickname) {
140
- const script = path.join(projectRoot, "scripts", "bus.sh");
141
- const res = spawnSync("bash", [script, "rename", subscriberId, nickname], { cwd: projectRoot });
142
- return res.status === 0;
143
- }
144
-
145
236
  function checkAndCleanupNickname(projectRoot, nickname) {
146
237
  if (!nickname) return { existing: null, cleaned: false };
147
- const busPath = path.join(projectRoot, ".ufoo", "bus", "bus.json");
238
+ const busPath = getUfooPaths(projectRoot).agentsFile;
148
239
  try {
149
240
  const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
150
- const entries = Object.entries(bus.subscribers || {})
241
+ const entries = Object.entries(bus.agents || {})
151
242
  .filter(([, meta]) => meta && meta.nickname === nickname);
152
243
 
153
244
  if (entries.length === 0) {
@@ -162,7 +253,7 @@ function checkAndCleanupNickname(projectRoot, nickname) {
162
253
 
163
254
  // Clean up offline agents with same nickname
164
255
  for (const [agentId] of entries) {
165
- delete bus.subscribers[agentId];
256
+ delete bus.agents[agentId];
166
257
  }
167
258
  fs.writeFileSync(busPath, JSON.stringify(bus, null, 2));
168
259
  return { existing: null, cleaned: true };
@@ -171,18 +262,38 @@ function checkAndCleanupNickname(projectRoot, nickname) {
171
262
  }
172
263
  }
173
264
 
174
- async function handleOps(projectRoot, ops = []) {
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
+
276
+ async function handleOps(projectRoot, ops = [], processManager = null) {
175
277
  const results = [];
176
278
  for (const op of ops) {
177
- if (op.action === "spawn") {
279
+ if (op.action === "launch") {
178
280
  const count = op.count || 1;
179
- 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
+ }
180
291
  const nickname = op.nickname || "";
181
292
  const startTime = new Date(Date.now() - 1000);
182
293
  const startIso = startTime.toISOString();
183
294
  if (nickname && count > 1) {
184
295
  results.push({
185
- action: "spawn",
296
+ action: "launch",
186
297
  ok: false,
187
298
  agent,
188
299
  count,
@@ -196,7 +307,7 @@ async function handleOps(projectRoot, ops = []) {
196
307
  if (existing) {
197
308
  // Agent with this nickname already exists and is active
198
309
  results.push({
199
- action: "spawn",
310
+ action: "launch",
200
311
  ok: true,
201
312
  agent,
202
313
  count,
@@ -208,8 +319,43 @@ async function handleOps(projectRoot, ops = []) {
208
319
  continue;
209
320
  }
210
321
  // eslint-disable-next-line no-await-in-loop
211
- await spawnAgent(projectRoot, agent, count, nickname);
212
- results.push({ action: "spawn", ok: true, agent, count, nickname: nickname || undefined });
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
+ }
351
+ results.push({
352
+ action: "launch",
353
+ mode: launchResult.mode,
354
+ ok: true,
355
+ agent,
356
+ count,
357
+ nickname: nickname || undefined
358
+ });
213
359
  if (nickname) {
214
360
  // eslint-disable-next-line no-await-in-loop
215
361
  const renameResult = await renameSpawnedAgent(projectRoot, agent, nickname, startIso);
@@ -218,78 +364,188 @@ async function handleOps(projectRoot, ops = []) {
218
364
  }
219
365
  }
220
366
  } catch (err) {
221
- results.push({ action: "spawn", ok: false, agent, count, error: err.message });
367
+ results.push({ action: "launch", ok: false, agent, count, error: err.message });
222
368
  }
223
369
  } else if (op.action === "close") {
224
370
  const ok = await closeAgent(projectRoot, op.agent_id);
225
371
  results.push({ action: "close", ok, agent_id: op.agent_id });
372
+ } else if (op.action === "rename") {
373
+ const agentId = op.agent_id || "";
374
+ const nickname = op.nickname || "";
375
+ if (!agentId || !nickname) {
376
+ results.push({
377
+ action: "rename",
378
+ ok: false,
379
+ agent_id: agentId,
380
+ nickname,
381
+ error: "rename requires agent_id and nickname",
382
+ });
383
+ continue;
384
+ }
385
+ try {
386
+ const eventBus = new EventBus(projectRoot);
387
+ eventBus.ensureBus();
388
+ eventBus.loadBusData();
389
+ let targetId = agentId;
390
+ if (!eventBus.busData?.agents?.[targetId]) {
391
+ const nicknameManager = new NicknameManager(eventBus.busData || { agents: {} });
392
+ const resolved = nicknameManager.resolveNickname(agentId);
393
+ if (resolved) targetId = resolved;
394
+ }
395
+ if (!eventBus.busData?.agents?.[targetId]) {
396
+ results.push({
397
+ action: "rename",
398
+ ok: false,
399
+ agent_id: agentId,
400
+ nickname,
401
+ error: `agent not found: ${agentId}`,
402
+ });
403
+ continue;
404
+ }
405
+ const result = await eventBus.rename(targetId, nickname, "ufoo-agent");
406
+ results.push({
407
+ action: "rename",
408
+ ok: true,
409
+ agent_id: result.subscriber,
410
+ nickname: result.newNickname,
411
+ old_nickname: result.oldNickname,
412
+ });
413
+ } catch (err) {
414
+ results.push({
415
+ action: "rename",
416
+ ok: false,
417
+ agent_id: agentId,
418
+ nickname,
419
+ error: err && err.message ? err.message : String(err || "rename failed"),
420
+ });
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
+ }
226
441
  }
227
442
  }
228
443
  return results;
229
444
  }
230
445
 
231
- function dispatchMessages(projectRoot, dispatch = [], daemonSubscriber = null) {
232
- const script = path.join(projectRoot, "scripts", "bus.sh");
233
- const defaultPublisher = daemonSubscriber || "ufoo-agent";
234
- const env = { ...process.env, AI_BUS_PUBLISHER: defaultPublisher };
446
+ async function dispatchMessages(projectRoot, dispatch = []) {
447
+ const eventBus = new EventBus(projectRoot);
448
+ // Always use "ufoo-agent" as the publisher for daemon messages
449
+ const defaultPublisher = "ufoo-agent";
235
450
  for (const item of dispatch) {
236
451
  if (!item || !item.target || !item.message) continue;
237
452
  const pub = item.publisher || defaultPublisher;
238
- env.AI_BUS_PUBLISHER = pub;
239
- if (item.target === "broadcast") {
240
- spawnSync("bash", [script, "broadcast", item.message], { env, cwd: projectRoot });
241
- } else {
242
- spawnSync("bash", [script, "send", item.target, item.message], { env, cwd: projectRoot });
453
+ try {
454
+ if (item.target === "broadcast") {
455
+ await eventBus.broadcast(item.message, pub);
456
+ } else {
457
+ await eventBus.send(item.target, item.message, pub);
458
+ }
459
+ } catch {
460
+ // ignore dispatch failures
243
461
  }
244
462
  }
245
463
  }
246
464
 
247
- function startBusBridge(projectRoot, onEvent, onStatus) {
248
- const script = path.join(projectRoot, "scripts", "bus.sh");
465
+ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
249
466
  const state = {
250
467
  subscriber: null,
251
468
  queueFile: null,
252
469
  pending: new Set(),
253
470
  };
471
+ const eventBus = new EventBus(projectRoot);
472
+ let joinInProgress = false;
254
473
 
255
- function ensureSubscriber() {
256
- if (state.subscriber) return;
257
- const debugFile = path.join(projectRoot, ".ufoo", "run", "bus-join-debug.txt");
474
+ function getAgentNickname(agentId) {
475
+ if (!agentId) return agentId;
258
476
  try {
259
- fs.writeFileSync(debugFile, `Attempting join at ${new Date().toISOString()}\n`, { flag: "a" });
260
- // Clear session env vars so join creates a new session
261
- const env = { ...process.env, CLAUDE_SESSION_ID: "", CODEX_SESSION_ID: "" };
262
- const res = spawnSync("bash", [script, "join"], { cwd: projectRoot, env });
263
- if (res.status !== 0) {
264
- const errMsg = (res.stderr || res.stdout || "").toString("utf8");
265
- fs.writeFileSync(debugFile, `Join failed: ${errMsg}\n`, { flag: "a" });
266
- return;
267
- }
268
- const out = (res.stdout || "").toString("utf8").trim();
269
- const sub = out.split(/\r?\n/).pop();
270
- if (!sub) {
271
- fs.writeFileSync(debugFile, `Join returned empty subscriber\n`, { flag: "a" });
272
- return;
477
+ const busPath = getUfooPaths(projectRoot).agentsFile;
478
+ const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
479
+ const meta = bus.agents && bus.agents[agentId];
480
+ if (meta && meta.nickname) {
481
+ return meta.nickname;
273
482
  }
274
- state.subscriber = sub;
275
- const safe = sub.replace(/:/g, "_");
276
- state.queueFile = path.join(projectRoot, ".ufoo", "bus", "queues", safe, "pending.jsonl");
277
- fs.writeFileSync(debugFile, `Successfully joined as ${sub}\n`, { flag: "a" });
278
- } catch (err) {
279
- fs.writeFileSync(debugFile, `Exception: ${err.message || err}\n`, { flag: "a" });
483
+ } catch {
484
+ // Ignore errors, return original ID
280
485
  }
486
+ return agentId;
487
+ }
488
+
489
+ function ensureSubscriber() {
490
+ if (state.subscriber || joinInProgress) return;
491
+ const debugFile = path.join(getUfooPaths(projectRoot).runDir, "bus-join-debug.txt");
492
+ joinInProgress = true;
493
+ (async () => {
494
+ try {
495
+ fs.writeFileSync(debugFile, `Attempting join at ${new Date().toISOString()}\n`, { flag: "a" });
496
+ // Determine agent type based on provider configuration
497
+ const agentType = provider === "codex-cli" ? "codex" : "claude-code";
498
+ // Use fixed ID "ufoo-agent" for daemon's bus identity with explicit nickname
499
+ const sub = await eventBus.join("ufoo-agent", agentType, "ufoo-agent");
500
+ if (!sub) {
501
+ fs.writeFileSync(debugFile, "Join returned empty subscriber\n", { flag: "a" });
502
+ return;
503
+ }
504
+ state.subscriber = sub;
505
+ const safe = subscriberToSafeName(sub);
506
+ state.queueFile = path.join(getUfooPaths(projectRoot).busQueuesDir, safe, "pending.jsonl");
507
+ fs.writeFileSync(debugFile, `Successfully joined as ${sub} (type: ${agentType})\n`, { flag: "a" });
508
+ } catch (err) {
509
+ fs.writeFileSync(debugFile, `Exception: ${err.message || err}\n`, { flag: "a" });
510
+ } finally {
511
+ joinInProgress = false;
512
+ }
513
+ })();
281
514
  }
282
515
 
283
516
  function poll() {
284
517
  ensureSubscriber();
518
+ if (typeof shouldDrain === "function" && !shouldDrain()) return;
285
519
  if (!state.queueFile) return;
286
520
  if (!fs.existsSync(state.queueFile)) return;
287
- let content;
521
+ let content = "";
522
+ let readOk = false;
523
+ const processingFile = `${state.queueFile}.processing.${process.pid}.${Date.now()}`;
288
524
  try {
289
- content = fs.readFileSync(state.queueFile, "utf8");
525
+ fs.renameSync(state.queueFile, processingFile);
526
+ content = fs.readFileSync(processingFile, "utf8");
527
+ readOk = true;
290
528
  } catch {
529
+ try {
530
+ if (fs.existsSync(processingFile)) {
531
+ fs.renameSync(processingFile, state.queueFile);
532
+ }
533
+ } catch {
534
+ // ignore rollback errors
535
+ }
291
536
  return;
537
+ } finally {
538
+ if (readOk) {
539
+ try {
540
+ if (fs.existsSync(processingFile)) {
541
+ fs.rmSync(processingFile, { force: true });
542
+ }
543
+ } catch {
544
+ // ignore cleanup errors
545
+ }
546
+ }
292
547
  }
548
+
293
549
  const lines = content.split(/\r?\n/).filter(Boolean);
294
550
  if (!lines.length) return;
295
551
  for (const line of lines) {
@@ -306,21 +562,17 @@ function startBusBridge(projectRoot, onEvent, onStatus) {
306
562
  publisher: evt.publisher,
307
563
  target: evt.target,
308
564
  message: evt.data?.message || "",
309
- ts: evt.ts,
565
+ ts: evt.timestamp || evt.ts,
310
566
  });
311
567
  }
312
568
  if (evt.publisher && state.pending.has(evt.publisher)) {
313
569
  state.pending.delete(evt.publisher);
314
570
  if (onStatus) {
315
- onStatus({ phase: "done", text: `${evt.publisher} done`, key: evt.publisher });
571
+ const displayName = getAgentNickname(evt.publisher);
572
+ onStatus({ phase: BUS_STATUS_PHASES.DONE, text: `${displayName} done`, key: evt.publisher });
316
573
  }
317
574
  }
318
575
  }
319
- try {
320
- fs.truncateSync(state.queueFile, 0);
321
- } catch {
322
- // ignore
323
- }
324
576
  }
325
577
 
326
578
  const interval = setInterval(poll, 1000);
@@ -329,13 +581,14 @@ function startBusBridge(projectRoot, onEvent, onStatus) {
329
581
  if (!target) return;
330
582
  state.pending.add(target);
331
583
  if (onStatus) {
332
- onStatus({ phase: "start", text: `${target} processing`, key: target });
584
+ const displayName = getAgentNickname(target);
585
+ onStatus({ phase: BUS_STATUS_PHASES.START, text: `${displayName} processing`, key: target });
333
586
  }
334
587
  },
335
588
  getSubscriber() {
336
589
  ensureSubscriber();
337
590
  try {
338
- fs.writeFileSync(path.join(projectRoot, ".ufoo", "run", "bridge-debug.txt"),
591
+ fs.writeFileSync(path.join(getUfooPaths(projectRoot).runDir, "bridge-debug.txt"),
339
592
  `subscriber: ${state.subscriber || "NULL"}\nqueue: ${state.queueFile || "NULL"}\n`);
340
593
  } catch {}
341
594
  return state.subscriber;
@@ -346,13 +599,64 @@ function startBusBridge(projectRoot, onEvent, onStatus) {
346
599
  };
347
600
  }
348
601
 
349
- function startDaemon({ projectRoot, provider, model }) {
350
- if (!fs.existsSync(path.join(projectRoot, ".ufoo"))) {
602
+ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
603
+ const paths = getUfooPaths(projectRoot);
604
+ if (!fs.existsSync(paths.ufooDir)) {
351
605
  throw new Error("Missing .ufoo. Run: ufoo init");
352
606
  }
353
607
 
354
- const runDir = path.join(projectRoot, ".ufoo", "run");
608
+ const runDir = paths.runDir;
355
609
  ensureDir(runDir);
610
+
611
+ // 文件锁机制:防止多个 daemon 同时启动
612
+ const lockFile = path.join(runDir, "daemon.lock");
613
+ let lockFd;
614
+ let recoveredStaleLock = false;
615
+ try {
616
+ // 尝试独占方式打开锁文件(如果已存在且被锁定则失败)
617
+ lockFd = fs.openSync(lockFile, "wx");
618
+ fs.writeSync(lockFd, `${process.pid}\n`);
619
+ } catch (err) {
620
+ if (err.code === "EEXIST") {
621
+ // 锁文件已存在,检查是否仍有效
622
+ let existingPid = null;
623
+ try {
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
+ }
629
+ } catch {
630
+ // ignore malformed lock file and treat as stale
631
+ }
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}`);
654
+ }
655
+ } else {
656
+ throw err;
657
+ }
658
+ }
659
+
356
660
  removeSocket(projectRoot);
357
661
  writePid(projectRoot);
358
662
 
@@ -361,101 +665,614 @@ function startDaemon({ projectRoot, provider, model }) {
361
665
  logFile.write(`[daemon] ${new Date().toISOString()} ${msg}\n`);
362
666
  };
363
667
 
364
- const sockets = new Set();
365
- const sendToSockets = (payload) => {
366
- const line = `${JSON.stringify(payload)}\n`;
367
- for (const sock of sockets) {
368
- if (!sock || sock.destroyed) continue;
668
+ // 创建进程管理器 - daemon 作为父进程监控所有 internal agents
669
+ const processManager = new AgentProcessManager(projectRoot);
670
+ log(`Process manager initialized`);
671
+
672
+ // Provider session cache (in-memory)
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
+ });
682
+
683
+ const buildRuntimeStatus = () =>
684
+ buildStatus(projectRoot, {
685
+ cronTasks: daemonCronController ? daemonCronController.listTasks() : [],
686
+ });
687
+
688
+ const cleanupInactiveSubscribers = () => {
689
+ try {
690
+ const syncBus = new EventBus(projectRoot);
691
+ syncBus.ensureBus();
692
+ syncBus.loadBusData();
693
+ syncBus.subscriberManager.cleanupInactive();
694
+ syncBus.saveBusData();
695
+ } catch {
696
+ // ignore cleanup errors
697
+ }
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;
724
+ }
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,
747
+ });
748
+ },
749
+ log,
750
+ });
751
+ },
752
+ log,
753
+ });
754
+ return;
755
+ }
756
+ if (req.type === IPC_REQUEST_TYPES.AGENT_REPORT) {
369
757
  try {
370
- sock.write(line);
371
- } catch {
372
- // ignore write errors
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 = "";
1052
+ }
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
+ });
1073
+ }
1074
+ }
1075
+
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(),
1088
+ });
1089
+ }
1090
+ probeHandles.delete(id);
1091
+ },
1092
+ });
1093
+ if (probeHandle) {
1094
+ probeHandles.set(subscriberId, probeHandle);
1095
+ }
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
+ );
373
1114
  }
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;
374
1132
  }
375
1133
  };
376
1134
 
377
- const busBridge = startBusBridge(projectRoot, (evt) => {
378
- sendToSockets({ type: "bus", data: evt });
379
- }, (status) => {
380
- sendToSockets({ type: "status", data: status });
381
- });
1135
+ ipcServer.listen(socketPath(projectRoot));
382
1136
 
383
- const server = net.createServer((socket) => {
384
- sockets.add(socket);
385
- socket.on("close", () => sockets.delete(socket));
386
- let buffer = "";
387
- socket.on("data", async (data) => {
388
- buffer += data.toString("utf8");
389
- const lines = buffer.split(/\r?\n/);
390
- buffer = lines.pop() || "";
391
- const complete = lines.filter((l) => l.trim());
392
- for (const line of complete) {
393
- const items = parseJsonLines(line);
394
- for (const req of items) {
395
- if (!req || typeof req !== "object") continue;
396
- if (req.type === "status") {
397
- const status = buildStatus(projectRoot);
398
- socket.write(`${JSON.stringify({ type: "status", data: status })}\n`);
399
- continue;
1137
+ log(`Started pid=${process.pid}`);
1138
+
1139
+ // 清理旧 daemon 留下的孤儿 internal agent 进程
1140
+ const EventBus = require("../bus");
1141
+ const { spawnSync } = require("child_process");
1142
+ const eventBus = new EventBus(projectRoot);
1143
+ try {
1144
+ eventBus.ensureBus();
1145
+ eventBus.loadBusData();
1146
+ const agents = eventBus.busData.agents || {};
1147
+
1148
+ // 查找所有 agent-runner 进程
1149
+ const psResult = spawnSync("ps", ["aux"], { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
1150
+ const lines = psResult.stdout ? psResult.stdout.split("\n") : [];
1151
+ const runnerProcesses = [];
1152
+
1153
+ for (const line of lines) {
1154
+ if (line.includes("agent-pty-runner") || line.includes("agent-runner")) {
1155
+ const parts = line.trim().split(/\s+/);
1156
+ if (parts.length >= 2) {
1157
+ const pid = parseInt(parts[1], 10);
1158
+ if (Number.isFinite(pid)) {
1159
+ runnerProcesses.push({ pid, line });
400
1160
  }
401
- if (req.type === "prompt") {
402
- log(`prompt ${String(req.text || "").slice(0, 200)}`);
403
- let result;
404
- try {
405
- result = await runUfooAgent({
406
- projectRoot,
407
- prompt: req.text || "",
408
- provider,
409
- model,
410
- });
411
- } catch (err) {
412
- log(`error ${err.message || String(err)}`);
413
- socket.write(
414
- `${JSON.stringify({
415
- type: "error",
416
- error: err.message || String(err),
417
- })}\n`,
418
- );
419
- continue;
420
- }
421
- if (!result.ok) {
422
- log(`agent-fail ${result.error || "agent failed"}`);
423
- socket.write(
424
- `${JSON.stringify({ type: "error", error: result.error || "agent failed" })}\n`,
425
- );
426
- continue;
427
- }
428
- for (const item of result.payload.dispatch || []) {
429
- if (item && item.target && item.target !== "broadcast") {
430
- busBridge.markPending(item.target);
1161
+ }
1162
+ }
1163
+ }
1164
+
1165
+ // 检查每个 runner 的父进程
1166
+ for (const runner of runnerProcesses) {
1167
+ try {
1168
+ const ppidResult = spawnSync("ps", ["-p", String(runner.pid), "-o", "ppid="], { encoding: "utf8" });
1169
+ const ppid = parseInt(ppidResult.stdout.trim(), 10);
1170
+
1171
+ if (Number.isFinite(ppid)) {
1172
+ // 检查父进程是否存在
1173
+ try {
1174
+ process.kill(ppid, 0);
1175
+ // 父进程还活着,检查是否是 daemon
1176
+ const ppidCmd = spawnSync("ps", ["-p", String(ppid), "-o", "command="], { encoding: "utf8" });
1177
+ const cmd = ppidCmd.stdout.trim();
1178
+
1179
+ if (!cmd.includes("daemon start")) {
1180
+ // 父进程不是 daemon,这是孤儿进程
1181
+ log(`Found orphan agent-runner process ${runner.pid} (parent ${ppid} is not a daemon)`);
1182
+ try {
1183
+ process.kill(runner.pid, "SIGTERM");
1184
+ log(`Killed orphan agent-runner ${runner.pid}`);
1185
+ } catch {
1186
+ // ignore
431
1187
  }
432
1188
  }
433
- dispatchMessages(projectRoot, result.payload.dispatch || [], busBridge.getSubscriber());
434
- const opsResults = await handleOps(projectRoot, result.payload.ops || []);
435
- log(`ok reply=${Boolean(result.payload.reply)} dispatch=${(result.payload.dispatch || []).length} ops=${(result.payload.ops || []).length}`);
436
- socket.write(
437
- `${JSON.stringify({
438
- type: "response",
439
- data: result.payload,
440
- opsResults,
441
- })}\n`,
442
- );
1189
+ } catch {
1190
+ // 父进程已死,杀掉孤儿进程
1191
+ log(`Found orphan agent-runner process ${runner.pid} (parent ${ppid} is dead)`);
1192
+ try {
1193
+ process.kill(runner.pid, "SIGTERM");
1194
+ log(`Killed orphan agent-runner ${runner.pid}`);
1195
+ } catch {
1196
+ // ignore
1197
+ }
443
1198
  }
444
1199
  }
1200
+ } catch {
1201
+ // ignore
445
1202
  }
446
- });
447
- });
1203
+ }
448
1204
 
449
- server.listen(socketPath(projectRoot));
450
- log(`Started pid=${process.pid}`);
1205
+ // 标记对应的 agents 为 inactive
1206
+ const adapterRouter = createTerminalAdapterRouter();
1207
+ for (const [subscriberId, meta] of Object.entries(agents)) {
1208
+ const launchMode = meta.launch_mode || "";
1209
+ const adapter = adapterRouter.getAdapter({ launchMode, agentId: subscriberId });
1210
+ if (launchMode && adapter.capabilities.supportsInternalQueueLoop) {
1211
+ if (meta.pid) {
1212
+ try {
1213
+ process.kill(meta.pid, 0);
1214
+ // 父 daemon 还活着,跳过
1215
+ } catch {
1216
+ // 父 daemon 已死,标记为 inactive
1217
+ // 注意:不更新 last_seen,保持原有时间戳,这样会自动超时
1218
+ meta.status = "inactive";
1219
+ log(`Marked orphan internal agent ${subscriberId} as inactive (parent daemon ${meta.pid} is dead)`);
1220
+ }
1221
+ }
1222
+ }
1223
+ }
1224
+ eventBus.saveBusData();
1225
+ } catch (err) {
1226
+ log(`Failed to cleanup orphan agents: ${err.message}`);
1227
+ }
1228
+
1229
+ const shouldResume = resumeMode === "force" || (resumeMode === "auto" && recoveredStaleLock);
1230
+ if (shouldResume) {
1231
+ const reason = resumeMode === "force" ? "forced by caller" : "stale daemon state detected";
1232
+ log(`Auto-recover enabled: ${reason}`);
1233
+ setTimeout(() => {
1234
+ resumeAgents(projectRoot, "", processManager).catch((err) => {
1235
+ log(`auto resume failed: ${err.message || String(err)}`);
1236
+ });
1237
+ }, 1500);
1238
+ }
451
1239
 
452
- process.on("exit", () => {
1240
+ const cleanup = () => {
1241
+ log(`Shutting down daemon (managed agents: ${processManager.count()})`);
1242
+
1243
+ if (daemonCronController) {
1244
+ daemonCronController.stopAll();
1245
+ daemonCronController = null;
1246
+ }
1247
+
1248
+ // 清理所有子进程
1249
+ processManager.cleanup();
1250
+
1251
+ ipcServer.stop();
453
1252
  busBridge.stop();
454
1253
  removeSocket(projectRoot);
455
- });
1254
+
1255
+ // 释放锁文件
1256
+ try {
1257
+ if (lockFd !== undefined) {
1258
+ fs.closeSync(lockFd);
1259
+ }
1260
+ const lockFile = path.join(getUfooPaths(projectRoot).runDir, "daemon.lock");
1261
+ if (fs.existsSync(lockFile)) {
1262
+ fs.unlinkSync(lockFile);
1263
+ }
1264
+ } catch {
1265
+ // ignore cleanup errors
1266
+ }
1267
+ };
1268
+
1269
+ process.on("exit", cleanup);
456
1270
  process.on("SIGTERM", () => {
457
- busBridge.stop();
458
- removeSocket(projectRoot);
1271
+ cleanup();
1272
+ process.exit(0);
1273
+ });
1274
+ process.on("SIGINT", () => {
1275
+ cleanup();
459
1276
  process.exit(0);
460
1277
  });
461
1278
  }
@@ -495,6 +1312,17 @@ function stopDaemon(projectRoot) {
495
1312
  // ignore
496
1313
  }
497
1314
  removeSocket(projectRoot);
1315
+
1316
+ // 清理锁文件
1317
+ try {
1318
+ const lockFile = path.join(getUfooPaths(projectRoot).runDir, "daemon.lock");
1319
+ if (fs.existsSync(lockFile)) {
1320
+ fs.unlinkSync(lockFile);
1321
+ }
1322
+ } catch {
1323
+ // ignore
1324
+ }
1325
+
498
1326
  return killed;
499
1327
  }
500
1328