u-foo 2.3.30 → 2.3.32
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/package.json +5 -1
- package/scripts/chat-app-smoke.js +30 -0
- package/scripts/ink-demo.js +23 -0
- package/scripts/ink-smoke.js +30 -0
- package/scripts/ucode-app-smoke.js +36 -0
- package/src/chat/commandExecutor.js +6 -2
- package/src/chat/daemonMessageRouter.js +9 -1
- package/src/chat/daemonTransport.js +2 -1
- package/src/chat/dashboardKeyController.js +0 -40
- package/src/chat/dashboardView.js +0 -20
- package/src/chat/index.js +9 -1
- package/src/chat/inputSubmitHandler.js +34 -0
- package/src/chat/projectCloseController.js +1 -1
- package/src/chat/shellCommand.js +42 -0
- package/src/chat/transport.js +16 -3
- package/src/cli.js +4 -3
- package/src/code/agent.js +4 -0
- package/src/code/nativeRunner.js +74 -0
- package/src/code/taskDecomposer.js +5 -4
- package/src/code/tui.js +73 -561
- package/src/daemon/index.js +169 -27
- package/src/daemon/ipcServer.js +23 -1
- package/src/daemon/promptRequest.js +6 -1
- package/src/daemon/run.js +11 -4
- package/src/projects/runtimes.js +1 -1
- package/src/ufoo/agentRegistryDiagnostics.js +43 -0
- package/src/ui/MIGRATION.md +382 -0
- package/src/ui/components/ChatApp.js +2950 -0
- package/src/ui/components/DashboardBar.js +417 -0
- package/src/ui/components/InkDemo.js +96 -0
- package/src/ui/components/MultilineInput.js +387 -0
- package/src/ui/components/UcodeApp.js +813 -0
- package/src/ui/components/agentMirror.js +725 -0
- package/src/ui/components/chatReducer.js +337 -0
- package/src/ui/format/index.js +997 -0
- package/src/ui/index.js +9 -0
- package/src/ui/runInk.js +57 -0
- package/src/utils/nodeExecutable.js +26 -0
package/src/daemon/index.js
CHANGED
|
@@ -36,6 +36,7 @@ const {
|
|
|
36
36
|
resolveDisplayNickname,
|
|
37
37
|
resolveScopedNickname,
|
|
38
38
|
} = require("./nicknameScope");
|
|
39
|
+
const { resolveNodeExecutable } = require("../utils/nodeExecutable");
|
|
39
40
|
|
|
40
41
|
let providerSessions = null;
|
|
41
42
|
let probeHandles = new Map();
|
|
@@ -124,6 +125,15 @@ function logPath(projectRoot) {
|
|
|
124
125
|
return getUfooPaths(projectRoot).ufooDaemonLog;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
function appendControlLog(projectRoot, msg) {
|
|
129
|
+
try {
|
|
130
|
+
ensureDir(path.dirname(logPath(projectRoot)));
|
|
131
|
+
fs.appendFileSync(logPath(projectRoot), `[daemon-control] ${new Date().toISOString()} ${msg}\n`);
|
|
132
|
+
} catch {
|
|
133
|
+
// ignore control logging errors
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
127
137
|
function writePid(projectRoot) {
|
|
128
138
|
fs.writeFileSync(pidPath(projectRoot), String(process.pid));
|
|
129
139
|
}
|
|
@@ -151,6 +161,10 @@ function checkPid(pid) {
|
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
163
|
|
|
164
|
+
function pidAlive(pid) {
|
|
165
|
+
return checkPid(pid).alive;
|
|
166
|
+
}
|
|
167
|
+
|
|
154
168
|
function readProcessArgs(pid) {
|
|
155
169
|
if (!Number.isFinite(pid) || pid <= 0) return "";
|
|
156
170
|
try {
|
|
@@ -171,6 +185,32 @@ function readProcessArgs(pid) {
|
|
|
171
185
|
return "";
|
|
172
186
|
}
|
|
173
187
|
|
|
188
|
+
function readProcessCwd(pid) {
|
|
189
|
+
if (!Number.isFinite(pid) || pid <= 0) return "";
|
|
190
|
+
try {
|
|
191
|
+
const res = spawnSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
192
|
+
encoding: "utf8",
|
|
193
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
194
|
+
});
|
|
195
|
+
if (!res || res.status !== 0 || !res.stdout) return "";
|
|
196
|
+
for (const line of String(res.stdout || "").split(/\r?\n/)) {
|
|
197
|
+
if (line.startsWith("n")) return line.slice(1);
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// ignore
|
|
201
|
+
}
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function sameProjectRoot(a, b) {
|
|
206
|
+
if (!a || !b) return false;
|
|
207
|
+
try {
|
|
208
|
+
return fs.realpathSync(a) === fs.realpathSync(b);
|
|
209
|
+
} catch {
|
|
210
|
+
return path.resolve(a) === path.resolve(b);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
174
214
|
function isLikelyDaemonProcess(pid) {
|
|
175
215
|
const args = readProcessArgs(pid);
|
|
176
216
|
if (!args || args === "__EPERM__") return null;
|
|
@@ -182,6 +222,37 @@ function isLikelyDaemonProcess(pid) {
|
|
|
182
222
|
return false;
|
|
183
223
|
}
|
|
184
224
|
|
|
225
|
+
function isPidFileDaemonForProject(projectRoot, pid, socketOwnerPids = new Set()) {
|
|
226
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
227
|
+
if (socketOwnerPids.has(pid)) return true;
|
|
228
|
+
if (looksLikeRunningDaemon(projectRoot, pid)) return true;
|
|
229
|
+
if (isLikelyDaemonProcess(pid) !== true) return false;
|
|
230
|
+
const cwd = readProcessCwd(pid);
|
|
231
|
+
return sameProjectRoot(cwd, projectRoot);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function socketOwnerDaemonPids(projectRoot) {
|
|
235
|
+
const sock = socketPath(projectRoot);
|
|
236
|
+
const out = new Set();
|
|
237
|
+
try {
|
|
238
|
+
const res = spawnSync("lsof", ["-nP", "-U"], {
|
|
239
|
+
encoding: "utf8",
|
|
240
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
241
|
+
});
|
|
242
|
+
if (!res || res.status !== 0 || !res.stdout) return [];
|
|
243
|
+
for (const line of String(res.stdout || "").split(/\r?\n/)) {
|
|
244
|
+
if (!line.includes(sock)) continue;
|
|
245
|
+
const parts = line.trim().split(/\s+/);
|
|
246
|
+
const pid = parseInt(parts[1], 10);
|
|
247
|
+
if (!Number.isFinite(pid) || pid <= 0) continue;
|
|
248
|
+
if (isLikelyDaemonProcess(pid) === true) out.add(pid);
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
// ignore lsof failures; pid file fallback still applies
|
|
252
|
+
}
|
|
253
|
+
return Array.from(out);
|
|
254
|
+
}
|
|
255
|
+
|
|
185
256
|
function looksLikeRunningDaemon(projectRoot, pid) {
|
|
186
257
|
const state = checkPid(pid);
|
|
187
258
|
if (!state.alive) return false;
|
|
@@ -216,6 +287,16 @@ function cleanupStaleState(projectRoot) {
|
|
|
216
287
|
removeSocket(projectRoot);
|
|
217
288
|
}
|
|
218
289
|
|
|
290
|
+
function removePidIfOwned(projectRoot, expectedPid = process.pid) {
|
|
291
|
+
const pid = readPid(projectRoot);
|
|
292
|
+
if (pid !== expectedPid) return;
|
|
293
|
+
try {
|
|
294
|
+
fs.unlinkSync(pidPath(projectRoot));
|
|
295
|
+
} catch {
|
|
296
|
+
// ignore cleanup errors
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
219
300
|
function removeSocket(projectRoot) {
|
|
220
301
|
const sock = socketPath(projectRoot);
|
|
221
302
|
if (fs.existsSync(sock)) fs.unlinkSync(sock);
|
|
@@ -1093,8 +1174,26 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1093
1174
|
writePid(projectRoot);
|
|
1094
1175
|
|
|
1095
1176
|
const logFile = fs.createWriteStream(logPath(projectRoot), { flags: "a" });
|
|
1177
|
+
const formatLogLine = (msg) => `[daemon] ${new Date().toISOString()} ${msg}\n`;
|
|
1096
1178
|
const log = (msg) => {
|
|
1097
|
-
logFile.write(
|
|
1179
|
+
logFile.write(formatLogLine(msg));
|
|
1180
|
+
};
|
|
1181
|
+
const logSync = (msg) => {
|
|
1182
|
+
const line = formatLogLine(msg);
|
|
1183
|
+
try {
|
|
1184
|
+
fs.appendFileSync(logPath(projectRoot), line);
|
|
1185
|
+
} catch {
|
|
1186
|
+
try {
|
|
1187
|
+
logFile.write(line);
|
|
1188
|
+
} catch {
|
|
1189
|
+
// ignore fatal logging errors
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
const formatFatalReason = (err) => {
|
|
1194
|
+
if (!err) return "unknown";
|
|
1195
|
+
if (err instanceof Error) return err.stack || err.message;
|
|
1196
|
+
return String(err);
|
|
1098
1197
|
};
|
|
1099
1198
|
const publishProjectRuntime = (status = "running") => {
|
|
1100
1199
|
if (isGlobalControllerProjectRoot(projectRoot)) {
|
|
@@ -1226,12 +1325,13 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1226
1325
|
if (!isRunning(root)) {
|
|
1227
1326
|
cleanupStaleState(root);
|
|
1228
1327
|
const daemonBin = path.join(__dirname, "..", "..", "bin", "ufoo.js");
|
|
1229
|
-
const child = spawn(
|
|
1328
|
+
const child = spawn(resolveNodeExecutable(), [daemonBin, "daemon", "--start"], {
|
|
1230
1329
|
detached: true,
|
|
1231
1330
|
stdio: "ignore",
|
|
1232
1331
|
cwd: root,
|
|
1233
1332
|
env: process.env,
|
|
1234
1333
|
});
|
|
1334
|
+
child.on("error", () => {});
|
|
1235
1335
|
child.unref();
|
|
1236
1336
|
}
|
|
1237
1337
|
|
|
@@ -2462,10 +2562,11 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2462
2562
|
}
|
|
2463
2563
|
|
|
2464
2564
|
let cleanedUp = false;
|
|
2465
|
-
const cleanup = () => {
|
|
2565
|
+
const cleanup = (reason = "exit", options = {}) => {
|
|
2466
2566
|
if (cleanedUp) return;
|
|
2467
2567
|
cleanedUp = true;
|
|
2468
|
-
|
|
2568
|
+
const writeLog = options.sync ? logSync : log;
|
|
2569
|
+
writeLog(`Shutting down daemon reason=${reason} (managed agents: ${processManager.count()})`);
|
|
2469
2570
|
clearInterval(runtimeHeartbeat);
|
|
2470
2571
|
try {
|
|
2471
2572
|
if (!isGlobalControllerProjectRoot(projectRoot)) {
|
|
@@ -2487,6 +2588,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2487
2588
|
ipcServer.stop();
|
|
2488
2589
|
busBridge.stop();
|
|
2489
2590
|
removeSocket(projectRoot);
|
|
2591
|
+
removePidIfOwned(projectRoot);
|
|
2490
2592
|
|
|
2491
2593
|
// 释放锁文件
|
|
2492
2594
|
try {
|
|
@@ -2502,46 +2604,84 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2502
2604
|
}
|
|
2503
2605
|
};
|
|
2504
2606
|
|
|
2505
|
-
process.on("
|
|
2607
|
+
process.on("beforeExit", (code) => {
|
|
2608
|
+
logSync(`beforeExit code=${code}`);
|
|
2609
|
+
});
|
|
2610
|
+
process.on("exit", (code) => {
|
|
2611
|
+
cleanup(`exit code=${code}`, { sync: true });
|
|
2612
|
+
});
|
|
2506
2613
|
process.on("SIGTERM", () => {
|
|
2507
|
-
cleanup();
|
|
2614
|
+
cleanup("SIGTERM", { sync: true });
|
|
2508
2615
|
process.exit(0);
|
|
2509
2616
|
});
|
|
2510
2617
|
process.on("SIGINT", () => {
|
|
2511
|
-
cleanup();
|
|
2618
|
+
cleanup("SIGINT", { sync: true });
|
|
2512
2619
|
process.exit(0);
|
|
2513
2620
|
});
|
|
2621
|
+
process.on("uncaughtException", (err) => {
|
|
2622
|
+
logSync(`uncaughtException: ${formatFatalReason(err)}`);
|
|
2623
|
+
cleanup("uncaughtException", { sync: true });
|
|
2624
|
+
process.exit(1);
|
|
2625
|
+
});
|
|
2626
|
+
process.on("unhandledRejection", (reason) => {
|
|
2627
|
+
logSync(`unhandledRejection: ${formatFatalReason(reason)}`);
|
|
2628
|
+
cleanup("unhandledRejection", { sync: true });
|
|
2629
|
+
process.exit(1);
|
|
2630
|
+
});
|
|
2514
2631
|
}
|
|
2515
2632
|
|
|
2516
|
-
function stopDaemon(projectRoot) {
|
|
2633
|
+
function stopDaemon(projectRoot, options = {}) {
|
|
2517
2634
|
const pid = readPid(projectRoot);
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2635
|
+
const pids = new Set(socketOwnerDaemonPids(projectRoot));
|
|
2636
|
+
if (pid && isPidFileDaemonForProject(projectRoot, pid, pids)) {
|
|
2637
|
+
pids.add(pid);
|
|
2521
2638
|
}
|
|
2639
|
+
const source = String(
|
|
2640
|
+
options.source
|
|
2641
|
+
|| process.env.UFOO_DAEMON_STOP_SOURCE
|
|
2642
|
+
|| `pid=${process.pid} cwd=${process.cwd()} argv=${process.argv.join(" ")}`
|
|
2643
|
+
).slice(0, 1200);
|
|
2644
|
+
appendControlLog(
|
|
2645
|
+
projectRoot,
|
|
2646
|
+
`stop requested source=${JSON.stringify(source)} pid_file=${pid || ""} target_pids=[${Array.from(pids).join(",")}]`
|
|
2647
|
+
);
|
|
2522
2648
|
let killed = false;
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2649
|
+
for (const targetPid of pids) {
|
|
2650
|
+
try {
|
|
2651
|
+
process.kill(targetPid, "SIGTERM");
|
|
2652
|
+
killed = true;
|
|
2653
|
+
} catch {
|
|
2654
|
+
// ignore kill errors (e.g., already dead)
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
const started = Date.now();
|
|
2658
|
+
while (Date.now() - started < 1500) {
|
|
2659
|
+
let anyAlive = false;
|
|
2660
|
+
for (const targetPid of pids) {
|
|
2661
|
+
if (pidAlive(targetPid)) {
|
|
2662
|
+
anyAlive = true;
|
|
2532
2663
|
}
|
|
2533
2664
|
}
|
|
2534
|
-
|
|
2665
|
+
if (!anyAlive) break;
|
|
2666
|
+
}
|
|
2667
|
+
// Force kill if still alive.
|
|
2668
|
+
for (const targetPid of pids) {
|
|
2535
2669
|
try {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2670
|
+
if (pidAlive(targetPid)) {
|
|
2671
|
+
process.kill(targetPid, "SIGKILL");
|
|
2672
|
+
killed = true;
|
|
2673
|
+
}
|
|
2539
2674
|
} catch {
|
|
2540
2675
|
// ignore if already dead
|
|
2541
2676
|
}
|
|
2542
|
-
} catch {
|
|
2543
|
-
// ignore kill errors (e.g., already dead)
|
|
2544
2677
|
}
|
|
2678
|
+
|
|
2679
|
+
const stillAlive = Array.from(pids).filter((targetPid) => pidAlive(targetPid));
|
|
2680
|
+
if (stillAlive.length > 0) {
|
|
2681
|
+
appendControlLog(projectRoot, `stop failed still_alive=[${stillAlive.join(",")}]`);
|
|
2682
|
+
return false;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2545
2685
|
try {
|
|
2546
2686
|
fs.unlinkSync(pidPath(projectRoot));
|
|
2547
2687
|
} catch {
|
|
@@ -2567,7 +2707,9 @@ function stopDaemon(projectRoot) {
|
|
|
2567
2707
|
// ignore
|
|
2568
2708
|
}
|
|
2569
2709
|
|
|
2570
|
-
|
|
2710
|
+
const stopped = killed || pids.size === 0;
|
|
2711
|
+
appendControlLog(projectRoot, `stop completed stopped=${stopped} killed=${killed} target_count=${pids.size}`);
|
|
2712
|
+
return stopped;
|
|
2571
2713
|
}
|
|
2572
2714
|
|
|
2573
2715
|
module.exports = { startDaemon, stopDaemon, isRunning, cleanupStaleState, socketPath };
|
package/src/daemon/ipcServer.js
CHANGED
|
@@ -56,6 +56,9 @@ function createDaemonIpcServer(options = {}) {
|
|
|
56
56
|
const server = net.createServer((socket) => {
|
|
57
57
|
sockets.add(socket);
|
|
58
58
|
socket.on("close", () => sockets.delete(socket));
|
|
59
|
+
socket.on("error", (err) => {
|
|
60
|
+
log(`ipc socket error: ${err && err.message ? err.message : String(err || "unknown error")}`);
|
|
61
|
+
});
|
|
59
62
|
let buffer = "";
|
|
60
63
|
socket.on("data", async (data) => {
|
|
61
64
|
buffer += data.toString("utf8");
|
|
@@ -66,12 +69,31 @@ function createDaemonIpcServer(options = {}) {
|
|
|
66
69
|
const items = parseJsonLines(line);
|
|
67
70
|
for (const req of items) {
|
|
68
71
|
if (!req || typeof req !== "object") continue;
|
|
69
|
-
|
|
72
|
+
try {
|
|
73
|
+
await handleRequest(req, socket);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const message = err && err.message ? err.message : String(err || "request failed");
|
|
76
|
+
const requestType = String(req.type || "unknown");
|
|
77
|
+
log(`ipc request failed type=${requestType}: ${err && err.stack ? err.stack : message}`);
|
|
78
|
+
try {
|
|
79
|
+
socket.write(`${JSON.stringify({
|
|
80
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
81
|
+
error: message,
|
|
82
|
+
request_type: requestType,
|
|
83
|
+
})}\n`);
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore failed error replies
|
|
86
|
+
}
|
|
87
|
+
}
|
|
70
88
|
}
|
|
71
89
|
}
|
|
72
90
|
});
|
|
73
91
|
});
|
|
74
92
|
|
|
93
|
+
server.on("error", (err) => {
|
|
94
|
+
log(`ipc server error: ${err && err.message ? err.message : String(err || "unknown error")}`);
|
|
95
|
+
});
|
|
96
|
+
|
|
75
97
|
function listen(sockPath) {
|
|
76
98
|
server.listen(sockPath);
|
|
77
99
|
}
|
|
@@ -112,7 +112,7 @@ function summarizeShadowPayload(payload = {}) {
|
|
|
112
112
|
async function handlePromptRequest(options = {}) {
|
|
113
113
|
const {
|
|
114
114
|
projectRoot,
|
|
115
|
-
req = {},
|
|
115
|
+
req: originalReq = {},
|
|
116
116
|
socket,
|
|
117
117
|
provider,
|
|
118
118
|
model,
|
|
@@ -130,6 +130,11 @@ async function handlePromptRequest(options = {}) {
|
|
|
130
130
|
log = () => {},
|
|
131
131
|
} = options;
|
|
132
132
|
|
|
133
|
+
const req = originalReq && typeof originalReq === "object" ? { ...originalReq } : {};
|
|
134
|
+
if ((req.text == null || req.text === "") && req.prompt != null) {
|
|
135
|
+
req.text = req.prompt;
|
|
136
|
+
}
|
|
137
|
+
|
|
133
138
|
log(`prompt ${String(req.text || "").slice(0, 200)}`);
|
|
134
139
|
const requestMeta = req.request_meta && typeof req.request_meta === "object" ? req.request_meta : {};
|
|
135
140
|
const messageId = normalizeMessageId(req);
|
package/src/daemon/run.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const { startDaemon, stopDaemon, isRunning } = require("./index");
|
|
3
3
|
const { loadConfig, defaultAgentModelForProvider } = require("../config");
|
|
4
|
+
const { resolveNodeExecutable } = require("../utils/nodeExecutable");
|
|
4
5
|
|
|
5
6
|
function runDaemonCli(argv) {
|
|
6
7
|
const cmd = argv[1] || "start";
|
|
@@ -19,7 +20,7 @@ function runDaemonCli(argv) {
|
|
|
19
20
|
if (isRunning(projectRoot)) return;
|
|
20
21
|
if (!process.env.UFOO_DAEMON_CHILD) {
|
|
21
22
|
const { spawn } = require("child_process");
|
|
22
|
-
const child = spawn(
|
|
23
|
+
const child = spawn(resolveNodeExecutable(), [path.join(__dirname, "..", "..", "bin", "ufoo.js"), "daemon", "start"], {
|
|
23
24
|
detached: true,
|
|
24
25
|
stdio: "ignore",
|
|
25
26
|
env: { ...process.env, UFOO_DAEMON_CHILD: "1" },
|
|
@@ -32,25 +33,31 @@ function runDaemonCli(argv) {
|
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
34
35
|
if (cmd === "stop" || cmd === "--stop") {
|
|
35
|
-
stopDaemon(projectRoot)
|
|
36
|
+
if (!stopDaemon(projectRoot, { source: process.env.UFOO_DAEMON_STOP_SOURCE || `daemon-cli:${cmd} pid=${process.pid}` })) {
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
38
41
|
if (cmd === "restart" || cmd === "--restart") {
|
|
39
42
|
// Stop if running
|
|
40
43
|
if (isRunning(projectRoot)) {
|
|
41
|
-
stopDaemon(projectRoot);
|
|
44
|
+
const stopped = stopDaemon(projectRoot, { source: process.env.UFOO_DAEMON_STOP_SOURCE || `daemon-cli:${cmd} pid=${process.pid}` });
|
|
42
45
|
// Wait for clean shutdown
|
|
43
46
|
let attempts = 0;
|
|
44
47
|
while (isRunning(projectRoot) && attempts < 50) {
|
|
45
48
|
attempts++;
|
|
46
49
|
require("child_process").spawnSync("sleep", ["0.1"]);
|
|
47
50
|
}
|
|
51
|
+
if (!stopped && isRunning(projectRoot)) {
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
48
55
|
}
|
|
49
56
|
// Start fresh daemon
|
|
50
57
|
if (!process.env.UFOO_DAEMON_CHILD) {
|
|
51
58
|
const { spawn } = require("child_process");
|
|
52
59
|
const childEnv = { ...process.env, UFOO_DAEMON_CHILD: "1" };
|
|
53
|
-
const child = spawn(
|
|
60
|
+
const child = spawn(resolveNodeExecutable(), [path.join(__dirname, "..", "..", "bin", "ufoo.js"), "daemon", "start"], {
|
|
54
61
|
detached: true,
|
|
55
62
|
stdio: "ignore",
|
|
56
63
|
env: childEnv,
|
package/src/projects/runtimes.js
CHANGED
|
@@ -17,7 +17,7 @@ function filterVisibleProjectRuntimes(rows = []) {
|
|
|
17
17
|
const sourceRows = Array.isArray(rows) ? rows : [];
|
|
18
18
|
return sourceRows.filter((row) => {
|
|
19
19
|
const status = String((row && row.status) || "").trim().toLowerCase();
|
|
20
|
-
return status
|
|
20
|
+
return status === "running";
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
|
|
4
|
+
const MAX_DIAGNOSTIC_LOG_BYTES = 5 * 1024 * 1024;
|
|
5
|
+
const emittedDiagnostics = new Set();
|
|
6
|
+
|
|
4
7
|
function isAgentsFile(filePath) {
|
|
5
8
|
return path.basename(filePath || "") === "all-agents.json"
|
|
6
9
|
&& path.basename(path.dirname(filePath || "")) === "agent";
|
|
@@ -62,11 +65,50 @@ function safePayload(payload = {}) {
|
|
|
62
65
|
return out;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
function diagnosticKey(agentsFilePath, event, payload = {}) {
|
|
69
|
+
if (event === "queue_entry_not_recovered") {
|
|
70
|
+
return [
|
|
71
|
+
agentsFilePath,
|
|
72
|
+
event,
|
|
73
|
+
payload.subscriber || "",
|
|
74
|
+
payload.reason || "",
|
|
75
|
+
].join("\0");
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function shouldSuppressDiagnostic(agentsFilePath, event, payload = {}) {
|
|
81
|
+
const key = diagnosticKey(agentsFilePath, event, payload);
|
|
82
|
+
if (!key) return false;
|
|
83
|
+
if (emittedDiagnostics.has(key)) return true;
|
|
84
|
+
emittedDiagnostics.add(key);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function enforceLogLimit(logPath) {
|
|
89
|
+
try {
|
|
90
|
+
const stat = fs.statSync(logPath);
|
|
91
|
+
if (stat.size <= MAX_DIAGNOSTIC_LOG_BYTES) return;
|
|
92
|
+
const line = JSON.stringify({
|
|
93
|
+
ts: new Date().toISOString(),
|
|
94
|
+
pid: process.pid,
|
|
95
|
+
ppid: process.ppid,
|
|
96
|
+
event: "diagnostics_log_truncated",
|
|
97
|
+
previous_size: stat.size,
|
|
98
|
+
});
|
|
99
|
+
fs.writeFileSync(logPath, `${line}\n`, "utf8");
|
|
100
|
+
} catch {
|
|
101
|
+
// Missing/unreadable log files are handled by the append path.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
65
105
|
function appendAgentRegistryDiagnostic(agentsFilePath, event, payload = {}) {
|
|
66
106
|
if (!agentsFilePath || !isAgentsFile(agentsFilePath)) return;
|
|
107
|
+
if (shouldSuppressDiagnostic(agentsFilePath, event, payload)) return;
|
|
67
108
|
try {
|
|
68
109
|
const logPath = getRegistryLogPath(agentsFilePath);
|
|
69
110
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
111
|
+
enforceLogLimit(logPath);
|
|
70
112
|
const line = JSON.stringify({
|
|
71
113
|
ts: new Date().toISOString(),
|
|
72
114
|
pid: process.pid,
|
|
@@ -88,4 +130,5 @@ module.exports = {
|
|
|
88
130
|
summarizeFile,
|
|
89
131
|
isAgentsFile,
|
|
90
132
|
getRegistryLogPath,
|
|
133
|
+
MAX_DIAGNOSTIC_LOG_BYTES,
|
|
91
134
|
};
|