u-foo 1.4.1 → 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.js +167 -4
- package/src/daemon/index.js +42 -2
- package/src/daemon/ops.js +199 -23
- package/src/projects/projectId.js +29 -0
- package/src/projects/registry.js +279 -0
package/bin/ufoo.js
CHANGED
|
@@ -6,24 +6,32 @@ const { runChat } = require("../src/chat");
|
|
|
6
6
|
const { runInternalRunner } = require("../src/agent/internalRunner");
|
|
7
7
|
const { runPtyRunner } = require("../src/agent/ptyRunner");
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const rawArgv = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
function hasGlobalModeFlag(args = []) {
|
|
12
|
+
return args.includes("-g") || args.includes("--global");
|
|
13
|
+
}
|
|
10
14
|
|
|
11
15
|
async function main() {
|
|
16
|
+
const globalMode = hasGlobalModeFlag(rawArgv);
|
|
17
|
+
const argv = rawArgv.filter((arg) => arg !== "-g" && arg !== "--global");
|
|
18
|
+
const cmd = argv[0];
|
|
19
|
+
|
|
12
20
|
if (!cmd) {
|
|
13
|
-
await runChat(process.cwd());
|
|
21
|
+
await runChat(process.cwd(), { globalMode });
|
|
14
22
|
return;
|
|
15
23
|
}
|
|
16
24
|
if (cmd === "daemon") {
|
|
17
|
-
runDaemonCli(
|
|
25
|
+
runDaemonCli(["daemon", ...argv.slice(1)]);
|
|
18
26
|
return;
|
|
19
27
|
}
|
|
20
28
|
if (cmd === "agent-runner") {
|
|
21
|
-
const agentType =
|
|
29
|
+
const agentType = argv[1] || "codex";
|
|
22
30
|
await runInternalRunner({ projectRoot: process.cwd(), agentType });
|
|
23
31
|
return;
|
|
24
32
|
}
|
|
25
33
|
if (cmd === "agent-pty-runner") {
|
|
26
|
-
const agentType =
|
|
34
|
+
const agentType = argv[1] || "codex";
|
|
27
35
|
try {
|
|
28
36
|
await runPtyRunner({ projectRoot: process.cwd(), agentType });
|
|
29
37
|
} catch (err) {
|
|
@@ -39,13 +47,13 @@ async function main() {
|
|
|
39
47
|
return;
|
|
40
48
|
}
|
|
41
49
|
if (cmd === "chat") {
|
|
42
|
-
await runChat(process.cwd());
|
|
50
|
+
await runChat(process.cwd(), { globalMode });
|
|
43
51
|
return;
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
// Handle resume command to resume/launch agent sessions
|
|
47
55
|
if (cmd === "resume") {
|
|
48
|
-
const target =
|
|
56
|
+
const target = argv[1];
|
|
49
57
|
if (!target) {
|
|
50
58
|
console.error("Error: resume requires an agent type or nickname");
|
|
51
59
|
console.error("Usage: ufoo resume <ucode|uclaude|ucodex|nickname>");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u-foo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://ufoo.dev",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"postinstall": "node scripts/postinstall.js",
|
|
44
44
|
"test": "jest",
|
|
45
45
|
"test:watch": "jest --watch",
|
|
46
|
-
"test:coverage": "jest --coverage"
|
|
46
|
+
"test:coverage": "jest --coverage",
|
|
47
|
+
"bench:global-switch": "node scripts/global-chat-switch-benchmark.js"
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"blessed": "^0.1.81",
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawn, spawnSync } = require("child_process");
|
|
6
|
+
const UfooInit = require("../src/init");
|
|
7
|
+
const { socketPath, isRunning } = require("../src/daemon");
|
|
8
|
+
const { connectWithRetry } = require("../src/chat/transport");
|
|
9
|
+
const { createDaemonTransport } = require("../src/chat/daemonTransport");
|
|
10
|
+
const { createDaemonCoordinator } = require("../src/chat/daemonCoordinator");
|
|
11
|
+
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseIntArg(argv, flag, fallback) {
|
|
17
|
+
const idx = argv.indexOf(flag);
|
|
18
|
+
if (idx < 0 || idx + 1 >= argv.length) return fallback;
|
|
19
|
+
const parsed = Number.parseInt(String(argv[idx + 1] || ""), 10);
|
|
20
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseStringArg(argv, flag, fallback) {
|
|
24
|
+
const idx = argv.indexOf(flag);
|
|
25
|
+
if (idx < 0 || idx + 1 >= argv.length) return fallback;
|
|
26
|
+
const value = String(argv[idx + 1] || "").trim();
|
|
27
|
+
return value || fallback;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasFlag(argv, flag) {
|
|
31
|
+
return argv.includes(flag);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function percentile(sortedValues, p) {
|
|
35
|
+
if (!Array.isArray(sortedValues) || sortedValues.length === 0) return 0;
|
|
36
|
+
const clamped = Math.max(0, Math.min(1, p));
|
|
37
|
+
const idx = Math.ceil(clamped * sortedValues.length) - 1;
|
|
38
|
+
const safeIdx = Math.max(0, Math.min(sortedValues.length - 1, idx));
|
|
39
|
+
return sortedValues[safeIdx];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function summarizeDurations(values) {
|
|
43
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
44
|
+
return {
|
|
45
|
+
count: 0,
|
|
46
|
+
minMs: 0,
|
|
47
|
+
maxMs: 0,
|
|
48
|
+
avgMs: 0,
|
|
49
|
+
p50Ms: 0,
|
|
50
|
+
p95Ms: 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
54
|
+
const total = values.reduce((sum, n) => sum + n, 0);
|
|
55
|
+
return {
|
|
56
|
+
count: values.length,
|
|
57
|
+
minMs: sorted[0],
|
|
58
|
+
maxMs: sorted[sorted.length - 1],
|
|
59
|
+
avgMs: total / values.length,
|
|
60
|
+
p50Ms: percentile(sorted, 0.5),
|
|
61
|
+
p95Ms: percentile(sorted, 0.95),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeProjectRootForCompare(projectRoot) {
|
|
66
|
+
const raw = String(projectRoot || "").trim();
|
|
67
|
+
if (!raw) return "";
|
|
68
|
+
try {
|
|
69
|
+
return fs.realpathSync.native(raw);
|
|
70
|
+
} catch {
|
|
71
|
+
return path.resolve(raw);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function waitForDaemonReady(projectRoot, timeoutMs = 20000) {
|
|
76
|
+
const startedAt = Date.now();
|
|
77
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
78
|
+
if (isRunning(projectRoot)) {
|
|
79
|
+
const client = await connectWithRetry(socketPath(projectRoot), 1, 0);
|
|
80
|
+
if (client) {
|
|
81
|
+
try {
|
|
82
|
+
client.end();
|
|
83
|
+
client.destroy();
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// eslint-disable-next-line no-await-in-loop
|
|
91
|
+
await sleep(150);
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function waitForDaemonStopped(projectRoot, timeoutMs = 10000) {
|
|
97
|
+
const startedAt = Date.now();
|
|
98
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
99
|
+
if (!isRunning(projectRoot)) return true;
|
|
100
|
+
// eslint-disable-next-line no-await-in-loop
|
|
101
|
+
await sleep(120);
|
|
102
|
+
}
|
|
103
|
+
return !isRunning(projectRoot);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createStatusWaiter() {
|
|
107
|
+
let latestProjectRoot = "";
|
|
108
|
+
const waiting = new Set();
|
|
109
|
+
const seen = [];
|
|
110
|
+
|
|
111
|
+
function settle(targetRoot, ok, error) {
|
|
112
|
+
for (const waiter of Array.from(waiting)) {
|
|
113
|
+
if (waiter.targetRoot !== targetRoot) continue;
|
|
114
|
+
waiting.delete(waiter);
|
|
115
|
+
if (ok) waiter.resolve(targetRoot);
|
|
116
|
+
else waiter.reject(error || new Error(`status wait failed: ${targetRoot}`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleMessage(msg) {
|
|
121
|
+
const type = msg && msg.type ? String(msg.type) : "";
|
|
122
|
+
seen.push({
|
|
123
|
+
ts: Date.now(),
|
|
124
|
+
type: type || "unknown",
|
|
125
|
+
projectRoot: msg && msg.data && msg.data.projectRoot ? String(msg.data.projectRoot) : "",
|
|
126
|
+
});
|
|
127
|
+
if (seen.length > 50) {
|
|
128
|
+
seen.shift();
|
|
129
|
+
}
|
|
130
|
+
if (!msg || msg.type !== "status") return false;
|
|
131
|
+
const rootRaw = msg.data && msg.data.projectRoot ? String(msg.data.projectRoot) : "";
|
|
132
|
+
const root = normalizeProjectRootForCompare(rootRaw);
|
|
133
|
+
if (!root) return false;
|
|
134
|
+
latestProjectRoot = root;
|
|
135
|
+
settle(root, true);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function waitForProject(targetRoot, timeoutMs = 5000) {
|
|
140
|
+
const normalizedTarget = normalizeProjectRootForCompare(targetRoot);
|
|
141
|
+
if (!normalizedTarget) {
|
|
142
|
+
return Promise.reject(new Error("invalid target root for status wait"));
|
|
143
|
+
}
|
|
144
|
+
if (latestProjectRoot === normalizedTarget) return Promise.resolve(normalizedTarget);
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const waiter = { targetRoot: normalizedTarget, resolve, reject, timer: null };
|
|
147
|
+
waiter.timer = setTimeout(() => {
|
|
148
|
+
waiting.delete(waiter);
|
|
149
|
+
const seenTail = seen.slice(-5)
|
|
150
|
+
.map((entry) => `${entry.type}:${entry.projectRoot || "-"}`)
|
|
151
|
+
.join(", ");
|
|
152
|
+
reject(new Error(`timeout waiting status for ${normalizedTarget}; seen=[${seenTail}]`));
|
|
153
|
+
}, timeoutMs);
|
|
154
|
+
const wrappedResolve = (value) => {
|
|
155
|
+
clearTimeout(waiter.timer);
|
|
156
|
+
resolve(value);
|
|
157
|
+
};
|
|
158
|
+
const wrappedReject = (err) => {
|
|
159
|
+
clearTimeout(waiter.timer);
|
|
160
|
+
reject(err);
|
|
161
|
+
};
|
|
162
|
+
waiting.add({
|
|
163
|
+
targetRoot: normalizedTarget,
|
|
164
|
+
resolve: wrappedResolve,
|
|
165
|
+
reject: wrappedReject,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function clearAll(err) {
|
|
171
|
+
for (const waiter of Array.from(waiting)) {
|
|
172
|
+
waiting.delete(waiter);
|
|
173
|
+
waiter.reject(err || new Error("status waiter cleared"));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
handleMessage,
|
|
179
|
+
waitForProject,
|
|
180
|
+
clearAll,
|
|
181
|
+
getSeen: () => seen.slice(),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function main() {
|
|
186
|
+
const argv = process.argv.slice(2);
|
|
187
|
+
const switches = parseIntArg(argv, "--switches", 50);
|
|
188
|
+
const keepTmp = hasFlag(argv, "--keep-tmp");
|
|
189
|
+
const jsonOnly = hasFlag(argv, "--json");
|
|
190
|
+
const tempParent = parseStringArg(argv, "--tmp-root", "/tmp");
|
|
191
|
+
|
|
192
|
+
const tempRoot = fs.mkdtempSync(path.join(tempParent, "ufoo-global-switch-bench-"));
|
|
193
|
+
const projectA = path.join(tempRoot, "project-a");
|
|
194
|
+
const projectB = path.join(tempRoot, "project-b");
|
|
195
|
+
fs.mkdirSync(projectA, { recursive: true });
|
|
196
|
+
fs.mkdirSync(projectB, { recursive: true });
|
|
197
|
+
|
|
198
|
+
let coordinator = null;
|
|
199
|
+
const statusWaiter = createStatusWaiter();
|
|
200
|
+
const daemonProcesses = new Map();
|
|
201
|
+
let exitCode = 0;
|
|
202
|
+
const errors = [];
|
|
203
|
+
const daemonBin = path.resolve(__dirname, "..", "bin", "ufoo.js");
|
|
204
|
+
|
|
205
|
+
function startManagedDaemon(projectRoot) {
|
|
206
|
+
const existing = daemonProcesses.get(projectRoot);
|
|
207
|
+
if (existing && !existing.child.killed && existing.child.exitCode === null) {
|
|
208
|
+
return existing.child;
|
|
209
|
+
}
|
|
210
|
+
const child = spawn(process.execPath, [daemonBin, "daemon", "start"], {
|
|
211
|
+
cwd: projectRoot,
|
|
212
|
+
env: { ...process.env, UFOO_DAEMON_CHILD: "1" },
|
|
213
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
214
|
+
});
|
|
215
|
+
const logs = { stdout: "", stderr: "" };
|
|
216
|
+
child.stdout.on("data", (chunk) => {
|
|
217
|
+
logs.stdout += String(chunk || "");
|
|
218
|
+
if (logs.stdout.length > 8000) logs.stdout = logs.stdout.slice(-8000);
|
|
219
|
+
});
|
|
220
|
+
child.stderr.on("data", (chunk) => {
|
|
221
|
+
logs.stderr += String(chunk || "");
|
|
222
|
+
if (logs.stderr.length > 8000) logs.stderr = logs.stderr.slice(-8000);
|
|
223
|
+
});
|
|
224
|
+
daemonProcesses.set(projectRoot, { child, logs });
|
|
225
|
+
return child;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function stopManagedDaemon(projectRoot) {
|
|
229
|
+
try {
|
|
230
|
+
spawnSync(process.execPath, [daemonBin, "daemon", "stop"], {
|
|
231
|
+
cwd: projectRoot,
|
|
232
|
+
stdio: "ignore",
|
|
233
|
+
});
|
|
234
|
+
} catch {
|
|
235
|
+
// ignore
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const init = new UfooInit(path.resolve(__dirname, ".."));
|
|
241
|
+
await init.init({ modules: "context,bus", project: projectA });
|
|
242
|
+
await init.init({ modules: "context,bus", project: projectB });
|
|
243
|
+
|
|
244
|
+
startManagedDaemon(projectA);
|
|
245
|
+
startManagedDaemon(projectB);
|
|
246
|
+
const readyA = await waitForDaemonReady(projectA);
|
|
247
|
+
const readyB = await waitForDaemonReady(projectB);
|
|
248
|
+
if (!readyA || !readyB) {
|
|
249
|
+
const aMeta = daemonProcesses.get(projectA);
|
|
250
|
+
const bMeta = daemonProcesses.get(projectB);
|
|
251
|
+
const aErr = aMeta ? aMeta.logs.stderr || aMeta.logs.stdout : "";
|
|
252
|
+
const bErr = bMeta ? bMeta.logs.stderr || bMeta.logs.stdout : "";
|
|
253
|
+
if (aErr) errors.push(`daemon A log: ${aErr.trim().slice(-400)}`);
|
|
254
|
+
if (bErr) errors.push(`daemon B log: ${bErr.trim().slice(-400)}`);
|
|
255
|
+
throw new Error(`daemon readiness failed: A=${readyA} B=${readyB}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const transport = createDaemonTransport({
|
|
259
|
+
projectRoot: projectA,
|
|
260
|
+
sockPath: socketPath(projectA),
|
|
261
|
+
isRunning,
|
|
262
|
+
startDaemon: startManagedDaemon,
|
|
263
|
+
connectWithRetry,
|
|
264
|
+
primaryRetries: 12,
|
|
265
|
+
secondaryRetries: 20,
|
|
266
|
+
retryDelayMs: 80,
|
|
267
|
+
restartDelayMs: 600,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
coordinator = createDaemonCoordinator({
|
|
271
|
+
projectRoot: projectA,
|
|
272
|
+
daemonTransport: transport,
|
|
273
|
+
handleMessage: statusWaiter.handleMessage,
|
|
274
|
+
queueStatusLine: () => {},
|
|
275
|
+
resolveStatusLine: () => {},
|
|
276
|
+
logMessage: () => {},
|
|
277
|
+
stopDaemon: stopManagedDaemon,
|
|
278
|
+
startDaemon: startManagedDaemon,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const connected = await coordinator.connect();
|
|
282
|
+
if (!connected) {
|
|
283
|
+
throw new Error("initial coordinator.connect() failed");
|
|
284
|
+
}
|
|
285
|
+
coordinator.requestStatus();
|
|
286
|
+
await statusWaiter.waitForProject(projectA, 5000);
|
|
287
|
+
|
|
288
|
+
const durations = [];
|
|
289
|
+
let routingChecksPassed = 0;
|
|
290
|
+
|
|
291
|
+
for (let i = 0; i < switches; i += 1) {
|
|
292
|
+
const targetRoot = i % 2 === 0 ? projectB : projectA;
|
|
293
|
+
const startedNs = process.hrtime.bigint();
|
|
294
|
+
// eslint-disable-next-line no-await-in-loop
|
|
295
|
+
const result = await coordinator.switchProject({
|
|
296
|
+
projectRoot: targetRoot,
|
|
297
|
+
sockPath: socketPath(targetRoot),
|
|
298
|
+
});
|
|
299
|
+
const durationMs = Number(process.hrtime.bigint() - startedNs) / 1e6;
|
|
300
|
+
durations.push(durationMs);
|
|
301
|
+
if (!result || result.ok !== true) {
|
|
302
|
+
errors.push(`switch ${i + 1} failed: ${(result && result.error) || "unknown"}`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
// eslint-disable-next-line no-await-in-loop
|
|
307
|
+
await statusWaiter.waitForProject(targetRoot, 5000);
|
|
308
|
+
routingChecksPassed += 1;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
errors.push(`switch ${i + 1} status mismatch: ${err.message || err}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const summary = summarizeDurations(durations);
|
|
315
|
+
const thresholds = {
|
|
316
|
+
p50MsLt500: summary.p50Ms < 500,
|
|
317
|
+
p95MsLt1200: summary.p95Ms < 1200,
|
|
318
|
+
};
|
|
319
|
+
const routeOk = routingChecksPassed === switches;
|
|
320
|
+
const pass = routeOk && thresholds.p50MsLt500 && thresholds.p95MsLt1200 && errors.length === 0;
|
|
321
|
+
exitCode = pass ? 0 : 2;
|
|
322
|
+
|
|
323
|
+
const report = {
|
|
324
|
+
switches,
|
|
325
|
+
routingChecksPassed,
|
|
326
|
+
routeOk,
|
|
327
|
+
summary,
|
|
328
|
+
thresholds,
|
|
329
|
+
pass,
|
|
330
|
+
tempRoot,
|
|
331
|
+
errors,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (jsonOnly) {
|
|
335
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
336
|
+
} else {
|
|
337
|
+
process.stdout.write("=== Global Chat Switch Benchmark ===\n");
|
|
338
|
+
process.stdout.write(`tempRoot: ${tempRoot}\n`);
|
|
339
|
+
process.stdout.write(`switches: ${switches}\n`);
|
|
340
|
+
process.stdout.write(`routing checks: ${routingChecksPassed}/${switches} (${routeOk ? "PASS" : "FAIL"})\n`);
|
|
341
|
+
process.stdout.write(
|
|
342
|
+
`latency ms: min=${summary.minMs.toFixed(2)} avg=${summary.avgMs.toFixed(2)} ` +
|
|
343
|
+
`p50=${summary.p50Ms.toFixed(2)} p95=${summary.p95Ms.toFixed(2)} max=${summary.maxMs.toFixed(2)}\n`
|
|
344
|
+
);
|
|
345
|
+
process.stdout.write(
|
|
346
|
+
`thresholds: p50<500=${thresholds.p50MsLt500 ? "PASS" : "FAIL"} ` +
|
|
347
|
+
`p95<1200=${thresholds.p95MsLt1200 ? "PASS" : "FAIL"}\n`
|
|
348
|
+
);
|
|
349
|
+
if (errors.length > 0) {
|
|
350
|
+
process.stdout.write("errors:\n");
|
|
351
|
+
errors.forEach((line) => process.stdout.write(`- ${line}\n`));
|
|
352
|
+
}
|
|
353
|
+
process.stdout.write(`overall: ${pass ? "PASS" : "FAIL"}\n`);
|
|
354
|
+
}
|
|
355
|
+
} finally {
|
|
356
|
+
statusWaiter.clearAll(new Error("benchmark teardown"));
|
|
357
|
+
if (coordinator) {
|
|
358
|
+
try {
|
|
359
|
+
coordinator.close();
|
|
360
|
+
} catch {
|
|
361
|
+
// ignore
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
stopManagedDaemon(projectA);
|
|
366
|
+
} catch {
|
|
367
|
+
// ignore
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
stopManagedDaemon(projectB);
|
|
371
|
+
} catch {
|
|
372
|
+
// ignore
|
|
373
|
+
}
|
|
374
|
+
for (const [projectRoot, meta] of daemonProcesses.entries()) {
|
|
375
|
+
const child = meta && meta.child;
|
|
376
|
+
if (!child || child.exitCode !== null) continue;
|
|
377
|
+
try {
|
|
378
|
+
child.kill("SIGTERM");
|
|
379
|
+
} catch {
|
|
380
|
+
// ignore
|
|
381
|
+
}
|
|
382
|
+
// Ensure child cannot leak if SIGTERM is ignored.
|
|
383
|
+
await sleep(80);
|
|
384
|
+
if (child.exitCode === null) {
|
|
385
|
+
try {
|
|
386
|
+
child.kill("SIGKILL");
|
|
387
|
+
} catch {
|
|
388
|
+
// ignore
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
daemonProcesses.delete(projectRoot);
|
|
392
|
+
}
|
|
393
|
+
await waitForDaemonStopped(projectA, 8000);
|
|
394
|
+
await waitForDaemonStopped(projectB, 8000);
|
|
395
|
+
if (!keepTmp) {
|
|
396
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
process.exit(exitCode);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
main().catch((err) => {
|
|
404
|
+
process.stderr.write(`${err && err.stack ? err.stack : err}\n`);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
});
|
|
@@ -2,8 +2,8 @@ function createChatLogController(options = {}) {
|
|
|
2
2
|
const {
|
|
3
3
|
logBox,
|
|
4
4
|
fsModule,
|
|
5
|
-
historyDir,
|
|
6
|
-
historyFile,
|
|
5
|
+
historyDir: historyDirOption,
|
|
6
|
+
historyFile: historyFileOption,
|
|
7
7
|
now = () => new Date().toISOString(),
|
|
8
8
|
} = options;
|
|
9
9
|
|
|
@@ -13,9 +13,11 @@ function createChatLogController(options = {}) {
|
|
|
13
13
|
if (!fsModule) {
|
|
14
14
|
throw new Error("createChatLogController requires fsModule");
|
|
15
15
|
}
|
|
16
|
-
if (!
|
|
16
|
+
if (!historyDirOption || !historyFileOption) {
|
|
17
17
|
throw new Error("createChatLogController requires historyDir/historyFile");
|
|
18
18
|
}
|
|
19
|
+
let historyDir = historyDirOption;
|
|
20
|
+
let historyFile = historyFileOption;
|
|
19
21
|
|
|
20
22
|
const SPACED_TYPES = new Set(["user", "reply", "bus", "dispatch", "error"]);
|
|
21
23
|
let lastLogWasSpacer = false;
|
|
@@ -95,9 +97,28 @@ function createChatLogController(options = {}) {
|
|
|
95
97
|
recordLog(item.type || "unknown", item.text, item.meta || {}, false);
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
} catch {
|
|
99
|
-
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err && err.code === "ENOENT") {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (err && typeof console !== "undefined" && typeof console.warn === "function") {
|
|
105
|
+
console.warn(`chat history load failed (${historyFile}): ${err.message || err}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setHistoryTarget(next = {}) {
|
|
111
|
+
if (!next.historyDir || !next.historyFile) {
|
|
112
|
+
throw new Error("setHistoryTarget requires historyDir/historyFile");
|
|
100
113
|
}
|
|
114
|
+
historyDir = next.historyDir;
|
|
115
|
+
historyFile = next.historyFile;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resetViewState() {
|
|
119
|
+
// Callers are expected to clear logBox separately; this only resets spacing trackers.
|
|
120
|
+
lastLogWasSpacer = false;
|
|
121
|
+
hasLoggedAny = false;
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
return {
|
|
@@ -107,6 +128,8 @@ function createChatLogController(options = {}) {
|
|
|
107
128
|
logMessage,
|
|
108
129
|
markStreamStart,
|
|
109
130
|
loadHistory,
|
|
131
|
+
setHistoryTarget,
|
|
132
|
+
resetViewState,
|
|
110
133
|
};
|
|
111
134
|
}
|
|
112
135
|
|