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