u-foo 1.0.3 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -11
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +132 -0
- package/SKILLS/uinit/SKILL.md +78 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +17 -0
- package/modules/AGENTS.template.md +29 -11
- package/modules/bus/README.md +33 -25
- package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +63 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +25 -4
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +30 -0
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +554 -33
- package/src/agent/internalRunner.js +150 -56
- package/src/agent/launcher.js +754 -0
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +340 -0
- package/src/agent/ptyRunner.js +847 -0
- package/src/agent/ptyWrapper.js +379 -0
- package/src/agent/readyDetector.js +175 -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 +46 -42
- 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 +172 -0
- package/src/bus/daemon.js +436 -0
- package/src/bus/index.js +842 -0
- package/src/bus/inject.js +315 -0
- package/src/bus/message.js +430 -0
- package/src/bus/nickname.js +88 -0
- package/src/bus/queue.js +136 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +312 -0
- package/src/bus/utils.js +363 -0
- 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 +154 -0
- package/src/chat/index.js +1011 -1392
- 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 +132 -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 +1162 -96
- 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 +1580 -0
- package/src/config.js +56 -3
- package/src/context/decisions.js +324 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +55 -0
- 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 +998 -170
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +630 -48
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +306 -0
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +31 -1
- package/src/daemon/status.js +48 -8
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +318 -0
- 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/skills/index.js +159 -0
- package/src/status/index.js +285 -0
- 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/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +107 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +76 -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/banner.sh +0 -89
- package/scripts/bus-alert.sh +0 -6
- package/scripts/bus-autotrigger.sh +0 -6
- package/scripts/bus-daemon.sh +0 -231
- package/scripts/bus-inject.sh +0 -144
- package/scripts/bus-listen.sh +0 -6
- package/scripts/bus.sh +0 -984
- package/scripts/context-decisions.sh +0 -167
- package/scripts/context-doctor.sh +0 -72
- package/scripts/context-lint.sh +0 -110
- package/scripts/doctor.sh +0 -22
- package/scripts/init.sh +0 -247
- package/scripts/skills.sh +0 -113
- package/scripts/status.sh +0 -125
package/src/daemon/index.js
CHANGED
|
@@ -1,25 +1,60 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
4
|
const { runUfooAgent } = require("../agent/ufooAgent");
|
|
5
|
-
const {
|
|
5
|
+
const { launchAgent, closeAgent, getRecoverableAgents, resumeAgents } = require("./ops");
|
|
6
6
|
const { buildStatus } = require("./status");
|
|
7
|
-
const
|
|
7
|
+
const EventBus = require("../bus");
|
|
8
|
+
const { AgentProcessManager } = require("./agentProcessManager");
|
|
9
|
+
const NicknameManager = require("../bus/nickname");
|
|
10
|
+
const { generateInstanceId, subscriberToSafeName } = require("../bus/utils");
|
|
11
|
+
const { createDaemonIpcServer } = require("./ipcServer");
|
|
12
|
+
const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../shared/eventContract");
|
|
13
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
14
|
+
const { scheduleProviderSessionProbe, loadProviderSessionCache } = require("./providerSessions");
|
|
15
|
+
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
16
|
+
const { createDaemonCronController } = require("./cronOps");
|
|
17
|
+
const { runAssistantTask } = require("../assistant/bridge");
|
|
18
|
+
const { runPromptWithAssistant } = require("./promptLoop");
|
|
19
|
+
const { handlePromptRequest } = require("./promptRequest");
|
|
20
|
+
const { recordAgentReport } = require("./reporting");
|
|
21
|
+
|
|
22
|
+
let providerSessions = null;
|
|
23
|
+
let probeHandles = new Map();
|
|
24
|
+
let daemonCronController = null;
|
|
8
25
|
|
|
9
26
|
function sleep(ms) {
|
|
10
27
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
28
|
}
|
|
12
29
|
|
|
30
|
+
function normalizeBusAgentType(agentType = "") {
|
|
31
|
+
const value = String(agentType || "").trim().toLowerCase();
|
|
32
|
+
if (!value) return "claude-code";
|
|
33
|
+
if (value === "codex") return "codex";
|
|
34
|
+
if (value === "claude" || value === "claude-code") return "claude-code";
|
|
35
|
+
if (value === "ufoo" || value === "ucode" || value === "ufoo-code") return "ufoo-code";
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeLaunchAgent(agent = "") {
|
|
40
|
+
const value = String(agent || "").trim().toLowerCase();
|
|
41
|
+
if (value === "codex") return "codex";
|
|
42
|
+
if (value === "claude" || value === "claude-code") return "claude";
|
|
43
|
+
if (value === "ufoo" || value === "ucode" || value === "ufoo-code") return "ufoo";
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
|
|
13
47
|
async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso) {
|
|
14
48
|
if (!nickname) return null;
|
|
15
|
-
const busPath =
|
|
16
|
-
const
|
|
17
|
-
const targetType = agentType === "codex" ? "codex" : "claude-code";
|
|
49
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
50
|
+
const targetType = normalizeBusAgentType(agentType);
|
|
18
51
|
const deadline = Date.now() + 10000;
|
|
52
|
+
const eventBus = new EventBus(projectRoot);
|
|
53
|
+
let lastError = null;
|
|
19
54
|
while (Date.now() < deadline) {
|
|
20
55
|
try {
|
|
21
56
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
22
|
-
let entries = Object.entries(bus.
|
|
57
|
+
let entries = Object.entries(bus.agents || {})
|
|
23
58
|
.filter(([, meta]) => meta && meta.agent_type === targetType && meta.status === "active");
|
|
24
59
|
if (startIso) {
|
|
25
60
|
entries = entries.filter(([, meta]) => (meta.joined_at || "") >= startIso);
|
|
@@ -32,16 +67,15 @@ async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso) {
|
|
|
32
67
|
if (candidates.length === 0) candidates = entries;
|
|
33
68
|
candidates.sort((a, b) => (a[1].joined_at || "").localeCompare(b[1].joined_at || ""));
|
|
34
69
|
const [agentId] = candidates[candidates.length - 1];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} catch {
|
|
70
|
+
await eventBus.rename(agentId, nickname, "ufoo-agent");
|
|
71
|
+
return { ok: true, agent_id: agentId, nickname };
|
|
72
|
+
} catch (err) {
|
|
73
|
+
lastError = err && err.message ? err.message : String(err || "rename failed");
|
|
40
74
|
// ignore and retry
|
|
41
75
|
}
|
|
42
76
|
await sleep(200);
|
|
43
77
|
}
|
|
44
|
-
return { ok: false, nickname, error: "rename timeout" };
|
|
78
|
+
return { ok: false, nickname, error: lastError || "rename timeout" };
|
|
45
79
|
}
|
|
46
80
|
|
|
47
81
|
function ensureDir(dir) {
|
|
@@ -49,15 +83,15 @@ function ensureDir(dir) {
|
|
|
49
83
|
}
|
|
50
84
|
|
|
51
85
|
function socketPath(projectRoot) {
|
|
52
|
-
return
|
|
86
|
+
return getUfooPaths(projectRoot).ufooSock;
|
|
53
87
|
}
|
|
54
88
|
|
|
55
89
|
function pidPath(projectRoot) {
|
|
56
|
-
return
|
|
90
|
+
return getUfooPaths(projectRoot).ufooDaemonPid;
|
|
57
91
|
}
|
|
58
92
|
|
|
59
93
|
function logPath(projectRoot) {
|
|
60
|
-
return
|
|
94
|
+
return getUfooPaths(projectRoot).ufooDaemonLog;
|
|
61
95
|
}
|
|
62
96
|
|
|
63
97
|
function writePid(projectRoot) {
|
|
@@ -72,21 +106,84 @@ function readPid(projectRoot) {
|
|
|
72
106
|
}
|
|
73
107
|
}
|
|
74
108
|
|
|
109
|
+
function checkPid(pid) {
|
|
110
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
111
|
+
return { alive: false, uncertain: false };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
process.kill(pid, 0);
|
|
115
|
+
return { alive: true, uncertain: false };
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (err && err.code === "EPERM") {
|
|
118
|
+
return { alive: true, uncertain: true };
|
|
119
|
+
}
|
|
120
|
+
return { alive: false, uncertain: false };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readProcessArgs(pid) {
|
|
125
|
+
if (!Number.isFinite(pid) || pid <= 0) return "";
|
|
126
|
+
try {
|
|
127
|
+
const res = spawnSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
128
|
+
encoding: "utf8",
|
|
129
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
130
|
+
});
|
|
131
|
+
if (res && res.error) {
|
|
132
|
+
if (res.error.code === "EPERM") return "__EPERM__";
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
if (res && res.status === 0) {
|
|
136
|
+
return String(res.stdout || "").trim();
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// ignore
|
|
140
|
+
}
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isLikelyDaemonProcess(pid) {
|
|
145
|
+
const args = readProcessArgs(pid);
|
|
146
|
+
if (!args || args === "__EPERM__") return null;
|
|
147
|
+
const text = args.toLowerCase();
|
|
148
|
+
const hasCliPattern = /\bufoo\s+daemon\s+(--start|start)\b/.test(text);
|
|
149
|
+
const hasNodePattern = /\bufoo\.js\s+daemon\s+(--start|start)\b/.test(text);
|
|
150
|
+
if (hasCliPattern || hasNodePattern) return true;
|
|
151
|
+
if (text.includes("/src/daemon/run.js")) return true;
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function looksLikeRunningDaemon(projectRoot, pid) {
|
|
156
|
+
const state = checkPid(pid);
|
|
157
|
+
if (!state.alive) return false;
|
|
158
|
+
const sock = socketPath(projectRoot);
|
|
159
|
+
if (!fs.existsSync(sock)) return false;
|
|
160
|
+
try {
|
|
161
|
+
const stat = fs.statSync(sock);
|
|
162
|
+
if (!stat.isSocket()) return false;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const procMatch = isLikelyDaemonProcess(pid);
|
|
167
|
+
if (procMatch === true) return true;
|
|
168
|
+
if (procMatch === false) return false;
|
|
169
|
+
if (!state.uncertain) return true;
|
|
170
|
+
const recordedPid = readPid(projectRoot);
|
|
171
|
+
return recordedPid === pid && fs.existsSync(sock);
|
|
172
|
+
}
|
|
173
|
+
|
|
75
174
|
function isRunning(projectRoot) {
|
|
76
175
|
const pid = readPid(projectRoot);
|
|
77
176
|
if (!pid) return false;
|
|
78
|
-
|
|
79
|
-
process.kill(pid, 0);
|
|
177
|
+
if (looksLikeRunningDaemon(projectRoot, pid)) {
|
|
80
178
|
return true;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
fs.unlinkSync(pidPath(projectRoot));
|
|
81
182
|
} catch {
|
|
82
|
-
|
|
83
|
-
fs.unlinkSync(pidPath(projectRoot));
|
|
84
|
-
} catch {
|
|
85
|
-
// ignore
|
|
86
|
-
}
|
|
87
|
-
removeSocket(projectRoot);
|
|
88
|
-
return false;
|
|
183
|
+
// ignore
|
|
89
184
|
}
|
|
185
|
+
removeSocket(projectRoot);
|
|
186
|
+
return false;
|
|
90
187
|
}
|
|
91
188
|
|
|
92
189
|
function removeSocket(projectRoot) {
|
|
@@ -108,7 +205,7 @@ function parseJsonLines(buffer) {
|
|
|
108
205
|
}
|
|
109
206
|
|
|
110
207
|
function readBus(projectRoot) {
|
|
111
|
-
const busPath =
|
|
208
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
112
209
|
try {
|
|
113
210
|
return JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
114
211
|
} catch {
|
|
@@ -119,7 +216,7 @@ function readBus(projectRoot) {
|
|
|
119
216
|
function listSubscribers(projectRoot, agentType) {
|
|
120
217
|
const bus = readBus(projectRoot);
|
|
121
218
|
if (!bus) return [];
|
|
122
|
-
return Object.entries(bus.
|
|
219
|
+
return Object.entries(bus.agents || {})
|
|
123
220
|
.filter(([, meta]) => meta && meta.agent_type === agentType)
|
|
124
221
|
.map(([id]) => id);
|
|
125
222
|
}
|
|
@@ -136,18 +233,12 @@ async function waitForNewSubscriber(projectRoot, agentType, existing, timeoutMs
|
|
|
136
233
|
return null;
|
|
137
234
|
}
|
|
138
235
|
|
|
139
|
-
function renameSubscriber(projectRoot, subscriberId, nickname) {
|
|
140
|
-
const script = path.join(projectRoot, "scripts", "bus.sh");
|
|
141
|
-
const res = spawnSync("bash", [script, "rename", subscriberId, nickname], { cwd: projectRoot });
|
|
142
|
-
return res.status === 0;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
236
|
function checkAndCleanupNickname(projectRoot, nickname) {
|
|
146
237
|
if (!nickname) return { existing: null, cleaned: false };
|
|
147
|
-
const busPath =
|
|
238
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
148
239
|
try {
|
|
149
240
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
150
|
-
const entries = Object.entries(bus.
|
|
241
|
+
const entries = Object.entries(bus.agents || {})
|
|
151
242
|
.filter(([, meta]) => meta && meta.nickname === nickname);
|
|
152
243
|
|
|
153
244
|
if (entries.length === 0) {
|
|
@@ -162,7 +253,7 @@ function checkAndCleanupNickname(projectRoot, nickname) {
|
|
|
162
253
|
|
|
163
254
|
// Clean up offline agents with same nickname
|
|
164
255
|
for (const [agentId] of entries) {
|
|
165
|
-
delete bus.
|
|
256
|
+
delete bus.agents[agentId];
|
|
166
257
|
}
|
|
167
258
|
fs.writeFileSync(busPath, JSON.stringify(bus, null, 2));
|
|
168
259
|
return { existing: null, cleaned: true };
|
|
@@ -171,18 +262,38 @@ function checkAndCleanupNickname(projectRoot, nickname) {
|
|
|
171
262
|
}
|
|
172
263
|
}
|
|
173
264
|
|
|
174
|
-
|
|
265
|
+
function resolveSubscriberNickname(projectRoot, subscriberId) {
|
|
266
|
+
if (!subscriberId) return "";
|
|
267
|
+
try {
|
|
268
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
269
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
270
|
+
return bus.agents?.[subscriberId]?.nickname || "";
|
|
271
|
+
} catch {
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
175
277
|
const results = [];
|
|
176
278
|
for (const op of ops) {
|
|
177
|
-
if (op.action === "
|
|
279
|
+
if (op.action === "launch") {
|
|
178
280
|
const count = op.count || 1;
|
|
179
|
-
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
|
+
}
|
|
180
291
|
const nickname = op.nickname || "";
|
|
181
292
|
const startTime = new Date(Date.now() - 1000);
|
|
182
293
|
const startIso = startTime.toISOString();
|
|
183
294
|
if (nickname && count > 1) {
|
|
184
295
|
results.push({
|
|
185
|
-
action: "
|
|
296
|
+
action: "launch",
|
|
186
297
|
ok: false,
|
|
187
298
|
agent,
|
|
188
299
|
count,
|
|
@@ -196,7 +307,7 @@ async function handleOps(projectRoot, ops = []) {
|
|
|
196
307
|
if (existing) {
|
|
197
308
|
// Agent with this nickname already exists and is active
|
|
198
309
|
results.push({
|
|
199
|
-
action: "
|
|
310
|
+
action: "launch",
|
|
200
311
|
ok: true,
|
|
201
312
|
agent,
|
|
202
313
|
count,
|
|
@@ -208,8 +319,43 @@ async function handleOps(projectRoot, ops = []) {
|
|
|
208
319
|
continue;
|
|
209
320
|
}
|
|
210
321
|
// eslint-disable-next-line no-await-in-loop
|
|
211
|
-
await
|
|
212
|
-
|
|
322
|
+
const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager);
|
|
323
|
+
if (launchResult.mode === "internal" && launchResult.subscriberIds && launchResult.subscriberIds.length > 0) {
|
|
324
|
+
const probeAgentType = agent === "codex"
|
|
325
|
+
? "codex"
|
|
326
|
+
: (agent === "claude" ? "claude-code" : "");
|
|
327
|
+
for (const subscriberId of launchResult.subscriberIds) {
|
|
328
|
+
if (!probeAgentType) continue;
|
|
329
|
+
const resolvedNickname = resolveSubscriberNickname(projectRoot, subscriberId) || nickname;
|
|
330
|
+
const probeHandle = scheduleProviderSessionProbe({
|
|
331
|
+
projectRoot,
|
|
332
|
+
subscriberId,
|
|
333
|
+
agentType: probeAgentType,
|
|
334
|
+
nickname: resolvedNickname,
|
|
335
|
+
onResolved: (id, resolved) => {
|
|
336
|
+
if (providerSessions) {
|
|
337
|
+
providerSessions.set(id, {
|
|
338
|
+
sessionId: resolved.sessionId,
|
|
339
|
+
source: resolved.source || "",
|
|
340
|
+
updated_at: new Date().toISOString(),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
probeHandles.delete(id);
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
if (probeHandle) {
|
|
347
|
+
probeHandles.set(subscriberId, probeHandle);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
results.push({
|
|
352
|
+
action: "launch",
|
|
353
|
+
mode: launchResult.mode,
|
|
354
|
+
ok: true,
|
|
355
|
+
agent,
|
|
356
|
+
count,
|
|
357
|
+
nickname: nickname || undefined
|
|
358
|
+
});
|
|
213
359
|
if (nickname) {
|
|
214
360
|
// eslint-disable-next-line no-await-in-loop
|
|
215
361
|
const renameResult = await renameSpawnedAgent(projectRoot, agent, nickname, startIso);
|
|
@@ -218,78 +364,188 @@ async function handleOps(projectRoot, ops = []) {
|
|
|
218
364
|
}
|
|
219
365
|
}
|
|
220
366
|
} catch (err) {
|
|
221
|
-
results.push({ action: "
|
|
367
|
+
results.push({ action: "launch", ok: false, agent, count, error: err.message });
|
|
222
368
|
}
|
|
223
369
|
} else if (op.action === "close") {
|
|
224
370
|
const ok = await closeAgent(projectRoot, op.agent_id);
|
|
225
371
|
results.push({ action: "close", ok, agent_id: op.agent_id });
|
|
372
|
+
} else if (op.action === "rename") {
|
|
373
|
+
const agentId = op.agent_id || "";
|
|
374
|
+
const nickname = op.nickname || "";
|
|
375
|
+
if (!agentId || !nickname) {
|
|
376
|
+
results.push({
|
|
377
|
+
action: "rename",
|
|
378
|
+
ok: false,
|
|
379
|
+
agent_id: agentId,
|
|
380
|
+
nickname,
|
|
381
|
+
error: "rename requires agent_id and nickname",
|
|
382
|
+
});
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const eventBus = new EventBus(projectRoot);
|
|
387
|
+
eventBus.ensureBus();
|
|
388
|
+
eventBus.loadBusData();
|
|
389
|
+
let targetId = agentId;
|
|
390
|
+
if (!eventBus.busData?.agents?.[targetId]) {
|
|
391
|
+
const nicknameManager = new NicknameManager(eventBus.busData || { agents: {} });
|
|
392
|
+
const resolved = nicknameManager.resolveNickname(agentId);
|
|
393
|
+
if (resolved) targetId = resolved;
|
|
394
|
+
}
|
|
395
|
+
if (!eventBus.busData?.agents?.[targetId]) {
|
|
396
|
+
results.push({
|
|
397
|
+
action: "rename",
|
|
398
|
+
ok: false,
|
|
399
|
+
agent_id: agentId,
|
|
400
|
+
nickname,
|
|
401
|
+
error: `agent not found: ${agentId}`,
|
|
402
|
+
});
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const result = await eventBus.rename(targetId, nickname, "ufoo-agent");
|
|
406
|
+
results.push({
|
|
407
|
+
action: "rename",
|
|
408
|
+
ok: true,
|
|
409
|
+
agent_id: result.subscriber,
|
|
410
|
+
nickname: result.newNickname,
|
|
411
|
+
old_nickname: result.oldNickname,
|
|
412
|
+
});
|
|
413
|
+
} catch (err) {
|
|
414
|
+
results.push({
|
|
415
|
+
action: "rename",
|
|
416
|
+
ok: false,
|
|
417
|
+
agent_id: agentId,
|
|
418
|
+
nickname,
|
|
419
|
+
error: err && err.message ? err.message : String(err || "rename failed"),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
} else if (op.action === "cron") {
|
|
423
|
+
if (!daemonCronController) {
|
|
424
|
+
results.push({
|
|
425
|
+
action: "cron",
|
|
426
|
+
ok: false,
|
|
427
|
+
error: "cron controller unavailable",
|
|
428
|
+
});
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const result = daemonCronController.handleCronOp(op);
|
|
433
|
+
results.push(result);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
results.push({
|
|
436
|
+
action: "cron",
|
|
437
|
+
ok: false,
|
|
438
|
+
error: err && err.message ? err.message : String(err || "cron failed"),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
226
441
|
}
|
|
227
442
|
}
|
|
228
443
|
return results;
|
|
229
444
|
}
|
|
230
445
|
|
|
231
|
-
function dispatchMessages(projectRoot, dispatch = []
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const
|
|
446
|
+
async function dispatchMessages(projectRoot, dispatch = []) {
|
|
447
|
+
const eventBus = new EventBus(projectRoot);
|
|
448
|
+
// Always use "ufoo-agent" as the publisher for daemon messages
|
|
449
|
+
const defaultPublisher = "ufoo-agent";
|
|
235
450
|
for (const item of dispatch) {
|
|
236
451
|
if (!item || !item.target || !item.message) continue;
|
|
237
452
|
const pub = item.publisher || defaultPublisher;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
453
|
+
try {
|
|
454
|
+
if (item.target === "broadcast") {
|
|
455
|
+
await eventBus.broadcast(item.message, pub);
|
|
456
|
+
} else {
|
|
457
|
+
await eventBus.send(item.target, item.message, pub);
|
|
458
|
+
}
|
|
459
|
+
} catch {
|
|
460
|
+
// ignore dispatch failures
|
|
243
461
|
}
|
|
244
462
|
}
|
|
245
463
|
}
|
|
246
464
|
|
|
247
|
-
function startBusBridge(projectRoot, onEvent, onStatus) {
|
|
248
|
-
const script = path.join(projectRoot, "scripts", "bus.sh");
|
|
465
|
+
function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
249
466
|
const state = {
|
|
250
467
|
subscriber: null,
|
|
251
468
|
queueFile: null,
|
|
252
469
|
pending: new Set(),
|
|
253
470
|
};
|
|
471
|
+
const eventBus = new EventBus(projectRoot);
|
|
472
|
+
let joinInProgress = false;
|
|
254
473
|
|
|
255
|
-
function
|
|
256
|
-
if (
|
|
257
|
-
const debugFile = path.join(projectRoot, ".ufoo", "run", "bus-join-debug.txt");
|
|
474
|
+
function getAgentNickname(agentId) {
|
|
475
|
+
if (!agentId) return agentId;
|
|
258
476
|
try {
|
|
259
|
-
|
|
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;
|
|
477
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
478
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
479
|
+
const meta = bus.agents && bus.agents[agentId];
|
|
480
|
+
if (meta && meta.nickname) {
|
|
481
|
+
return meta.nickname;
|
|
273
482
|
}
|
|
274
|
-
|
|
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" });
|
|
483
|
+
} catch {
|
|
484
|
+
// Ignore errors, return original ID
|
|
280
485
|
}
|
|
486
|
+
return agentId;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function ensureSubscriber() {
|
|
490
|
+
if (state.subscriber || joinInProgress) return;
|
|
491
|
+
const debugFile = path.join(getUfooPaths(projectRoot).runDir, "bus-join-debug.txt");
|
|
492
|
+
joinInProgress = true;
|
|
493
|
+
(async () => {
|
|
494
|
+
try {
|
|
495
|
+
fs.writeFileSync(debugFile, `Attempting join at ${new Date().toISOString()}\n`, { flag: "a" });
|
|
496
|
+
// Determine agent type based on provider configuration
|
|
497
|
+
const agentType = provider === "codex-cli" ? "codex" : "claude-code";
|
|
498
|
+
// Use fixed ID "ufoo-agent" for daemon's bus identity with explicit nickname
|
|
499
|
+
const sub = await eventBus.join("ufoo-agent", agentType, "ufoo-agent");
|
|
500
|
+
if (!sub) {
|
|
501
|
+
fs.writeFileSync(debugFile, "Join returned empty subscriber\n", { flag: "a" });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
state.subscriber = sub;
|
|
505
|
+
const safe = subscriberToSafeName(sub);
|
|
506
|
+
state.queueFile = path.join(getUfooPaths(projectRoot).busQueuesDir, safe, "pending.jsonl");
|
|
507
|
+
fs.writeFileSync(debugFile, `Successfully joined as ${sub} (type: ${agentType})\n`, { flag: "a" });
|
|
508
|
+
} catch (err) {
|
|
509
|
+
fs.writeFileSync(debugFile, `Exception: ${err.message || err}\n`, { flag: "a" });
|
|
510
|
+
} finally {
|
|
511
|
+
joinInProgress = false;
|
|
512
|
+
}
|
|
513
|
+
})();
|
|
281
514
|
}
|
|
282
515
|
|
|
283
516
|
function poll() {
|
|
284
517
|
ensureSubscriber();
|
|
518
|
+
if (typeof shouldDrain === "function" && !shouldDrain()) return;
|
|
285
519
|
if (!state.queueFile) return;
|
|
286
520
|
if (!fs.existsSync(state.queueFile)) return;
|
|
287
|
-
let content;
|
|
521
|
+
let content = "";
|
|
522
|
+
let readOk = false;
|
|
523
|
+
const processingFile = `${state.queueFile}.processing.${process.pid}.${Date.now()}`;
|
|
288
524
|
try {
|
|
289
|
-
|
|
525
|
+
fs.renameSync(state.queueFile, processingFile);
|
|
526
|
+
content = fs.readFileSync(processingFile, "utf8");
|
|
527
|
+
readOk = true;
|
|
290
528
|
} catch {
|
|
529
|
+
try {
|
|
530
|
+
if (fs.existsSync(processingFile)) {
|
|
531
|
+
fs.renameSync(processingFile, state.queueFile);
|
|
532
|
+
}
|
|
533
|
+
} catch {
|
|
534
|
+
// ignore rollback errors
|
|
535
|
+
}
|
|
291
536
|
return;
|
|
537
|
+
} finally {
|
|
538
|
+
if (readOk) {
|
|
539
|
+
try {
|
|
540
|
+
if (fs.existsSync(processingFile)) {
|
|
541
|
+
fs.rmSync(processingFile, { force: true });
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
// ignore cleanup errors
|
|
545
|
+
}
|
|
546
|
+
}
|
|
292
547
|
}
|
|
548
|
+
|
|
293
549
|
const lines = content.split(/\r?\n/).filter(Boolean);
|
|
294
550
|
if (!lines.length) return;
|
|
295
551
|
for (const line of lines) {
|
|
@@ -306,21 +562,17 @@ function startBusBridge(projectRoot, onEvent, onStatus) {
|
|
|
306
562
|
publisher: evt.publisher,
|
|
307
563
|
target: evt.target,
|
|
308
564
|
message: evt.data?.message || "",
|
|
309
|
-
ts: evt.ts,
|
|
565
|
+
ts: evt.timestamp || evt.ts,
|
|
310
566
|
});
|
|
311
567
|
}
|
|
312
568
|
if (evt.publisher && state.pending.has(evt.publisher)) {
|
|
313
569
|
state.pending.delete(evt.publisher);
|
|
314
570
|
if (onStatus) {
|
|
315
|
-
|
|
571
|
+
const displayName = getAgentNickname(evt.publisher);
|
|
572
|
+
onStatus({ phase: BUS_STATUS_PHASES.DONE, text: `${displayName} done`, key: evt.publisher });
|
|
316
573
|
}
|
|
317
574
|
}
|
|
318
575
|
}
|
|
319
|
-
try {
|
|
320
|
-
fs.truncateSync(state.queueFile, 0);
|
|
321
|
-
} catch {
|
|
322
|
-
// ignore
|
|
323
|
-
}
|
|
324
576
|
}
|
|
325
577
|
|
|
326
578
|
const interval = setInterval(poll, 1000);
|
|
@@ -329,13 +581,14 @@ function startBusBridge(projectRoot, onEvent, onStatus) {
|
|
|
329
581
|
if (!target) return;
|
|
330
582
|
state.pending.add(target);
|
|
331
583
|
if (onStatus) {
|
|
332
|
-
|
|
584
|
+
const displayName = getAgentNickname(target);
|
|
585
|
+
onStatus({ phase: BUS_STATUS_PHASES.START, text: `${displayName} processing`, key: target });
|
|
333
586
|
}
|
|
334
587
|
},
|
|
335
588
|
getSubscriber() {
|
|
336
589
|
ensureSubscriber();
|
|
337
590
|
try {
|
|
338
|
-
fs.writeFileSync(path.join(projectRoot
|
|
591
|
+
fs.writeFileSync(path.join(getUfooPaths(projectRoot).runDir, "bridge-debug.txt"),
|
|
339
592
|
`subscriber: ${state.subscriber || "NULL"}\nqueue: ${state.queueFile || "NULL"}\n`);
|
|
340
593
|
} catch {}
|
|
341
594
|
return state.subscriber;
|
|
@@ -346,13 +599,64 @@ function startBusBridge(projectRoot, onEvent, onStatus) {
|
|
|
346
599
|
};
|
|
347
600
|
}
|
|
348
601
|
|
|
349
|
-
function startDaemon({ projectRoot, provider, model }) {
|
|
350
|
-
|
|
602
|
+
function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
603
|
+
const paths = getUfooPaths(projectRoot);
|
|
604
|
+
if (!fs.existsSync(paths.ufooDir)) {
|
|
351
605
|
throw new Error("Missing .ufoo. Run: ufoo init");
|
|
352
606
|
}
|
|
353
607
|
|
|
354
|
-
const runDir =
|
|
608
|
+
const runDir = paths.runDir;
|
|
355
609
|
ensureDir(runDir);
|
|
610
|
+
|
|
611
|
+
// 文件锁机制:防止多个 daemon 同时启动
|
|
612
|
+
const lockFile = path.join(runDir, "daemon.lock");
|
|
613
|
+
let lockFd;
|
|
614
|
+
let recoveredStaleLock = false;
|
|
615
|
+
try {
|
|
616
|
+
// 尝试独占方式打开锁文件(如果已存在且被锁定则失败)
|
|
617
|
+
lockFd = fs.openSync(lockFile, "wx");
|
|
618
|
+
fs.writeSync(lockFd, `${process.pid}\n`);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
if (err.code === "EEXIST") {
|
|
621
|
+
// 锁文件已存在,检查是否仍有效
|
|
622
|
+
let existingPid = null;
|
|
623
|
+
try {
|
|
624
|
+
const raw = fs.readFileSync(lockFile, "utf8").trim();
|
|
625
|
+
const parsed = parseInt(raw, 10);
|
|
626
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
627
|
+
existingPid = parsed;
|
|
628
|
+
}
|
|
629
|
+
} catch {
|
|
630
|
+
// ignore malformed lock file and treat as stale
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let lockHeld = false;
|
|
634
|
+
if (existingPid) {
|
|
635
|
+
lockHeld = looksLikeRunningDaemon(projectRoot, existingPid);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (lockHeld) {
|
|
639
|
+
throw new Error(`Daemon already running with PID ${existingPid}`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 进程已死或锁文件损坏,清理旧锁后重试
|
|
643
|
+
try {
|
|
644
|
+
fs.unlinkSync(lockFile);
|
|
645
|
+
recoveredStaleLock = true;
|
|
646
|
+
} catch (unlinkErr) {
|
|
647
|
+
throw new Error(`Failed to remove stale daemon lock: ${unlinkErr.message}`);
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
lockFd = fs.openSync(lockFile, "wx");
|
|
651
|
+
fs.writeSync(lockFd, `${process.pid}\n`);
|
|
652
|
+
} catch (retryErr) {
|
|
653
|
+
throw new Error(`Failed to acquire daemon lock: ${retryErr.message}`);
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
throw err;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
356
660
|
removeSocket(projectRoot);
|
|
357
661
|
writePid(projectRoot);
|
|
358
662
|
|
|
@@ -361,101 +665,614 @@ function startDaemon({ projectRoot, provider, model }) {
|
|
|
361
665
|
logFile.write(`[daemon] ${new Date().toISOString()} ${msg}\n`);
|
|
362
666
|
};
|
|
363
667
|
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
668
|
+
// 创建进程管理器 - daemon 作为父进程监控所有 internal agents
|
|
669
|
+
const processManager = new AgentProcessManager(projectRoot);
|
|
670
|
+
log(`Process manager initialized`);
|
|
671
|
+
|
|
672
|
+
// Provider session cache (in-memory)
|
|
673
|
+
providerSessions = loadProviderSessionCache(projectRoot);
|
|
674
|
+
probeHandles = new Map();
|
|
675
|
+
daemonCronController = createDaemonCronController({
|
|
676
|
+
dispatch: async ({ taskId, target, message }) => {
|
|
677
|
+
await dispatchMessages(projectRoot, [{ target, message }]);
|
|
678
|
+
log(`cron:${taskId} -> ${target}`);
|
|
679
|
+
},
|
|
680
|
+
log,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const buildRuntimeStatus = () =>
|
|
684
|
+
buildStatus(projectRoot, {
|
|
685
|
+
cronTasks: daemonCronController ? daemonCronController.listTasks() : [],
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const cleanupInactiveSubscribers = () => {
|
|
689
|
+
try {
|
|
690
|
+
const syncBus = new EventBus(projectRoot);
|
|
691
|
+
syncBus.ensureBus();
|
|
692
|
+
syncBus.loadBusData();
|
|
693
|
+
syncBus.subscriberManager.cleanupInactive();
|
|
694
|
+
syncBus.saveBusData();
|
|
695
|
+
} catch {
|
|
696
|
+
// ignore cleanup errors
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
let handleIpcRequest = async () => {};
|
|
701
|
+
const ipcServer = createDaemonIpcServer({
|
|
702
|
+
projectRoot,
|
|
703
|
+
parseJsonLines,
|
|
704
|
+
handleRequest: async (req, socket) => handleIpcRequest(req, socket),
|
|
705
|
+
buildStatus: () => buildRuntimeStatus(),
|
|
706
|
+
cleanupInactive: cleanupInactiveSubscribers,
|
|
707
|
+
log,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
const busBridge = startBusBridge(projectRoot, provider, (evt) => {
|
|
711
|
+
ipcServer.sendToSockets({ type: IPC_RESPONSE_TYPES.BUS, data: evt });
|
|
712
|
+
}, (status) => {
|
|
713
|
+
ipcServer.sendToSockets({ type: IPC_RESPONSE_TYPES.STATUS, data: status });
|
|
714
|
+
}, () => ipcServer.hasClients());
|
|
715
|
+
|
|
716
|
+
handleIpcRequest = async (req, socket) => {
|
|
717
|
+
if (!req || typeof req !== "object") return;
|
|
718
|
+
if (req.type === IPC_REQUEST_TYPES.STATUS) {
|
|
719
|
+
cleanupInactiveSubscribers();
|
|
720
|
+
const status = buildRuntimeStatus();
|
|
721
|
+
socket.write(`${JSON.stringify({ type: IPC_RESPONSE_TYPES.STATUS, data: status })}
|
|
722
|
+
`);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (req.type === IPC_REQUEST_TYPES.PROMPT) {
|
|
726
|
+
await handlePromptRequest({
|
|
727
|
+
projectRoot,
|
|
728
|
+
req,
|
|
729
|
+
socket,
|
|
730
|
+
provider,
|
|
731
|
+
model,
|
|
732
|
+
processManager,
|
|
733
|
+
runPromptWithAssistant,
|
|
734
|
+
runUfooAgent,
|
|
735
|
+
runAssistantTask,
|
|
736
|
+
dispatchMessages,
|
|
737
|
+
handleOps,
|
|
738
|
+
markPending: (target) => busBridge.markPending(target),
|
|
739
|
+
reportTaskStatus: async (report) => {
|
|
740
|
+
await recordAgentReport({
|
|
741
|
+
projectRoot,
|
|
742
|
+
report,
|
|
743
|
+
onStatus: (status) => {
|
|
744
|
+
ipcServer.sendToSockets({
|
|
745
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
746
|
+
data: status,
|
|
747
|
+
});
|
|
748
|
+
},
|
|
749
|
+
log,
|
|
750
|
+
});
|
|
751
|
+
},
|
|
752
|
+
log,
|
|
753
|
+
});
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (req.type === IPC_REQUEST_TYPES.AGENT_REPORT) {
|
|
369
757
|
try {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
758
|
+
const report = req.report && typeof req.report === "object" ? req.report : {};
|
|
759
|
+
const { entry } = await recordAgentReport({
|
|
760
|
+
projectRoot,
|
|
761
|
+
report: {
|
|
762
|
+
...report,
|
|
763
|
+
source: report.source || "cli",
|
|
764
|
+
},
|
|
765
|
+
onStatus: (status) => {
|
|
766
|
+
ipcServer.sendToSockets({
|
|
767
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
768
|
+
data: status,
|
|
769
|
+
});
|
|
770
|
+
},
|
|
771
|
+
log,
|
|
772
|
+
});
|
|
773
|
+
socket.write(
|
|
774
|
+
`${JSON.stringify({
|
|
775
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
776
|
+
data: {
|
|
777
|
+
reply: `Report received (${entry.phase})`,
|
|
778
|
+
report: entry,
|
|
779
|
+
},
|
|
780
|
+
})}
|
|
781
|
+
`,
|
|
782
|
+
);
|
|
783
|
+
ipcServer.sendToSockets({
|
|
784
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
785
|
+
data: buildRuntimeStatus(),
|
|
786
|
+
});
|
|
787
|
+
} catch (err) {
|
|
788
|
+
socket.write(
|
|
789
|
+
`${JSON.stringify({
|
|
790
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
791
|
+
error: err.message || "agent_report failed",
|
|
792
|
+
})}
|
|
793
|
+
`,
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (req.type === IPC_REQUEST_TYPES.BUS_SEND) {
|
|
799
|
+
// Direct bus send request from chat UI
|
|
800
|
+
const { target, message } = req;
|
|
801
|
+
if (!target || !message) {
|
|
802
|
+
socket.write(
|
|
803
|
+
`${JSON.stringify({
|
|
804
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
805
|
+
error: "bus_send requires target and message",
|
|
806
|
+
})}
|
|
807
|
+
`,
|
|
808
|
+
);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
const publisher = busBridge.getSubscriber() || "ufoo-agent";
|
|
813
|
+
const eventBus = new EventBus(projectRoot);
|
|
814
|
+
await eventBus.send(target, message, publisher);
|
|
815
|
+
busBridge.markPending(target);
|
|
816
|
+
log(`bus_send target=${target} publisher=${publisher}`);
|
|
817
|
+
socket.write(
|
|
818
|
+
`${JSON.stringify({
|
|
819
|
+
type: IPC_RESPONSE_TYPES.BUS_SEND_OK,
|
|
820
|
+
})}
|
|
821
|
+
`,
|
|
822
|
+
);
|
|
823
|
+
} catch (err) {
|
|
824
|
+
log(`bus_send failed: ${err.message}`);
|
|
825
|
+
socket.write(
|
|
826
|
+
`${JSON.stringify({
|
|
827
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
828
|
+
error: err.message || "bus_send failed",
|
|
829
|
+
})}
|
|
830
|
+
`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (req.type === IPC_REQUEST_TYPES.CLOSE_AGENT) {
|
|
836
|
+
const { agent_id } = req;
|
|
837
|
+
if (!agent_id) {
|
|
838
|
+
socket.write(
|
|
839
|
+
`${JSON.stringify({
|
|
840
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
841
|
+
error: "close_agent requires agent_id",
|
|
842
|
+
})}
|
|
843
|
+
`,
|
|
844
|
+
);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
const op = { action: "close", agent_id };
|
|
849
|
+
const opsResults = await handleOps(projectRoot, [op], processManager);
|
|
850
|
+
const closeResult = opsResults.find((r) => r.action === "close");
|
|
851
|
+
const ok = closeResult ? closeResult.ok !== false : true;
|
|
852
|
+
const reply = ok
|
|
853
|
+
? `Closed ${agent_id}`
|
|
854
|
+
: `Close failed: ${closeResult?.error || "unknown error"}`;
|
|
855
|
+
socket.write(
|
|
856
|
+
`${JSON.stringify({
|
|
857
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
858
|
+
data: { reply, dispatch: [], ops: [op] },
|
|
859
|
+
opsResults,
|
|
860
|
+
})}
|
|
861
|
+
`,
|
|
862
|
+
);
|
|
863
|
+
cleanupInactiveSubscribers();
|
|
864
|
+
ipcServer.sendToSockets({
|
|
865
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
866
|
+
data: buildRuntimeStatus(),
|
|
867
|
+
});
|
|
868
|
+
} catch (err) {
|
|
869
|
+
socket.write(
|
|
870
|
+
`${JSON.stringify({
|
|
871
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
872
|
+
error: err.message || "close_agent failed",
|
|
873
|
+
})}
|
|
874
|
+
`,
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (req.type === IPC_REQUEST_TYPES.LAUNCH_AGENT) {
|
|
880
|
+
const { agent, count, nickname } = req;
|
|
881
|
+
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
882
|
+
if (!normalizedAgent) {
|
|
883
|
+
socket.write(
|
|
884
|
+
`${JSON.stringify({
|
|
885
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
886
|
+
error: "launch_agent requires agent=codex|claude|ucode",
|
|
887
|
+
})}
|
|
888
|
+
`,
|
|
889
|
+
);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const parsedCount = parseInt(count, 10);
|
|
893
|
+
const finalCount = Number.isFinite(parsedCount) && parsedCount > 0 ? parsedCount : 1;
|
|
894
|
+
const op = {
|
|
895
|
+
action: "launch",
|
|
896
|
+
agent: normalizedAgent,
|
|
897
|
+
count: finalCount,
|
|
898
|
+
nickname: nickname || "",
|
|
899
|
+
};
|
|
900
|
+
try {
|
|
901
|
+
const opsResults = await handleOps(projectRoot, [op], processManager);
|
|
902
|
+
const launchResult = opsResults.find((r) => r.action === "launch");
|
|
903
|
+
const ok = launchResult ? launchResult.ok !== false : true;
|
|
904
|
+
const reply = ok
|
|
905
|
+
? `Launched ${op.count} ${agent} agent(s)`
|
|
906
|
+
: `Launch failed: ${launchResult?.error || "unknown error"}`;
|
|
907
|
+
socket.write(
|
|
908
|
+
`${JSON.stringify({
|
|
909
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
910
|
+
data: {
|
|
911
|
+
reply,
|
|
912
|
+
dispatch: [],
|
|
913
|
+
ops: [op],
|
|
914
|
+
},
|
|
915
|
+
opsResults,
|
|
916
|
+
})}
|
|
917
|
+
`,
|
|
918
|
+
);
|
|
919
|
+
cleanupInactiveSubscribers();
|
|
920
|
+
ipcServer.sendToSockets({
|
|
921
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
922
|
+
data: buildRuntimeStatus(),
|
|
923
|
+
});
|
|
924
|
+
} catch (err) {
|
|
925
|
+
socket.write(
|
|
926
|
+
`${JSON.stringify({
|
|
927
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
928
|
+
error: err.message || "launch_agent failed",
|
|
929
|
+
})}
|
|
930
|
+
`,
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (req.type === IPC_REQUEST_TYPES.RESUME_AGENTS) {
|
|
936
|
+
const target = req.target || "";
|
|
937
|
+
try {
|
|
938
|
+
const result = await resumeAgents(projectRoot, target, processManager);
|
|
939
|
+
const resumedCount = result.resumed.length;
|
|
940
|
+
const skippedCount = result.skipped.length;
|
|
941
|
+
const reply = resumedCount > 0
|
|
942
|
+
? `Resumed ${resumedCount} agent(s)` + (skippedCount ? `, skipped ${skippedCount}` : "")
|
|
943
|
+
: (skippedCount ? `No agents resumed (skipped ${skippedCount})` : "No agents resumed");
|
|
944
|
+
socket.write(
|
|
945
|
+
`${JSON.stringify({
|
|
946
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
947
|
+
data: {
|
|
948
|
+
reply,
|
|
949
|
+
resume: result,
|
|
950
|
+
},
|
|
951
|
+
})}
|
|
952
|
+
`,
|
|
953
|
+
);
|
|
954
|
+
} catch (err) {
|
|
955
|
+
socket.write(
|
|
956
|
+
`${JSON.stringify({
|
|
957
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
958
|
+
error: err.message || "resume_agents failed",
|
|
959
|
+
})}
|
|
960
|
+
`,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (req.type === IPC_REQUEST_TYPES.LIST_RECOVERABLE_AGENTS) {
|
|
966
|
+
const target = req.target || "";
|
|
967
|
+
try {
|
|
968
|
+
const result = getRecoverableAgents(projectRoot, target);
|
|
969
|
+
const count = result.recoverable.length;
|
|
970
|
+
const reply = count > 0 ? `Found ${count} recoverable agent(s)` : "No recoverable agents";
|
|
971
|
+
socket.write(
|
|
972
|
+
`${JSON.stringify({
|
|
973
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
974
|
+
data: {
|
|
975
|
+
reply,
|
|
976
|
+
recoverable: result,
|
|
977
|
+
},
|
|
978
|
+
})}
|
|
979
|
+
`,
|
|
980
|
+
);
|
|
981
|
+
} catch (err) {
|
|
982
|
+
socket.write(
|
|
983
|
+
`${JSON.stringify({
|
|
984
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
985
|
+
error: err.message || "list_recoverable_agents failed",
|
|
986
|
+
})}
|
|
987
|
+
`,
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (req.type === IPC_REQUEST_TYPES.REGISTER_AGENT) {
|
|
993
|
+
// Manual agent launch requests daemon to register it
|
|
994
|
+
const { agentType, nickname, parentPid, launchMode, tmuxPane, tty, skipProbe } = req;
|
|
995
|
+
if (!agentType) {
|
|
996
|
+
socket.write(
|
|
997
|
+
`${JSON.stringify({
|
|
998
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
999
|
+
error: "register_agent requires agentType",
|
|
1000
|
+
})}
|
|
1001
|
+
`,
|
|
1002
|
+
);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
const crypto = require("crypto");
|
|
1007
|
+
const requestedReuse = req.reuseSession && typeof req.reuseSession === "object"
|
|
1008
|
+
? req.reuseSession
|
|
1009
|
+
: null;
|
|
1010
|
+
const reuseSessionId = typeof requestedReuse?.sessionId === "string"
|
|
1011
|
+
? requestedReuse.sessionId.trim()
|
|
1012
|
+
: "";
|
|
1013
|
+
const reuseSubscriberId = typeof requestedReuse?.subscriberId === "string"
|
|
1014
|
+
? requestedReuse.subscriberId.trim()
|
|
1015
|
+
: "";
|
|
1016
|
+
const reuseProviderSessionId = typeof requestedReuse?.providerSessionId === "string"
|
|
1017
|
+
? requestedReuse.providerSessionId.trim()
|
|
1018
|
+
: "";
|
|
1019
|
+
|
|
1020
|
+
let sessionId = crypto.randomBytes(4).toString("hex");
|
|
1021
|
+
let subscriberId = `${agentType}:${sessionId}`;
|
|
1022
|
+
if (reuseSessionId && reuseSubscriberId === `${agentType}:${reuseSessionId}`) {
|
|
1023
|
+
sessionId = reuseSessionId;
|
|
1024
|
+
subscriberId = reuseSubscriberId;
|
|
1025
|
+
} else if (reuseSessionId || reuseSubscriberId) {
|
|
1026
|
+
log(`register_agent ignored invalid reuseSession for ${agentType}`);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Daemon registers the agent in bus
|
|
1030
|
+
const eventBus = new EventBus(projectRoot);
|
|
1031
|
+
await eventBus.init();
|
|
1032
|
+
eventBus.loadBusData();
|
|
1033
|
+
const parsedParentPid = Number.parseInt(parentPid, 10);
|
|
1034
|
+
if (!Number.isFinite(parsedParentPid) || parsedParentPid <= 0) {
|
|
1035
|
+
throw new Error("register_agent requires valid parentPid");
|
|
1036
|
+
}
|
|
1037
|
+
const joinOptions = {
|
|
1038
|
+
parentPid: Number.isFinite(parsedParentPid) ? parsedParentPid : undefined,
|
|
1039
|
+
launchMode: launchMode || "",
|
|
1040
|
+
tmuxPane: tmuxPane || "",
|
|
1041
|
+
tty: tty || "",
|
|
1042
|
+
reuseSessionId,
|
|
1043
|
+
reuseProviderSessionId,
|
|
1044
|
+
};
|
|
1045
|
+
if (skipProbe) joinOptions.skipProbe = true;
|
|
1046
|
+
|
|
1047
|
+
let finalNickname = nickname || "";
|
|
1048
|
+
if (finalNickname) {
|
|
1049
|
+
const nickCheck = checkAndCleanupNickname(projectRoot, finalNickname);
|
|
1050
|
+
if (nickCheck.existing) {
|
|
1051
|
+
finalNickname = "";
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
await eventBus.join(
|
|
1055
|
+
sessionId,
|
|
1056
|
+
normalizeBusAgentType(agentType),
|
|
1057
|
+
finalNickname,
|
|
1058
|
+
joinOptions,
|
|
1059
|
+
);
|
|
1060
|
+
if (finalNickname) {
|
|
1061
|
+
eventBus.rename(subscriberId, finalNickname, "ufoo-agent");
|
|
1062
|
+
}
|
|
1063
|
+
eventBus.saveBusData();
|
|
1064
|
+
const resolvedNickname = resolveSubscriberNickname(projectRoot, subscriberId) || finalNickname || "";
|
|
1065
|
+
|
|
1066
|
+
if (!skipProbe && reuseProviderSessionId) {
|
|
1067
|
+
if (providerSessions) {
|
|
1068
|
+
providerSessions.set(subscriberId, {
|
|
1069
|
+
sessionId: reuseProviderSessionId,
|
|
1070
|
+
source: "reuse",
|
|
1071
|
+
updated_at: new Date().toISOString(),
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (!skipProbe) {
|
|
1077
|
+
const probeHandle = scheduleProviderSessionProbe({
|
|
1078
|
+
projectRoot,
|
|
1079
|
+
subscriberId,
|
|
1080
|
+
agentType,
|
|
1081
|
+
nickname: resolvedNickname,
|
|
1082
|
+
onResolved: (id, resolved) => {
|
|
1083
|
+
if (providerSessions) {
|
|
1084
|
+
providerSessions.set(id, {
|
|
1085
|
+
sessionId: resolved.sessionId,
|
|
1086
|
+
source: resolved.source || "",
|
|
1087
|
+
updated_at: new Date().toISOString(),
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
probeHandles.delete(id);
|
|
1091
|
+
},
|
|
1092
|
+
});
|
|
1093
|
+
if (probeHandle) {
|
|
1094
|
+
probeHandles.set(subscriberId, probeHandle);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
socket.write(
|
|
1098
|
+
`${JSON.stringify({
|
|
1099
|
+
type: IPC_RESPONSE_TYPES.REGISTER_OK,
|
|
1100
|
+
subscriberId,
|
|
1101
|
+
nickname: resolvedNickname,
|
|
1102
|
+
})}
|
|
1103
|
+
`,
|
|
1104
|
+
);
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
log(`register_agent failed: ${err.message}`);
|
|
1107
|
+
socket.write(
|
|
1108
|
+
`${JSON.stringify({
|
|
1109
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1110
|
+
error: err.message || "register_agent failed",
|
|
1111
|
+
})}
|
|
1112
|
+
`,
|
|
1113
|
+
);
|
|
373
1114
|
}
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (req.type === IPC_REQUEST_TYPES.AGENT_READY) {
|
|
1118
|
+
const { subscriberId } = req;
|
|
1119
|
+
if (!subscriberId) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
log(`agent_ready id=${subscriberId} - triggering probe immediately`);
|
|
1123
|
+
const probeHandle = probeHandles.get(subscriberId);
|
|
1124
|
+
if (probeHandle && typeof probeHandle.triggerNow === "function") {
|
|
1125
|
+
probeHandle.triggerNow().catch((err) => {
|
|
1126
|
+
log(`agent_ready probe trigger failed for ${subscriberId}: ${err.message}`);
|
|
1127
|
+
});
|
|
1128
|
+
} else {
|
|
1129
|
+
log(`agent_ready no probe handle found for ${subscriberId}`);
|
|
1130
|
+
}
|
|
1131
|
+
return;
|
|
374
1132
|
}
|
|
375
1133
|
};
|
|
376
1134
|
|
|
377
|
-
|
|
378
|
-
sendToSockets({ type: "bus", data: evt });
|
|
379
|
-
}, (status) => {
|
|
380
|
-
sendToSockets({ type: "status", data: status });
|
|
381
|
-
});
|
|
1135
|
+
ipcServer.listen(socketPath(projectRoot));
|
|
382
1136
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
1137
|
+
log(`Started pid=${process.pid}`);
|
|
1138
|
+
|
|
1139
|
+
// 清理旧 daemon 留下的孤儿 internal agent 进程
|
|
1140
|
+
const EventBus = require("../bus");
|
|
1141
|
+
const { spawnSync } = require("child_process");
|
|
1142
|
+
const eventBus = new EventBus(projectRoot);
|
|
1143
|
+
try {
|
|
1144
|
+
eventBus.ensureBus();
|
|
1145
|
+
eventBus.loadBusData();
|
|
1146
|
+
const agents = eventBus.busData.agents || {};
|
|
1147
|
+
|
|
1148
|
+
// 查找所有 agent-runner 进程
|
|
1149
|
+
const psResult = spawnSync("ps", ["aux"], { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
|
|
1150
|
+
const lines = psResult.stdout ? psResult.stdout.split("\n") : [];
|
|
1151
|
+
const runnerProcesses = [];
|
|
1152
|
+
|
|
1153
|
+
for (const line of lines) {
|
|
1154
|
+
if (line.includes("agent-pty-runner") || line.includes("agent-runner")) {
|
|
1155
|
+
const parts = line.trim().split(/\s+/);
|
|
1156
|
+
if (parts.length >= 2) {
|
|
1157
|
+
const pid = parseInt(parts[1], 10);
|
|
1158
|
+
if (Number.isFinite(pid)) {
|
|
1159
|
+
runnerProcesses.push({ pid, line });
|
|
400
1160
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
for (const item of result.payload.dispatch || []) {
|
|
429
|
-
if (item && item.target && item.target !== "broadcast") {
|
|
430
|
-
busBridge.markPending(item.target);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// 检查每个 runner 的父进程
|
|
1166
|
+
for (const runner of runnerProcesses) {
|
|
1167
|
+
try {
|
|
1168
|
+
const ppidResult = spawnSync("ps", ["-p", String(runner.pid), "-o", "ppid="], { encoding: "utf8" });
|
|
1169
|
+
const ppid = parseInt(ppidResult.stdout.trim(), 10);
|
|
1170
|
+
|
|
1171
|
+
if (Number.isFinite(ppid)) {
|
|
1172
|
+
// 检查父进程是否存在
|
|
1173
|
+
try {
|
|
1174
|
+
process.kill(ppid, 0);
|
|
1175
|
+
// 父进程还活着,检查是否是 daemon
|
|
1176
|
+
const ppidCmd = spawnSync("ps", ["-p", String(ppid), "-o", "command="], { encoding: "utf8" });
|
|
1177
|
+
const cmd = ppidCmd.stdout.trim();
|
|
1178
|
+
|
|
1179
|
+
if (!cmd.includes("daemon start")) {
|
|
1180
|
+
// 父进程不是 daemon,这是孤儿进程
|
|
1181
|
+
log(`Found orphan agent-runner process ${runner.pid} (parent ${ppid} is not a daemon)`);
|
|
1182
|
+
try {
|
|
1183
|
+
process.kill(runner.pid, "SIGTERM");
|
|
1184
|
+
log(`Killed orphan agent-runner ${runner.pid}`);
|
|
1185
|
+
} catch {
|
|
1186
|
+
// ignore
|
|
431
1187
|
}
|
|
432
1188
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
log(`
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
);
|
|
1189
|
+
} catch {
|
|
1190
|
+
// 父进程已死,杀掉孤儿进程
|
|
1191
|
+
log(`Found orphan agent-runner process ${runner.pid} (parent ${ppid} is dead)`);
|
|
1192
|
+
try {
|
|
1193
|
+
process.kill(runner.pid, "SIGTERM");
|
|
1194
|
+
log(`Killed orphan agent-runner ${runner.pid}`);
|
|
1195
|
+
} catch {
|
|
1196
|
+
// ignore
|
|
1197
|
+
}
|
|
443
1198
|
}
|
|
444
1199
|
}
|
|
1200
|
+
} catch {
|
|
1201
|
+
// ignore
|
|
445
1202
|
}
|
|
446
|
-
}
|
|
447
|
-
});
|
|
1203
|
+
}
|
|
448
1204
|
|
|
449
|
-
|
|
450
|
-
|
|
1205
|
+
// 标记对应的 agents 为 inactive
|
|
1206
|
+
const adapterRouter = createTerminalAdapterRouter();
|
|
1207
|
+
for (const [subscriberId, meta] of Object.entries(agents)) {
|
|
1208
|
+
const launchMode = meta.launch_mode || "";
|
|
1209
|
+
const adapter = adapterRouter.getAdapter({ launchMode, agentId: subscriberId });
|
|
1210
|
+
if (launchMode && adapter.capabilities.supportsInternalQueueLoop) {
|
|
1211
|
+
if (meta.pid) {
|
|
1212
|
+
try {
|
|
1213
|
+
process.kill(meta.pid, 0);
|
|
1214
|
+
// 父 daemon 还活着,跳过
|
|
1215
|
+
} catch {
|
|
1216
|
+
// 父 daemon 已死,标记为 inactive
|
|
1217
|
+
// 注意:不更新 last_seen,保持原有时间戳,这样会自动超时
|
|
1218
|
+
meta.status = "inactive";
|
|
1219
|
+
log(`Marked orphan internal agent ${subscriberId} as inactive (parent daemon ${meta.pid} is dead)`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
eventBus.saveBusData();
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
log(`Failed to cleanup orphan agents: ${err.message}`);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const shouldResume = resumeMode === "force" || (resumeMode === "auto" && recoveredStaleLock);
|
|
1230
|
+
if (shouldResume) {
|
|
1231
|
+
const reason = resumeMode === "force" ? "forced by caller" : "stale daemon state detected";
|
|
1232
|
+
log(`Auto-recover enabled: ${reason}`);
|
|
1233
|
+
setTimeout(() => {
|
|
1234
|
+
resumeAgents(projectRoot, "", processManager).catch((err) => {
|
|
1235
|
+
log(`auto resume failed: ${err.message || String(err)}`);
|
|
1236
|
+
});
|
|
1237
|
+
}, 1500);
|
|
1238
|
+
}
|
|
451
1239
|
|
|
452
|
-
|
|
1240
|
+
const cleanup = () => {
|
|
1241
|
+
log(`Shutting down daemon (managed agents: ${processManager.count()})`);
|
|
1242
|
+
|
|
1243
|
+
if (daemonCronController) {
|
|
1244
|
+
daemonCronController.stopAll();
|
|
1245
|
+
daemonCronController = null;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// 清理所有子进程
|
|
1249
|
+
processManager.cleanup();
|
|
1250
|
+
|
|
1251
|
+
ipcServer.stop();
|
|
453
1252
|
busBridge.stop();
|
|
454
1253
|
removeSocket(projectRoot);
|
|
455
|
-
|
|
1254
|
+
|
|
1255
|
+
// 释放锁文件
|
|
1256
|
+
try {
|
|
1257
|
+
if (lockFd !== undefined) {
|
|
1258
|
+
fs.closeSync(lockFd);
|
|
1259
|
+
}
|
|
1260
|
+
const lockFile = path.join(getUfooPaths(projectRoot).runDir, "daemon.lock");
|
|
1261
|
+
if (fs.existsSync(lockFile)) {
|
|
1262
|
+
fs.unlinkSync(lockFile);
|
|
1263
|
+
}
|
|
1264
|
+
} catch {
|
|
1265
|
+
// ignore cleanup errors
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
process.on("exit", cleanup);
|
|
456
1270
|
process.on("SIGTERM", () => {
|
|
457
|
-
|
|
458
|
-
|
|
1271
|
+
cleanup();
|
|
1272
|
+
process.exit(0);
|
|
1273
|
+
});
|
|
1274
|
+
process.on("SIGINT", () => {
|
|
1275
|
+
cleanup();
|
|
459
1276
|
process.exit(0);
|
|
460
1277
|
});
|
|
461
1278
|
}
|
|
@@ -495,6 +1312,17 @@ function stopDaemon(projectRoot) {
|
|
|
495
1312
|
// ignore
|
|
496
1313
|
}
|
|
497
1314
|
removeSocket(projectRoot);
|
|
1315
|
+
|
|
1316
|
+
// 清理锁文件
|
|
1317
|
+
try {
|
|
1318
|
+
const lockFile = path.join(getUfooPaths(projectRoot).runDir, "daemon.lock");
|
|
1319
|
+
if (fs.existsSync(lockFile)) {
|
|
1320
|
+
fs.unlinkSync(lockFile);
|
|
1321
|
+
}
|
|
1322
|
+
} catch {
|
|
1323
|
+
// ignore
|
|
1324
|
+
}
|
|
1325
|
+
|
|
498
1326
|
return killed;
|
|
499
1327
|
}
|
|
500
1328
|
|