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.
Files changed (38) hide show
  1. package/package.json +5 -1
  2. package/scripts/chat-app-smoke.js +30 -0
  3. package/scripts/ink-demo.js +23 -0
  4. package/scripts/ink-smoke.js +30 -0
  5. package/scripts/ucode-app-smoke.js +36 -0
  6. package/src/chat/commandExecutor.js +6 -2
  7. package/src/chat/daemonMessageRouter.js +9 -1
  8. package/src/chat/daemonTransport.js +2 -1
  9. package/src/chat/dashboardKeyController.js +0 -40
  10. package/src/chat/dashboardView.js +0 -20
  11. package/src/chat/index.js +9 -1
  12. package/src/chat/inputSubmitHandler.js +34 -0
  13. package/src/chat/projectCloseController.js +1 -1
  14. package/src/chat/shellCommand.js +42 -0
  15. package/src/chat/transport.js +16 -3
  16. package/src/cli.js +4 -3
  17. package/src/code/agent.js +4 -0
  18. package/src/code/nativeRunner.js +74 -0
  19. package/src/code/taskDecomposer.js +5 -4
  20. package/src/code/tui.js +73 -561
  21. package/src/daemon/index.js +169 -27
  22. package/src/daemon/ipcServer.js +23 -1
  23. package/src/daemon/promptRequest.js +6 -1
  24. package/src/daemon/run.js +11 -4
  25. package/src/projects/runtimes.js +1 -1
  26. package/src/ufoo/agentRegistryDiagnostics.js +43 -0
  27. package/src/ui/MIGRATION.md +382 -0
  28. package/src/ui/components/ChatApp.js +2950 -0
  29. package/src/ui/components/DashboardBar.js +417 -0
  30. package/src/ui/components/InkDemo.js +96 -0
  31. package/src/ui/components/MultilineInput.js +387 -0
  32. package/src/ui/components/UcodeApp.js +813 -0
  33. package/src/ui/components/agentMirror.js +725 -0
  34. package/src/ui/components/chatReducer.js +337 -0
  35. package/src/ui/format/index.js +997 -0
  36. package/src/ui/index.js +9 -0
  37. package/src/ui/runInk.js +57 -0
  38. package/src/utils/nodeExecutable.js +26 -0
@@ -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(`[daemon] ${new Date().toISOString()} ${msg}\n`);
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(process.execPath, [daemonBin, "daemon", "--start"], {
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
- log(`Shutting down daemon (managed agents: ${processManager.count()})`);
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("exit", cleanup);
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
- if (!pid) {
2519
- removeSocket(projectRoot);
2520
- return false;
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
- try {
2524
- process.kill(pid, "SIGTERM");
2525
- const started = Date.now();
2526
- while (Date.now() - started < 1500) {
2527
- try {
2528
- process.kill(pid, 0);
2529
- } catch {
2530
- killed = true;
2531
- break;
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
- // Force kill if still alive.
2665
+ if (!anyAlive) break;
2666
+ }
2667
+ // Force kill if still alive.
2668
+ for (const targetPid of pids) {
2535
2669
  try {
2536
- process.kill(pid, 0);
2537
- process.kill(pid, "SIGKILL");
2538
- killed = true;
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
- return killed;
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 };
@@ -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
- await handleRequest(req, socket);
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(process.execPath, [path.join(__dirname, "..", "..", "bin", "ufoo.js"), "daemon", "start"], {
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(process.execPath, [path.join(__dirname, "..", "..", "bin", "ufoo.js"), "daemon", "start"], {
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,
@@ -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 !== "stopped";
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
  };