u-foo 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ufoo.js +15 -7
- package/package.json +3 -2
- package/scripts/global-chat-switch-benchmark.js +406 -0
- package/src/chat/chatLogController.js +28 -5
- package/src/chat/commandExecutor.js +127 -3
- package/src/chat/commands.js +8 -0
- package/src/chat/daemonConnection.js +36 -1
- package/src/chat/daemonCoordinator.js +36 -0
- package/src/chat/daemonTransport.js +36 -5
- package/src/chat/dashboardKeyController.js +80 -1
- package/src/chat/dashboardView.js +289 -93
- package/src/chat/index.js +537 -37
- package/src/chat/inputHistoryController.js +33 -3
- package/src/chat/inputListenerController.js +22 -12
- package/src/chat/layout.js +12 -7
- package/src/chat/streamTracker.js +6 -0
- package/src/cli/onlineCoreCommands.js +6 -0
- package/src/cli.js +167 -4
- package/src/daemon/index.js +42 -2
- package/src/daemon/ops.js +199 -23
- package/src/online/bridge.js +11 -1
- package/src/online/runner.js +41 -9
- package/src/online/server.js +27 -0
- package/src/projects/projectId.js +29 -0
- package/src/projects/registry.js +279 -0
|
@@ -2,16 +2,18 @@ const fs = require("fs");
|
|
|
2
2
|
|
|
3
3
|
function createInputHistoryController(options = {}) {
|
|
4
4
|
const {
|
|
5
|
-
inputHistoryFile,
|
|
6
|
-
historyDir,
|
|
5
|
+
inputHistoryFile: inputHistoryFileOption,
|
|
6
|
+
historyDir: historyDirOption,
|
|
7
7
|
setInputValue = () => {},
|
|
8
8
|
getInputValue = () => "",
|
|
9
9
|
fsMod = fs,
|
|
10
10
|
} = options;
|
|
11
11
|
|
|
12
|
-
if (!
|
|
12
|
+
if (!inputHistoryFileOption || !historyDirOption) {
|
|
13
13
|
throw new Error("createInputHistoryController requires inputHistoryFile and historyDir");
|
|
14
14
|
}
|
|
15
|
+
let inputHistoryFile = inputHistoryFileOption;
|
|
16
|
+
let historyDir = historyDirOption;
|
|
15
17
|
|
|
16
18
|
const inputHistory = [];
|
|
17
19
|
let historyIndex = 0;
|
|
@@ -24,6 +26,9 @@ function createInputHistoryController(options = {}) {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function loadInputHistory(limit = 2000) {
|
|
29
|
+
inputHistory.length = 0;
|
|
30
|
+
historyIndex = 0;
|
|
31
|
+
historyDraft = "";
|
|
27
32
|
try {
|
|
28
33
|
const raw = fsMod.readFileSync(inputHistoryFile, "utf8");
|
|
29
34
|
const lines = String(raw || "").trim().split(/\r?\n/).filter(Boolean);
|
|
@@ -85,6 +90,28 @@ function createInputHistoryController(options = {}) {
|
|
|
85
90
|
setIndexToEnd();
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
function setHistoryTarget(next = {}) {
|
|
94
|
+
if (!next.inputHistoryFile || !next.historyDir) {
|
|
95
|
+
throw new Error("setHistoryTarget requires inputHistoryFile and historyDir");
|
|
96
|
+
}
|
|
97
|
+
inputHistoryFile = next.inputHistoryFile;
|
|
98
|
+
historyDir = next.historyDir;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function restoreDraft(draft = "") {
|
|
102
|
+
const nextDraft = String(draft || "");
|
|
103
|
+
setInputValue(nextDraft);
|
|
104
|
+
historyIndex = inputHistory.length;
|
|
105
|
+
historyDraft = nextDraft;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getDraftForPersistence() {
|
|
109
|
+
if (historyIndex === inputHistory.length) {
|
|
110
|
+
return String(getInputValue() || "");
|
|
111
|
+
}
|
|
112
|
+
return String(historyDraft || "");
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
return {
|
|
89
116
|
loadInputHistory,
|
|
90
117
|
updateDraftFromInput,
|
|
@@ -92,6 +119,9 @@ function createInputHistoryController(options = {}) {
|
|
|
92
119
|
historyDown,
|
|
93
120
|
commitSubmittedText,
|
|
94
121
|
setIndexToEnd,
|
|
122
|
+
setHistoryTarget,
|
|
123
|
+
restoreDraft,
|
|
124
|
+
getDraftForPersistence,
|
|
95
125
|
getState: () => ({
|
|
96
126
|
history: [...inputHistory],
|
|
97
127
|
historyIndex,
|
|
@@ -204,23 +204,33 @@ function createInputListenerController(options = {}) {
|
|
|
204
204
|
|
|
205
205
|
if (keyName === "up" || keyName === "down") {
|
|
206
206
|
const innerWidth = getWrapWidth();
|
|
207
|
-
if (innerWidth
|
|
208
|
-
|
|
209
|
-
const value = (textarea && textarea.value) || "";
|
|
210
|
-
const { row, col } = getCursorRowCol(value, cursorPos, innerWidth);
|
|
211
|
-
if (getPreferredCol() === null) setPreferredCol(col);
|
|
212
|
-
const totalRows = countLines(value, innerWidth);
|
|
213
|
-
|
|
214
|
-
if (keyName === "down" && row >= totalRows - 1) {
|
|
207
|
+
if (innerWidth <= 0) {
|
|
208
|
+
if (keyName === "down") {
|
|
215
209
|
enterDashboardMode();
|
|
216
210
|
return;
|
|
217
211
|
}
|
|
212
|
+
ensureInputCursorVisible();
|
|
213
|
+
updateCursor(textarea);
|
|
214
|
+
render(textarea);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
218
217
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
const cursorPos = getCursorPos();
|
|
219
|
+
const value = (textarea && textarea.value) || "";
|
|
220
|
+
const { row, col } = getCursorRowCol(value, cursorPos, innerWidth);
|
|
221
|
+
if (getPreferredCol() === null) setPreferredCol(col);
|
|
222
|
+
const totalRows = countLines(value, innerWidth);
|
|
223
|
+
|
|
224
|
+
if (keyName === "down" && row >= totalRows - 1) {
|
|
225
|
+
enterDashboardMode();
|
|
226
|
+
return;
|
|
223
227
|
}
|
|
228
|
+
|
|
229
|
+
const targetRow = keyName === "up"
|
|
230
|
+
? Math.max(0, row - 1)
|
|
231
|
+
: Math.min(totalRows - 1, row + 1);
|
|
232
|
+
setCursorPos(getCursorPosForRowCol(value, targetRow, getPreferredCol(), innerWidth));
|
|
233
|
+
|
|
224
234
|
ensureInputCursorVisible();
|
|
225
235
|
updateCursor(textarea);
|
|
226
236
|
render(textarea);
|
package/src/chat/layout.js
CHANGED
|
@@ -2,8 +2,13 @@ function createChatLayout(options = {}) {
|
|
|
2
2
|
const {
|
|
3
3
|
blessed,
|
|
4
4
|
currentInputHeight = 4,
|
|
5
|
+
dashboardHeight = 1,
|
|
5
6
|
version = "unknown",
|
|
6
7
|
} = options;
|
|
8
|
+
const normalizedDashboardHeight = Number.isFinite(dashboardHeight) && dashboardHeight > 0
|
|
9
|
+
? Math.floor(dashboardHeight)
|
|
10
|
+
: 1;
|
|
11
|
+
const reservedBottomLines = Math.max(2, currentInputHeight + 1);
|
|
7
12
|
|
|
8
13
|
const screen = blessed.screen({
|
|
9
14
|
smartCSR: true,
|
|
@@ -33,7 +38,7 @@ function createChatLayout(options = {}) {
|
|
|
33
38
|
top: 0,
|
|
34
39
|
left: 0,
|
|
35
40
|
width: "100%",
|
|
36
|
-
height:
|
|
41
|
+
height: `100%-${reservedBottomLines}`, // Will be adjusted dynamically
|
|
37
42
|
tags: true,
|
|
38
43
|
scrollable: true,
|
|
39
44
|
alwaysScroll: true,
|
|
@@ -97,7 +102,7 @@ function createChatLayout(options = {}) {
|
|
|
97
102
|
bottom: 0,
|
|
98
103
|
left: 0,
|
|
99
104
|
width: "100%",
|
|
100
|
-
height:
|
|
105
|
+
height: normalizedDashboardHeight,
|
|
101
106
|
style: { fg: "gray" },
|
|
102
107
|
tags: true,
|
|
103
108
|
});
|
|
@@ -105,7 +110,7 @@ function createChatLayout(options = {}) {
|
|
|
105
110
|
// Bottom border line for input area (above dashboard)
|
|
106
111
|
const inputBottomLine = blessed.line({
|
|
107
112
|
parent: screen,
|
|
108
|
-
bottom:
|
|
113
|
+
bottom: normalizedDashboardHeight,
|
|
109
114
|
left: 1,
|
|
110
115
|
width: "100%-2",
|
|
111
116
|
orientation: "horizontal",
|
|
@@ -115,10 +120,10 @@ function createChatLayout(options = {}) {
|
|
|
115
120
|
// Prompt indicator
|
|
116
121
|
const promptBox = blessed.box({
|
|
117
122
|
parent: screen,
|
|
118
|
-
bottom:
|
|
123
|
+
bottom: normalizedDashboardHeight + 1,
|
|
119
124
|
left: 0,
|
|
120
125
|
width: 2,
|
|
121
|
-
height: currentInputHeight -
|
|
126
|
+
height: Math.max(1, currentInputHeight - normalizedDashboardHeight - 2),
|
|
122
127
|
content: ">",
|
|
123
128
|
style: { fg: "cyan" },
|
|
124
129
|
});
|
|
@@ -126,10 +131,10 @@ function createChatLayout(options = {}) {
|
|
|
126
131
|
// Input area without left/right border
|
|
127
132
|
const input = blessed.textarea({
|
|
128
133
|
parent: screen,
|
|
129
|
-
bottom:
|
|
134
|
+
bottom: normalizedDashboardHeight + 1,
|
|
130
135
|
left: 2,
|
|
131
136
|
width: "100%-2",
|
|
132
|
-
height: currentInputHeight -
|
|
137
|
+
height: Math.max(1, currentInputHeight - normalizedDashboardHeight - 2),
|
|
133
138
|
inputOnFocus: true,
|
|
134
139
|
keys: true,
|
|
135
140
|
});
|
|
@@ -136,11 +136,17 @@ function createStreamTracker(options = {}) {
|
|
|
136
136
|
return true;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
function discardAll() {
|
|
140
|
+
streamStates.clear();
|
|
141
|
+
pendingDeliveries.clear();
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
return {
|
|
140
145
|
beginStream,
|
|
141
146
|
appendStreamDelta,
|
|
142
147
|
finalizeStream,
|
|
143
148
|
hasStream,
|
|
149
|
+
discardAll,
|
|
144
150
|
markPendingDelivery,
|
|
145
151
|
getPendingState,
|
|
146
152
|
consumePendingDelivery,
|
|
@@ -201,6 +201,8 @@ function runOnlineInbox(nickname, opts = {}) {
|
|
|
201
201
|
checkInbox(nickname, {
|
|
202
202
|
clear: !!opts.clear,
|
|
203
203
|
unread: !!opts.unread,
|
|
204
|
+
room: opts.room || "",
|
|
205
|
+
channel: opts.channel || "",
|
|
204
206
|
});
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -280,6 +282,8 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
|
|
|
280
282
|
return runOnlineInbox(payload.nickname, {
|
|
281
283
|
clear: opts.clear,
|
|
282
284
|
unread: opts.unread,
|
|
285
|
+
room: opts.room || "",
|
|
286
|
+
channel: opts.channel || "",
|
|
283
287
|
});
|
|
284
288
|
default:
|
|
285
289
|
throw createUnknownOnlineError(subcmd);
|
|
@@ -374,6 +378,8 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
|
|
|
374
378
|
return runOnlineInbox(argv[1], {
|
|
375
379
|
clear: hasFallbackFlag(argv, "--clear"),
|
|
376
380
|
unread: hasFallbackFlag(argv, "--unread"),
|
|
381
|
+
room: getFallbackOpt(argv, "--room"),
|
|
382
|
+
channel: getFallbackOpt(argv, "--channel"),
|
|
377
383
|
});
|
|
378
384
|
}
|
|
379
385
|
default:
|
package/src/cli.js
CHANGED
|
@@ -7,6 +7,9 @@ const { runBusCoreCommand } = require("./cli/busCoreCommands");
|
|
|
7
7
|
const { runCtxCommand } = require("./cli/ctxCoreCommands");
|
|
8
8
|
const { runOnlineCommand } = require("./cli/onlineCoreCommands");
|
|
9
9
|
const { runGroupCoreCommand } = require("./cli/groupCoreCommands");
|
|
10
|
+
const { listProjectRuntimes, getCurrentProjectRuntime } = require("./projects/registry");
|
|
11
|
+
const { canonicalProjectRoot, buildProjectId } = require("./projects/projectId");
|
|
12
|
+
const { getUfooPaths } = require("./ufoo/paths");
|
|
10
13
|
|
|
11
14
|
function getPackageRoot() {
|
|
12
15
|
return path.resolve(__dirname, "..");
|
|
@@ -161,6 +164,111 @@ function parseJsonObject(text, fallback = {}) {
|
|
|
161
164
|
return parsed;
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
function formatProjectRuntimeLine(row, index = 0) {
|
|
168
|
+
const idx = String(index + 1).padStart(2, " ");
|
|
169
|
+
const name = String(row.project_name || "-");
|
|
170
|
+
const status = String(row.status || "-");
|
|
171
|
+
const seen = row.last_seen || "-";
|
|
172
|
+
const projectRoot = String(row.project_root || "-");
|
|
173
|
+
return `${idx}. ${name} [${status}] last_seen=${seen} ${projectRoot}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function printProjectList(rows = [], write = (line) => console.log(line)) {
|
|
177
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
178
|
+
write("No projects found.");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
write("=== Projects ===");
|
|
182
|
+
rows.forEach((row, index) => {
|
|
183
|
+
write(formatProjectRuntimeLine(row, index));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildCurrentProjectFallback(projectRoot) {
|
|
188
|
+
let canonical = "";
|
|
189
|
+
let projectId = "";
|
|
190
|
+
try {
|
|
191
|
+
canonical = canonicalProjectRoot(projectRoot);
|
|
192
|
+
} catch {
|
|
193
|
+
canonical = path.resolve(projectRoot || process.cwd());
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
projectId = buildProjectId(canonical);
|
|
197
|
+
} catch {
|
|
198
|
+
projectId = "";
|
|
199
|
+
}
|
|
200
|
+
const paths = getUfooPaths(canonical);
|
|
201
|
+
return {
|
|
202
|
+
version: 1,
|
|
203
|
+
project_id: projectId || null,
|
|
204
|
+
project_root: canonical,
|
|
205
|
+
project_name: path.basename(canonical) || canonical,
|
|
206
|
+
daemon_pid: null,
|
|
207
|
+
socket_path: paths.ufooSock,
|
|
208
|
+
status: "untracked",
|
|
209
|
+
last_seen: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function printCurrentProject(runtime, write = (line) => console.log(line)) {
|
|
214
|
+
const row = runtime || {};
|
|
215
|
+
write("=== Current Project ===");
|
|
216
|
+
write(`name: ${row.project_name || "-"}`);
|
|
217
|
+
write(`status: ${row.status || "-"}`);
|
|
218
|
+
write(`path: ${row.project_root || "-"}`);
|
|
219
|
+
write(`daemon_pid: ${row.daemon_pid || "-"}`);
|
|
220
|
+
write(`socket: ${row.socket_path || "-"}`);
|
|
221
|
+
write(`last_seen: ${row.last_seen || "-"}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function projectSwitchV1Error() {
|
|
225
|
+
const err = new Error("project switch is chat-only in v1");
|
|
226
|
+
err.code = "UFOO_PROJECT_SWITCH_CHAT_ONLY";
|
|
227
|
+
err.exitCode = 2;
|
|
228
|
+
return err;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function runProjectCommand({
|
|
232
|
+
subcommand = "list",
|
|
233
|
+
outputJson = false,
|
|
234
|
+
cwd = process.cwd(),
|
|
235
|
+
write = (line) => console.log(line),
|
|
236
|
+
writeError = (line) => console.error(line),
|
|
237
|
+
} = {}) {
|
|
238
|
+
const sub = String(subcommand || "list").trim().toLowerCase();
|
|
239
|
+
try {
|
|
240
|
+
if (sub === "list") {
|
|
241
|
+
const rows = listProjectRuntimes({ validate: true, cleanupTmp: true });
|
|
242
|
+
if (outputJson) {
|
|
243
|
+
write(JSON.stringify(rows, null, 2));
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
printProjectList(rows, write);
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
if (sub === "current") {
|
|
250
|
+
const current = getCurrentProjectRuntime(cwd, { validate: true })
|
|
251
|
+
|| buildCurrentProjectFallback(cwd);
|
|
252
|
+
if (outputJson) {
|
|
253
|
+
write(JSON.stringify(current, null, 2));
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
printCurrentProject(current, write);
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
if (sub === "switch") {
|
|
260
|
+
const err = projectSwitchV1Error();
|
|
261
|
+
writeError(err.message);
|
|
262
|
+
return err.exitCode || 2;
|
|
263
|
+
}
|
|
264
|
+
writeError("project requires list|current|switch subcommand");
|
|
265
|
+
return 1;
|
|
266
|
+
} catch (err) {
|
|
267
|
+
writeError(err.message || String(err));
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
164
272
|
function normalizeReportPhase(action = "") {
|
|
165
273
|
const value = String(action || "").trim().toLowerCase();
|
|
166
274
|
if (value === "start") return "start";
|
|
@@ -244,9 +352,46 @@ async function runCli(argv) {
|
|
|
244
352
|
program
|
|
245
353
|
.command("chat")
|
|
246
354
|
.description("Launch ufoo chat UI")
|
|
247
|
-
.
|
|
355
|
+
.option("-g, --global", "Launch in global multi-project mode")
|
|
356
|
+
.action((opts) => {
|
|
248
357
|
const repoRoot = getPackageRoot();
|
|
249
|
-
|
|
358
|
+
const args = ["chat"];
|
|
359
|
+
if (opts.global === true) args.push("-g");
|
|
360
|
+
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), ...args]);
|
|
361
|
+
});
|
|
362
|
+
const project = program.command("project").description("Project runtime commands");
|
|
363
|
+
project
|
|
364
|
+
.command("list")
|
|
365
|
+
.description("List runtime projects discovered from global registry")
|
|
366
|
+
.option("--json", "Output as JSON")
|
|
367
|
+
.action((opts) => {
|
|
368
|
+
process.exitCode = runProjectCommand({
|
|
369
|
+
subcommand: "list",
|
|
370
|
+
outputJson: opts.json === true,
|
|
371
|
+
cwd: process.cwd(),
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
project
|
|
375
|
+
.command("current")
|
|
376
|
+
.description("Show current project runtime context")
|
|
377
|
+
.option("--json", "Output as JSON")
|
|
378
|
+
.action((opts) => {
|
|
379
|
+
process.exitCode = runProjectCommand({
|
|
380
|
+
subcommand: "current",
|
|
381
|
+
outputJson: opts.json === true,
|
|
382
|
+
cwd: process.cwd(),
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
project
|
|
386
|
+
.command("switch")
|
|
387
|
+
.description("Switch active project (chat-only in v1)")
|
|
388
|
+
.argument("<indexOrPath>", "Project index from list output or absolute path")
|
|
389
|
+
.action(() => {
|
|
390
|
+
process.exitCode = runProjectCommand({
|
|
391
|
+
subcommand: "switch",
|
|
392
|
+
outputJson: false,
|
|
393
|
+
cwd: process.cwd(),
|
|
394
|
+
});
|
|
250
395
|
});
|
|
251
396
|
program
|
|
252
397
|
.command("launch")
|
|
@@ -1112,7 +1257,11 @@ async function runCli(argv) {
|
|
|
1112
1257
|
console.log(" ufoo doctor");
|
|
1113
1258
|
console.log(" ufoo status");
|
|
1114
1259
|
console.log(" ufoo daemon --start|--stop|--status");
|
|
1115
|
-
console.log(" ufoo
|
|
1260
|
+
console.log(" ufoo -g");
|
|
1261
|
+
console.log(" ufoo chat [-g]");
|
|
1262
|
+
console.log(" ufoo project list [--json]");
|
|
1263
|
+
console.log(" ufoo project current [--json]");
|
|
1264
|
+
console.log(" ufoo project switch <index|path>");
|
|
1116
1265
|
console.log(" ufoo resume [nickname]");
|
|
1117
1266
|
console.log(" ufoo recover [list [target] | run <target>] [--json]");
|
|
1118
1267
|
console.log(" ufoo report <start|progress|done|error|list> [message] [--task <id>] [--agent <id>]");
|
|
@@ -1175,7 +1324,21 @@ async function runCli(argv) {
|
|
|
1175
1324
|
return;
|
|
1176
1325
|
}
|
|
1177
1326
|
if (cmd === "chat") {
|
|
1178
|
-
|
|
1327
|
+
const chatArgs = ["chat"];
|
|
1328
|
+
if (rest.includes("-g") || rest.includes("--global")) {
|
|
1329
|
+
chatArgs.push("-g");
|
|
1330
|
+
}
|
|
1331
|
+
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), ...chatArgs]);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (cmd === "project") {
|
|
1335
|
+
const sub = String(rest[0] || "list").trim().toLowerCase();
|
|
1336
|
+
const outputJson = rest.includes("--json");
|
|
1337
|
+
process.exitCode = runProjectCommand({
|
|
1338
|
+
subcommand: sub,
|
|
1339
|
+
outputJson,
|
|
1340
|
+
cwd: process.cwd(),
|
|
1341
|
+
});
|
|
1179
1342
|
return;
|
|
1180
1343
|
}
|
|
1181
1344
|
if (cmd === "resume") {
|
package/src/daemon/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const { generateInstanceId, subscriberToSafeName } = require("../bus/utils");
|
|
|
11
11
|
const { createDaemonIpcServer } = require("./ipcServer");
|
|
12
12
|
const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../shared/eventContract");
|
|
13
13
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
14
|
+
const { upsertProjectRuntime, markProjectStopped } = require("../projects/registry");
|
|
14
15
|
const { scheduleProviderSessionProbe, loadProviderSessionCache } = require("./providerSessions");
|
|
15
16
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
16
17
|
const { createDaemonCronController } = require("./cronOps");
|
|
@@ -25,6 +26,7 @@ let providerSessions = null;
|
|
|
25
26
|
let probeHandles = new Map();
|
|
26
27
|
let daemonCronController = null;
|
|
27
28
|
let daemonGroupOrchestrator = null;
|
|
29
|
+
const PROJECT_RUNTIME_HEARTBEAT_MS = 10 * 1000;
|
|
28
30
|
|
|
29
31
|
function sleep(ms) {
|
|
30
32
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -323,7 +325,10 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
323
325
|
continue;
|
|
324
326
|
}
|
|
325
327
|
// eslint-disable-next-line no-await-in-loop
|
|
326
|
-
const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager
|
|
328
|
+
const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager, {
|
|
329
|
+
launchScope: op.launch_scope || "",
|
|
330
|
+
terminalApp: op.terminal_app || "",
|
|
331
|
+
});
|
|
327
332
|
if (launchResult.mode === "internal" && launchResult.subscriberIds && launchResult.subscriberIds.length > 0) {
|
|
328
333
|
const probeAgentType = agent === "codex"
|
|
329
334
|
? "codex"
|
|
@@ -359,6 +364,7 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
359
364
|
agent,
|
|
360
365
|
count,
|
|
361
366
|
nickname: nickname || undefined,
|
|
367
|
+
launch_scope: launchResult.launchScope || undefined,
|
|
362
368
|
subscriber_ids: Array.isArray(launchResult.subscriberIds) ? launchResult.subscriberIds.slice() : [],
|
|
363
369
|
});
|
|
364
370
|
if (nickname) {
|
|
@@ -669,6 +675,19 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
669
675
|
const log = (msg) => {
|
|
670
676
|
logFile.write(`[daemon] ${new Date().toISOString()} ${msg}\n`);
|
|
671
677
|
};
|
|
678
|
+
const publishProjectRuntime = (status = "running") => {
|
|
679
|
+
try {
|
|
680
|
+
upsertProjectRuntime({
|
|
681
|
+
projectRoot,
|
|
682
|
+
daemonPid: process.pid,
|
|
683
|
+
socketPath: socketPath(projectRoot),
|
|
684
|
+
status,
|
|
685
|
+
lastSeen: new Date().toISOString(),
|
|
686
|
+
});
|
|
687
|
+
} catch (err) {
|
|
688
|
+
log(`project runtime update failed (${status}): ${err.message || err}`);
|
|
689
|
+
}
|
|
690
|
+
};
|
|
672
691
|
|
|
673
692
|
// 创建进程管理器 - daemon 作为父进程监控所有 internal agents
|
|
674
693
|
const processManager = new AgentProcessManager(projectRoot);
|
|
@@ -952,7 +971,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
952
971
|
}
|
|
953
972
|
if (req.type === IPC_REQUEST_TYPES.LAUNCH_AGENT) {
|
|
954
973
|
log(`launch_agent received: agent=${req.agent} count=${req.count}`);
|
|
955
|
-
const { agent, count, nickname } = req;
|
|
974
|
+
const { agent, count, nickname, launch_scope, terminal_app } = req;
|
|
956
975
|
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
957
976
|
if (!normalizedAgent) {
|
|
958
977
|
socket.write(
|
|
@@ -971,6 +990,8 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
971
990
|
agent: normalizedAgent,
|
|
972
991
|
count: finalCount,
|
|
973
992
|
nickname: nickname || "",
|
|
993
|
+
launch_scope: launch_scope || "",
|
|
994
|
+
terminal_app: terminal_app || "",
|
|
974
995
|
};
|
|
975
996
|
try {
|
|
976
997
|
const opsResults = await handleOps(projectRoot, [op], processManager);
|
|
@@ -1515,6 +1536,10 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1515
1536
|
};
|
|
1516
1537
|
|
|
1517
1538
|
ipcServer.listen(socketPath(projectRoot));
|
|
1539
|
+
publishProjectRuntime("running");
|
|
1540
|
+
const runtimeHeartbeat = setInterval(() => {
|
|
1541
|
+
publishProjectRuntime("running");
|
|
1542
|
+
}, PROJECT_RUNTIME_HEARTBEAT_MS);
|
|
1518
1543
|
|
|
1519
1544
|
log(`Started pid=${process.pid}`);
|
|
1520
1545
|
|
|
@@ -1619,8 +1644,17 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1619
1644
|
}, 1500);
|
|
1620
1645
|
}
|
|
1621
1646
|
|
|
1647
|
+
let cleanedUp = false;
|
|
1622
1648
|
const cleanup = () => {
|
|
1649
|
+
if (cleanedUp) return;
|
|
1650
|
+
cleanedUp = true;
|
|
1623
1651
|
log(`Shutting down daemon (managed agents: ${processManager.count()})`);
|
|
1652
|
+
clearInterval(runtimeHeartbeat);
|
|
1653
|
+
try {
|
|
1654
|
+
markProjectStopped(projectRoot);
|
|
1655
|
+
} catch {
|
|
1656
|
+
// ignore cleanup errors
|
|
1657
|
+
}
|
|
1624
1658
|
|
|
1625
1659
|
if (daemonCronController) {
|
|
1626
1660
|
daemonCronController.stopAll();
|
|
@@ -1706,6 +1740,12 @@ function stopDaemon(projectRoot) {
|
|
|
1706
1740
|
// ignore
|
|
1707
1741
|
}
|
|
1708
1742
|
|
|
1743
|
+
try {
|
|
1744
|
+
markProjectStopped(projectRoot);
|
|
1745
|
+
} catch {
|
|
1746
|
+
// ignore
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1709
1749
|
return killed;
|
|
1710
1750
|
}
|
|
1711
1751
|
|