u-foo 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -8
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +117 -0
- package/SKILLS/uinit/SKILL.md +73 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo.js +13 -0
- package/modules/AGENTS.template.md +15 -7
- package/modules/bus/README.md +28 -23
- package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +61 -1
- package/package.json +16 -4
- package/scripts/.archived/bash-to-js-migration/README.md +46 -0
- package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
- package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
- package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
- package/scripts/banner.sh +2 -89
- package/scripts/postinstall.js +59 -0
- package/src/agent/cliRunner.js +33 -5
- package/src/agent/internalRunner.js +78 -51
- package/src/agent/launcher.js +702 -0
- package/src/agent/notifier.js +200 -0
- package/src/agent/ptyRunner.js +377 -0
- package/src/agent/ptyWrapper.js +354 -0
- package/src/agent/readyDetector.js +159 -0
- package/src/agent/ufooAgent.js +37 -42
- package/src/bus/API_DESIGN.md +204 -0
- package/src/bus/activate.js +156 -0
- package/src/bus/daemon.js +308 -0
- package/src/bus/index.js +785 -0
- package/src/bus/inject.js +285 -0
- package/src/bus/message.js +302 -0
- package/src/bus/nickname.js +86 -0
- package/src/bus/queue.js +131 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/subscriber.js +296 -0
- package/src/bus/utils.js +357 -0
- package/src/chat/index.js +1842 -249
- package/src/cli.js +658 -95
- package/src/config.js +9 -2
- package/src/context/decisions.js +314 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +38 -0
- package/src/daemon/index.js +749 -94
- package/src/daemon/ops.js +395 -51
- package/src/daemon/providerSessions.js +291 -0
- package/src/daemon/run.js +34 -1
- package/src/daemon/status.js +24 -7
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +264 -0
- package/src/skills/index.js +159 -0
- package/src/status/index.js +252 -0
- package/src/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +41 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +73 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
- /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
- /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
- /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
- /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
- /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
- /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
- /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
- /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
- /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
- /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
package/src/daemon/index.js
CHANGED
|
@@ -1,10 +1,87 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const net = require("net");
|
|
4
|
+
const { spawnSync } = require("child_process");
|
|
4
5
|
const { runUfooAgent } = require("../agent/ufooAgent");
|
|
5
|
-
const {
|
|
6
|
+
const { launchAgent, closeAgent, resumeAgents } = require("./ops");
|
|
6
7
|
const { buildStatus } = require("./status");
|
|
7
|
-
const
|
|
8
|
+
const EventBus = require("../bus");
|
|
9
|
+
const NicknameManager = require("../bus/nickname");
|
|
10
|
+
const { generateInstanceId, subscriberToSafeName } = require("../bus/utils");
|
|
11
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
12
|
+
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);
|
|
34
|
+
|
|
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
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 获取所有进程数量
|
|
65
|
+
*/
|
|
66
|
+
count() {
|
|
67
|
+
return this.processes.size;
|
|
68
|
+
}
|
|
69
|
+
|
|
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
|
+
}
|
|
84
|
+
}
|
|
8
85
|
|
|
9
86
|
function sleep(ms) {
|
|
10
87
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -12,14 +89,15 @@ function sleep(ms) {
|
|
|
12
89
|
|
|
13
90
|
async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso) {
|
|
14
91
|
if (!nickname) return null;
|
|
15
|
-
const busPath =
|
|
16
|
-
const script = path.join(projectRoot, "scripts", "bus.sh");
|
|
92
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
17
93
|
const targetType = agentType === "codex" ? "codex" : "claude-code";
|
|
18
94
|
const deadline = Date.now() + 10000;
|
|
95
|
+
const eventBus = new EventBus(projectRoot);
|
|
96
|
+
let lastError = null;
|
|
19
97
|
while (Date.now() < deadline) {
|
|
20
98
|
try {
|
|
21
99
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
22
|
-
let entries = Object.entries(bus.
|
|
100
|
+
let entries = Object.entries(bus.agents || {})
|
|
23
101
|
.filter(([, meta]) => meta && meta.agent_type === targetType && meta.status === "active");
|
|
24
102
|
if (startIso) {
|
|
25
103
|
entries = entries.filter(([, meta]) => (meta.joined_at || "") >= startIso);
|
|
@@ -32,16 +110,15 @@ async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso) {
|
|
|
32
110
|
if (candidates.length === 0) candidates = entries;
|
|
33
111
|
candidates.sort((a, b) => (a[1].joined_at || "").localeCompare(b[1].joined_at || ""));
|
|
34
112
|
const [agentId] = candidates[candidates.length - 1];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} catch {
|
|
113
|
+
await eventBus.rename(agentId, nickname, "ufoo-agent");
|
|
114
|
+
return { ok: true, agent_id: agentId, nickname };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
lastError = err && err.message ? err.message : String(err || "rename failed");
|
|
40
117
|
// ignore and retry
|
|
41
118
|
}
|
|
42
119
|
await sleep(200);
|
|
43
120
|
}
|
|
44
|
-
return { ok: false, nickname, error: "rename timeout" };
|
|
121
|
+
return { ok: false, nickname, error: lastError || "rename timeout" };
|
|
45
122
|
}
|
|
46
123
|
|
|
47
124
|
function ensureDir(dir) {
|
|
@@ -49,15 +126,15 @@ function ensureDir(dir) {
|
|
|
49
126
|
}
|
|
50
127
|
|
|
51
128
|
function socketPath(projectRoot) {
|
|
52
|
-
return
|
|
129
|
+
return getUfooPaths(projectRoot).ufooSock;
|
|
53
130
|
}
|
|
54
131
|
|
|
55
132
|
function pidPath(projectRoot) {
|
|
56
|
-
return
|
|
133
|
+
return getUfooPaths(projectRoot).ufooDaemonPid;
|
|
57
134
|
}
|
|
58
135
|
|
|
59
136
|
function logPath(projectRoot) {
|
|
60
|
-
return
|
|
137
|
+
return getUfooPaths(projectRoot).ufooDaemonLog;
|
|
61
138
|
}
|
|
62
139
|
|
|
63
140
|
function writePid(projectRoot) {
|
|
@@ -77,14 +154,12 @@ function isRunning(projectRoot) {
|
|
|
77
154
|
if (!pid) return false;
|
|
78
155
|
try {
|
|
79
156
|
process.kill(pid, 0);
|
|
157
|
+
// PID 存活即认为 daemon 正在运行
|
|
158
|
+
// 不调用 isDaemonProcess() — ps 命令可能有瞬态失败导致误判
|
|
159
|
+
// 不删除 PID/socket — 破坏性操作会导致竞争条件
|
|
80
160
|
return true;
|
|
81
161
|
} catch {
|
|
82
|
-
|
|
83
|
-
fs.unlinkSync(pidPath(projectRoot));
|
|
84
|
-
} catch {
|
|
85
|
-
// ignore
|
|
86
|
-
}
|
|
87
|
-
removeSocket(projectRoot);
|
|
162
|
+
// 进程已死
|
|
88
163
|
return false;
|
|
89
164
|
}
|
|
90
165
|
}
|
|
@@ -108,7 +183,7 @@ function parseJsonLines(buffer) {
|
|
|
108
183
|
}
|
|
109
184
|
|
|
110
185
|
function readBus(projectRoot) {
|
|
111
|
-
const busPath =
|
|
186
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
112
187
|
try {
|
|
113
188
|
return JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
114
189
|
} catch {
|
|
@@ -119,7 +194,7 @@ function readBus(projectRoot) {
|
|
|
119
194
|
function listSubscribers(projectRoot, agentType) {
|
|
120
195
|
const bus = readBus(projectRoot);
|
|
121
196
|
if (!bus) return [];
|
|
122
|
-
return Object.entries(bus.
|
|
197
|
+
return Object.entries(bus.agents || {})
|
|
123
198
|
.filter(([, meta]) => meta && meta.agent_type === agentType)
|
|
124
199
|
.map(([id]) => id);
|
|
125
200
|
}
|
|
@@ -136,18 +211,12 @@ async function waitForNewSubscriber(projectRoot, agentType, existing, timeoutMs
|
|
|
136
211
|
return null;
|
|
137
212
|
}
|
|
138
213
|
|
|
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
214
|
function checkAndCleanupNickname(projectRoot, nickname) {
|
|
146
215
|
if (!nickname) return { existing: null, cleaned: false };
|
|
147
|
-
const busPath =
|
|
216
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
148
217
|
try {
|
|
149
218
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
150
|
-
const entries = Object.entries(bus.
|
|
219
|
+
const entries = Object.entries(bus.agents || {})
|
|
151
220
|
.filter(([, meta]) => meta && meta.nickname === nickname);
|
|
152
221
|
|
|
153
222
|
if (entries.length === 0) {
|
|
@@ -162,7 +231,7 @@ function checkAndCleanupNickname(projectRoot, nickname) {
|
|
|
162
231
|
|
|
163
232
|
// Clean up offline agents with same nickname
|
|
164
233
|
for (const [agentId] of entries) {
|
|
165
|
-
delete bus.
|
|
234
|
+
delete bus.agents[agentId];
|
|
166
235
|
}
|
|
167
236
|
fs.writeFileSync(busPath, JSON.stringify(bus, null, 2));
|
|
168
237
|
return { existing: null, cleaned: true };
|
|
@@ -171,10 +240,10 @@ function checkAndCleanupNickname(projectRoot, nickname) {
|
|
|
171
240
|
}
|
|
172
241
|
}
|
|
173
242
|
|
|
174
|
-
async function handleOps(projectRoot, ops = []) {
|
|
243
|
+
async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
175
244
|
const results = [];
|
|
176
245
|
for (const op of ops) {
|
|
177
|
-
if (op.action === "
|
|
246
|
+
if (op.action === "launch") {
|
|
178
247
|
const count = op.count || 1;
|
|
179
248
|
const agent = op.agent === "codex" ? "codex" : "claude";
|
|
180
249
|
const nickname = op.nickname || "";
|
|
@@ -182,7 +251,7 @@ async function handleOps(projectRoot, ops = []) {
|
|
|
182
251
|
const startIso = startTime.toISOString();
|
|
183
252
|
if (nickname && count > 1) {
|
|
184
253
|
results.push({
|
|
185
|
-
action: "
|
|
254
|
+
action: "launch",
|
|
186
255
|
ok: false,
|
|
187
256
|
agent,
|
|
188
257
|
count,
|
|
@@ -196,7 +265,7 @@ async function handleOps(projectRoot, ops = []) {
|
|
|
196
265
|
if (existing) {
|
|
197
266
|
// Agent with this nickname already exists and is active
|
|
198
267
|
results.push({
|
|
199
|
-
action: "
|
|
268
|
+
action: "launch",
|
|
200
269
|
ok: true,
|
|
201
270
|
agent,
|
|
202
271
|
count,
|
|
@@ -208,8 +277,15 @@ async function handleOps(projectRoot, ops = []) {
|
|
|
208
277
|
continue;
|
|
209
278
|
}
|
|
210
279
|
// eslint-disable-next-line no-await-in-loop
|
|
211
|
-
await
|
|
212
|
-
results.push({
|
|
280
|
+
const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager);
|
|
281
|
+
results.push({
|
|
282
|
+
action: "launch",
|
|
283
|
+
mode: launchResult.mode,
|
|
284
|
+
ok: true,
|
|
285
|
+
agent,
|
|
286
|
+
count,
|
|
287
|
+
nickname: nickname || undefined
|
|
288
|
+
});
|
|
213
289
|
if (nickname) {
|
|
214
290
|
// eslint-disable-next-line no-await-in-loop
|
|
215
291
|
const renameResult = await renameSpawnedAgent(projectRoot, agent, nickname, startIso);
|
|
@@ -218,78 +294,169 @@ async function handleOps(projectRoot, ops = []) {
|
|
|
218
294
|
}
|
|
219
295
|
}
|
|
220
296
|
} catch (err) {
|
|
221
|
-
results.push({ action: "
|
|
297
|
+
results.push({ action: "launch", ok: false, agent, count, error: err.message });
|
|
222
298
|
}
|
|
223
299
|
} else if (op.action === "close") {
|
|
224
300
|
const ok = await closeAgent(projectRoot, op.agent_id);
|
|
225
301
|
results.push({ action: "close", ok, agent_id: op.agent_id });
|
|
302
|
+
} else if (op.action === "rename") {
|
|
303
|
+
const agentId = op.agent_id || "";
|
|
304
|
+
const nickname = op.nickname || "";
|
|
305
|
+
if (!agentId || !nickname) {
|
|
306
|
+
results.push({
|
|
307
|
+
action: "rename",
|
|
308
|
+
ok: false,
|
|
309
|
+
agent_id: agentId,
|
|
310
|
+
nickname,
|
|
311
|
+
error: "rename requires agent_id and nickname",
|
|
312
|
+
});
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const eventBus = new EventBus(projectRoot);
|
|
317
|
+
eventBus.ensureBus();
|
|
318
|
+
eventBus.loadBusData();
|
|
319
|
+
let targetId = agentId;
|
|
320
|
+
if (!eventBus.busData?.agents?.[targetId]) {
|
|
321
|
+
const nicknameManager = new NicknameManager(eventBus.busData || { agents: {} });
|
|
322
|
+
const resolved = nicknameManager.resolveNickname(agentId);
|
|
323
|
+
if (resolved) targetId = resolved;
|
|
324
|
+
}
|
|
325
|
+
if (!eventBus.busData?.agents?.[targetId]) {
|
|
326
|
+
results.push({
|
|
327
|
+
action: "rename",
|
|
328
|
+
ok: false,
|
|
329
|
+
agent_id: agentId,
|
|
330
|
+
nickname,
|
|
331
|
+
error: `agent not found: ${agentId}`,
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const result = await eventBus.rename(targetId, nickname, "ufoo-agent");
|
|
336
|
+
results.push({
|
|
337
|
+
action: "rename",
|
|
338
|
+
ok: true,
|
|
339
|
+
agent_id: result.subscriber,
|
|
340
|
+
nickname: result.newNickname,
|
|
341
|
+
old_nickname: result.oldNickname,
|
|
342
|
+
});
|
|
343
|
+
} catch (err) {
|
|
344
|
+
results.push({
|
|
345
|
+
action: "rename",
|
|
346
|
+
ok: false,
|
|
347
|
+
agent_id: agentId,
|
|
348
|
+
nickname,
|
|
349
|
+
error: err && err.message ? err.message : String(err || "rename failed"),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
226
352
|
}
|
|
227
353
|
}
|
|
228
354
|
return results;
|
|
229
355
|
}
|
|
230
356
|
|
|
231
|
-
function dispatchMessages(projectRoot, dispatch = []
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const
|
|
357
|
+
async function dispatchMessages(projectRoot, dispatch = []) {
|
|
358
|
+
const eventBus = new EventBus(projectRoot);
|
|
359
|
+
// Always use "ufoo-agent" as the publisher for daemon messages
|
|
360
|
+
const defaultPublisher = "ufoo-agent";
|
|
235
361
|
for (const item of dispatch) {
|
|
236
362
|
if (!item || !item.target || !item.message) continue;
|
|
237
363
|
const pub = item.publisher || defaultPublisher;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
364
|
+
try {
|
|
365
|
+
if (item.target === "broadcast") {
|
|
366
|
+
await eventBus.broadcast(item.message, pub);
|
|
367
|
+
} else {
|
|
368
|
+
await eventBus.send(item.target, item.message, pub);
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
// ignore dispatch failures
|
|
243
372
|
}
|
|
244
373
|
}
|
|
245
374
|
}
|
|
246
375
|
|
|
247
|
-
function startBusBridge(projectRoot, onEvent, onStatus) {
|
|
248
|
-
const script = path.join(projectRoot, "scripts", "bus.sh");
|
|
376
|
+
function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
249
377
|
const state = {
|
|
250
378
|
subscriber: null,
|
|
251
379
|
queueFile: null,
|
|
252
380
|
pending: new Set(),
|
|
253
381
|
};
|
|
382
|
+
const eventBus = new EventBus(projectRoot);
|
|
383
|
+
let joinInProgress = false;
|
|
254
384
|
|
|
255
|
-
function
|
|
256
|
-
if (
|
|
257
|
-
const debugFile = path.join(projectRoot, ".ufoo", "run", "bus-join-debug.txt");
|
|
385
|
+
function getAgentNickname(agentId) {
|
|
386
|
+
if (!agentId) return agentId;
|
|
258
387
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
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;
|
|
388
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
389
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
390
|
+
const meta = bus.agents && bus.agents[agentId];
|
|
391
|
+
if (meta && meta.nickname) {
|
|
392
|
+
return meta.nickname;
|
|
273
393
|
}
|
|
274
|
-
|
|
275
|
-
|
|
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" });
|
|
394
|
+
} catch {
|
|
395
|
+
// Ignore errors, return original ID
|
|
280
396
|
}
|
|
397
|
+
return agentId;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function ensureSubscriber() {
|
|
401
|
+
if (state.subscriber || joinInProgress) return;
|
|
402
|
+
const debugFile = path.join(getUfooPaths(projectRoot).runDir, "bus-join-debug.txt");
|
|
403
|
+
joinInProgress = true;
|
|
404
|
+
(async () => {
|
|
405
|
+
try {
|
|
406
|
+
fs.writeFileSync(debugFile, `Attempting join at ${new Date().toISOString()}\n`, { flag: "a" });
|
|
407
|
+
// Determine agent type based on provider configuration
|
|
408
|
+
const agentType = provider === "codex-cli" ? "codex" : "claude-code";
|
|
409
|
+
// Use fixed ID "ufoo-agent" for daemon's bus identity with explicit nickname
|
|
410
|
+
const sub = await eventBus.join("ufoo-agent", agentType, "ufoo-agent");
|
|
411
|
+
if (!sub) {
|
|
412
|
+
fs.writeFileSync(debugFile, "Join returned empty subscriber\n", { flag: "a" });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
state.subscriber = sub;
|
|
416
|
+
const safe = subscriberToSafeName(sub);
|
|
417
|
+
state.queueFile = path.join(getUfooPaths(projectRoot).busQueuesDir, safe, "pending.jsonl");
|
|
418
|
+
fs.writeFileSync(debugFile, `Successfully joined as ${sub} (type: ${agentType})\n`, { flag: "a" });
|
|
419
|
+
} catch (err) {
|
|
420
|
+
fs.writeFileSync(debugFile, `Exception: ${err.message || err}\n`, { flag: "a" });
|
|
421
|
+
} finally {
|
|
422
|
+
joinInProgress = false;
|
|
423
|
+
}
|
|
424
|
+
})();
|
|
281
425
|
}
|
|
282
426
|
|
|
283
427
|
function poll() {
|
|
284
428
|
ensureSubscriber();
|
|
429
|
+
if (typeof shouldDrain === "function" && !shouldDrain()) return;
|
|
285
430
|
if (!state.queueFile) return;
|
|
286
431
|
if (!fs.existsSync(state.queueFile)) return;
|
|
287
|
-
let content;
|
|
432
|
+
let content = "";
|
|
433
|
+
let readOk = false;
|
|
434
|
+
const processingFile = `${state.queueFile}.processing.${process.pid}.${Date.now()}`;
|
|
288
435
|
try {
|
|
289
|
-
|
|
436
|
+
fs.renameSync(state.queueFile, processingFile);
|
|
437
|
+
content = fs.readFileSync(processingFile, "utf8");
|
|
438
|
+
readOk = true;
|
|
290
439
|
} catch {
|
|
440
|
+
try {
|
|
441
|
+
if (fs.existsSync(processingFile)) {
|
|
442
|
+
fs.renameSync(processingFile, state.queueFile);
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
// ignore rollback errors
|
|
446
|
+
}
|
|
291
447
|
return;
|
|
448
|
+
} finally {
|
|
449
|
+
if (readOk) {
|
|
450
|
+
try {
|
|
451
|
+
if (fs.existsSync(processingFile)) {
|
|
452
|
+
fs.rmSync(processingFile, { force: true });
|
|
453
|
+
}
|
|
454
|
+
} catch {
|
|
455
|
+
// ignore cleanup errors
|
|
456
|
+
}
|
|
457
|
+
}
|
|
292
458
|
}
|
|
459
|
+
|
|
293
460
|
const lines = content.split(/\r?\n/).filter(Boolean);
|
|
294
461
|
if (!lines.length) return;
|
|
295
462
|
for (const line of lines) {
|
|
@@ -306,21 +473,17 @@ function startBusBridge(projectRoot, onEvent, onStatus) {
|
|
|
306
473
|
publisher: evt.publisher,
|
|
307
474
|
target: evt.target,
|
|
308
475
|
message: evt.data?.message || "",
|
|
309
|
-
ts: evt.ts,
|
|
476
|
+
ts: evt.timestamp || evt.ts,
|
|
310
477
|
});
|
|
311
478
|
}
|
|
312
479
|
if (evt.publisher && state.pending.has(evt.publisher)) {
|
|
313
480
|
state.pending.delete(evt.publisher);
|
|
314
481
|
if (onStatus) {
|
|
315
|
-
|
|
482
|
+
const displayName = getAgentNickname(evt.publisher);
|
|
483
|
+
onStatus({ phase: "done", text: `${displayName} done`, key: evt.publisher });
|
|
316
484
|
}
|
|
317
485
|
}
|
|
318
486
|
}
|
|
319
|
-
try {
|
|
320
|
-
fs.truncateSync(state.queueFile, 0);
|
|
321
|
-
} catch {
|
|
322
|
-
// ignore
|
|
323
|
-
}
|
|
324
487
|
}
|
|
325
488
|
|
|
326
489
|
const interval = setInterval(poll, 1000);
|
|
@@ -329,13 +492,14 @@ function startBusBridge(projectRoot, onEvent, onStatus) {
|
|
|
329
492
|
if (!target) return;
|
|
330
493
|
state.pending.add(target);
|
|
331
494
|
if (onStatus) {
|
|
332
|
-
|
|
495
|
+
const displayName = getAgentNickname(target);
|
|
496
|
+
onStatus({ phase: "start", text: `${displayName} processing`, key: target });
|
|
333
497
|
}
|
|
334
498
|
},
|
|
335
499
|
getSubscriber() {
|
|
336
500
|
ensureSubscriber();
|
|
337
501
|
try {
|
|
338
|
-
fs.writeFileSync(path.join(projectRoot
|
|
502
|
+
fs.writeFileSync(path.join(getUfooPaths(projectRoot).runDir, "bridge-debug.txt"),
|
|
339
503
|
`subscriber: ${state.subscriber || "NULL"}\nqueue: ${state.queueFile || "NULL"}\n`);
|
|
340
504
|
} catch {}
|
|
341
505
|
return state.subscriber;
|
|
@@ -346,13 +510,51 @@ function startBusBridge(projectRoot, onEvent, onStatus) {
|
|
|
346
510
|
};
|
|
347
511
|
}
|
|
348
512
|
|
|
349
|
-
function startDaemon({ projectRoot, provider, model }) {
|
|
350
|
-
|
|
513
|
+
function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
514
|
+
const paths = getUfooPaths(projectRoot);
|
|
515
|
+
if (!fs.existsSync(paths.ufooDir)) {
|
|
351
516
|
throw new Error("Missing .ufoo. Run: ufoo init");
|
|
352
517
|
}
|
|
353
518
|
|
|
354
|
-
const runDir =
|
|
519
|
+
const runDir = paths.runDir;
|
|
355
520
|
ensureDir(runDir);
|
|
521
|
+
|
|
522
|
+
// 文件锁机制:防止多个 daemon 同时启动
|
|
523
|
+
const lockFile = path.join(runDir, "daemon.lock");
|
|
524
|
+
let lockFd;
|
|
525
|
+
try {
|
|
526
|
+
lockFd = fs.openSync(lockFile, "wx");
|
|
527
|
+
fs.writeSync(lockFd, `${process.pid}\n`);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
if (err.code === "EEXIST") {
|
|
530
|
+
// 锁文件已存在,检查持有者是否还活着
|
|
531
|
+
let existingPid;
|
|
532
|
+
try {
|
|
533
|
+
existingPid = parseInt(fs.readFileSync(lockFile, "utf8").trim(), 10);
|
|
534
|
+
} catch {
|
|
535
|
+
existingPid = NaN;
|
|
536
|
+
}
|
|
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
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// 持有者已死,接管锁
|
|
550
|
+
try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
|
|
551
|
+
lockFd = fs.openSync(lockFile, "wx");
|
|
552
|
+
fs.writeSync(lockFd, `${process.pid}\n`);
|
|
553
|
+
} else {
|
|
554
|
+
throw err;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
356
558
|
removeSocket(projectRoot);
|
|
357
559
|
writePid(projectRoot);
|
|
358
560
|
|
|
@@ -361,6 +563,16 @@ function startDaemon({ projectRoot, provider, model }) {
|
|
|
361
563
|
logFile.write(`[daemon] ${new Date().toISOString()} ${msg}\n`);
|
|
362
564
|
};
|
|
363
565
|
|
|
566
|
+
// 创建进程管理器 - daemon 作为父进程监控所有 internal agents
|
|
567
|
+
const processManager = new AgentProcessManager(projectRoot);
|
|
568
|
+
log(`Process manager initialized`);
|
|
569
|
+
|
|
570
|
+
// 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
|
+
|
|
364
576
|
const sockets = new Set();
|
|
365
577
|
const sendToSockets = (payload) => {
|
|
366
578
|
const line = `${JSON.stringify(payload)}\n`;
|
|
@@ -374,11 +586,38 @@ function startDaemon({ projectRoot, provider, model }) {
|
|
|
374
586
|
}
|
|
375
587
|
};
|
|
376
588
|
|
|
377
|
-
const busBridge = startBusBridge(projectRoot, (evt) => {
|
|
589
|
+
const busBridge = startBusBridge(projectRoot, provider, (evt) => {
|
|
378
590
|
sendToSockets({ type: "bus", data: evt });
|
|
379
591
|
}, (status) => {
|
|
380
592
|
sendToSockets({ type: "status", data: status });
|
|
381
|
-
});
|
|
593
|
+
}, () => sockets.size > 0);
|
|
594
|
+
|
|
595
|
+
// 定期检测状态变化并推送(仅当有变化时)
|
|
596
|
+
let lastActiveJson = "";
|
|
597
|
+
const statusSyncInterval = setInterval(() => {
|
|
598
|
+
if (sockets.size === 0) return; // 没有客户端连接时跳过
|
|
599
|
+
try {
|
|
600
|
+
// 先清理不活跃的订阅者,确保状态准确
|
|
601
|
+
const syncBus = new EventBus(projectRoot);
|
|
602
|
+
syncBus.ensureBus();
|
|
603
|
+
syncBus.loadBusData();
|
|
604
|
+
syncBus.subscriberManager.cleanupInactive();
|
|
605
|
+
syncBus.saveBusData();
|
|
606
|
+
} catch {
|
|
607
|
+
// ignore cleanup errors
|
|
608
|
+
}
|
|
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
|
|
619
|
+
}
|
|
620
|
+
}, 3000); // 每3秒检测一次
|
|
382
621
|
|
|
383
622
|
const server = net.createServer((socket) => {
|
|
384
623
|
sockets.add(socket);
|
|
@@ -394,6 +633,16 @@ function startDaemon({ projectRoot, provider, model }) {
|
|
|
394
633
|
for (const req of items) {
|
|
395
634
|
if (!req || typeof req !== "object") continue;
|
|
396
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
|
+
}
|
|
397
646
|
const status = buildStatus(projectRoot);
|
|
398
647
|
socket.write(`${JSON.stringify({ type: "status", data: status })}\n`);
|
|
399
648
|
continue;
|
|
@@ -430,8 +679,8 @@ function startDaemon({ projectRoot, provider, model }) {
|
|
|
430
679
|
busBridge.markPending(item.target);
|
|
431
680
|
}
|
|
432
681
|
}
|
|
433
|
-
dispatchMessages(projectRoot, result.payload.dispatch || []
|
|
434
|
-
const opsResults = await handleOps(projectRoot, result.payload.ops || []);
|
|
682
|
+
await dispatchMessages(projectRoot, result.payload.dispatch || []);
|
|
683
|
+
const opsResults = await handleOps(projectRoot, result.payload.ops || [], processManager);
|
|
435
684
|
log(`ok reply=${Boolean(result.payload.reply)} dispatch=${(result.payload.dispatch || []).length} ops=${(result.payload.ops || []).length}`);
|
|
436
685
|
socket.write(
|
|
437
686
|
`${JSON.stringify({
|
|
@@ -440,6 +689,279 @@ function startDaemon({ projectRoot, provider, model }) {
|
|
|
440
689
|
opsResults,
|
|
441
690
|
})}\n`,
|
|
442
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;
|
|
810
|
+
}
|
|
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;
|
|
838
|
+
}
|
|
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
|
+
}
|
|
870
|
+
|
|
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
|
+
},
|
|
917
|
+
});
|
|
918
|
+
// 保存handle,用于agent_ready时提前触发
|
|
919
|
+
if (probeHandle) {
|
|
920
|
+
probeHandles.set(subscriberId, probeHandle);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
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;
|
|
443
965
|
}
|
|
444
966
|
}
|
|
445
967
|
}
|
|
@@ -449,13 +971,135 @@ function startDaemon({ projectRoot, provider, model }) {
|
|
|
449
971
|
server.listen(socketPath(projectRoot));
|
|
450
972
|
log(`Started pid=${process.pid}`);
|
|
451
973
|
|
|
452
|
-
|
|
974
|
+
// 清理旧 daemon 留下的孤儿 internal agent 进程
|
|
975
|
+
const EventBus = require("../bus");
|
|
976
|
+
const eventBus = new EventBus(projectRoot);
|
|
977
|
+
try {
|
|
978
|
+
eventBus.ensureBus();
|
|
979
|
+
eventBus.loadBusData();
|
|
980
|
+
const agents = eventBus.busData.agents || {};
|
|
981
|
+
|
|
982
|
+
// 查找所有 agent-runner 进程
|
|
983
|
+
const psResult = spawnSync("ps", ["aux"], { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
|
|
984
|
+
const lines = psResult.stdout ? psResult.stdout.split("\n") : [];
|
|
985
|
+
const runnerProcesses = [];
|
|
986
|
+
|
|
987
|
+
for (const line of lines) {
|
|
988
|
+
if (line.includes("agent-pty-runner") || line.includes("agent-runner")) {
|
|
989
|
+
const parts = line.trim().split(/\s+/);
|
|
990
|
+
if (parts.length >= 2) {
|
|
991
|
+
const pid = parseInt(parts[1], 10);
|
|
992
|
+
if (Number.isFinite(pid)) {
|
|
993
|
+
runnerProcesses.push({ pid, line });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// 检查每个 runner 的父进程
|
|
1000
|
+
for (const runner of runnerProcesses) {
|
|
1001
|
+
try {
|
|
1002
|
+
const ppidResult = spawnSync("ps", ["-p", String(runner.pid), "-o", "ppid="], { encoding: "utf8" });
|
|
1003
|
+
const ppid = parseInt(ppidResult.stdout.trim(), 10);
|
|
1004
|
+
|
|
1005
|
+
if (Number.isFinite(ppid)) {
|
|
1006
|
+
// 检查父进程是否存在
|
|
1007
|
+
try {
|
|
1008
|
+
process.kill(ppid, 0);
|
|
1009
|
+
// 父进程还活着,检查是否是 daemon
|
|
1010
|
+
const ppidCmd = spawnSync("ps", ["-p", String(ppid), "-o", "command="], { encoding: "utf8" });
|
|
1011
|
+
const cmd = ppidCmd.stdout.trim();
|
|
1012
|
+
|
|
1013
|
+
if (!cmd.includes("daemon start")) {
|
|
1014
|
+
// 父进程不是 daemon,这是孤儿进程
|
|
1015
|
+
log(`Found orphan agent-runner process ${runner.pid} (parent ${ppid} is not a daemon)`);
|
|
1016
|
+
try {
|
|
1017
|
+
process.kill(runner.pid, "SIGTERM");
|
|
1018
|
+
log(`Killed orphan agent-runner ${runner.pid}`);
|
|
1019
|
+
} catch {
|
|
1020
|
+
// ignore
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
} catch {
|
|
1024
|
+
// 父进程已死,杀掉孤儿进程
|
|
1025
|
+
log(`Found orphan agent-runner process ${runner.pid} (parent ${ppid} is dead)`);
|
|
1026
|
+
try {
|
|
1027
|
+
process.kill(runner.pid, "SIGTERM");
|
|
1028
|
+
log(`Killed orphan agent-runner ${runner.pid}`);
|
|
1029
|
+
} catch {
|
|
1030
|
+
// ignore
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
} catch {
|
|
1035
|
+
// ignore
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 标记对应的 agents 为 inactive
|
|
1040
|
+
for (const [subscriberId, meta] of Object.entries(agents)) {
|
|
1041
|
+
if (meta.launch_mode && meta.launch_mode.startsWith("internal")) {
|
|
1042
|
+
if (meta.pid) {
|
|
1043
|
+
try {
|
|
1044
|
+
process.kill(meta.pid, 0);
|
|
1045
|
+
// 父 daemon 还活着,跳过
|
|
1046
|
+
} catch {
|
|
1047
|
+
// 父 daemon 已死,标记为 inactive
|
|
1048
|
+
// 设置 last_seen 为很久以前,强制立即超时
|
|
1049
|
+
meta.status = "inactive";
|
|
1050
|
+
meta.last_seen = "2020-01-01T00:00:00.000Z";
|
|
1051
|
+
log(`Marked orphan internal agent ${subscriberId} as inactive (parent daemon ${meta.pid} is dead)`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
eventBus.saveBusData();
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
log(`Failed to cleanup orphan agents: ${err.message}`);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const config = loadConfig(projectRoot);
|
|
1062
|
+
const autoResumeEnabled = config.autoResume !== false;
|
|
1063
|
+
const shouldResume = resumeMode === "force" || (resumeMode === "auto" && autoResumeEnabled);
|
|
1064
|
+
if (shouldResume) {
|
|
1065
|
+
setTimeout(() => {
|
|
1066
|
+
resumeAgents(projectRoot, "", processManager).catch((err) => {
|
|
1067
|
+
log(`auto resume failed: ${err.message || String(err)}`);
|
|
1068
|
+
});
|
|
1069
|
+
}, 1500);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const cleanup = () => {
|
|
1073
|
+
log(`Shutting down daemon (managed agents: ${processManager.count()})`);
|
|
1074
|
+
|
|
1075
|
+
// 清理所有子进程
|
|
1076
|
+
processManager.cleanup();
|
|
1077
|
+
|
|
1078
|
+
clearInterval(statusSyncInterval);
|
|
453
1079
|
busBridge.stop();
|
|
454
1080
|
removeSocket(projectRoot);
|
|
455
|
-
|
|
1081
|
+
|
|
1082
|
+
// 释放锁文件
|
|
1083
|
+
try {
|
|
1084
|
+
if (lockFd !== undefined) {
|
|
1085
|
+
fs.closeSync(lockFd);
|
|
1086
|
+
}
|
|
1087
|
+
const lockFile = path.join(getUfooPaths(projectRoot).runDir, "daemon.lock");
|
|
1088
|
+
if (fs.existsSync(lockFile)) {
|
|
1089
|
+
fs.unlinkSync(lockFile);
|
|
1090
|
+
}
|
|
1091
|
+
} catch {
|
|
1092
|
+
// ignore cleanup errors
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
process.on("exit", cleanup);
|
|
456
1097
|
process.on("SIGTERM", () => {
|
|
457
|
-
|
|
458
|
-
|
|
1098
|
+
cleanup();
|
|
1099
|
+
process.exit(0);
|
|
1100
|
+
});
|
|
1101
|
+
process.on("SIGINT", () => {
|
|
1102
|
+
cleanup();
|
|
459
1103
|
process.exit(0);
|
|
460
1104
|
});
|
|
461
1105
|
}
|
|
@@ -495,6 +1139,17 @@ function stopDaemon(projectRoot) {
|
|
|
495
1139
|
// ignore
|
|
496
1140
|
}
|
|
497
1141
|
removeSocket(projectRoot);
|
|
1142
|
+
|
|
1143
|
+
// 清理锁文件
|
|
1144
|
+
try {
|
|
1145
|
+
const lockFile = path.join(getUfooPaths(projectRoot).runDir, "daemon.lock");
|
|
1146
|
+
if (fs.existsSync(lockFile)) {
|
|
1147
|
+
fs.unlinkSync(lockFile);
|
|
1148
|
+
}
|
|
1149
|
+
} catch {
|
|
1150
|
+
// ignore
|
|
1151
|
+
}
|
|
1152
|
+
|
|
498
1153
|
return killed;
|
|
499
1154
|
}
|
|
500
1155
|
|