u-foo 3.0.21 → 3.0.22
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 +13 -7
- package/README.zh-CN.md +11 -7
- package/package.json +1 -1
- package/src/agents/launch/launcher.js +52 -29
- package/src/app/chat/commandExecutor.js +15 -0
- package/src/app/chat/daemonConnection.js +7 -2
- package/src/app/chat/daemonCoordinator.js +1 -0
- package/src/app/chat/daemonTransport.js +21 -5
- package/src/app/chat/projectCloseController.js +6 -1
- package/src/app/chat/transport.js +84 -8
- package/src/app/cli/run.js +44 -7
- package/src/config.js +48 -0
- package/src/runtime/contracts/eventContract.js +1 -0
- package/src/runtime/daemon/agentProcessManager.js +34 -14
- package/src/runtime/daemon/controlPlaneService.js +14 -5
- package/src/runtime/daemon/endpoint.js +79 -0
- package/src/runtime/daemon/globalDaemon.js +394 -0
- package/src/runtime/daemon/groupOrchestrator.js +6 -0
- package/src/runtime/daemon/index.js +272 -109
- package/src/runtime/daemon/mcpHttpServer.js +5 -0
- package/src/runtime/daemon/ops.js +48 -17
- package/src/runtime/daemon/processLifecycle.js +127 -0
- package/src/runtime/daemon/projectContext.js +46 -0
- package/src/runtime/daemon/projectRuntime.js +221 -0
- package/src/runtime/daemon/projectRuntimeGateway.js +72 -0
- package/src/runtime/daemon/projectRuntimeManager.js +268 -0
- package/src/runtime/daemon/run.js +66 -12
- package/src/runtime/projects/registry.js +26 -4
- package/src/runtime/projects/runtimes.js +1 -1
- package/src/ui/rustChatHost.js +46 -8
|
@@ -13,7 +13,11 @@ const { generateInstanceId, subscriberToSafeName } = require("../../coordination
|
|
|
13
13
|
const { createDaemonIpcServer } = require("./ipcServer");
|
|
14
14
|
const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../contracts/eventContract");
|
|
15
15
|
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
upsertProjectRuntime,
|
|
18
|
+
markProjectDormant,
|
|
19
|
+
markProjectStopped,
|
|
20
|
+
} = require("../projects");
|
|
17
21
|
const { scheduleProviderSessionResolve, resolveSessionFromFile, persistProviderSession, loadProviderSessionCache } = require("./providerSessions");
|
|
18
22
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
19
23
|
const { createDaemonCronController } = require("./cronOps");
|
|
@@ -48,12 +52,18 @@ const { resolveNodeExecutable } = require("../process/nodeExecutable");
|
|
|
48
52
|
const {
|
|
49
53
|
createProjectRuntimeControlPlane,
|
|
50
54
|
} = require("./projectRuntimeControlPlane");
|
|
51
|
-
const {
|
|
55
|
+
const { createProjectContext } = require("./projectContext");
|
|
56
|
+
const { createProjectRuntime } = require("./projectRuntime");
|
|
57
|
+
const { registerDaemonRuntimeLifecycle } = require("./processLifecycle");
|
|
58
|
+
const {
|
|
59
|
+
createManagedProjectRuntimeGateway,
|
|
60
|
+
} = require("./projectRuntimeGateway");
|
|
61
|
+
const {
|
|
62
|
+
loadConfig,
|
|
63
|
+
normalizeDaemonTopology,
|
|
64
|
+
normalizeMcpPort,
|
|
65
|
+
} = require("../../config");
|
|
52
66
|
|
|
53
|
-
let providerSessions = null;
|
|
54
|
-
let sessionResolveHandles = new Map();
|
|
55
|
-
let daemonCronController = null;
|
|
56
|
-
let daemonGroupOrchestrator = null;
|
|
57
67
|
const PROJECT_RUNTIME_HEARTBEAT_MS = 10 * 1000;
|
|
58
68
|
|
|
59
69
|
function sleep(ms) {
|
|
@@ -522,7 +532,10 @@ function resolveSubscriberNickname(projectRoot, subscriberId) {
|
|
|
522
532
|
}
|
|
523
533
|
}
|
|
524
534
|
|
|
525
|
-
async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
535
|
+
async function handleOps(projectRoot, ops = [], processManager = null, runtimeServices = {}) {
|
|
536
|
+
const providerSessions = runtimeServices.providerSessions || null;
|
|
537
|
+
const sessionResolveHandles = runtimeServices.sessionResolveHandles || new Map();
|
|
538
|
+
const daemonCronController = runtimeServices.cronController || null;
|
|
526
539
|
const results = [];
|
|
527
540
|
for (const op of ops) {
|
|
528
541
|
if (op.action === "launch") {
|
|
@@ -575,6 +588,9 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
575
588
|
scopedNickname,
|
|
576
589
|
launchScope: op.launch_scope || "",
|
|
577
590
|
terminalApp: op.terminal_app || "",
|
|
591
|
+
tmuxTarget: op.tmux_target || op.tmuxTarget || "",
|
|
592
|
+
tmuxPane: op.tmux_pane || op.tmuxPane || "",
|
|
593
|
+
tmuxSession: op.tmux_session || op.tmuxSession || "",
|
|
578
594
|
tmuxLayoutContext:
|
|
579
595
|
op.tmux_layout_context && typeof op.tmux_layout_context === "object"
|
|
580
596
|
? op.tmux_layout_context
|
|
@@ -1151,7 +1167,19 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain, o
|
|
|
1151
1167
|
};
|
|
1152
1168
|
}
|
|
1153
1169
|
|
|
1154
|
-
function startDaemon({
|
|
1170
|
+
function startDaemon({
|
|
1171
|
+
projectRoot,
|
|
1172
|
+
provider,
|
|
1173
|
+
model,
|
|
1174
|
+
resumeMode = "auto",
|
|
1175
|
+
daemonTopology = "project",
|
|
1176
|
+
runtimeGeneration = 1,
|
|
1177
|
+
globalRuntimeRouter = null,
|
|
1178
|
+
beforeCleanup = null,
|
|
1179
|
+
manageProcessState = true,
|
|
1180
|
+
listenProjectSocket = true,
|
|
1181
|
+
registrySocketPath = "",
|
|
1182
|
+
}) {
|
|
1155
1183
|
const paths = getUfooPaths(projectRoot);
|
|
1156
1184
|
if (!fs.existsSync(paths.ufooDir)) {
|
|
1157
1185
|
throw new Error("Missing .ufoo. Run: ufoo init");
|
|
@@ -1160,57 +1188,59 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1160
1188
|
const runDir = paths.runDir;
|
|
1161
1189
|
ensureDir(runDir);
|
|
1162
1190
|
|
|
1163
|
-
// 文件锁机制:防止多个 daemon 同时启动
|
|
1164
1191
|
const lockFile = path.join(runDir, "daemon.lock");
|
|
1165
1192
|
let lockFd;
|
|
1166
1193
|
let recoveredStaleLock = false;
|
|
1167
|
-
|
|
1168
|
-
//
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1194
|
+
if (manageProcessState) {
|
|
1195
|
+
// 文件锁机制:防止多个 daemon 同时启动
|
|
1196
|
+
try {
|
|
1197
|
+
// 尝试独占方式打开锁文件(如果已存在且被锁定则失败)
|
|
1198
|
+
lockFd = fs.openSync(lockFile, "wx");
|
|
1199
|
+
fs.writeSync(lockFd, `${process.pid}\n`);
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
if (err.code === "EEXIST") {
|
|
1202
|
+
// 锁文件已存在,检查是否仍有效
|
|
1203
|
+
let existingPid = null;
|
|
1204
|
+
try {
|
|
1205
|
+
const raw = fs.readFileSync(lockFile, "utf8").trim();
|
|
1206
|
+
const parsed = parseInt(raw, 10);
|
|
1207
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1208
|
+
existingPid = parsed;
|
|
1209
|
+
}
|
|
1210
|
+
} catch {
|
|
1211
|
+
// ignore malformed lock file and treat as stale
|
|
1180
1212
|
}
|
|
1181
|
-
} catch {
|
|
1182
|
-
// ignore malformed lock file and treat as stale
|
|
1183
|
-
}
|
|
1184
1213
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1214
|
+
let lockHeld = false;
|
|
1215
|
+
if (existingPid) {
|
|
1216
|
+
lockHeld = looksLikeRunningDaemon(projectRoot, existingPid);
|
|
1217
|
+
}
|
|
1189
1218
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1219
|
+
if (lockHeld) {
|
|
1220
|
+
throw new Error(`Daemon already running with PID ${existingPid}`);
|
|
1221
|
+
}
|
|
1193
1222
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1223
|
+
// 进程已死或锁文件损坏,清理旧锁后重试
|
|
1224
|
+
try {
|
|
1225
|
+
fs.unlinkSync(lockFile);
|
|
1226
|
+
recoveredStaleLock = true;
|
|
1227
|
+
} catch (unlinkErr) {
|
|
1228
|
+
throw new Error(`Failed to remove stale daemon lock: ${unlinkErr.message}`);
|
|
1229
|
+
}
|
|
1230
|
+
try {
|
|
1231
|
+
lockFd = fs.openSync(lockFile, "wx");
|
|
1232
|
+
fs.writeSync(lockFd, `${process.pid}\n`);
|
|
1233
|
+
} catch (retryErr) {
|
|
1234
|
+
throw new Error(`Failed to acquire daemon lock: ${retryErr.message}`);
|
|
1235
|
+
}
|
|
1236
|
+
} else {
|
|
1237
|
+
throw err;
|
|
1206
1238
|
}
|
|
1207
|
-
} else {
|
|
1208
|
-
throw err;
|
|
1209
1239
|
}
|
|
1210
1240
|
}
|
|
1211
1241
|
|
|
1212
|
-
removeSocket(projectRoot);
|
|
1213
|
-
writePid(projectRoot);
|
|
1242
|
+
if (listenProjectSocket) removeSocket(projectRoot);
|
|
1243
|
+
if (manageProcessState) writePid(projectRoot);
|
|
1214
1244
|
|
|
1215
1245
|
const logFile = fs.createWriteStream(logPath(projectRoot), { flags: "a" });
|
|
1216
1246
|
const formatLogLine = (msg) => `[daemon] ${new Date().toISOString()} ${msg}\n`;
|
|
@@ -1229,6 +1259,21 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1229
1259
|
}
|
|
1230
1260
|
}
|
|
1231
1261
|
};
|
|
1262
|
+
const runtimeConfig = loadConfig(projectRoot);
|
|
1263
|
+
const runtimeContext = createProjectContext({
|
|
1264
|
+
projectRoot,
|
|
1265
|
+
config: {
|
|
1266
|
+
...runtimeConfig,
|
|
1267
|
+
daemonTopology: normalizeDaemonTopology(
|
|
1268
|
+
daemonTopology || process.env.UFOO_DAEMON_TOPOLOGY || runtimeConfig.daemonTopology
|
|
1269
|
+
),
|
|
1270
|
+
},
|
|
1271
|
+
provider,
|
|
1272
|
+
model,
|
|
1273
|
+
runtimeGeneration,
|
|
1274
|
+
});
|
|
1275
|
+
const projectRuntime = createProjectRuntime(runtimeContext);
|
|
1276
|
+
void projectRuntime.activate();
|
|
1232
1277
|
const formatFatalReason = (err) => {
|
|
1233
1278
|
if (!err) return "unknown";
|
|
1234
1279
|
if (err instanceof Error) return err.stack || err.message;
|
|
@@ -1242,7 +1287,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1242
1287
|
upsertProjectRuntime({
|
|
1243
1288
|
projectRoot,
|
|
1244
1289
|
daemonPid: process.pid,
|
|
1245
|
-
socketPath: socketPath(projectRoot),
|
|
1290
|
+
socketPath: registrySocketPath || socketPath(projectRoot),
|
|
1246
1291
|
status,
|
|
1247
1292
|
lastSeen: new Date().toISOString(),
|
|
1248
1293
|
});
|
|
@@ -1252,30 +1297,54 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1252
1297
|
};
|
|
1253
1298
|
|
|
1254
1299
|
// 创建进程管理器 - daemon 作为父进程监控所有 internal agents
|
|
1255
|
-
const processManager =
|
|
1300
|
+
const processManager = projectRuntime.own(
|
|
1301
|
+
"processManager",
|
|
1302
|
+
new AgentProcessManager(projectRoot)
|
|
1303
|
+
);
|
|
1256
1304
|
log(`Process manager initialized`);
|
|
1257
1305
|
|
|
1258
1306
|
// Provider session cache (in-memory)
|
|
1259
|
-
providerSessions =
|
|
1260
|
-
|
|
1261
|
-
|
|
1307
|
+
const providerSessions = projectRuntime.own(
|
|
1308
|
+
"providerSessions",
|
|
1309
|
+
loadProviderSessionCache(projectRoot)
|
|
1310
|
+
);
|
|
1311
|
+
const sessionResolveHandles = projectRuntime.own(
|
|
1312
|
+
"sessionResolveHandles",
|
|
1313
|
+
new Map()
|
|
1314
|
+
);
|
|
1315
|
+
const runtimeServices = {
|
|
1316
|
+
providerSessions,
|
|
1317
|
+
sessionResolveHandles,
|
|
1318
|
+
cronController: null,
|
|
1319
|
+
groupOrchestrator: null,
|
|
1320
|
+
};
|
|
1321
|
+
const handleRuntimeOps = (root, ops, manager = processManager) =>
|
|
1322
|
+
handleOps(root, ops, manager, runtimeServices);
|
|
1323
|
+
const daemonCronController = projectRuntime.own("cronController", createDaemonCronController({
|
|
1262
1324
|
projectRoot,
|
|
1263
1325
|
dispatch: async ({ taskId, target, message }) => {
|
|
1264
1326
|
await dispatchMessages(projectRoot, [{ target, message }]);
|
|
1265
1327
|
log(`cron:${taskId} -> ${target}`);
|
|
1266
1328
|
},
|
|
1267
1329
|
log,
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1330
|
+
}));
|
|
1331
|
+
runtimeServices.cronController = daemonCronController;
|
|
1332
|
+
const daemonGroupOrchestrator = projectRuntime.own("groupOrchestrator", createGroupOrchestrator({
|
|
1270
1333
|
projectRoot,
|
|
1271
|
-
handleOps,
|
|
1334
|
+
handleOps: handleRuntimeOps,
|
|
1272
1335
|
processManager,
|
|
1273
|
-
});
|
|
1336
|
+
}));
|
|
1337
|
+
runtimeServices.groupOrchestrator = daemonGroupOrchestrator;
|
|
1274
1338
|
|
|
1275
|
-
const buildRuntimeStatus = () =>
|
|
1276
|
-
buildStatus(projectRoot, {
|
|
1339
|
+
const buildRuntimeStatus = () => ({
|
|
1340
|
+
...buildStatus(projectRoot, {
|
|
1277
1341
|
cronTasks: daemonCronController ? daemonCronController.listTasks() : [],
|
|
1278
|
-
})
|
|
1342
|
+
}),
|
|
1343
|
+
runtime: projectRuntime.status(),
|
|
1344
|
+
...(globalRuntimeRouter && typeof globalRuntimeRouter.status === "function"
|
|
1345
|
+
? { global_daemon: globalRuntimeRouter.status() }
|
|
1346
|
+
: {}),
|
|
1347
|
+
});
|
|
1279
1348
|
|
|
1280
1349
|
const cleanupInactiveSubscribers = () => {
|
|
1281
1350
|
try {
|
|
@@ -1298,6 +1367,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1298
1367
|
cleanupInactive: cleanupInactiveSubscribers,
|
|
1299
1368
|
log,
|
|
1300
1369
|
});
|
|
1370
|
+
projectRuntime.own("ipcServer", ipcServer);
|
|
1301
1371
|
|
|
1302
1372
|
const publishAgentReportResult = (entry) => {
|
|
1303
1373
|
ipcServer.sendToSockets({
|
|
@@ -1346,6 +1416,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1346
1416
|
log(`report bus event failed request=${meta.requestId || ""} error=${err.message || String(err)}`);
|
|
1347
1417
|
}
|
|
1348
1418
|
});
|
|
1419
|
+
projectRuntime.own("busBridge", busBridge);
|
|
1349
1420
|
const deliveryScheduler = new DeliveryScheduler(projectRoot, {
|
|
1350
1421
|
log,
|
|
1351
1422
|
emitDelivery: async ({ subscriber, status, error } = {}) => {
|
|
@@ -1355,16 +1426,35 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1355
1426
|
},
|
|
1356
1427
|
});
|
|
1357
1428
|
deliveryScheduler.start();
|
|
1429
|
+
projectRuntime.own("deliveryScheduler", deliveryScheduler);
|
|
1358
1430
|
const runtimeControlPlane = createProjectRuntimeControlPlane({ projectRoot });
|
|
1431
|
+
projectRuntime.own("runtimeControlPlane", runtimeControlPlane);
|
|
1359
1432
|
let mcpHttpServer = null;
|
|
1433
|
+
let globalProjectRuntimeGateway = null;
|
|
1434
|
+
let ownsGlobalProjectRuntimeGateway = false;
|
|
1360
1435
|
if (isGlobalControllerProjectRoot(projectRoot) && process.env.UFOO_MCP_HTTP_DISABLED !== "1") {
|
|
1361
1436
|
const { createGlobalMcpHttpServer } = require("./mcpHttpServer");
|
|
1362
1437
|
const config = loadConfig(projectRoot);
|
|
1363
1438
|
const configuredPort = process.env.UFOO_MCP_PORT || config.mcpPort;
|
|
1439
|
+
globalProjectRuntimeGateway =
|
|
1440
|
+
globalRuntimeRouter
|
|
1441
|
+
&& globalRuntimeRouter.projectRuntimeGateway
|
|
1442
|
+
? globalRuntimeRouter.projectRuntimeGateway
|
|
1443
|
+
: createManagedProjectRuntimeGateway({
|
|
1444
|
+
managerOptions: {
|
|
1445
|
+
authorizeProjectRoot: (targetRoot) =>
|
|
1446
|
+
fs.existsSync(getUfooPaths(targetRoot).ufooDir),
|
|
1447
|
+
},
|
|
1448
|
+
});
|
|
1449
|
+
ownsGlobalProjectRuntimeGateway =
|
|
1450
|
+
globalProjectRuntimeGateway !== (
|
|
1451
|
+
globalRuntimeRouter && globalRuntimeRouter.projectRuntimeGateway
|
|
1452
|
+
);
|
|
1364
1453
|
mcpHttpServer = createGlobalMcpHttpServer({
|
|
1365
1454
|
projectRoot,
|
|
1366
1455
|
port: normalizeMcpPort(configuredPort),
|
|
1367
1456
|
log,
|
|
1457
|
+
projectRuntimeGateway: globalProjectRuntimeGateway,
|
|
1368
1458
|
});
|
|
1369
1459
|
mcpHttpServer.start().catch((err) => {
|
|
1370
1460
|
logSync(`MCP HTTP startup failed: ${formatFatalReason(err)}`);
|
|
@@ -1376,6 +1466,37 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1376
1466
|
|
|
1377
1467
|
handleIpcRequest = async (req, socket) => {
|
|
1378
1468
|
if (!req || typeof req !== "object") return;
|
|
1469
|
+
const routedProjectRoot = String(req.project_root || req.projectRoot || "").trim();
|
|
1470
|
+
if (
|
|
1471
|
+
globalRuntimeRouter
|
|
1472
|
+
&& req.type === IPC_REQUEST_TYPES.CLOSE_PROJECT_RUNTIME
|
|
1473
|
+
) {
|
|
1474
|
+
try {
|
|
1475
|
+
const result = globalRuntimeRouter.closeProject(routedProjectRoot, {
|
|
1476
|
+
terminateAgents:
|
|
1477
|
+
req.terminate_agents === true || req.terminateAgents === true,
|
|
1478
|
+
});
|
|
1479
|
+
socket.write(`${JSON.stringify({
|
|
1480
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1481
|
+
data: result,
|
|
1482
|
+
})}\n`);
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
socket.write(`${JSON.stringify({
|
|
1485
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1486
|
+
error: err.message || String(err),
|
|
1487
|
+
code: err.code || "close_project_runtime_failed",
|
|
1488
|
+
})}\n`);
|
|
1489
|
+
}
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (
|
|
1493
|
+
globalRuntimeRouter
|
|
1494
|
+
&& routedProjectRoot
|
|
1495
|
+
&& !sameProjectRoot(routedProjectRoot, projectRoot)
|
|
1496
|
+
) {
|
|
1497
|
+
await globalRuntimeRouter.handleRequest(routedProjectRoot, req, socket);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1379
1500
|
if (await runtimeControlPlane.handleRequest(req, socket)) return;
|
|
1380
1501
|
if (req.type === IPC_REQUEST_TYPES.MCP_STATUS || req.type === IPC_REQUEST_TYPES.MCP_RESTART) {
|
|
1381
1502
|
if (!isGlobalControllerProjectRoot(projectRoot)) {
|
|
@@ -1431,7 +1552,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1431
1552
|
runUfooAgent,
|
|
1432
1553
|
runUfooRouteAgent,
|
|
1433
1554
|
dispatchMessages,
|
|
1434
|
-
handleOps,
|
|
1555
|
+
handleOps: handleRuntimeOps,
|
|
1435
1556
|
markPending: (target) => busBridge.markPending(target),
|
|
1436
1557
|
reportTaskStatus: async (report) => {
|
|
1437
1558
|
await recordAgentReport({
|
|
@@ -1466,7 +1587,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1466
1587
|
const init = new (require("../../app/cli/features/init"))(repoRoot);
|
|
1467
1588
|
await init.init({ targets: "context,bus", project: root });
|
|
1468
1589
|
}
|
|
1469
|
-
if (!isRunning(root)) {
|
|
1590
|
+
if (!globalRuntimeRouter && !isRunning(root)) {
|
|
1470
1591
|
cleanupStaleState(root);
|
|
1471
1592
|
const daemonBin = path.join(__dirname, "..", "..", "..", "bin", "ufoo.js");
|
|
1472
1593
|
const child = spawn(resolveNodeExecutable(), [daemonBin, "daemon", "--start"], {
|
|
@@ -1490,11 +1611,15 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1490
1611
|
delete nextMeta.force_project_root;
|
|
1491
1612
|
delete nextMeta.force_project_name;
|
|
1492
1613
|
|
|
1493
|
-
|
|
1614
|
+
const routedRequest = {
|
|
1494
1615
|
type: IPC_REQUEST_TYPES.PROMPT,
|
|
1495
1616
|
text: String(prompt || ""),
|
|
1496
1617
|
request_meta: nextMeta,
|
|
1497
|
-
}
|
|
1618
|
+
};
|
|
1619
|
+
if (globalRuntimeRouter && typeof globalRuntimeRouter.request === "function") {
|
|
1620
|
+
return globalRuntimeRouter.request(root, routedRequest, { timeoutMs: 12000 });
|
|
1621
|
+
}
|
|
1622
|
+
return sendPromptRequestToProject(root, routedRequest);
|
|
1498
1623
|
},
|
|
1499
1624
|
log,
|
|
1500
1625
|
});
|
|
@@ -1656,7 +1781,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1656
1781
|
}
|
|
1657
1782
|
try {
|
|
1658
1783
|
const op = { action: "close", agent_id };
|
|
1659
|
-
const opsResults = await
|
|
1784
|
+
const opsResults = await handleRuntimeOps(projectRoot, [op], processManager);
|
|
1660
1785
|
const closeResult = opsResults.find((r) => r.action === "close");
|
|
1661
1786
|
const ok = closeResult ? closeResult.ok !== false : true;
|
|
1662
1787
|
const reply = ok
|
|
@@ -1697,6 +1822,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1697
1822
|
prompt_profile,
|
|
1698
1823
|
launch_scope,
|
|
1699
1824
|
terminal_app,
|
|
1825
|
+
tmux_target,
|
|
1826
|
+
tmux_pane,
|
|
1827
|
+
tmux_session,
|
|
1700
1828
|
host_inject_sock,
|
|
1701
1829
|
host_daemon_sock,
|
|
1702
1830
|
host_name,
|
|
@@ -1735,6 +1863,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1735
1863
|
nickname: explicitNickname,
|
|
1736
1864
|
launch_scope: launch_scope || "",
|
|
1737
1865
|
terminal_app: terminal_app || "",
|
|
1866
|
+
tmux_target: tmux_target || req.tmuxTarget || "",
|
|
1867
|
+
tmux_pane: tmux_pane || req.tmuxPane || "",
|
|
1868
|
+
tmux_session: tmux_session || req.tmuxSession || "",
|
|
1738
1869
|
host_inject_sock: host_inject_sock || "",
|
|
1739
1870
|
host_daemon_sock: host_daemon_sock || "",
|
|
1740
1871
|
host_name: host_name || "",
|
|
@@ -1830,7 +1961,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1830
1961
|
};
|
|
1831
1962
|
}
|
|
1832
1963
|
try {
|
|
1833
|
-
const opsResults = await
|
|
1964
|
+
const opsResults = await handleRuntimeOps(projectRoot, [op], processManager);
|
|
1834
1965
|
const launchResult = opsResults.find((r) => r.action === "launch");
|
|
1835
1966
|
if (soloLaunchBootstrap && launchResult && launchResult.ok !== false) {
|
|
1836
1967
|
const subscriberId = pickLaunchSubscriber(projectRoot, launchResult, explicitNickname || "");
|
|
@@ -1862,7 +1993,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1862
1993
|
projectRoot,
|
|
1863
1994
|
launchResult,
|
|
1864
1995
|
roleTarget,
|
|
1865
|
-
|
|
1996
|
+
handleRuntimeOps,
|
|
1866
1997
|
processManager
|
|
1867
1998
|
);
|
|
1868
1999
|
const roleError = roleResult.error || "role assignment failed";
|
|
@@ -1992,6 +2123,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1992
2123
|
const hostName = req.host_name || req.hostName || "";
|
|
1993
2124
|
const hostSessionId = req.host_session_id || req.hostSessionId || "";
|
|
1994
2125
|
const terminalApp = req.terminal_app || req.terminalApp || "";
|
|
2126
|
+
const tmuxTarget = req.tmux_target || req.tmuxTarget || "";
|
|
2127
|
+
const tmuxPane = req.tmux_pane || req.tmuxPane || "";
|
|
2128
|
+
const tmuxSession = req.tmux_session || req.tmuxSession || "";
|
|
1995
2129
|
const hostCapabilities =
|
|
1996
2130
|
req.host_capabilities && typeof req.host_capabilities === "object"
|
|
1997
2131
|
? req.host_capabilities
|
|
@@ -2008,6 +2142,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2008
2142
|
host_name: hostName,
|
|
2009
2143
|
host_session_id: hostSessionId,
|
|
2010
2144
|
terminal_app: terminalApp,
|
|
2145
|
+
tmux_target: tmuxTarget,
|
|
2146
|
+
tmux_pane: tmuxPane,
|
|
2147
|
+
tmux_session: tmuxSession,
|
|
2011
2148
|
host_capabilities: hostCapabilities,
|
|
2012
2149
|
});
|
|
2013
2150
|
const ok = result && result.ok !== false;
|
|
@@ -2523,7 +2660,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2523
2660
|
}
|
|
2524
2661
|
};
|
|
2525
2662
|
|
|
2526
|
-
ipcServer.listen(socketPath(projectRoot));
|
|
2663
|
+
if (listenProjectSocket) ipcServer.listen(socketPath(projectRoot));
|
|
2527
2664
|
publishProjectRuntime("running");
|
|
2528
2665
|
const runtimeHeartbeat = setInterval(() => {
|
|
2529
2666
|
publishProjectRuntime("running");
|
|
@@ -2531,11 +2668,14 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2531
2668
|
|
|
2532
2669
|
log(`Started pid=${process.pid}`);
|
|
2533
2670
|
|
|
2534
|
-
//
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2671
|
+
// Legacy project daemons own internal runners as child processes. A
|
|
2672
|
+
// global/hybrid runtime deliberately does not: runners must survive host
|
|
2673
|
+
// replacement and keep consuming their durable queues.
|
|
2674
|
+
if (runtimeContext.daemonTopology === "project") {
|
|
2675
|
+
const EventBus = require("../../coordination/bus");
|
|
2676
|
+
const { spawnSync } = require("child_process");
|
|
2677
|
+
const eventBus = new EventBus(projectRoot);
|
|
2678
|
+
try {
|
|
2539
2679
|
eventBus.ensureBus();
|
|
2540
2680
|
eventBus.loadBusData();
|
|
2541
2681
|
const agents = eventBus.busData.agents || {};
|
|
@@ -2616,9 +2756,10 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2616
2756
|
}
|
|
2617
2757
|
}
|
|
2618
2758
|
}
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2759
|
+
eventBus.saveBusData();
|
|
2760
|
+
} catch (err) {
|
|
2761
|
+
log(`Failed to cleanup orphan agents: ${err.message}`);
|
|
2762
|
+
}
|
|
2622
2763
|
}
|
|
2623
2764
|
|
|
2624
2765
|
const shouldResume = resumeMode === "force" || (resumeMode === "auto" && recoveredStaleLock);
|
|
@@ -2633,15 +2774,28 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2633
2774
|
}
|
|
2634
2775
|
|
|
2635
2776
|
let cleanedUp = false;
|
|
2777
|
+
let unregisterProcessLifecycle = () => {};
|
|
2636
2778
|
const cleanup = (reason = "exit", options = {}) => {
|
|
2637
2779
|
if (cleanedUp) return;
|
|
2638
2780
|
cleanedUp = true;
|
|
2781
|
+
unregisterProcessLifecycle();
|
|
2639
2782
|
const writeLog = options.sync ? logSync : log;
|
|
2640
2783
|
writeLog(`Shutting down daemon reason=${reason} (managed agents: ${processManager.count()})`);
|
|
2784
|
+
if (typeof beforeCleanup === "function") {
|
|
2785
|
+
try {
|
|
2786
|
+
beforeCleanup({ reason, options, projectRoot, runtime: projectRuntime });
|
|
2787
|
+
} catch (err) {
|
|
2788
|
+
writeLog(`before cleanup hook failed: ${formatFatalReason(err)}`);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2641
2791
|
clearInterval(runtimeHeartbeat);
|
|
2642
2792
|
try {
|
|
2643
2793
|
if (!isGlobalControllerProjectRoot(projectRoot)) {
|
|
2644
|
-
|
|
2794
|
+
if (reason === "global-runtime-idle") {
|
|
2795
|
+
markProjectDormant(projectRoot);
|
|
2796
|
+
} else {
|
|
2797
|
+
markProjectStopped(projectRoot);
|
|
2798
|
+
}
|
|
2645
2799
|
}
|
|
2646
2800
|
} catch {
|
|
2647
2801
|
// ignore cleanup errors
|
|
@@ -2649,9 +2803,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2649
2803
|
|
|
2650
2804
|
if (daemonCronController) {
|
|
2651
2805
|
daemonCronController.stopAll();
|
|
2652
|
-
daemonCronController = null;
|
|
2653
2806
|
}
|
|
2654
|
-
daemonGroupOrchestrator = null;
|
|
2655
2807
|
|
|
2656
2808
|
runtimeControlPlane.stop();
|
|
2657
2809
|
if (mcpHttpServer) {
|
|
@@ -2659,54 +2811,65 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2659
2811
|
writeLog(`MCP HTTP shutdown failed: ${formatFatalReason(err)}`);
|
|
2660
2812
|
});
|
|
2661
2813
|
}
|
|
2814
|
+
if (globalProjectRuntimeGateway && ownsGlobalProjectRuntimeGateway) {
|
|
2815
|
+
globalProjectRuntimeGateway.dispose();
|
|
2816
|
+
}
|
|
2817
|
+
globalProjectRuntimeGateway = null;
|
|
2662
2818
|
|
|
2663
|
-
//
|
|
2664
|
-
|
|
2819
|
+
// Project topology preserves the legacy parent-owned runner contract.
|
|
2820
|
+
// Global/hybrid runtimes detach so daemon restart does not terminate work.
|
|
2821
|
+
processManager.cleanup({
|
|
2822
|
+
terminate: runtimeContext.daemonTopology === "project",
|
|
2823
|
+
});
|
|
2665
2824
|
|
|
2666
2825
|
ipcServer.stop();
|
|
2667
2826
|
deliveryScheduler.stop();
|
|
2668
2827
|
busBridge.stop();
|
|
2669
|
-
|
|
2670
|
-
|
|
2828
|
+
projectRuntime.dispose();
|
|
2829
|
+
if (listenProjectSocket) removeSocket(projectRoot);
|
|
2830
|
+
if (manageProcessState) removePidIfOwned(projectRoot);
|
|
2671
2831
|
|
|
2672
2832
|
// 释放锁文件
|
|
2673
2833
|
try {
|
|
2674
2834
|
if (lockFd !== undefined) {
|
|
2675
2835
|
fs.closeSync(lockFd);
|
|
2676
2836
|
}
|
|
2677
|
-
|
|
2678
|
-
if (fs.existsSync(lockFile)) {
|
|
2837
|
+
if (manageProcessState && fs.existsSync(lockFile)) {
|
|
2679
2838
|
fs.unlinkSync(lockFile);
|
|
2680
2839
|
}
|
|
2681
2840
|
} catch {
|
|
2682
2841
|
// ignore cleanup errors
|
|
2683
2842
|
}
|
|
2843
|
+
try {
|
|
2844
|
+
logFile.end();
|
|
2845
|
+
} catch {
|
|
2846
|
+
// ignore log stream shutdown errors
|
|
2847
|
+
}
|
|
2684
2848
|
};
|
|
2685
2849
|
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
cleanup
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
});
|
|
2850
|
+
unregisterProcessLifecycle = registerDaemonRuntimeLifecycle(
|
|
2851
|
+
`${runtimeContext.projectId}:${runtimeContext.runtimeGeneration}`,
|
|
2852
|
+
{
|
|
2853
|
+
onBeforeExit: (code) => {
|
|
2854
|
+
logSync(`beforeExit code=${code}`);
|
|
2855
|
+
},
|
|
2856
|
+
onFatal: ({ kind, error }) => {
|
|
2857
|
+
logSync(`${kind}: ${formatFatalReason(error)}`);
|
|
2858
|
+
},
|
|
2859
|
+
cleanup,
|
|
2860
|
+
}
|
|
2861
|
+
);
|
|
2862
|
+
|
|
2863
|
+
return {
|
|
2864
|
+
context: runtimeContext,
|
|
2865
|
+
runtime: projectRuntime,
|
|
2866
|
+
cleanup,
|
|
2867
|
+
status: buildRuntimeStatus,
|
|
2868
|
+
handleRequest: handleIpcRequest,
|
|
2869
|
+
socket_path: listenProjectSocket
|
|
2870
|
+
? socketPath(projectRoot)
|
|
2871
|
+
: (registrySocketPath || ""),
|
|
2872
|
+
};
|
|
2710
2873
|
}
|
|
2711
2874
|
|
|
2712
2875
|
function stopDaemon(projectRoot, options = {}) {
|
|
@@ -182,6 +182,11 @@ class GlobalMcpHttpServer {
|
|
|
182
182
|
active_wait_count: activeWaits,
|
|
183
183
|
http_request_count: this.httpRequestCount,
|
|
184
184
|
started_at: this.startedAt || null,
|
|
185
|
+
project_runtime_manager:
|
|
186
|
+
this.projectRuntimeGateway
|
|
187
|
+
&& typeof this.projectRuntimeGateway.status === "function"
|
|
188
|
+
? this.projectRuntimeGateway.status()
|
|
189
|
+
: null,
|
|
185
190
|
};
|
|
186
191
|
}
|
|
187
192
|
|