@wrongstack/webui 0.148.0 → 0.236.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/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/assets/css.worker-CvXBzhp8.js +89 -0
- package/dist/assets/html.worker-BO6WuOEO.js +502 -0
- package/dist/assets/index-Cm_R0cfw.css +2 -0
- package/dist/assets/index-DIwGHhP2.js +163 -0
- package/dist/assets/json.worker-BkJRGcCJ.js +58 -0
- package/dist/assets/ts.worker-B0J26iPs.js +67734 -0
- package/dist/assets/vendor-BtOIO1oa.js +1303 -0
- package/dist/assets/vendor-CEQg2uSG.css +1 -0
- package/dist/index.css +103 -0
- package/dist/index.css.map +1 -0
- package/dist/index.html +4 -4
- package/dist/index.js +12701 -5772
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +1742 -203
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.d.ts +170 -2
- package/dist/server/index.js +1740 -202
- package/dist/server/index.js.map +1 -1
- package/package.json +35 -30
- package/dist/assets/index-DZfZgZld.js +0 -90
- package/dist/assets/index-DegAHH7h.css +0 -2
- package/dist/assets/vendor-CHXeWZ2s.js +0 -81
- package/dist/assets/vendor-XkZLp0g1.css +0 -1
package/dist/server/entry.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
2
9
|
// src/server/index.ts
|
|
3
|
-
import { expectDefined as expectDefined2 } from "@wrongstack/core";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
10
|
+
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker } from "@wrongstack/core";
|
|
11
|
+
import { makeMailboxTool, makeMailSendTool, makeMailInboxTool, mailboxSessionTag } from "@wrongstack/core";
|
|
12
|
+
import {
|
|
13
|
+
BrainMonitor,
|
|
14
|
+
DefaultBrainArbiter,
|
|
15
|
+
ObservableBrainArbiter,
|
|
16
|
+
createAutonomyBrain,
|
|
17
|
+
createTieredBrainArbiter
|
|
18
|
+
} from "@wrongstack/core";
|
|
19
|
+
import * as fs6 from "fs/promises";
|
|
20
|
+
import * as path8 from "path";
|
|
6
21
|
|
|
7
22
|
// src/server/http-server.ts
|
|
8
23
|
import * as fs from "fs/promises";
|
|
@@ -28,7 +43,7 @@ function injectWsPort(html, wsPort) {
|
|
|
28
43
|
${html}`;
|
|
29
44
|
}
|
|
30
45
|
function buildCspHeader(wsPort) {
|
|
31
|
-
return `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort} wss://127.0.0.1:${wsPort} ws://[::1]:${wsPort} wss://[::1]:${wsPort}; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
|
|
46
|
+
return `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort} wss://127.0.0.1:${wsPort} ws://[::1]:${wsPort} wss://[::1]:${wsPort}; img-src 'self' data:; font-src 'self' data:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
|
|
32
47
|
}
|
|
33
48
|
function isInsideDist(candidate, distDir) {
|
|
34
49
|
const root = path.resolve(distDir);
|
|
@@ -42,6 +57,15 @@ function createHttpServer(opts) {
|
|
|
42
57
|
return http.createServer(async (req, res) => {
|
|
43
58
|
try {
|
|
44
59
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
60
|
+
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
61
|
+
await handleApiSessions(res, opts.globalRoot);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const agentsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/agents$/);
|
|
65
|
+
if (agentsMatch && req.method === "GET") {
|
|
66
|
+
await handleApiSessionAgents(res, opts.globalRoot, agentsMatch[1]);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
45
69
|
let filePath;
|
|
46
70
|
if (url.pathname === "/" || url.pathname === "") {
|
|
47
71
|
filePath = path.join(distDir, "index.html");
|
|
@@ -98,6 +122,84 @@ function createHttpServer(opts) {
|
|
|
98
122
|
}
|
|
99
123
|
});
|
|
100
124
|
}
|
|
125
|
+
async function handleApiSessions(res, globalRoot) {
|
|
126
|
+
if (!globalRoot) {
|
|
127
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
128
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const { SessionRegistry } = await import("@wrongstack/core");
|
|
133
|
+
const registry = new SessionRegistry(globalRoot);
|
|
134
|
+
const sessions = await registry.list();
|
|
135
|
+
const result = sessions.map((s) => ({
|
|
136
|
+
sessionId: s.sessionId,
|
|
137
|
+
projectSlug: s.projectSlug,
|
|
138
|
+
projectName: s.projectName,
|
|
139
|
+
projectRoot: s.projectRoot,
|
|
140
|
+
workingDir: s.workingDir,
|
|
141
|
+
status: s.status,
|
|
142
|
+
pid: s.pid,
|
|
143
|
+
startedAt: s.startedAt,
|
|
144
|
+
lastHeartbeatAt: s.lastHeartbeatAt,
|
|
145
|
+
agentCount: s.agentCount,
|
|
146
|
+
agents: s.agents.map((a) => ({
|
|
147
|
+
id: a.id,
|
|
148
|
+
name: a.name,
|
|
149
|
+
status: a.status,
|
|
150
|
+
currentTool: a.currentTool,
|
|
151
|
+
iterations: a.iterations,
|
|
152
|
+
toolCalls: a.toolCalls,
|
|
153
|
+
lastActivityAt: a.lastActivityAt
|
|
154
|
+
}))
|
|
155
|
+
}));
|
|
156
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
157
|
+
res.end(JSON.stringify(result));
|
|
158
|
+
} catch (err) {
|
|
159
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
160
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
164
|
+
if (!globalRoot) {
|
|
165
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
166
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const { SessionRegistry } = await import("@wrongstack/core");
|
|
171
|
+
const registry = new SessionRegistry(globalRoot);
|
|
172
|
+
const entry = await registry.get(sessionId);
|
|
173
|
+
if (!entry) {
|
|
174
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
175
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
179
|
+
res.end(JSON.stringify({
|
|
180
|
+
sessionId: entry.sessionId,
|
|
181
|
+
projectName: entry.projectName,
|
|
182
|
+
status: entry.status,
|
|
183
|
+
agents: entry.agents.map((a) => ({
|
|
184
|
+
id: a.id,
|
|
185
|
+
name: a.name,
|
|
186
|
+
status: a.status,
|
|
187
|
+
currentTool: a.currentTool,
|
|
188
|
+
iterations: a.iterations,
|
|
189
|
+
toolCalls: a.toolCalls,
|
|
190
|
+
lastActivityAt: a.lastActivityAt
|
|
191
|
+
}))
|
|
192
|
+
}));
|
|
193
|
+
} catch (err) {
|
|
194
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
195
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/server/file-handlers.ts
|
|
200
|
+
import * as fs2 from "fs/promises";
|
|
201
|
+
import * as path2 from "path";
|
|
202
|
+
import { atomicWrite } from "@wrongstack/core";
|
|
101
203
|
|
|
102
204
|
// src/server/file-picker.ts
|
|
103
205
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -147,6 +249,193 @@ function rankFiles(paths, query, limit) {
|
|
|
147
249
|
return scored.slice(0, limit).map((s) => s.path);
|
|
148
250
|
}
|
|
149
251
|
|
|
252
|
+
// src/server/ws-utils.ts
|
|
253
|
+
import { randomBytes } from "crypto";
|
|
254
|
+
import { WebSocket } from "ws";
|
|
255
|
+
function send(ws, msg) {
|
|
256
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
257
|
+
ws.send(JSON.stringify(msg));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function broadcast(clients, msg) {
|
|
261
|
+
const data = JSON.stringify(msg);
|
|
262
|
+
for (const [ws] of clients) {
|
|
263
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
264
|
+
try {
|
|
265
|
+
ws.send(data);
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function sendResult(ws, success, message) {
|
|
272
|
+
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
273
|
+
}
|
|
274
|
+
function errMessage(err) {
|
|
275
|
+
return err instanceof Error ? err.message : String(err);
|
|
276
|
+
}
|
|
277
|
+
function generateAuthToken() {
|
|
278
|
+
return randomBytes(16).toString("hex");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/server/file-handlers.ts
|
|
282
|
+
async function handleFilesTree(ws, msg, projectRoot) {
|
|
283
|
+
const payload = msg.payload;
|
|
284
|
+
const rawPath = payload?.path?.trim();
|
|
285
|
+
const treeRoot = rawPath && rawPath !== "." ? path2.resolve(projectRoot, rawPath) : projectRoot;
|
|
286
|
+
if (!treeRoot.startsWith(projectRoot + path2.sep) && treeRoot !== projectRoot) {
|
|
287
|
+
send(ws, {
|
|
288
|
+
type: "files.tree",
|
|
289
|
+
payload: { root: projectRoot, tree: [], error: "Path outside project root" }
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const pathPrefix = treeRoot === projectRoot ? "" : (path2.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
|
|
294
|
+
async function buildTree(dir, rel, depth) {
|
|
295
|
+
if (depth > 10) return [];
|
|
296
|
+
let entries = [];
|
|
297
|
+
try {
|
|
298
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
299
|
+
} catch {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
entries.sort((a, b) => {
|
|
303
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
304
|
+
return a.name.localeCompare(b.name);
|
|
305
|
+
});
|
|
306
|
+
const nodes = [];
|
|
307
|
+
for (const e of entries) {
|
|
308
|
+
if (isHiddenEntry(e.name)) continue;
|
|
309
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
310
|
+
const childAbs = path2.join(dir, e.name);
|
|
311
|
+
const childPath = pathPrefix + childRel;
|
|
312
|
+
if (e.isDirectory()) {
|
|
313
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
314
|
+
const children = await buildTree(childAbs, childRel, depth + 1);
|
|
315
|
+
nodes.push({ name: e.name, path: childPath, type: "directory", children });
|
|
316
|
+
} else if (e.isFile()) {
|
|
317
|
+
nodes.push({ name: e.name, path: childPath, type: "file" });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return nodes;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const tree = await buildTree(treeRoot, "", 0);
|
|
324
|
+
const rootLabel = treeRoot === projectRoot ? projectRoot : path2.relative(projectRoot, treeRoot) || ".";
|
|
325
|
+
send(ws, { type: "files.tree", payload: { root: rootLabel, tree } });
|
|
326
|
+
} catch (err) {
|
|
327
|
+
const rootLabel = treeRoot === projectRoot ? projectRoot : path2.relative(projectRoot, treeRoot) || ".";
|
|
328
|
+
send(ws, {
|
|
329
|
+
type: "files.tree",
|
|
330
|
+
payload: { root: rootLabel, tree: [], error: errMessage(err) }
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function handleFilesRead(ws, msg, projectRoot) {
|
|
335
|
+
const { filePath } = msg.payload;
|
|
336
|
+
const resolved = path2.resolve(projectRoot, filePath);
|
|
337
|
+
if (!resolved.startsWith(projectRoot + path2.sep) && resolved !== projectRoot) {
|
|
338
|
+
send(ws, { type: "files.read", payload: { filePath, content: "", error: "Forbidden" } });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const content = await fs2.readFile(resolved, "utf8");
|
|
343
|
+
send(ws, { type: "files.read", payload: { filePath, content } });
|
|
344
|
+
} catch (err) {
|
|
345
|
+
send(ws, {
|
|
346
|
+
type: "files.read",
|
|
347
|
+
payload: { filePath, content: "", error: errMessage(err) }
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function handleFilesWrite(ws, msg, projectRoot) {
|
|
352
|
+
const { filePath, content } = msg.payload;
|
|
353
|
+
const resolved = path2.resolve(projectRoot, filePath);
|
|
354
|
+
if (!resolved.startsWith(projectRoot + path2.sep) && resolved !== projectRoot) {
|
|
355
|
+
send(ws, { type: "files.written", payload: { filePath, success: false, error: "Forbidden" } });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
await atomicWrite(resolved, content);
|
|
360
|
+
send(ws, { type: "files.written", payload: { filePath, success: true } });
|
|
361
|
+
} catch (err) {
|
|
362
|
+
send(ws, {
|
|
363
|
+
type: "files.written",
|
|
364
|
+
payload: { filePath, success: false, error: errMessage(err) }
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function handleFilesList(ws, msg, projectRoot) {
|
|
369
|
+
const payload = msg.payload ?? {};
|
|
370
|
+
const limit = payload.limit ?? 50;
|
|
371
|
+
const listRoot = payload.path ? path2.resolve(projectRoot, payload.path) : projectRoot;
|
|
372
|
+
if (!listRoot.startsWith(projectRoot + path2.sep) && listRoot !== projectRoot) {
|
|
373
|
+
send(ws, { type: "files.list", payload: { files: [] } });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const results = [];
|
|
377
|
+
async function walk(dir, rel, depth) {
|
|
378
|
+
if (depth > 8 || results.length >= 600) return;
|
|
379
|
+
let entries = [];
|
|
380
|
+
try {
|
|
381
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
382
|
+
} catch {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
for (const e of entries) {
|
|
386
|
+
if (results.length >= 600) return;
|
|
387
|
+
if (isHiddenEntry(e.name)) continue;
|
|
388
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
389
|
+
if (e.isDirectory()) {
|
|
390
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
391
|
+
await walk(path2.join(dir, e.name), childRel, depth + 1);
|
|
392
|
+
} else if (e.isFile()) {
|
|
393
|
+
results.push(childRel);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
await walk(listRoot, "", 0);
|
|
398
|
+
send(ws, {
|
|
399
|
+
type: "files.list",
|
|
400
|
+
payload: { files: rankFiles(results, payload.query ?? "", limit) }
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/server/memory-handlers.ts
|
|
405
|
+
async function handleMemoryList(ws, memoryStore) {
|
|
406
|
+
try {
|
|
407
|
+
const text = await memoryStore.readAll();
|
|
408
|
+
send(ws, { type: "memory.list", payload: { text } });
|
|
409
|
+
} catch (err) {
|
|
410
|
+
send(ws, {
|
|
411
|
+
type: "memory.list",
|
|
412
|
+
payload: { text: "", error: errMessage(err) }
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async function handleMemoryRemember(ws, msg, memoryStore) {
|
|
417
|
+
const { text, scope } = msg.payload;
|
|
418
|
+
try {
|
|
419
|
+
await memoryStore.remember(text, scope ?? "project-memory");
|
|
420
|
+
sendResult(ws, true, "Saved to memory");
|
|
421
|
+
} catch (err) {
|
|
422
|
+
sendResult(ws, false, errMessage(err));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async function handleMemoryForget(ws, msg, memoryStore) {
|
|
426
|
+
const { text, scope } = msg.payload;
|
|
427
|
+
try {
|
|
428
|
+
const removed = await memoryStore.forget(text, scope ?? "project-memory");
|
|
429
|
+
sendResult(
|
|
430
|
+
ws,
|
|
431
|
+
removed > 0,
|
|
432
|
+
removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
|
|
433
|
+
);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
sendResult(ws, false, errMessage(err));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
150
439
|
// src/server/index.ts
|
|
151
440
|
import {
|
|
152
441
|
Agent,
|
|
@@ -170,16 +459,20 @@ import {
|
|
|
170
459
|
ProviderRegistry,
|
|
171
460
|
TOKENS as TOKENS2,
|
|
172
461
|
ToolRegistry,
|
|
173
|
-
atomicWrite as
|
|
462
|
+
atomicWrite as atomicWrite5,
|
|
174
463
|
createDefaultPipelines,
|
|
464
|
+
createSessionEventBridge,
|
|
465
|
+
resolveSessionLoggingConfig,
|
|
175
466
|
DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
176
467
|
DEFAULT_SESSION_PRUNE_DAYS,
|
|
177
468
|
DEFAULT_TOOLS_CONFIG,
|
|
178
|
-
listContextWindowModes,
|
|
179
469
|
repairToolUseAdjacency,
|
|
180
|
-
resolveContextWindowPolicy
|
|
470
|
+
resolveContextWindowPolicy,
|
|
471
|
+
enhanceUserPrompt,
|
|
472
|
+
recentTextTurns
|
|
181
473
|
} from "@wrongstack/core";
|
|
182
474
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
475
|
+
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
183
476
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
184
477
|
import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
185
478
|
import { WebSocketServer } from "ws";
|
|
@@ -1423,19 +1716,112 @@ var WorktreeWebSocketHandler = class {
|
|
|
1423
1716
|
}
|
|
1424
1717
|
};
|
|
1425
1718
|
|
|
1719
|
+
// src/server/mailbox-handlers.ts
|
|
1720
|
+
import * as path3 from "path";
|
|
1721
|
+
import { GlobalMailbox } from "@wrongstack/core";
|
|
1722
|
+
function resolveProjectDir(projectRoot, globalRoot) {
|
|
1723
|
+
const { createHash } = __require("crypto");
|
|
1724
|
+
const hash = createHash("sha256").update(path3.resolve(projectRoot)).digest("hex").slice(0, 6);
|
|
1725
|
+
const slug = path3.basename(projectRoot).toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40) || "project";
|
|
1726
|
+
return path3.join(globalRoot, "projects", `${slug}-${hash}`);
|
|
1727
|
+
}
|
|
1728
|
+
async function handleMailboxMessages(ws, deps, payload) {
|
|
1729
|
+
try {
|
|
1730
|
+
const dir = resolveProjectDir(deps.projectRoot, deps.globalRoot);
|
|
1731
|
+
const mb = new GlobalMailbox(dir);
|
|
1732
|
+
const messages = await mb.query({
|
|
1733
|
+
limit: payload?.limit ?? 30,
|
|
1734
|
+
to: payload?.agentId,
|
|
1735
|
+
unreadBy: payload?.unreadOnly ? payload.agentId : void 0
|
|
1736
|
+
});
|
|
1737
|
+
send(ws, {
|
|
1738
|
+
type: "mailbox.messages",
|
|
1739
|
+
payload: {
|
|
1740
|
+
messages: messages.map((m) => ({
|
|
1741
|
+
id: m.id,
|
|
1742
|
+
from: m.from,
|
|
1743
|
+
to: m.to,
|
|
1744
|
+
type: m.type,
|
|
1745
|
+
subject: m.subject,
|
|
1746
|
+
body: m.body,
|
|
1747
|
+
priority: m.priority,
|
|
1748
|
+
readBy: m.readBy,
|
|
1749
|
+
readByCount: Object.keys(m.readBy).length,
|
|
1750
|
+
completed: m.completed,
|
|
1751
|
+
completedBy: m.completedBy,
|
|
1752
|
+
outcome: m.outcome,
|
|
1753
|
+
timestamp: m.timestamp,
|
|
1754
|
+
replyTo: m.replyTo,
|
|
1755
|
+
senderSessionId: m.senderSessionId
|
|
1756
|
+
}))
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
} catch (err) {
|
|
1760
|
+
send(ws, { type: "mailbox.messages", payload: { messages: [], error: errMessage(err) } });
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
async function handleMailboxAgents(ws, deps, payload) {
|
|
1764
|
+
try {
|
|
1765
|
+
const dir = resolveProjectDir(deps.projectRoot, deps.globalRoot);
|
|
1766
|
+
const mb = new GlobalMailbox(dir);
|
|
1767
|
+
const agents = payload?.onlineOnly ? await mb.getOnlineAgents() : await mb.getAgentStatuses();
|
|
1768
|
+
send(ws, {
|
|
1769
|
+
type: "mailbox.agents",
|
|
1770
|
+
payload: {
|
|
1771
|
+
agents: agents.map((a) => ({
|
|
1772
|
+
agentId: a.agentId,
|
|
1773
|
+
name: a.name,
|
|
1774
|
+
role: a.role,
|
|
1775
|
+
sessionId: a.sessionId,
|
|
1776
|
+
status: a.status,
|
|
1777
|
+
currentTool: a.currentTool,
|
|
1778
|
+
currentTask: a.currentTask,
|
|
1779
|
+
iterations: a.iterations,
|
|
1780
|
+
toolCalls: a.toolCalls,
|
|
1781
|
+
lastSeenAt: a.lastSeenAt,
|
|
1782
|
+
online: a.online,
|
|
1783
|
+
pid: a.pid,
|
|
1784
|
+
source: a.source
|
|
1785
|
+
}))
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
} catch (err) {
|
|
1789
|
+
send(ws, { type: "mailbox.agents", payload: { agents: [], error: errMessage(err) } });
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
async function handleMailboxClear(ws, deps) {
|
|
1793
|
+
try {
|
|
1794
|
+
const dir = resolveProjectDir(deps.projectRoot, deps.globalRoot);
|
|
1795
|
+
const mb = new GlobalMailbox(dir);
|
|
1796
|
+
await mb.clearAll();
|
|
1797
|
+
send(ws, { type: "mailbox.cleared", payload: {} });
|
|
1798
|
+
} catch (err) {
|
|
1799
|
+
send(ws, { type: "mailbox.cleared", payload: { error: errMessage(err) } });
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1426
1803
|
// src/server/ws-auth.ts
|
|
1427
|
-
import { Buffer } from "buffer";
|
|
1804
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
1428
1805
|
import { timingSafeEqual } from "crypto";
|
|
1429
1806
|
function isLoopbackHostname(hostname) {
|
|
1430
1807
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1431
1808
|
}
|
|
1809
|
+
function isTrustedLoopbackOrigin(origin) {
|
|
1810
|
+
try {
|
|
1811
|
+
const url = new URL(origin);
|
|
1812
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
1813
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
1814
|
+
} catch {
|
|
1815
|
+
return false;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1432
1818
|
function isLoopbackBind(wsHost) {
|
|
1433
1819
|
return wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
|
|
1434
1820
|
}
|
|
1435
1821
|
function tokenMatches(provided, expected) {
|
|
1436
1822
|
if (!provided) return false;
|
|
1437
|
-
const a =
|
|
1438
|
-
const b =
|
|
1823
|
+
const a = Buffer2.from(provided);
|
|
1824
|
+
const b = Buffer2.from(expected);
|
|
1439
1825
|
if (a.length !== b.length) return false;
|
|
1440
1826
|
return timingSafeEqual(a, b);
|
|
1441
1827
|
}
|
|
@@ -1467,7 +1853,12 @@ function verifyClient(input) {
|
|
|
1467
1853
|
}
|
|
1468
1854
|
try {
|
|
1469
1855
|
const { hostname } = new URL(origin);
|
|
1470
|
-
if (isLoopbackHostname(hostname))
|
|
1856
|
+
if (isLoopbackHostname(hostname)) {
|
|
1857
|
+
if (wsHost === "0.0.0.0" && !isTrustedLoopbackOrigin(origin)) {
|
|
1858
|
+
return false;
|
|
1859
|
+
}
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1471
1862
|
return tokenOk;
|
|
1472
1863
|
} catch {
|
|
1473
1864
|
return false;
|
|
@@ -1512,14 +1903,14 @@ function registerShutdownHandlers(res) {
|
|
|
1512
1903
|
|
|
1513
1904
|
// src/server/instance-registry.ts
|
|
1514
1905
|
import * as os from "os";
|
|
1515
|
-
import * as
|
|
1516
|
-
import * as
|
|
1517
|
-
import { atomicWrite } from "@wrongstack/core";
|
|
1906
|
+
import * as path4 from "path";
|
|
1907
|
+
import * as fs3 from "fs/promises";
|
|
1908
|
+
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
1518
1909
|
function defaultBaseDir() {
|
|
1519
|
-
return
|
|
1910
|
+
return path4.join(os.homedir(), ".wrongstack");
|
|
1520
1911
|
}
|
|
1521
1912
|
function registryPath(baseDir = defaultBaseDir()) {
|
|
1522
|
-
return
|
|
1913
|
+
return path4.join(baseDir, "webui-instances.json");
|
|
1523
1914
|
}
|
|
1524
1915
|
function isPidAlive(pid) {
|
|
1525
1916
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -1532,7 +1923,7 @@ function isPidAlive(pid) {
|
|
|
1532
1923
|
}
|
|
1533
1924
|
async function load(file) {
|
|
1534
1925
|
try {
|
|
1535
|
-
const raw = await
|
|
1926
|
+
const raw = await fs3.readFile(file, "utf8");
|
|
1536
1927
|
const parsed = JSON.parse(raw);
|
|
1537
1928
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
1538
1929
|
return parsed;
|
|
@@ -1542,7 +1933,7 @@ async function load(file) {
|
|
|
1542
1933
|
return { version: 1, instances: [] };
|
|
1543
1934
|
}
|
|
1544
1935
|
async function save(file, instances) {
|
|
1545
|
-
await
|
|
1936
|
+
await atomicWrite2(file, `${JSON.stringify({ version: 1, instances }, null, 2)}
|
|
1546
1937
|
`, {
|
|
1547
1938
|
mode: 384
|
|
1548
1939
|
});
|
|
@@ -1591,16 +1982,16 @@ function formatInstances(instances) {
|
|
|
1591
1982
|
// src/server/port-utils.ts
|
|
1592
1983
|
import * as net from "net";
|
|
1593
1984
|
function isPortFree(host, port) {
|
|
1594
|
-
return new Promise((
|
|
1985
|
+
return new Promise((resolve5) => {
|
|
1595
1986
|
const srv = net.createServer();
|
|
1596
|
-
srv.once("error", () =>
|
|
1987
|
+
srv.once("error", () => resolve5(false));
|
|
1597
1988
|
srv.once("listening", () => {
|
|
1598
|
-
srv.close(() =>
|
|
1989
|
+
srv.close(() => resolve5(true));
|
|
1599
1990
|
});
|
|
1600
1991
|
try {
|
|
1601
1992
|
srv.listen(port, host);
|
|
1602
1993
|
} catch {
|
|
1603
|
-
|
|
1994
|
+
resolve5(false);
|
|
1604
1995
|
}
|
|
1605
1996
|
});
|
|
1606
1997
|
}
|
|
@@ -1676,15 +2067,15 @@ function computeUsageCost(usage, rates) {
|
|
|
1676
2067
|
}
|
|
1677
2068
|
|
|
1678
2069
|
// src/server/provider-config-io.ts
|
|
1679
|
-
import * as
|
|
1680
|
-
import * as
|
|
1681
|
-
import { atomicWrite as
|
|
2070
|
+
import * as fs4 from "fs/promises";
|
|
2071
|
+
import * as path5 from "path";
|
|
2072
|
+
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
1682
2073
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
1683
2074
|
import { DefaultSecretVault } from "@wrongstack/core";
|
|
1684
2075
|
async function loadSavedProviders(configPath, vault) {
|
|
1685
2076
|
let raw;
|
|
1686
2077
|
try {
|
|
1687
|
-
raw = await
|
|
2078
|
+
raw = await fs4.readFile(configPath, "utf8");
|
|
1688
2079
|
} catch {
|
|
1689
2080
|
return {};
|
|
1690
2081
|
}
|
|
@@ -1701,7 +2092,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
1701
2092
|
let raw;
|
|
1702
2093
|
let fileExists = true;
|
|
1703
2094
|
try {
|
|
1704
|
-
raw = await
|
|
2095
|
+
raw = await fs4.readFile(configPath, "utf8");
|
|
1705
2096
|
} catch (err) {
|
|
1706
2097
|
if (err.code !== "ENOENT") {
|
|
1707
2098
|
throw new Error(
|
|
@@ -1726,7 +2117,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
1726
2117
|
}
|
|
1727
2118
|
parsed.providers = providers;
|
|
1728
2119
|
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
1729
|
-
await
|
|
2120
|
+
await atomicWrite3(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
1730
2121
|
}
|
|
1731
2122
|
|
|
1732
2123
|
// src/server/provider-keys.ts
|
|
@@ -1825,38 +2216,9 @@ function removeProvider(providers, providerId) {
|
|
|
1825
2216
|
return { ok: true, message: `Provider "${providerId}" removed` };
|
|
1826
2217
|
}
|
|
1827
2218
|
|
|
1828
|
-
// src/server/ws-utils.ts
|
|
1829
|
-
import { randomBytes } from "crypto";
|
|
1830
|
-
import { WebSocket } from "ws";
|
|
1831
|
-
function send(ws, msg) {
|
|
1832
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1833
|
-
ws.send(JSON.stringify(msg));
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
function broadcast(clients, msg) {
|
|
1837
|
-
const data = JSON.stringify(msg);
|
|
1838
|
-
for (const [ws] of clients) {
|
|
1839
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1840
|
-
try {
|
|
1841
|
-
ws.send(data);
|
|
1842
|
-
} catch {
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
function sendResult(ws, success, message) {
|
|
1848
|
-
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
1849
|
-
}
|
|
1850
|
-
function errMessage(err) {
|
|
1851
|
-
return err instanceof Error ? err.message : String(err);
|
|
1852
|
-
}
|
|
1853
|
-
function generateAuthToken() {
|
|
1854
|
-
return randomBytes(16).toString("hex");
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
2219
|
// src/server/provider-handlers.ts
|
|
1858
2220
|
function createProviderHandlers(deps) {
|
|
1859
|
-
const { globalConfigPath, vault } = deps;
|
|
2221
|
+
const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps;
|
|
1860
2222
|
let configWriteLock = deps.getConfigWriteLock();
|
|
1861
2223
|
async function loadConfigProviders() {
|
|
1862
2224
|
return loadSavedProviders(globalConfigPath, vault);
|
|
@@ -1864,7 +2226,12 @@ function createProviderHandlers(deps) {
|
|
|
1864
2226
|
async function saveConfigProviders(providers) {
|
|
1865
2227
|
const next = configWriteLock.then(() => saveProviders(globalConfigPath, vault, providers)).catch((err) => {
|
|
1866
2228
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1867
|
-
console.error(
|
|
2229
|
+
console.error(JSON.stringify({
|
|
2230
|
+
level: "error",
|
|
2231
|
+
event: "webui.provider_save_failed",
|
|
2232
|
+
message: msg,
|
|
2233
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2234
|
+
}));
|
|
1868
2235
|
});
|
|
1869
2236
|
configWriteLock = next;
|
|
1870
2237
|
deps.setConfigWriteLock(next);
|
|
@@ -1906,6 +2273,28 @@ function createProviderHandlers(deps) {
|
|
|
1906
2273
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
1907
2274
|
if (result.ok) await saveConfigProviders(providers);
|
|
1908
2275
|
sendResult(ws, result.ok, result.message);
|
|
2276
|
+
if (result.ok) {
|
|
2277
|
+
console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
2278
|
+
broadcast2(clients, {
|
|
2279
|
+
type: "providers.saved",
|
|
2280
|
+
payload: {
|
|
2281
|
+
providers: Object.entries(providers).map(([id, cfg]) => {
|
|
2282
|
+
const keys = normalizeKeys(cfg);
|
|
2283
|
+
return {
|
|
2284
|
+
id,
|
|
2285
|
+
family: cfg.family ?? id,
|
|
2286
|
+
baseUrl: cfg.baseUrl,
|
|
2287
|
+
apiKeys: keys.map((k) => ({
|
|
2288
|
+
label: k.label,
|
|
2289
|
+
maskedKey: maskedKey(k.apiKey),
|
|
2290
|
+
isActive: k.label === cfg.activeKey,
|
|
2291
|
+
createdAt: k.createdAt
|
|
2292
|
+
}))
|
|
2293
|
+
};
|
|
2294
|
+
})
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
1909
2298
|
} catch (err) {
|
|
1910
2299
|
sendResult(ws, false, errMessage(err));
|
|
1911
2300
|
}
|
|
@@ -1924,12 +2313,14 @@ function createProviderHandlers(deps) {
|
|
|
1924
2313
|
}
|
|
1925
2314
|
|
|
1926
2315
|
// src/server/setup-events.ts
|
|
2316
|
+
import * as path6 from "path";
|
|
1927
2317
|
function setupEvents(deps) {
|
|
1928
|
-
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms } = deps;
|
|
2318
|
+
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge } = deps;
|
|
1929
2319
|
events.on("iteration.started", (e) => {
|
|
2320
|
+
const maxIt = typeof context.meta["maxIterations"] === "number" ? context.meta["maxIterations"] : config.tools?.maxIterations ?? 100;
|
|
1930
2321
|
broadcast2(clients, {
|
|
1931
2322
|
type: "iteration.started",
|
|
1932
|
-
payload: { index: e.index, maxIterations:
|
|
2323
|
+
payload: { index: e.index, maxIterations: maxIt }
|
|
1933
2324
|
});
|
|
1934
2325
|
});
|
|
1935
2326
|
events.on("provider.text_delta", (e) => {
|
|
@@ -1943,19 +2334,70 @@ function setupEvents(deps) {
|
|
|
1943
2334
|
type: "tool.started",
|
|
1944
2335
|
payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
|
|
1945
2336
|
});
|
|
2337
|
+
sessionBridge?.append({
|
|
2338
|
+
type: "tool_call_start",
|
|
2339
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2340
|
+
name: e.name,
|
|
2341
|
+
id: e.id,
|
|
2342
|
+
input: e.input
|
|
2343
|
+
}).catch(() => {
|
|
2344
|
+
});
|
|
1946
2345
|
});
|
|
1947
2346
|
events.on("tool.progress", (e) => {
|
|
1948
2347
|
broadcast2(clients, {
|
|
1949
2348
|
type: "tool.progress",
|
|
1950
2349
|
payload: { id: e.id, name: e.name, eventType: e.event.type, text: e.event.text }
|
|
1951
2350
|
});
|
|
2351
|
+
sessionBridge?.append({
|
|
2352
|
+
type: "tool_progress",
|
|
2353
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2354
|
+
name: e.name,
|
|
2355
|
+
id: e.id,
|
|
2356
|
+
event: { type: e.event.type, text: e.event.text, data: e.event.data }
|
|
2357
|
+
}).catch(() => {
|
|
2358
|
+
});
|
|
1952
2359
|
});
|
|
1953
2360
|
events.on("tool.executed", (e) => {
|
|
1954
2361
|
broadcast2(clients, {
|
|
1955
2362
|
type: "tool.executed",
|
|
1956
2363
|
payload: { id: e.id, name: e.name, durationMs: e.durationMs, ok: e.ok, input: e.input, output: e.output }
|
|
1957
2364
|
});
|
|
2365
|
+
sessionBridge?.append({
|
|
2366
|
+
type: "tool_call_end",
|
|
2367
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2368
|
+
name: e.name,
|
|
2369
|
+
id: e.id ?? "",
|
|
2370
|
+
durationMs: e.durationMs,
|
|
2371
|
+
outputSize: e.outputBytes ?? 0,
|
|
2372
|
+
ok: e.ok,
|
|
2373
|
+
outputBytes: e.outputBytes,
|
|
2374
|
+
outputTokens: e.outputTokens,
|
|
2375
|
+
outputLines: e.outputLines
|
|
2376
|
+
}).catch(() => {
|
|
2377
|
+
});
|
|
1958
2378
|
broadcast2(clients, { type: "todos.updated", payload: { todos: [...context.todos] } });
|
|
2379
|
+
if (e.name === "task" || e.name === "plan" || e.name === "todo") {
|
|
2380
|
+
void (async () => {
|
|
2381
|
+
try {
|
|
2382
|
+
const taskPath = context.meta["task.path"];
|
|
2383
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
2384
|
+
const { loadTasks } = await import("@wrongstack/core");
|
|
2385
|
+
const file = await loadTasks(taskPath);
|
|
2386
|
+
broadcast2(clients, { type: "tasks.updated", payload: { tasks: file?.tasks ?? [] } });
|
|
2387
|
+
}
|
|
2388
|
+
} catch {
|
|
2389
|
+
}
|
|
2390
|
+
try {
|
|
2391
|
+
const planPath = context.meta["plan.path"];
|
|
2392
|
+
if (typeof planPath === "string" && planPath) {
|
|
2393
|
+
const { loadPlan } = await import("@wrongstack/core");
|
|
2394
|
+
const plan = await loadPlan(planPath);
|
|
2395
|
+
broadcast2(clients, { type: "plan.updated", payload: { plan: plan ?? { version: 1, sessionId: context.session?.id ?? "", updatedAt: (/* @__PURE__ */ new Date()).toISOString(), items: [] } } });
|
|
2396
|
+
}
|
|
2397
|
+
} catch {
|
|
2398
|
+
}
|
|
2399
|
+
})();
|
|
2400
|
+
}
|
|
1959
2401
|
});
|
|
1960
2402
|
events.on("provider.response", (e) => {
|
|
1961
2403
|
broadcast2(clients, { type: "provider.response", payload: { usage: e.usage, stopReason: e.stopReason, messageId: "current" } });
|
|
@@ -1970,32 +2412,274 @@ function setupEvents(deps) {
|
|
|
1970
2412
|
});
|
|
1971
2413
|
events.on("error", (e) => {
|
|
1972
2414
|
broadcast2(clients, { type: "error", payload: { phase: e.phase, message: e.err instanceof Error ? e.err.message : String(e.err) } });
|
|
2415
|
+
sessionBridge?.append({
|
|
2416
|
+
type: "error",
|
|
2417
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2418
|
+
message: e.err instanceof Error ? e.err.message : String(e.err),
|
|
2419
|
+
phase: e.phase
|
|
2420
|
+
}).catch(() => {
|
|
2421
|
+
});
|
|
2422
|
+
});
|
|
2423
|
+
events.on("provider.retry", (e) => {
|
|
2424
|
+
sessionBridge?.append({
|
|
2425
|
+
type: "provider_retry",
|
|
2426
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2427
|
+
providerId: e.providerId,
|
|
2428
|
+
attempt: e.attempt,
|
|
2429
|
+
delayMs: e.delayMs,
|
|
2430
|
+
status: e.status,
|
|
2431
|
+
description: e.description
|
|
2432
|
+
}).catch(() => {
|
|
2433
|
+
});
|
|
2434
|
+
});
|
|
2435
|
+
events.on("provider.error", (e) => {
|
|
2436
|
+
sessionBridge?.append({
|
|
2437
|
+
type: "provider_error",
|
|
2438
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2439
|
+
providerId: e.providerId,
|
|
2440
|
+
status: e.status,
|
|
2441
|
+
description: e.description,
|
|
2442
|
+
retryable: e.retryable
|
|
2443
|
+
}).catch(() => {
|
|
2444
|
+
});
|
|
2445
|
+
});
|
|
2446
|
+
events.onPattern("mailbox.received", (_e, payload) => {
|
|
2447
|
+
broadcast2(clients, { type: "mailbox.received", payload });
|
|
1973
2448
|
});
|
|
1974
|
-
|
|
2449
|
+
events.onPattern("mailbox.agent_registered", (_e, payload) => {
|
|
2450
|
+
broadcast2(clients, { type: "mailbox.agent_registered", payload });
|
|
2451
|
+
});
|
|
2452
|
+
const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: { kind, sessionId: context.session.id, ...payload } });
|
|
1975
2453
|
events.on("subagent.spawned", (e) => forwardSubagent("spawned", { subagentId: e.subagentId, taskId: e.taskId, name: e.name, provider: e.provider, model: e.model, description: e.description }));
|
|
1976
2454
|
events.on("subagent.task_started", (e) => forwardSubagent("task_started", { subagentId: e.subagentId, taskId: e.taskId, description: e.description }));
|
|
1977
2455
|
events.on("subagent.tool_executed", (e) => forwardSubagent("tool_executed", { subagentId: e.subagentId, toolName: e.name, durationMs: e.durationMs, ok: e.ok }));
|
|
1978
|
-
events.on("subagent.iteration_summary", (e) => forwardSubagent("iteration_summary", { subagentId: e.subagentId, iteration: e.iteration, toolCalls: e.toolCalls, costUsd: e.costUsd, currentTool: e.currentTool }));
|
|
2456
|
+
events.on("subagent.iteration_summary", (e) => forwardSubagent("iteration_summary", { subagentId: e.subagentId, iteration: e.iteration, toolCalls: e.toolCalls, costUsd: e.costUsd, currentTool: e.currentTool, partialText: e.partialText }));
|
|
1979
2457
|
events.on("subagent.budget_extended", (e) => forwardSubagent("budget_extended", { subagentId: e.subagentId, totalExtensions: e.totalExtensions }));
|
|
1980
2458
|
events.on("subagent.ctx_pct", (e) => forwardSubagent("ctx_pct", { subagentId: e.subagentId, load: e.load, tokens: e.tokens, maxContext: e.maxContext }));
|
|
1981
|
-
events.on("subagent.task_completed", (e) => forwardSubagent("task_completed", { subagentId: e.subagentId, status: e.status, iterations: e.iterations, toolCalls: e.toolCalls, error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0 }));
|
|
2459
|
+
events.on("subagent.task_completed", (e) => forwardSubagent("task_completed", { subagentId: e.subagentId, status: e.status, iterations: e.iterations, toolCalls: e.toolCalls, finalText: e.finalText, error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0 }));
|
|
2460
|
+
let leaderSpawned = false;
|
|
2461
|
+
events.on("iteration.started", () => {
|
|
2462
|
+
if (!leaderSpawned) {
|
|
2463
|
+
leaderSpawned = true;
|
|
2464
|
+
const provider = context.provider?.id ?? "unknown";
|
|
2465
|
+
forwardSubagent("spawned", {
|
|
2466
|
+
subagentId: "leader",
|
|
2467
|
+
name: "LEADER",
|
|
2468
|
+
provider,
|
|
2469
|
+
model: context.model,
|
|
2470
|
+
description: `Main agent session (${context.session.id})`
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
events.on("tool.executed", (e) => {
|
|
2475
|
+
forwardSubagent("tool_executed", {
|
|
2476
|
+
subagentId: "leader",
|
|
2477
|
+
toolName: e.name,
|
|
2478
|
+
durationMs: e.durationMs,
|
|
2479
|
+
ok: e.ok
|
|
2480
|
+
});
|
|
2481
|
+
});
|
|
2482
|
+
events.on("provider.response", (e) => {
|
|
2483
|
+
if (e.usage?.input != null) {
|
|
2484
|
+
const maxCtx = context.provider.capabilities.maxContext;
|
|
2485
|
+
const pct = maxCtx > 0 ? e.usage.input / maxCtx : 0;
|
|
2486
|
+
const costUsd = context.tokenCounter.estimateCost().total;
|
|
2487
|
+
forwardSubagent("ctx_pct", {
|
|
2488
|
+
subagentId: "leader",
|
|
2489
|
+
load: pct,
|
|
2490
|
+
tokens: e.usage.input,
|
|
2491
|
+
maxContext: maxCtx,
|
|
2492
|
+
costUsd
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
events.on("iteration.completed", () => {
|
|
2497
|
+
if (!leaderSpawned) {
|
|
2498
|
+
leaderSpawned = true;
|
|
2499
|
+
const provider = context.provider?.id ?? "unknown";
|
|
2500
|
+
forwardSubagent("spawned", {
|
|
2501
|
+
subagentId: "leader",
|
|
2502
|
+
name: "LEADER",
|
|
2503
|
+
provider,
|
|
2504
|
+
model: context.model,
|
|
2505
|
+
description: `Main agent session (${context.session.id})`
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2509
|
+
events.onPattern("mailbox.*", (eventName, payload) => {
|
|
2510
|
+
broadcast2(clients, { type: "mailbox.event", payload: { event: eventName, ...payload } });
|
|
2511
|
+
});
|
|
2512
|
+
events.onPattern("brain.*", (eventName, payload) => {
|
|
2513
|
+
broadcast2(clients, { type: "brain.event", payload: { event: eventName, ...payload } });
|
|
2514
|
+
});
|
|
2515
|
+
const globalRoot = globalConfigPath ? path6.dirname(globalConfigPath) : void 0;
|
|
2516
|
+
if (globalRoot) {
|
|
2517
|
+
const statusInterval = setInterval(async () => {
|
|
2518
|
+
try {
|
|
2519
|
+
const { SessionRegistry } = await import("@wrongstack/core");
|
|
2520
|
+
const registry = new SessionRegistry(globalRoot);
|
|
2521
|
+
const sessions = await registry.list();
|
|
2522
|
+
const live = sessions.filter((s) => s.status !== "stale").map((s) => ({
|
|
2523
|
+
sessionId: s.sessionId,
|
|
2524
|
+
projectName: s.projectName,
|
|
2525
|
+
projectSlug: s.projectSlug,
|
|
2526
|
+
projectRoot: s.projectRoot,
|
|
2527
|
+
workingDir: s.workingDir,
|
|
2528
|
+
gitBranch: s.gitBranch,
|
|
2529
|
+
status: s.status,
|
|
2530
|
+
pid: s.pid,
|
|
2531
|
+
startedAt: s.startedAt,
|
|
2532
|
+
agentCount: s.agentCount,
|
|
2533
|
+
agents: (s.agents ?? []).map((a) => ({
|
|
2534
|
+
id: a.id,
|
|
2535
|
+
name: a.name,
|
|
2536
|
+
status: a.status,
|
|
2537
|
+
currentTool: a.currentTool,
|
|
2538
|
+
iterations: a.iterations,
|
|
2539
|
+
toolCalls: a.toolCalls,
|
|
2540
|
+
lastActivityAt: a.lastActivityAt
|
|
2541
|
+
}))
|
|
2542
|
+
}));
|
|
2543
|
+
broadcast2(clients, { type: "sessions.status_update", payload: { sessions: live } });
|
|
2544
|
+
} catch {
|
|
2545
|
+
}
|
|
2546
|
+
}, 5e3);
|
|
2547
|
+
if (statusInterval.unref) statusInterval.unref();
|
|
2548
|
+
}
|
|
1982
2549
|
}
|
|
1983
2550
|
|
|
1984
|
-
// src/server/
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
return JSON.stringify(c);
|
|
1992
|
-
} catch {
|
|
1993
|
-
return String(c);
|
|
1994
|
-
}
|
|
2551
|
+
// src/server/custom-context-modes.ts
|
|
2552
|
+
import { listContextWindowModes, atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
2553
|
+
import * as fs5 from "fs/promises";
|
|
2554
|
+
import * as path7 from "path";
|
|
2555
|
+
var STORE_FILENAME = "custom-context-modes.json";
|
|
2556
|
+
function storePath(wrongstackDir) {
|
|
2557
|
+
return path7.join(wrongstackDir, STORE_FILENAME);
|
|
1995
2558
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2559
|
+
var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
|
|
2560
|
+
function createCustomModeStore(wrongstackDir) {
|
|
2561
|
+
const modes = /* @__PURE__ */ new Map();
|
|
2562
|
+
const load2 = async () => {
|
|
2563
|
+
modes.clear();
|
|
2564
|
+
try {
|
|
2565
|
+
const raw = await fs5.readFile(storePath(wrongstackDir), "utf8");
|
|
2566
|
+
const parsed = JSON.parse(raw);
|
|
2567
|
+
if (Array.isArray(parsed.modes)) {
|
|
2568
|
+
for (const m of parsed.modes) {
|
|
2569
|
+
if (m.id && !BUILTIN_IDS.has(m.id)) {
|
|
2570
|
+
modes.set(m.id, { ...m, custom: true });
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
} catch {
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
const save2 = async () => {
|
|
2578
|
+
const arr = [...modes.values()];
|
|
2579
|
+
const json = JSON.stringify({ modes: arr }, null, 2);
|
|
2580
|
+
await atomicWrite4(storePath(wrongstackDir), json);
|
|
2581
|
+
};
|
|
2582
|
+
const create = (mode) => {
|
|
2583
|
+
if (!mode.id || typeof mode.id !== "string") {
|
|
2584
|
+
return { ok: false, error: "id is required" };
|
|
2585
|
+
}
|
|
2586
|
+
if (BUILTIN_IDS.has(mode.id)) {
|
|
2587
|
+
return { ok: false, error: `Cannot override built-in mode "${mode.id}"` };
|
|
2588
|
+
}
|
|
2589
|
+
if (modes.has(mode.id)) {
|
|
2590
|
+
return { ok: false, error: `Mode "${mode.id}" already exists` };
|
|
2591
|
+
}
|
|
2592
|
+
if (!mode.name) {
|
|
2593
|
+
return { ok: false, error: "name is required" };
|
|
2594
|
+
}
|
|
2595
|
+
const entry = {
|
|
2596
|
+
id: mode.id,
|
|
2597
|
+
name: mode.name,
|
|
2598
|
+
description: mode.description || "",
|
|
2599
|
+
thresholds: {
|
|
2600
|
+
warn: mode.thresholds?.warn ?? 0.6,
|
|
2601
|
+
soft: mode.thresholds?.soft ?? 0.75,
|
|
2602
|
+
hard: mode.thresholds?.hard ?? 0.9
|
|
2603
|
+
},
|
|
2604
|
+
aggressiveOn: mode.aggressiveOn || "soft",
|
|
2605
|
+
preserveK: mode.preserveK ?? 10,
|
|
2606
|
+
eliseThreshold: mode.eliseThreshold ?? 2e3,
|
|
2607
|
+
targetLoad: mode.targetLoad ?? 0.65,
|
|
2608
|
+
custom: true
|
|
2609
|
+
};
|
|
2610
|
+
modes.set(mode.id, entry);
|
|
2611
|
+
void save2();
|
|
2612
|
+
return { ok: true };
|
|
2613
|
+
};
|
|
2614
|
+
const update = (id, patch) => {
|
|
2615
|
+
if (BUILTIN_IDS.has(id)) {
|
|
2616
|
+
return { ok: false, error: `Cannot modify built-in mode "${id}"` };
|
|
2617
|
+
}
|
|
2618
|
+
const existing = modes.get(id);
|
|
2619
|
+
if (!existing) {
|
|
2620
|
+
return { ok: false, error: `Mode "${id}" not found` };
|
|
2621
|
+
}
|
|
2622
|
+
const next = { ...existing };
|
|
2623
|
+
if (patch.name !== void 0) next.name = patch.name;
|
|
2624
|
+
if (patch.description !== void 0) next.description = patch.description;
|
|
2625
|
+
if (patch.thresholds) {
|
|
2626
|
+
next.thresholds = {
|
|
2627
|
+
warn: patch.thresholds.warn ?? existing.thresholds.warn,
|
|
2628
|
+
soft: patch.thresholds.soft ?? existing.thresholds.soft,
|
|
2629
|
+
hard: patch.thresholds.hard ?? existing.thresholds.hard
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
if (patch.preserveK !== void 0) next.preserveK = patch.preserveK;
|
|
2633
|
+
if (patch.eliseThreshold !== void 0) next.eliseThreshold = patch.eliseThreshold;
|
|
2634
|
+
if (patch.targetLoad !== void 0) next.targetLoad = patch.targetLoad;
|
|
2635
|
+
if (patch.aggressiveOn !== void 0) next.aggressiveOn = patch.aggressiveOn;
|
|
2636
|
+
modes.set(id, next);
|
|
2637
|
+
void save2();
|
|
2638
|
+
return { ok: true };
|
|
2639
|
+
};
|
|
2640
|
+
const remove = (id) => {
|
|
2641
|
+
if (BUILTIN_IDS.has(id)) {
|
|
2642
|
+
return { ok: false, error: `Cannot delete built-in mode "${id}"` };
|
|
2643
|
+
}
|
|
2644
|
+
if (!modes.delete(id)) {
|
|
2645
|
+
return { ok: false, error: `Mode "${id}" not found` };
|
|
2646
|
+
}
|
|
2647
|
+
void save2();
|
|
2648
|
+
return { ok: true };
|
|
2649
|
+
};
|
|
2650
|
+
const list = () => {
|
|
2651
|
+
const builtins = listContextWindowModes().map((m) => ({
|
|
2652
|
+
id: m.id,
|
|
2653
|
+
name: m.name,
|
|
2654
|
+
description: m.description,
|
|
2655
|
+
thresholds: { ...m.thresholds },
|
|
2656
|
+
aggressiveOn: m.aggressiveOn,
|
|
2657
|
+
preserveK: m.preserveK,
|
|
2658
|
+
eliseThreshold: m.eliseThreshold,
|
|
2659
|
+
targetLoad: m.targetLoad,
|
|
2660
|
+
custom: false
|
|
2661
|
+
}));
|
|
2662
|
+
const custom = [...modes.values()];
|
|
2663
|
+
return [...builtins, ...custom];
|
|
2664
|
+
};
|
|
2665
|
+
return { modes, load: load2, save: save2, create, update, remove, list };
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// src/server/token-estimator.ts
|
|
2669
|
+
function estimateTokens(s) {
|
|
2670
|
+
return Math.ceil(s.length / 4);
|
|
2671
|
+
}
|
|
2672
|
+
function stringifyContent(c) {
|
|
2673
|
+
if (typeof c === "string") return c;
|
|
2674
|
+
try {
|
|
2675
|
+
return JSON.stringify(c);
|
|
2676
|
+
} catch {
|
|
2677
|
+
return String(c);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
function messageTokens(content) {
|
|
2681
|
+
if (typeof content === "string") return estimateTokens(content);
|
|
2682
|
+
if (!Array.isArray(content)) return 0;
|
|
1999
2683
|
let tk = 0;
|
|
2000
2684
|
for (const b of content) {
|
|
2001
2685
|
if (b.type === "text") tk += estimateTokens(b.text ?? "");
|
|
@@ -2050,16 +2734,32 @@ async function startWebUI(opts = {}) {
|
|
|
2050
2734
|
httpPort = await findFreePort(wsHost, requestedHttpPort);
|
|
2051
2735
|
wsPort = await findFreePort(wsHost, requestedWsPort, { exclude: /* @__PURE__ */ new Set([httpPort]) });
|
|
2052
2736
|
if (httpPort !== requestedHttpPort) {
|
|
2053
|
-
console.warn(
|
|
2737
|
+
console.warn(JSON.stringify({
|
|
2738
|
+
level: "warn",
|
|
2739
|
+
event: "webui.port_reassigned",
|
|
2740
|
+
protocol: "HTTP",
|
|
2741
|
+
requested: requestedHttpPort,
|
|
2742
|
+
assigned: httpPort,
|
|
2743
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2744
|
+
}));
|
|
2054
2745
|
}
|
|
2055
2746
|
if (wsPort !== requestedWsPort) {
|
|
2056
|
-
console.warn(
|
|
2747
|
+
console.warn(JSON.stringify({
|
|
2748
|
+
level: "warn",
|
|
2749
|
+
event: "webui.port_reassigned",
|
|
2750
|
+
protocol: "WS",
|
|
2751
|
+
requested: requestedWsPort,
|
|
2752
|
+
assigned: wsPort,
|
|
2753
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2754
|
+
}));
|
|
2057
2755
|
}
|
|
2058
2756
|
}
|
|
2059
2757
|
console.log("[WebUI] Starting backend services...");
|
|
2060
2758
|
const boot = await bootConfig();
|
|
2061
|
-
const { config: baseConfig, vault, globalConfigPath,
|
|
2759
|
+
const { config: baseConfig, vault, globalConfigPath, wpaths, logger } = boot;
|
|
2062
2760
|
let config = baseConfig;
|
|
2761
|
+
let projectRoot = boot.projectRoot;
|
|
2762
|
+
let workingDir = projectRoot;
|
|
2063
2763
|
let configWriteLock = Promise.resolve();
|
|
2064
2764
|
console.log("[WebUI] Config loaded:", config.provider ?? "(none)", "/", config.model ?? "(none)");
|
|
2065
2765
|
if (!config.provider && config.providers && typeof config.providers === "object" && config.providers !== null && !Array.isArray(config.providers) && Object.keys(config.providers).length > 0) {
|
|
@@ -2083,7 +2783,12 @@ async function startWebUI(opts = {}) {
|
|
|
2083
2783
|
for (const f of factories) providerRegistry.register(f);
|
|
2084
2784
|
console.log("[WebUI] Provider registry loaded:", providerRegistry.list().length, "providers");
|
|
2085
2785
|
} catch (err) {
|
|
2086
|
-
console.warn(
|
|
2786
|
+
console.warn(JSON.stringify({
|
|
2787
|
+
level: "warn",
|
|
2788
|
+
event: "webui.provider_registry_load_failed",
|
|
2789
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2790
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2791
|
+
}));
|
|
2087
2792
|
}
|
|
2088
2793
|
const toolRegistry = new ToolRegistry();
|
|
2089
2794
|
toolRegistry.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
|
|
@@ -2094,10 +2799,13 @@ async function startWebUI(opts = {}) {
|
|
|
2094
2799
|
toolRegistry.register(searchMemoryTool(memoryStore));
|
|
2095
2800
|
toolRegistry.register(relatedMemoryTool(memoryStore));
|
|
2096
2801
|
}
|
|
2097
|
-
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
2098
2802
|
const events = new EventBus();
|
|
2099
2803
|
events.setLogger(logger);
|
|
2100
|
-
|
|
2804
|
+
toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
|
|
2805
|
+
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
2806
|
+
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
2807
|
+
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
2808
|
+
let sessionStore = new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
2101
2809
|
sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
|
|
2102
2810
|
if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
|
|
2103
2811
|
}).catch(() => void 0);
|
|
@@ -2111,6 +2819,42 @@ async function startWebUI(opts = {}) {
|
|
|
2111
2819
|
});
|
|
2112
2820
|
let sessionStartedAt = Date.now();
|
|
2113
2821
|
console.log("[WebUI] Session created:", session.id);
|
|
2822
|
+
try {
|
|
2823
|
+
await touchProjectEntry(projectRoot, workingDir);
|
|
2824
|
+
} catch {
|
|
2825
|
+
}
|
|
2826
|
+
let statusTracker;
|
|
2827
|
+
try {
|
|
2828
|
+
const registry = getSessionRegistry(wpaths.globalRoot);
|
|
2829
|
+
await registry.register({
|
|
2830
|
+
sessionId: session.id,
|
|
2831
|
+
projectSlug: wpaths.projectSlug,
|
|
2832
|
+
projectRoot,
|
|
2833
|
+
projectName: path8.basename(projectRoot),
|
|
2834
|
+
workingDir,
|
|
2835
|
+
pid: process.pid,
|
|
2836
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2837
|
+
});
|
|
2838
|
+
statusTracker = new AgentStatusTracker({ events, registry });
|
|
2839
|
+
statusTracker.start();
|
|
2840
|
+
const stopTracking = async () => {
|
|
2841
|
+
try {
|
|
2842
|
+
await registry.markClosing();
|
|
2843
|
+
statusTracker?.stop();
|
|
2844
|
+
} catch {
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
process.once("beforeExit", () => {
|
|
2848
|
+
void stopTracking();
|
|
2849
|
+
});
|
|
2850
|
+
process.once("SIGINT", () => {
|
|
2851
|
+
void stopTracking();
|
|
2852
|
+
});
|
|
2853
|
+
process.once("SIGTERM", () => {
|
|
2854
|
+
void stopTracking();
|
|
2855
|
+
});
|
|
2856
|
+
} catch {
|
|
2857
|
+
}
|
|
2114
2858
|
const tokenCounter = new DefaultTokenCounter2({
|
|
2115
2859
|
registry: modelsRegistry,
|
|
2116
2860
|
providerId: config.provider
|
|
@@ -2119,6 +2863,13 @@ async function startWebUI(opts = {}) {
|
|
|
2119
2863
|
const activeMode = await modeStore.getActiveMode();
|
|
2120
2864
|
let modeId = activeMode?.id ?? "default";
|
|
2121
2865
|
const modePrompt = activeMode?.prompt ?? "";
|
|
2866
|
+
const customModeStore = createCustomModeStore(wpaths.configDir);
|
|
2867
|
+
await customModeStore.load();
|
|
2868
|
+
console.log(
|
|
2869
|
+
"[WebUI] Custom context modes loaded:",
|
|
2870
|
+
customModeStore.list().filter((m) => m.custom).length,
|
|
2871
|
+
"custom"
|
|
2872
|
+
);
|
|
2122
2873
|
const resolvedModel = await modelsRegistry.getModel(config.provider, config.model);
|
|
2123
2874
|
const modelCapabilities = resolvedModel?.capabilities ? {
|
|
2124
2875
|
maxContextTokens: resolvedModel.capabilities.maxContext,
|
|
@@ -2135,12 +2886,19 @@ async function startWebUI(opts = {}) {
|
|
|
2135
2886
|
modePrompt,
|
|
2136
2887
|
modelCapabilities
|
|
2137
2888
|
});
|
|
2889
|
+
let onlineAgents = [];
|
|
2890
|
+
try {
|
|
2891
|
+
const systemMailbox = new GlobalMailbox2(wpaths.projectDir);
|
|
2892
|
+
onlineAgents = await systemMailbox.getAgentStatuses();
|
|
2893
|
+
} catch {
|
|
2894
|
+
}
|
|
2138
2895
|
const systemPrompt = await systemPromptBuilder.build({
|
|
2139
2896
|
cwd: projectRoot,
|
|
2140
2897
|
projectRoot,
|
|
2141
2898
|
tools: toolRegistry.list(),
|
|
2142
2899
|
provider: config.provider,
|
|
2143
|
-
model: config.model
|
|
2900
|
+
model: config.model,
|
|
2901
|
+
onlineAgents
|
|
2144
2902
|
});
|
|
2145
2903
|
let provider;
|
|
2146
2904
|
if (!needsProvider) {
|
|
@@ -2157,7 +2915,12 @@ async function startWebUI(opts = {}) {
|
|
|
2157
2915
|
provider = makeProviderFromConfig(config.provider, cfgWithType);
|
|
2158
2916
|
}
|
|
2159
2917
|
} catch (err) {
|
|
2160
|
-
console.error(
|
|
2918
|
+
console.error(JSON.stringify({
|
|
2919
|
+
level: "error",
|
|
2920
|
+
event: "webui.provider_create_failed",
|
|
2921
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2922
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2923
|
+
}));
|
|
2161
2924
|
throw err;
|
|
2162
2925
|
}
|
|
2163
2926
|
} else {
|
|
@@ -2174,7 +2937,12 @@ async function startWebUI(opts = {}) {
|
|
|
2174
2937
|
});
|
|
2175
2938
|
console.log("[WebUI] Using saved provider:", firstKey);
|
|
2176
2939
|
} catch (err) {
|
|
2177
|
-
console.error(
|
|
2940
|
+
console.error(JSON.stringify({
|
|
2941
|
+
level: "error",
|
|
2942
|
+
event: "webui.provider_stub_create_failed",
|
|
2943
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2944
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2945
|
+
}));
|
|
2178
2946
|
throw err;
|
|
2179
2947
|
}
|
|
2180
2948
|
} else {
|
|
@@ -2189,13 +2957,160 @@ async function startWebUI(opts = {}) {
|
|
|
2189
2957
|
session,
|
|
2190
2958
|
signal: new AbortController().signal,
|
|
2191
2959
|
tokenCounter,
|
|
2192
|
-
cwd:
|
|
2960
|
+
cwd: workingDir,
|
|
2193
2961
|
projectRoot,
|
|
2194
2962
|
model: config.model
|
|
2195
2963
|
});
|
|
2196
2964
|
const initialContextPolicy = resolveContextWindowPolicy(config.context);
|
|
2197
2965
|
context.meta["contextWindowMode"] = initialContextPolicy.id;
|
|
2198
2966
|
context.meta["contextWindowPolicy"] = initialContextPolicy;
|
|
2967
|
+
{
|
|
2968
|
+
const autonomyCfg = config.autonomy ?? {};
|
|
2969
|
+
const rawMode = autonomyCfg["defaultMode"];
|
|
2970
|
+
context.meta["autonomy"] = rawMode === "suggest" || rawMode === "auto" ? rawMode : "off";
|
|
2971
|
+
context.meta["autonomyDelayMs"] = autonomyCfg["autoProceedDelayMs"] ?? 45e3;
|
|
2972
|
+
context.meta["autoProceedMaxIterations"] = autonomyCfg["autoProceedMaxIterations"] ?? 50;
|
|
2973
|
+
context.meta["yolo"] = autonomyCfg["yolo"] ?? config.yolo ?? false;
|
|
2974
|
+
context.meta["chime"] = autonomyCfg["chime"] ?? false;
|
|
2975
|
+
context.meta["confirmExit"] = autonomyCfg["confirmExit"] !== false;
|
|
2976
|
+
context.meta["streamFleet"] = autonomyCfg["streamFleet"] !== false;
|
|
2977
|
+
context.meta["enhanceEnabled"] = autonomyCfg["enhance"] ?? true;
|
|
2978
|
+
context.meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
|
|
2979
|
+
context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
|
|
2980
|
+
context.meta["nextPrediction"] = config.nextPrediction ?? false;
|
|
2981
|
+
context.meta["featureMcp"] = config.features.mcp !== false;
|
|
2982
|
+
context.meta["featurePlugins"] = config.features.plugins !== false;
|
|
2983
|
+
context.meta["featureMemory"] = config.features.memory !== false;
|
|
2984
|
+
context.meta["featureSkills"] = config.features.skills !== false;
|
|
2985
|
+
context.meta["featureModelsRegistry"] = config.features.modelsRegistry !== false;
|
|
2986
|
+
context.meta["indexOnStart"] = config.indexing?.onSessionStart !== false;
|
|
2987
|
+
context.meta["contextAutoCompact"] = config.context?.autoCompact !== false;
|
|
2988
|
+
context.meta["contextStrategy"] = config.context?.strategy ?? "hybrid";
|
|
2989
|
+
context.meta["logLevel"] = config.log?.level ?? "info";
|
|
2990
|
+
context.meta["auditLevel"] = config.session?.auditLevel ?? "standard";
|
|
2991
|
+
context.meta["maxIterations"] = config.tools?.maxIterations ?? 500;
|
|
2992
|
+
}
|
|
2993
|
+
const PREF_KEYS = [
|
|
2994
|
+
"autonomy",
|
|
2995
|
+
"autonomyDelayMs",
|
|
2996
|
+
"autoProceedMaxIterations",
|
|
2997
|
+
"yolo",
|
|
2998
|
+
"maxIterations",
|
|
2999
|
+
"chime",
|
|
3000
|
+
"confirmExit",
|
|
3001
|
+
"streamFleet",
|
|
3002
|
+
"nextPrediction",
|
|
3003
|
+
"enhanceEnabled",
|
|
3004
|
+
"enhanceDelayMs",
|
|
3005
|
+
"enhanceLanguage",
|
|
3006
|
+
"featureMcp",
|
|
3007
|
+
"featurePlugins",
|
|
3008
|
+
"featureMemory",
|
|
3009
|
+
"featureSkills",
|
|
3010
|
+
"featureModelsRegistry",
|
|
3011
|
+
"indexOnStart",
|
|
3012
|
+
"contextAutoCompact",
|
|
3013
|
+
"contextStrategy",
|
|
3014
|
+
"logLevel",
|
|
3015
|
+
"auditLevel"
|
|
3016
|
+
];
|
|
3017
|
+
const prefSnapshot = () => {
|
|
3018
|
+
const snapshot = {};
|
|
3019
|
+
for (const k of PREF_KEYS) {
|
|
3020
|
+
if (k in context.meta) snapshot[k] = context.meta[k];
|
|
3021
|
+
}
|
|
3022
|
+
return snapshot;
|
|
3023
|
+
};
|
|
3024
|
+
const persistPrefsToConfig = async (payload) => {
|
|
3025
|
+
const write = async () => {
|
|
3026
|
+
let raw;
|
|
3027
|
+
try {
|
|
3028
|
+
raw = await fs6.readFile(globalConfigPath, "utf8");
|
|
3029
|
+
} catch {
|
|
3030
|
+
raw = "{}";
|
|
3031
|
+
}
|
|
3032
|
+
let parsed;
|
|
3033
|
+
try {
|
|
3034
|
+
parsed = JSON.parse(raw);
|
|
3035
|
+
} catch {
|
|
3036
|
+
logger.warn(`prefs: refusing to overwrite corrupt config at ${globalConfigPath}`);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
const decrypted = decryptConfigSecrets2(parsed, vault);
|
|
3040
|
+
const autonomyCfg = decrypted.autonomy ?? {};
|
|
3041
|
+
let autonomyTouched = false;
|
|
3042
|
+
const setAutonomy = (key, val) => {
|
|
3043
|
+
autonomyCfg[key] = val;
|
|
3044
|
+
autonomyTouched = true;
|
|
3045
|
+
};
|
|
3046
|
+
if (typeof payload["autonomy"] === "string" && ["off", "suggest", "auto"].includes(payload["autonomy"])) {
|
|
3047
|
+
setAutonomy("defaultMode", payload["autonomy"]);
|
|
3048
|
+
}
|
|
3049
|
+
if (typeof payload["autonomyDelayMs"] === "number") setAutonomy("autoProceedDelayMs", payload["autonomyDelayMs"]);
|
|
3050
|
+
if (typeof payload["autoProceedMaxIterations"] === "number") setAutonomy("autoProceedMaxIterations", payload["autoProceedMaxIterations"]);
|
|
3051
|
+
if (typeof payload["yolo"] === "boolean") setAutonomy("yolo", payload["yolo"]);
|
|
3052
|
+
if (typeof payload["chime"] === "boolean") setAutonomy("chime", payload["chime"]);
|
|
3053
|
+
if (typeof payload["confirmExit"] === "boolean") setAutonomy("confirmExit", payload["confirmExit"]);
|
|
3054
|
+
if (typeof payload["streamFleet"] === "boolean") setAutonomy("streamFleet", payload["streamFleet"]);
|
|
3055
|
+
if (typeof payload["enhanceEnabled"] === "boolean") setAutonomy("enhance", payload["enhanceEnabled"]);
|
|
3056
|
+
if (typeof payload["enhanceDelayMs"] === "number") setAutonomy("enhanceDelayMs", payload["enhanceDelayMs"]);
|
|
3057
|
+
if (typeof payload["enhanceLanguage"] === "string") setAutonomy("enhanceLanguage", payload["enhanceLanguage"]);
|
|
3058
|
+
if (autonomyTouched) decrypted.autonomy = autonomyCfg;
|
|
3059
|
+
if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
|
|
3060
|
+
const FEATURE_MAP = {
|
|
3061
|
+
featureMcp: "mcp",
|
|
3062
|
+
featurePlugins: "plugins",
|
|
3063
|
+
featureMemory: "memory",
|
|
3064
|
+
featureSkills: "skills",
|
|
3065
|
+
featureModelsRegistry: "modelsRegistry"
|
|
3066
|
+
};
|
|
3067
|
+
for (const [prefKey, cfgKey] of Object.entries(FEATURE_MAP)) {
|
|
3068
|
+
if (typeof payload[prefKey] === "boolean") {
|
|
3069
|
+
const feats = decrypted.features ?? {};
|
|
3070
|
+
feats[cfgKey] = payload[prefKey];
|
|
3071
|
+
decrypted.features = feats;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
if (typeof payload["contextAutoCompact"] === "boolean" || typeof payload["contextStrategy"] === "string") {
|
|
3075
|
+
const ctxCfg = decrypted.context ?? {};
|
|
3076
|
+
if (typeof payload["contextAutoCompact"] === "boolean") ctxCfg.autoCompact = payload["contextAutoCompact"];
|
|
3077
|
+
if (typeof payload["contextStrategy"] === "string") ctxCfg.strategy = payload["contextStrategy"];
|
|
3078
|
+
decrypted.context = ctxCfg;
|
|
3079
|
+
}
|
|
3080
|
+
if (typeof payload["logLevel"] === "string") {
|
|
3081
|
+
const logCfg = decrypted.log ?? {};
|
|
3082
|
+
logCfg.level = payload["logLevel"];
|
|
3083
|
+
decrypted.log = logCfg;
|
|
3084
|
+
}
|
|
3085
|
+
if (typeof payload["auditLevel"] === "string") {
|
|
3086
|
+
const sessionCfg = decrypted.session ?? {};
|
|
3087
|
+
sessionCfg.auditLevel = payload["auditLevel"];
|
|
3088
|
+
decrypted.session = sessionCfg;
|
|
3089
|
+
}
|
|
3090
|
+
if (typeof payload["indexOnStart"] === "boolean") {
|
|
3091
|
+
const indexingCfg = decrypted.indexing ?? {};
|
|
3092
|
+
indexingCfg.onSessionStart = payload["indexOnStart"];
|
|
3093
|
+
decrypted.indexing = indexingCfg;
|
|
3094
|
+
}
|
|
3095
|
+
if (typeof payload["maxIterations"] === "number") {
|
|
3096
|
+
const toolsCfg = decrypted.tools ?? {};
|
|
3097
|
+
toolsCfg.maxIterations = payload["maxIterations"];
|
|
3098
|
+
decrypted.tools = toolsCfg;
|
|
3099
|
+
}
|
|
3100
|
+
const encrypted = encryptConfigSecrets2(decrypted, vault);
|
|
3101
|
+
await atomicWrite5(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
3102
|
+
};
|
|
3103
|
+
const next = configWriteLock.then(write);
|
|
3104
|
+
configWriteLock = next.then(
|
|
3105
|
+
() => void 0,
|
|
3106
|
+
() => void 0
|
|
3107
|
+
);
|
|
3108
|
+
try {
|
|
3109
|
+
await next;
|
|
3110
|
+
} catch (err) {
|
|
3111
|
+
logger.warn(`prefs: failed to persist to config: ${errMessage(err)}`);
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
2199
3114
|
const pipelines = createDefaultPipelines();
|
|
2200
3115
|
const collabBus = new CollaborationBus();
|
|
2201
3116
|
const collabPause = collabPauseMiddleware(collabBus, { logger });
|
|
@@ -2259,8 +3174,9 @@ async function startWebUI(opts = {}) {
|
|
|
2259
3174
|
}
|
|
2260
3175
|
const secretScrubber = container.resolve(TOKENS2.SecretScrubber);
|
|
2261
3176
|
const renderer = container.has(TOKENS2.Renderer) ? container.resolve(TOKENS2.Renderer) : void 0;
|
|
3177
|
+
const permissionPolicy = container.resolve(TOKENS2.PermissionPolicy);
|
|
2262
3178
|
const toolExecutor = new ToolExecutor(toolRegistry, {
|
|
2263
|
-
permissionPolicy
|
|
3179
|
+
permissionPolicy,
|
|
2264
3180
|
secretScrubber,
|
|
2265
3181
|
renderer,
|
|
2266
3182
|
events,
|
|
@@ -2284,6 +3200,78 @@ async function startWebUI(opts = {}) {
|
|
|
2284
3200
|
toolExecutor
|
|
2285
3201
|
});
|
|
2286
3202
|
console.log("[WebUI] Agent initialized");
|
|
3203
|
+
const brainSettings = { maxAutoRisk: "medium" };
|
|
3204
|
+
const autonomousBrain = {
|
|
3205
|
+
decide: (request) => createAutonomyBrain({
|
|
3206
|
+
provider,
|
|
3207
|
+
model: context.model,
|
|
3208
|
+
maxAutoRisk: "all"
|
|
3209
|
+
// the tiered ceiling gates risk — keep inner permissive
|
|
3210
|
+
}).decide(request)
|
|
3211
|
+
};
|
|
3212
|
+
const brain = new ObservableBrainArbiter(
|
|
3213
|
+
createTieredBrainArbiter({
|
|
3214
|
+
policy: new DefaultBrainArbiter(),
|
|
3215
|
+
autonomous: autonomousBrain,
|
|
3216
|
+
getMaxAutoRisk: () => brainSettings.maxAutoRisk
|
|
3217
|
+
}),
|
|
3218
|
+
events
|
|
3219
|
+
);
|
|
3220
|
+
container.bind(TOKENS2.BrainArbiter, () => brain);
|
|
3221
|
+
const brainMailbox = new GlobalMailbox2(wpaths.projectDir, events);
|
|
3222
|
+
const brainMonitor = new BrainMonitor({
|
|
3223
|
+
events,
|
|
3224
|
+
brain,
|
|
3225
|
+
intervene: async ({ subject, body }) => {
|
|
3226
|
+
const tag = mailboxSessionTag(session.id);
|
|
3227
|
+
await brainMailbox.send({
|
|
3228
|
+
from: `brain@${tag}`,
|
|
3229
|
+
to: `leader@${tag}`,
|
|
3230
|
+
type: "steer",
|
|
3231
|
+
subject,
|
|
3232
|
+
body,
|
|
3233
|
+
priority: "high"
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
});
|
|
3237
|
+
brainMonitor.start();
|
|
3238
|
+
console.log("[WebUI] Brain initialized (tiered policy \u2192 LLM, monitor active)");
|
|
3239
|
+
const brainLog = [];
|
|
3240
|
+
const pushBrainLog = (entry) => {
|
|
3241
|
+
brainLog.push(entry);
|
|
3242
|
+
if (brainLog.length > 20) brainLog.shift();
|
|
3243
|
+
};
|
|
3244
|
+
events.on(
|
|
3245
|
+
"brain.decision_answered",
|
|
3246
|
+
(e) => pushBrainLog({
|
|
3247
|
+
at: e.at,
|
|
3248
|
+
kind: "answered",
|
|
3249
|
+
question: e.request.question,
|
|
3250
|
+
outcome: e.decision.type === "answer" ? e.decision.optionId ?? e.decision.text : ""
|
|
3251
|
+
})
|
|
3252
|
+
);
|
|
3253
|
+
events.on(
|
|
3254
|
+
"brain.decision_ask_human",
|
|
3255
|
+
(e) => pushBrainLog({ at: e.at, kind: "ask_human", question: e.request.question, outcome: "needs human judgement" })
|
|
3256
|
+
);
|
|
3257
|
+
events.on(
|
|
3258
|
+
"brain.decision_denied",
|
|
3259
|
+
(e) => pushBrainLog({
|
|
3260
|
+
at: e.at,
|
|
3261
|
+
kind: "denied",
|
|
3262
|
+
question: e.request.question,
|
|
3263
|
+
outcome: e.decision.type === "deny" ? e.decision.reason : ""
|
|
3264
|
+
})
|
|
3265
|
+
);
|
|
3266
|
+
events.on(
|
|
3267
|
+
"brain.intervention",
|
|
3268
|
+
(e) => pushBrainLog({
|
|
3269
|
+
at: e.at,
|
|
3270
|
+
kind: "intervention",
|
|
3271
|
+
question: e.request.question,
|
|
3272
|
+
outcome: e.intervened ? "steered the agent" : "observed (no action)"
|
|
3273
|
+
})
|
|
3274
|
+
);
|
|
2287
3275
|
const autoPhaseHandler = new AutoPhaseWebSocketHandler(
|
|
2288
3276
|
agent,
|
|
2289
3277
|
context,
|
|
@@ -2322,15 +3310,16 @@ async function startWebUI(opts = {}) {
|
|
|
2322
3310
|
inputCost,
|
|
2323
3311
|
outputCost,
|
|
2324
3312
|
cacheReadCost,
|
|
2325
|
-
projectName:
|
|
2326
|
-
|
|
3313
|
+
projectName: path8.basename(projectRoot) || projectRoot,
|
|
3314
|
+
projectRoot,
|
|
3315
|
+
cwd: workingDir,
|
|
2327
3316
|
mode: modeId,
|
|
2328
3317
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
|
|
2329
3318
|
wsToken
|
|
2330
3319
|
};
|
|
2331
3320
|
}
|
|
2332
3321
|
const wsToken = generateAuthToken();
|
|
2333
|
-
console.log(
|
|
3322
|
+
console.log("[WebUI] WS auth token generated (redacted from logs)");
|
|
2334
3323
|
const verifyClient2 = (info) => verifyClient({
|
|
2335
3324
|
origin: info.origin,
|
|
2336
3325
|
url: info.req.url ?? "",
|
|
@@ -2353,6 +3342,13 @@ async function startWebUI(opts = {}) {
|
|
|
2353
3342
|
maxPayload: WS_MAX_PAYLOAD
|
|
2354
3343
|
}) : null;
|
|
2355
3344
|
const clients = /* @__PURE__ */ new Map();
|
|
3345
|
+
context.onWorkingDirChanged((newDir) => {
|
|
3346
|
+
workingDir = newDir;
|
|
3347
|
+
broadcast(clients, {
|
|
3348
|
+
type: "working_dir.changed",
|
|
3349
|
+
payload: { cwd: newDir, projectRoot }
|
|
3350
|
+
});
|
|
3351
|
+
});
|
|
2356
3352
|
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
2357
3353
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
2358
3354
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
@@ -2377,9 +3373,15 @@ async function startWebUI(opts = {}) {
|
|
|
2377
3373
|
const handleConnection = (ws) => {
|
|
2378
3374
|
const client = { ws, sessionId: session.id, connectedAt: Date.now() };
|
|
2379
3375
|
clients.set(ws, client);
|
|
2380
|
-
console.log("[WebUI] Client connected, total:", clients.size);
|
|
2381
3376
|
void sessionStartPayload().then((payload) => {
|
|
2382
3377
|
send(ws, { type: "session.start", payload });
|
|
3378
|
+
}).catch((err) => {
|
|
3379
|
+
console.warn(JSON.stringify({
|
|
3380
|
+
level: "warn",
|
|
3381
|
+
event: "webui.session_start_payload_failed",
|
|
3382
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3383
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3384
|
+
}));
|
|
2383
3385
|
});
|
|
2384
3386
|
autoPhaseHandler.addClient(ws);
|
|
2385
3387
|
worktreeHandler.addClient(ws);
|
|
@@ -2411,47 +3413,130 @@ async function startWebUI(opts = {}) {
|
|
|
2411
3413
|
await handleMessage(ws, client, rawObj);
|
|
2412
3414
|
}
|
|
2413
3415
|
} catch (err) {
|
|
2414
|
-
console.error(
|
|
3416
|
+
console.error(JSON.stringify({
|
|
3417
|
+
level: "error",
|
|
3418
|
+
event: "webui.ws_message_parse_failed",
|
|
3419
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3420
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3421
|
+
}));
|
|
2415
3422
|
}
|
|
2416
3423
|
});
|
|
2417
3424
|
ws.on("close", () => {
|
|
2418
3425
|
clients.delete(ws);
|
|
2419
3426
|
rateLimits.delete(String(ws));
|
|
2420
|
-
console.log("[WebUI] Client disconnected, total:", clients.size);
|
|
2421
3427
|
if (pendingConfirms.size > 0) {
|
|
2422
|
-
for (const [id,
|
|
2423
|
-
|
|
3428
|
+
for (const [id, resolve5] of pendingConfirms) {
|
|
3429
|
+
resolve5("no");
|
|
2424
3430
|
pendingConfirms.delete(id);
|
|
2425
3431
|
}
|
|
2426
3432
|
}
|
|
2427
3433
|
});
|
|
2428
3434
|
ws.on("error", (err) => {
|
|
2429
|
-
console.warn(
|
|
3435
|
+
console.warn(JSON.stringify({
|
|
3436
|
+
level: "warn",
|
|
3437
|
+
event: "webui.client_socket_error",
|
|
3438
|
+
message: err.message,
|
|
3439
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3440
|
+
}));
|
|
2430
3441
|
});
|
|
2431
3442
|
};
|
|
3443
|
+
const sessionLogging = resolveSessionLoggingConfig(
|
|
3444
|
+
config
|
|
3445
|
+
);
|
|
3446
|
+
const sessionBridge = createSessionEventBridge(
|
|
3447
|
+
() => context.session ?? session,
|
|
3448
|
+
sessionLogging.auditLevel,
|
|
3449
|
+
{ sampling: sessionLogging.sampling }
|
|
3450
|
+
);
|
|
2432
3451
|
let eventsArmed = false;
|
|
2433
3452
|
const armOnce = (label) => {
|
|
2434
3453
|
if (eventsArmed) return;
|
|
2435
3454
|
eventsArmed = true;
|
|
2436
3455
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
2437
|
-
setupEvents({ events, broadcast, clients, config, context, pendingConfirms });
|
|
3456
|
+
setupEvents({ events, broadcast, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge });
|
|
2438
3457
|
};
|
|
2439
3458
|
wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
|
|
2440
3459
|
wssPrimary.on("connection", handleConnection);
|
|
2441
3460
|
wssPrimary.on("error", (err) => {
|
|
2442
|
-
console.error(
|
|
3461
|
+
console.error(JSON.stringify({
|
|
3462
|
+
level: "error",
|
|
3463
|
+
event: "webui.ws_server_error",
|
|
3464
|
+
host: wsHost,
|
|
3465
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3466
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3467
|
+
}));
|
|
2443
3468
|
});
|
|
2444
3469
|
if (wssSecondary) {
|
|
2445
3470
|
wssSecondary.on("listening", () => armOnce(`::1:${wsPort}`));
|
|
2446
3471
|
wssSecondary.on("connection", handleConnection);
|
|
2447
3472
|
wssSecondary.on("error", (err) => {
|
|
2448
3473
|
if (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL") {
|
|
2449
|
-
console.warn(
|
|
3474
|
+
console.warn(JSON.stringify({
|
|
3475
|
+
level: "warn",
|
|
3476
|
+
event: "webui.ipv6_unavailable",
|
|
3477
|
+
code: err.code,
|
|
3478
|
+
message: err.message,
|
|
3479
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3480
|
+
}));
|
|
2450
3481
|
} else {
|
|
2451
|
-
console.error(
|
|
3482
|
+
console.error(JSON.stringify({
|
|
3483
|
+
level: "error",
|
|
3484
|
+
event: "webui.ws_server_error",
|
|
3485
|
+
host: "::1",
|
|
3486
|
+
message: err.message,
|
|
3487
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3488
|
+
}));
|
|
2452
3489
|
}
|
|
2453
3490
|
});
|
|
2454
3491
|
}
|
|
3492
|
+
async function touchProjectEntry(root, workDir) {
|
|
3493
|
+
const resolved = path8.resolve(root);
|
|
3494
|
+
const manifest = await loadManifest(globalConfigPath);
|
|
3495
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3496
|
+
const existing = manifest.projects.find((p) => path8.resolve(p.root) === resolved);
|
|
3497
|
+
if (existing) {
|
|
3498
|
+
existing.lastSeen = now;
|
|
3499
|
+
if (workDir) existing.lastWorkingDir = path8.resolve(workDir);
|
|
3500
|
+
} else {
|
|
3501
|
+
manifest.projects.push({
|
|
3502
|
+
name: path8.basename(resolved),
|
|
3503
|
+
root: resolved,
|
|
3504
|
+
slug: generateProjectSlug(resolved),
|
|
3505
|
+
createdAt: now,
|
|
3506
|
+
lastSeen: now,
|
|
3507
|
+
lastWorkingDir: workDir ? path8.resolve(workDir) : void 0
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
await saveManifest(manifest, globalConfigPath);
|
|
3511
|
+
await ensureProjectDataDir(generateProjectSlug(resolved), globalConfigPath);
|
|
3512
|
+
}
|
|
3513
|
+
function projectsJsonPath(globalConfigPath2) {
|
|
3514
|
+
const base = path8.dirname(globalConfigPath2);
|
|
3515
|
+
return path8.join(base, "projects.json");
|
|
3516
|
+
}
|
|
3517
|
+
async function loadManifest(globalConfigPath2) {
|
|
3518
|
+
try {
|
|
3519
|
+
const raw = await fs6.readFile(projectsJsonPath(globalConfigPath2), "utf8");
|
|
3520
|
+
const parsed = JSON.parse(raw);
|
|
3521
|
+
return { projects: parsed.projects ?? [] };
|
|
3522
|
+
} catch {
|
|
3523
|
+
return { projects: [] };
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
async function saveManifest(manifest, globalConfigPath2) {
|
|
3527
|
+
const file = projectsJsonPath(globalConfigPath2);
|
|
3528
|
+
await fs6.mkdir(path8.dirname(file), { recursive: true });
|
|
3529
|
+
await fs6.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
3530
|
+
}
|
|
3531
|
+
function generateProjectSlug(rootPath) {
|
|
3532
|
+
return projectSlug(rootPath);
|
|
3533
|
+
}
|
|
3534
|
+
async function ensureProjectDataDir(slug, globalConfigPath2) {
|
|
3535
|
+
const base = path8.dirname(globalConfigPath2);
|
|
3536
|
+
const dir = path8.join(base, "projects", slug);
|
|
3537
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
3538
|
+
return dir;
|
|
3539
|
+
}
|
|
2455
3540
|
async function handleMessage(ws, _client, msg) {
|
|
2456
3541
|
switch (msg.type) {
|
|
2457
3542
|
// Collaboration messages short-circuit the user/agent flow.
|
|
@@ -2479,7 +3564,8 @@ async function startWebUI(opts = {}) {
|
|
|
2479
3564
|
runLock = new AbortController();
|
|
2480
3565
|
const thisRun = runLock;
|
|
2481
3566
|
try {
|
|
2482
|
-
const
|
|
3567
|
+
const maxIt = typeof context.meta["maxIterations"] === "number" ? context.meta["maxIterations"] : void 0;
|
|
3568
|
+
const result = await agent.run(content, { signal: thisRun.signal, maxIterations: maxIt });
|
|
2483
3569
|
send(ws, {
|
|
2484
3570
|
type: "run.result",
|
|
2485
3571
|
payload: {
|
|
@@ -2510,10 +3596,10 @@ async function startWebUI(opts = {}) {
|
|
|
2510
3596
|
}
|
|
2511
3597
|
case "tool.confirm_result": {
|
|
2512
3598
|
const { id, decision } = msg.payload;
|
|
2513
|
-
const
|
|
2514
|
-
if (
|
|
3599
|
+
const resolve5 = pendingConfirms.get(id);
|
|
3600
|
+
if (resolve5) {
|
|
2515
3601
|
pendingConfirms.delete(id);
|
|
2516
|
-
|
|
3602
|
+
resolve5(decision);
|
|
2517
3603
|
}
|
|
2518
3604
|
break;
|
|
2519
3605
|
}
|
|
@@ -2525,6 +3611,15 @@ async function startWebUI(opts = {}) {
|
|
|
2525
3611
|
send(ws, { type: "pong", payload: {} });
|
|
2526
3612
|
break;
|
|
2527
3613
|
case "session.new": {
|
|
3614
|
+
try {
|
|
3615
|
+
await session.append({
|
|
3616
|
+
type: "session_end",
|
|
3617
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3618
|
+
usage: tokenCounter.total()
|
|
3619
|
+
});
|
|
3620
|
+
await session.close();
|
|
3621
|
+
} catch {
|
|
3622
|
+
}
|
|
2528
3623
|
session = await sessionStore.create({
|
|
2529
3624
|
id: "",
|
|
2530
3625
|
title: "",
|
|
@@ -2618,29 +3713,35 @@ async function startWebUI(opts = {}) {
|
|
|
2618
3713
|
}
|
|
2619
3714
|
case "context.modes.list": {
|
|
2620
3715
|
const active = String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID);
|
|
3716
|
+
const allModes = customModeStore.list().map((m) => ({
|
|
3717
|
+
id: m.id,
|
|
3718
|
+
name: m.name,
|
|
3719
|
+
description: m.description,
|
|
3720
|
+
isActive: m.id === active,
|
|
3721
|
+
thresholds: m.thresholds,
|
|
3722
|
+
preserveK: m.preserveK,
|
|
3723
|
+
eliseThreshold: m.eliseThreshold,
|
|
3724
|
+
custom: m.custom === true
|
|
3725
|
+
}));
|
|
2621
3726
|
send(ws, {
|
|
2622
3727
|
type: "context.modes.list",
|
|
2623
|
-
payload: {
|
|
2624
|
-
activeId: active,
|
|
2625
|
-
modes: listContextWindowModes().map((m) => ({
|
|
2626
|
-
id: m.id,
|
|
2627
|
-
name: m.name,
|
|
2628
|
-
description: m.description,
|
|
2629
|
-
isActive: m.id === active,
|
|
2630
|
-
thresholds: m.thresholds,
|
|
2631
|
-
preserveK: m.preserveK,
|
|
2632
|
-
eliseThreshold: m.eliseThreshold
|
|
2633
|
-
}))
|
|
2634
|
-
}
|
|
3728
|
+
payload: { activeId: active, modes: allModes }
|
|
2635
3729
|
});
|
|
2636
3730
|
break;
|
|
2637
3731
|
}
|
|
2638
3732
|
case "context.mode.switch": {
|
|
2639
3733
|
const { id } = msg.payload;
|
|
2640
|
-
|
|
3734
|
+
let policy = resolveContextWindowPolicy({}, id);
|
|
2641
3735
|
if (policy.id !== id) {
|
|
2642
|
-
|
|
2643
|
-
|
|
3736
|
+
const customModes = customModeStore.list().filter(
|
|
3737
|
+
(m) => m.custom === true
|
|
3738
|
+
);
|
|
3739
|
+
const custom = customModes.find((m) => m.id === id);
|
|
3740
|
+
if (!custom) {
|
|
3741
|
+
sendResult(ws, false, `Unknown context mode "${id}"`);
|
|
3742
|
+
break;
|
|
3743
|
+
}
|
|
3744
|
+
policy = custom;
|
|
2644
3745
|
}
|
|
2645
3746
|
context.meta["contextWindowMode"] = policy.id;
|
|
2646
3747
|
context.meta["contextWindowPolicy"] = policy;
|
|
@@ -2651,6 +3752,48 @@ async function startWebUI(opts = {}) {
|
|
|
2651
3752
|
});
|
|
2652
3753
|
break;
|
|
2653
3754
|
}
|
|
3755
|
+
case "context.mode.create": {
|
|
3756
|
+
const payload = msg.payload;
|
|
3757
|
+
const result = customModeStore.create({
|
|
3758
|
+
id: payload.id,
|
|
3759
|
+
name: payload.name,
|
|
3760
|
+
description: payload.description,
|
|
3761
|
+
thresholds: payload.thresholds,
|
|
3762
|
+
preserveK: payload.preserveK,
|
|
3763
|
+
eliseThreshold: payload.eliseThreshold,
|
|
3764
|
+
custom: true,
|
|
3765
|
+
aggressiveOn: "soft",
|
|
3766
|
+
targetLoad: 0.65
|
|
3767
|
+
});
|
|
3768
|
+
sendResult(ws, result.ok, result.error ?? `Mode "${payload.id}" created`);
|
|
3769
|
+
break;
|
|
3770
|
+
}
|
|
3771
|
+
case "context.mode.update": {
|
|
3772
|
+
const payload = msg.payload;
|
|
3773
|
+
const result = customModeStore.update(payload.id, {
|
|
3774
|
+
name: payload.name,
|
|
3775
|
+
description: payload.description,
|
|
3776
|
+
thresholds: payload.thresholds ? {
|
|
3777
|
+
warn: payload.thresholds.warn ?? 0.6,
|
|
3778
|
+
soft: payload.thresholds.soft ?? 0.75,
|
|
3779
|
+
hard: payload.thresholds.hard ?? 0.9
|
|
3780
|
+
} : void 0,
|
|
3781
|
+
preserveK: payload.preserveK,
|
|
3782
|
+
eliseThreshold: payload.eliseThreshold
|
|
3783
|
+
});
|
|
3784
|
+
sendResult(ws, result.ok, result.error ?? `Mode "${payload.id}" updated`);
|
|
3785
|
+
break;
|
|
3786
|
+
}
|
|
3787
|
+
case "context.mode.delete": {
|
|
3788
|
+
const { id } = msg.payload;
|
|
3789
|
+
if (String(context.meta["contextWindowMode"] ?? "") === id) {
|
|
3790
|
+
context.meta["contextWindowMode"] = DEFAULT_CONTEXT_WINDOW_MODE_ID;
|
|
3791
|
+
context.meta["contextWindowPolicy"] = resolveContextWindowPolicy({}, DEFAULT_CONTEXT_WINDOW_MODE_ID);
|
|
3792
|
+
}
|
|
3793
|
+
const result = customModeStore.remove(id);
|
|
3794
|
+
sendResult(ws, result.ok, result.error ?? `Mode "${id}" deleted`);
|
|
3795
|
+
break;
|
|
3796
|
+
}
|
|
2654
3797
|
case "providers.list": {
|
|
2655
3798
|
const providers = await modelsRegistry.listProviders();
|
|
2656
3799
|
const savedIds = new Set(Object.keys(config.providers ?? {}));
|
|
@@ -2730,15 +3873,20 @@ async function startWebUI(opts = {}) {
|
|
|
2730
3873
|
updateAutoCompactionMaxContext?.(newProv);
|
|
2731
3874
|
try {
|
|
2732
3875
|
configWriteLock = configWriteLock.then(async () => {
|
|
2733
|
-
const raw = await
|
|
3876
|
+
const raw = await fs6.readFile(globalConfigPath, "utf8");
|
|
2734
3877
|
const parsed = JSON.parse(raw);
|
|
2735
3878
|
parsed.provider = newProvider;
|
|
2736
3879
|
parsed.model = newModel;
|
|
2737
|
-
await
|
|
3880
|
+
await atomicWrite5(globalConfigPath, JSON.stringify(parsed, null, 2));
|
|
2738
3881
|
});
|
|
2739
3882
|
await configWriteLock;
|
|
2740
3883
|
} catch (err) {
|
|
2741
|
-
console.warn(
|
|
3884
|
+
console.warn(JSON.stringify({
|
|
3885
|
+
level: "warn",
|
|
3886
|
+
event: "webui.config_save_failed",
|
|
3887
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3888
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3889
|
+
}));
|
|
2742
3890
|
}
|
|
2743
3891
|
send(ws, {
|
|
2744
3892
|
type: "key.operation_result",
|
|
@@ -2757,6 +3905,57 @@ async function startWebUI(opts = {}) {
|
|
|
2757
3905
|
broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
|
|
2758
3906
|
break;
|
|
2759
3907
|
}
|
|
3908
|
+
case "model.refine": {
|
|
3909
|
+
const { text } = msg.payload;
|
|
3910
|
+
if (!text?.trim()) {
|
|
3911
|
+
send(ws, {
|
|
3912
|
+
type: "model.refine_result",
|
|
3913
|
+
payload: { refined: "", english: "", error: "Empty text" }
|
|
3914
|
+
});
|
|
3915
|
+
break;
|
|
3916
|
+
}
|
|
3917
|
+
try {
|
|
3918
|
+
const history = recentTextTurns(context.messages);
|
|
3919
|
+
const result = await enhanceUserPrompt({
|
|
3920
|
+
provider: context.provider,
|
|
3921
|
+
model: context.model,
|
|
3922
|
+
text,
|
|
3923
|
+
history,
|
|
3924
|
+
timeoutMs: 9e4,
|
|
3925
|
+
onError: (reason) => {
|
|
3926
|
+
console.warn(JSON.stringify({
|
|
3927
|
+
level: "warn",
|
|
3928
|
+
event: "model.refine_failed",
|
|
3929
|
+
reason,
|
|
3930
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3931
|
+
}));
|
|
3932
|
+
}
|
|
3933
|
+
});
|
|
3934
|
+
if (result) {
|
|
3935
|
+
send(ws, {
|
|
3936
|
+
type: "model.refine_result",
|
|
3937
|
+
payload: { refined: result.refined, english: result.english }
|
|
3938
|
+
});
|
|
3939
|
+
} else {
|
|
3940
|
+
send(ws, {
|
|
3941
|
+
type: "model.refine_result",
|
|
3942
|
+
payload: { refined: text, english: text, error: "Refinement returned no result" }
|
|
3943
|
+
});
|
|
3944
|
+
}
|
|
3945
|
+
} catch (err) {
|
|
3946
|
+
console.error(JSON.stringify({
|
|
3947
|
+
level: "error",
|
|
3948
|
+
event: "model.refine.error",
|
|
3949
|
+
error: errMessage(err),
|
|
3950
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3951
|
+
}));
|
|
3952
|
+
send(ws, {
|
|
3953
|
+
type: "model.refine_result",
|
|
3954
|
+
payload: { refined: text, english: text, error: errMessage(err) }
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3957
|
+
break;
|
|
3958
|
+
}
|
|
2760
3959
|
case "key.add":
|
|
2761
3960
|
case "key.update": {
|
|
2762
3961
|
const { providerId, label, apiKey } = msg.payload;
|
|
@@ -2832,6 +4031,11 @@ async function startWebUI(opts = {}) {
|
|
|
2832
4031
|
}
|
|
2833
4032
|
const resumed = await sessionStore.resume(id);
|
|
2834
4033
|
try {
|
|
4034
|
+
await session.append({
|
|
4035
|
+
type: "session_end",
|
|
4036
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4037
|
+
usage: tokenCounter.total()
|
|
4038
|
+
});
|
|
2835
4039
|
await session.close();
|
|
2836
4040
|
} catch {
|
|
2837
4041
|
}
|
|
@@ -2875,42 +4079,13 @@ async function startWebUI(opts = {}) {
|
|
|
2875
4079
|
send(ws, { type: "tools.list", payload: { tools: list } });
|
|
2876
4080
|
break;
|
|
2877
4081
|
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
payload: { text: "", error: errMessage(err) }
|
|
2886
|
-
});
|
|
2887
|
-
}
|
|
2888
|
-
break;
|
|
2889
|
-
}
|
|
2890
|
-
case "memory.remember": {
|
|
2891
|
-
const { text, scope } = msg.payload;
|
|
2892
|
-
try {
|
|
2893
|
-
await memoryStore.remember(text, scope ?? "project-memory");
|
|
2894
|
-
sendResult(ws, true, "Saved to memory");
|
|
2895
|
-
} catch (err) {
|
|
2896
|
-
sendResult(ws, false, errMessage(err));
|
|
2897
|
-
}
|
|
2898
|
-
break;
|
|
2899
|
-
}
|
|
2900
|
-
case "memory.forget": {
|
|
2901
|
-
const { text, scope } = msg.payload;
|
|
2902
|
-
try {
|
|
2903
|
-
const removed = await memoryStore.forget(text, scope ?? "project-memory");
|
|
2904
|
-
sendResult(
|
|
2905
|
-
ws,
|
|
2906
|
-
removed > 0,
|
|
2907
|
-
removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
|
|
2908
|
-
);
|
|
2909
|
-
} catch (err) {
|
|
2910
|
-
sendResult(ws, false, errMessage(err));
|
|
2911
|
-
}
|
|
2912
|
-
break;
|
|
2913
|
-
}
|
|
4082
|
+
// ── Memory operations — delegated to shared handlers (memory-handlers.ts) ──
|
|
4083
|
+
case "memory.list":
|
|
4084
|
+
return handleMemoryList(ws, memoryStore);
|
|
4085
|
+
case "memory.remember":
|
|
4086
|
+
return handleMemoryRemember(ws, msg, memoryStore);
|
|
4087
|
+
case "memory.forget":
|
|
4088
|
+
return handleMemoryForget(ws, msg, memoryStore);
|
|
2914
4089
|
case "skills.list": {
|
|
2915
4090
|
if (!skillLoader) {
|
|
2916
4091
|
send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
|
|
@@ -3010,6 +4185,24 @@ async function startWebUI(opts = {}) {
|
|
|
3010
4185
|
broadcast(clients, { type: "todos.updated", payload: { todos: next } });
|
|
3011
4186
|
break;
|
|
3012
4187
|
}
|
|
4188
|
+
case "tasks.get": {
|
|
4189
|
+
const taskPath = context.meta["task.path"];
|
|
4190
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
4191
|
+
try {
|
|
4192
|
+
const { loadTasks } = await import("@wrongstack/core");
|
|
4193
|
+
const file = await loadTasks(taskPath);
|
|
4194
|
+
send(ws, {
|
|
4195
|
+
type: "tasks.updated",
|
|
4196
|
+
payload: { tasks: file?.tasks ?? [] }
|
|
4197
|
+
});
|
|
4198
|
+
} catch {
|
|
4199
|
+
send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
4200
|
+
}
|
|
4201
|
+
} else {
|
|
4202
|
+
send(ws, { type: "tasks.updated", payload: { tasks: [], error: "Task storage not configured." } });
|
|
4203
|
+
}
|
|
4204
|
+
break;
|
|
4205
|
+
}
|
|
3013
4206
|
case "plan.get": {
|
|
3014
4207
|
const planPath = context.meta["plan.path"];
|
|
3015
4208
|
if (typeof planPath === "string" && planPath) {
|
|
@@ -3077,37 +4270,18 @@ async function startWebUI(opts = {}) {
|
|
|
3077
4270
|
}
|
|
3078
4271
|
break;
|
|
3079
4272
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
for (const e of entries) {
|
|
3093
|
-
if (results.length >= 600) return;
|
|
3094
|
-
if (isHiddenEntry(e.name)) continue;
|
|
3095
|
-
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
3096
|
-
if (e.isDirectory()) {
|
|
3097
|
-
if (SKIP_DIRS.has(e.name)) continue;
|
|
3098
|
-
await walk(path4.join(dir, e.name), childRel, depth + 1);
|
|
3099
|
-
} else if (e.isFile()) {
|
|
3100
|
-
results.push(childRel);
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
|
-
await walk(projectRoot, "", 0);
|
|
3105
|
-
send(ws, {
|
|
3106
|
-
type: "files.list",
|
|
3107
|
-
payload: { files: rankFiles(results, payload.query ?? "", limit) }
|
|
3108
|
-
});
|
|
3109
|
-
break;
|
|
3110
|
-
}
|
|
4273
|
+
// ── File operations — delegated to shared handlers (file-handlers.ts) ──
|
|
4274
|
+
// These handlers are also used by the CLI's webui-server.ts. When
|
|
4275
|
+
// adding or modifying file-operation WebSocket messages, update
|
|
4276
|
+
// file-handlers.ts — NOT these case blocks individually.
|
|
4277
|
+
case "files.list":
|
|
4278
|
+
return handleFilesList(ws, msg, projectRoot);
|
|
4279
|
+
case "files.tree":
|
|
4280
|
+
return handleFilesTree(ws, msg, projectRoot);
|
|
4281
|
+
case "files.read":
|
|
4282
|
+
return handleFilesRead(ws, msg, projectRoot);
|
|
4283
|
+
case "files.write":
|
|
4284
|
+
return handleFilesWrite(ws, msg, projectRoot);
|
|
3111
4285
|
case "modes.list": {
|
|
3112
4286
|
try {
|
|
3113
4287
|
const modes = await modeStore.listModes();
|
|
@@ -3245,8 +4419,8 @@ async function startWebUI(opts = {}) {
|
|
|
3245
4419
|
}
|
|
3246
4420
|
case "goal.get": {
|
|
3247
4421
|
try {
|
|
3248
|
-
const goalPath =
|
|
3249
|
-
const raw = await
|
|
4422
|
+
const goalPath = path8.join(projectRoot, ".wrongstack", "goal.json");
|
|
4423
|
+
const raw = await fs6.readFile(goalPath, "utf8");
|
|
3250
4424
|
const goal = JSON.parse(raw);
|
|
3251
4425
|
broadcast(clients, { type: "goal.updated", payload: goal });
|
|
3252
4426
|
} catch {
|
|
@@ -3258,13 +4432,55 @@ async function startWebUI(opts = {}) {
|
|
|
3258
4432
|
const { mode } = msg.payload;
|
|
3259
4433
|
context.meta["autonomy"] = mode;
|
|
3260
4434
|
sendResult(ws, true, `Autonomy mode set to "${mode}"`);
|
|
4435
|
+
broadcast(clients, { type: "prefs.updated", payload: { autonomy: mode } });
|
|
4436
|
+
void persistPrefsToConfig({ autonomy: mode });
|
|
4437
|
+
break;
|
|
4438
|
+
}
|
|
4439
|
+
case "prefs.update": {
|
|
4440
|
+
const payload = msg.payload;
|
|
4441
|
+
for (const [key, val] of Object.entries(payload)) {
|
|
4442
|
+
context.meta[key] = val;
|
|
4443
|
+
}
|
|
4444
|
+
void persistPrefsToConfig(payload);
|
|
4445
|
+
if (typeof payload["yolo"] === "boolean") {
|
|
4446
|
+
permissionPolicy.setYolo?.(payload["yolo"]);
|
|
4447
|
+
}
|
|
4448
|
+
if (typeof payload["featureMcp"] === "boolean")
|
|
4449
|
+
config.features.mcp = payload["featureMcp"];
|
|
4450
|
+
if (typeof payload["featurePlugins"] === "boolean")
|
|
4451
|
+
config.features.plugins = payload["featurePlugins"];
|
|
4452
|
+
if (typeof payload["featureMemory"] === "boolean")
|
|
4453
|
+
config.features.memory = payload["featureMemory"];
|
|
4454
|
+
if (typeof payload["featureSkills"] === "boolean")
|
|
4455
|
+
config.features.skills = payload["featureSkills"];
|
|
4456
|
+
if (typeof payload["featureModelsRegistry"] === "boolean")
|
|
4457
|
+
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
4458
|
+
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
4459
|
+
if (payload["contextAutoCompact"] && autoCompactor) {
|
|
4460
|
+
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
4461
|
+
pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
|
|
4462
|
+
} else {
|
|
4463
|
+
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
if (typeof payload["logLevel"] === "string") {
|
|
4467
|
+
const valid = ["debug", "info", "warn", "error"];
|
|
4468
|
+
if (valid.includes(payload["logLevel"])) {
|
|
4469
|
+
logger.level = payload["logLevel"];
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
|
|
4473
|
+
break;
|
|
4474
|
+
}
|
|
4475
|
+
case "prefs.get": {
|
|
4476
|
+
send(ws, { type: "prefs.updated", payload: prefSnapshot() });
|
|
3261
4477
|
break;
|
|
3262
4478
|
}
|
|
3263
4479
|
case "session.checkpoints": {
|
|
3264
4480
|
try {
|
|
3265
4481
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
3266
4482
|
const rewinder = new DefaultSessionRewinder(
|
|
3267
|
-
|
|
4483
|
+
path8.join(projectRoot, ".wrongstack", "sessions"),
|
|
3268
4484
|
projectRoot
|
|
3269
4485
|
);
|
|
3270
4486
|
const checkpoints = await rewinder.listCheckpoints(session.id);
|
|
@@ -3285,7 +4501,7 @@ async function startWebUI(opts = {}) {
|
|
|
3285
4501
|
try {
|
|
3286
4502
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
3287
4503
|
const rewinder = new DefaultSessionRewinder(
|
|
3288
|
-
|
|
4504
|
+
path8.join(projectRoot, ".wrongstack", "sessions"),
|
|
3289
4505
|
projectRoot
|
|
3290
4506
|
);
|
|
3291
4507
|
await rewinder.rewindToCheckpoint(session.id, checkpointIndex);
|
|
@@ -3300,6 +4516,309 @@ async function startWebUI(opts = {}) {
|
|
|
3300
4516
|
}
|
|
3301
4517
|
break;
|
|
3302
4518
|
}
|
|
4519
|
+
// ── Project management ────────────────────────────────────────────
|
|
4520
|
+
case "projects.list": {
|
|
4521
|
+
try {
|
|
4522
|
+
const manifest = await loadManifest(globalConfigPath);
|
|
4523
|
+
send(ws, {
|
|
4524
|
+
type: "projects.list",
|
|
4525
|
+
payload: { projects: manifest.projects }
|
|
4526
|
+
});
|
|
4527
|
+
} catch (err) {
|
|
4528
|
+
send(ws, {
|
|
4529
|
+
type: "projects.list",
|
|
4530
|
+
payload: { projects: [], error: errMessage(err) }
|
|
4531
|
+
});
|
|
4532
|
+
}
|
|
4533
|
+
break;
|
|
4534
|
+
}
|
|
4535
|
+
case "projects.add": {
|
|
4536
|
+
const { root: addRoot, name: displayName } = msg.payload;
|
|
4537
|
+
try {
|
|
4538
|
+
const resolved = path8.resolve(addRoot);
|
|
4539
|
+
await fs6.access(resolved);
|
|
4540
|
+
const stat2 = await fs6.stat(resolved);
|
|
4541
|
+
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4542
|
+
const manifest = await loadManifest(globalConfigPath);
|
|
4543
|
+
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
4544
|
+
if (existing) {
|
|
4545
|
+
send(ws, {
|
|
4546
|
+
type: "projects.added",
|
|
4547
|
+
payload: {
|
|
4548
|
+
name: existing.name,
|
|
4549
|
+
root: existing.root,
|
|
4550
|
+
slug: existing.slug,
|
|
4551
|
+
message: `Already registered as "${existing.name}"`
|
|
4552
|
+
}
|
|
4553
|
+
});
|
|
4554
|
+
break;
|
|
4555
|
+
}
|
|
4556
|
+
const name = displayName?.trim() || path8.basename(resolved);
|
|
4557
|
+
const slug = generateProjectSlug(resolved);
|
|
4558
|
+
await ensureProjectDataDir(slug, globalConfigPath);
|
|
4559
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4560
|
+
manifest.projects.push({ name, root: resolved, slug, lastSeen: now, createdAt: now });
|
|
4561
|
+
await saveManifest(manifest, globalConfigPath);
|
|
4562
|
+
send(ws, {
|
|
4563
|
+
type: "projects.added",
|
|
4564
|
+
payload: {
|
|
4565
|
+
name,
|
|
4566
|
+
root: resolved,
|
|
4567
|
+
slug,
|
|
4568
|
+
message: `Registered project "${name}"`
|
|
4569
|
+
}
|
|
4570
|
+
});
|
|
4571
|
+
} catch (err) {
|
|
4572
|
+
send(ws, {
|
|
4573
|
+
type: "projects.added",
|
|
4574
|
+
payload: {
|
|
4575
|
+
name: path8.basename(addRoot),
|
|
4576
|
+
root: addRoot,
|
|
4577
|
+
slug: "",
|
|
4578
|
+
message: errMessage(err)
|
|
4579
|
+
}
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
break;
|
|
4583
|
+
}
|
|
4584
|
+
case "projects.select": {
|
|
4585
|
+
const { root: selRoot, name: selName } = msg.payload;
|
|
4586
|
+
try {
|
|
4587
|
+
const resolved = path8.resolve(selRoot);
|
|
4588
|
+
try {
|
|
4589
|
+
await fs6.access(resolved);
|
|
4590
|
+
const stat2 = await fs6.stat(resolved);
|
|
4591
|
+
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4592
|
+
} catch (err) {
|
|
4593
|
+
send(ws, {
|
|
4594
|
+
type: "projects.selected",
|
|
4595
|
+
payload: {
|
|
4596
|
+
root: selRoot,
|
|
4597
|
+
name: selName || path8.basename(selRoot),
|
|
4598
|
+
message: `Cannot switch: ${errMessage(err)}`
|
|
4599
|
+
}
|
|
4600
|
+
});
|
|
4601
|
+
break;
|
|
4602
|
+
}
|
|
4603
|
+
const manifest = await loadManifest(globalConfigPath);
|
|
4604
|
+
const entry = manifest.projects.find((p) => p.root === resolved);
|
|
4605
|
+
if (entry) {
|
|
4606
|
+
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
4607
|
+
entry.lastWorkingDir = resolved;
|
|
4608
|
+
} else {
|
|
4609
|
+
const name = selName?.trim() || path8.basename(resolved);
|
|
4610
|
+
const slug = generateProjectSlug(resolved);
|
|
4611
|
+
manifest.projects.push({
|
|
4612
|
+
name,
|
|
4613
|
+
root: resolved,
|
|
4614
|
+
slug,
|
|
4615
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4616
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4617
|
+
lastWorkingDir: resolved
|
|
4618
|
+
});
|
|
4619
|
+
await ensureProjectDataDir(slug, globalConfigPath);
|
|
4620
|
+
}
|
|
4621
|
+
await saveManifest(manifest, globalConfigPath);
|
|
4622
|
+
if (runLock) {
|
|
4623
|
+
runLock.abort();
|
|
4624
|
+
runLock = null;
|
|
4625
|
+
}
|
|
4626
|
+
projectRoot = resolved;
|
|
4627
|
+
workingDir = resolved;
|
|
4628
|
+
context.cwd = workingDir;
|
|
4629
|
+
context.projectRoot = projectRoot;
|
|
4630
|
+
const newSessionsDir = path8.join(
|
|
4631
|
+
path8.dirname(globalConfigPath),
|
|
4632
|
+
"projects",
|
|
4633
|
+
entry?.slug ?? generateProjectSlug(resolved),
|
|
4634
|
+
"sessions"
|
|
4635
|
+
);
|
|
4636
|
+
await fs6.mkdir(newSessionsDir, { recursive: true });
|
|
4637
|
+
const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
|
|
4638
|
+
const oldSessionId = session.id;
|
|
4639
|
+
try {
|
|
4640
|
+
await session.append({
|
|
4641
|
+
type: "session_end",
|
|
4642
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4643
|
+
usage: tokenCounter.total()
|
|
4644
|
+
});
|
|
4645
|
+
await session.close();
|
|
4646
|
+
} catch {
|
|
4647
|
+
}
|
|
4648
|
+
sessionStore = newSessionStore;
|
|
4649
|
+
session = await sessionStore.create({
|
|
4650
|
+
id: "",
|
|
4651
|
+
title: "",
|
|
4652
|
+
model: config.model,
|
|
4653
|
+
provider: config.provider
|
|
4654
|
+
});
|
|
4655
|
+
context.session = session;
|
|
4656
|
+
context.state.replaceMessages([]);
|
|
4657
|
+
context.state.replaceTodos([]);
|
|
4658
|
+
context.readFiles.clear();
|
|
4659
|
+
context.fileMtimes.clear();
|
|
4660
|
+
tokenCounter.reset();
|
|
4661
|
+
sessionStartedAt = Date.now();
|
|
4662
|
+
send(ws, {
|
|
4663
|
+
type: "projects.selected",
|
|
4664
|
+
payload: {
|
|
4665
|
+
root: resolved,
|
|
4666
|
+
name: selName || path8.basename(resolved),
|
|
4667
|
+
message: `Switched to ${selName || path8.basename(resolved)}`
|
|
4668
|
+
}
|
|
4669
|
+
});
|
|
4670
|
+
broadcast(clients, {
|
|
4671
|
+
type: "subagent.event",
|
|
4672
|
+
payload: {
|
|
4673
|
+
kind: "session_stopped",
|
|
4674
|
+
sessionId: oldSessionId
|
|
4675
|
+
}
|
|
4676
|
+
});
|
|
4677
|
+
broadcast(clients, {
|
|
4678
|
+
type: "session.start",
|
|
4679
|
+
payload: {
|
|
4680
|
+
...await sessionStartPayload(),
|
|
4681
|
+
reset: true,
|
|
4682
|
+
clearedSessionId: oldSessionId
|
|
4683
|
+
}
|
|
4684
|
+
});
|
|
4685
|
+
} catch (err) {
|
|
4686
|
+
send(ws, {
|
|
4687
|
+
type: "projects.selected",
|
|
4688
|
+
payload: {
|
|
4689
|
+
root: selRoot,
|
|
4690
|
+
name: selName || path8.basename(selRoot),
|
|
4691
|
+
message: errMessage(err)
|
|
4692
|
+
}
|
|
4693
|
+
});
|
|
4694
|
+
}
|
|
4695
|
+
break;
|
|
4696
|
+
}
|
|
4697
|
+
// ── Working directory (within current project) ───────────────────
|
|
4698
|
+
case "working_dir.set": {
|
|
4699
|
+
const { path: newPath } = msg.payload;
|
|
4700
|
+
try {
|
|
4701
|
+
const resolved = path8.resolve(projectRoot, newPath);
|
|
4702
|
+
if (!resolved.startsWith(projectRoot + path8.sep) && resolved !== projectRoot) {
|
|
4703
|
+
sendResult(ws, false, `Path must stay inside the project root: ${projectRoot}`);
|
|
4704
|
+
break;
|
|
4705
|
+
}
|
|
4706
|
+
try {
|
|
4707
|
+
await fs6.access(resolved);
|
|
4708
|
+
const stat2 = await fs6.stat(resolved);
|
|
4709
|
+
if (!stat2.isDirectory()) throw new Error("Not a directory");
|
|
4710
|
+
} catch {
|
|
4711
|
+
sendResult(ws, false, `Directory not found or not accessible: ${resolved}`);
|
|
4712
|
+
break;
|
|
4713
|
+
}
|
|
4714
|
+
workingDir = resolved;
|
|
4715
|
+
context.cwd = resolved;
|
|
4716
|
+
broadcast(clients, {
|
|
4717
|
+
type: "working_dir.changed",
|
|
4718
|
+
payload: { cwd: resolved, projectRoot }
|
|
4719
|
+
});
|
|
4720
|
+
sendResult(ws, true, `Working directory set to ${resolved}`);
|
|
4721
|
+
} catch (err) {
|
|
4722
|
+
sendResult(ws, false, errMessage(err));
|
|
4723
|
+
}
|
|
4724
|
+
break;
|
|
4725
|
+
}
|
|
4726
|
+
// ── Shell open — spawn terminal or file manager at a path ─────────
|
|
4727
|
+
case "shell.open": {
|
|
4728
|
+
const { path: targetPath, target } = msg.payload;
|
|
4729
|
+
try {
|
|
4730
|
+
const resolved = path8.resolve(targetPath);
|
|
4731
|
+
await fs6.access(resolved);
|
|
4732
|
+
const { exec } = await import("child_process");
|
|
4733
|
+
const platform = process.platform;
|
|
4734
|
+
let cmd;
|
|
4735
|
+
if (target === "file-manager") {
|
|
4736
|
+
if (platform === "win32") {
|
|
4737
|
+
cmd = `explorer "${resolved}"`;
|
|
4738
|
+
} else if (platform === "darwin") {
|
|
4739
|
+
cmd = `open "${resolved}"`;
|
|
4740
|
+
} else {
|
|
4741
|
+
cmd = `xdg-open "${resolved}"`;
|
|
4742
|
+
}
|
|
4743
|
+
} else {
|
|
4744
|
+
if (platform === "win32") {
|
|
4745
|
+
cmd = `start cmd /k cd /d "${resolved}"`;
|
|
4746
|
+
} else if (platform === "darwin") {
|
|
4747
|
+
cmd = `open -a Terminal "${resolved}"`;
|
|
4748
|
+
} else {
|
|
4749
|
+
cmd = `x-terminal-emulator --working-directory="${resolved}" 2>/dev/null || gnome-terminal --working-directory="${resolved}" 2>/dev/null || xterm -e "cd '${resolved}' && $SHELL"`;
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
exec(cmd, { timeout: 5e3 }, (err) => {
|
|
4753
|
+
if (err) {
|
|
4754
|
+
logger.warn(`shell.open failed: ${err.message}`);
|
|
4755
|
+
}
|
|
4756
|
+
});
|
|
4757
|
+
sendResult(ws, true, `Opened ${target} at ${resolved}`);
|
|
4758
|
+
} catch (err) {
|
|
4759
|
+
sendResult(ws, false, errMessage(err));
|
|
4760
|
+
}
|
|
4761
|
+
break;
|
|
4762
|
+
}
|
|
4763
|
+
// ── Mailbox operations — project-level inter-agent messaging ────
|
|
4764
|
+
case "mailbox.messages":
|
|
4765
|
+
return handleMailboxMessages(
|
|
4766
|
+
ws,
|
|
4767
|
+
{ projectRoot, globalRoot: path8.dirname(globalConfigPath) },
|
|
4768
|
+
msg.payload
|
|
4769
|
+
);
|
|
4770
|
+
case "mailbox.agents":
|
|
4771
|
+
return handleMailboxAgents(
|
|
4772
|
+
ws,
|
|
4773
|
+
{ projectRoot, globalRoot: path8.dirname(globalConfigPath) },
|
|
4774
|
+
msg.payload
|
|
4775
|
+
);
|
|
4776
|
+
case "mailbox.clear":
|
|
4777
|
+
return handleMailboxClear(
|
|
4778
|
+
ws,
|
|
4779
|
+
{ projectRoot, globalRoot: path8.dirname(globalConfigPath) }
|
|
4780
|
+
);
|
|
4781
|
+
// ── Brain — status, autonomy ceiling, direct decision support ───
|
|
4782
|
+
case "brain.status":
|
|
4783
|
+
send(ws, {
|
|
4784
|
+
type: "brain.status",
|
|
4785
|
+
payload: { maxAutoRisk: brainSettings.maxAutoRisk, log: brainLog }
|
|
4786
|
+
});
|
|
4787
|
+
break;
|
|
4788
|
+
case "brain.risk": {
|
|
4789
|
+
const level = msg.payload?.level ?? "";
|
|
4790
|
+
const valid = ["off", "low", "medium", "high", "all"];
|
|
4791
|
+
if (!valid.includes(level)) {
|
|
4792
|
+
sendResult(ws, false, `Unknown risk level "${level}". Use: ${valid.join(", ")}.`);
|
|
4793
|
+
break;
|
|
4794
|
+
}
|
|
4795
|
+
brainSettings.maxAutoRisk = level;
|
|
4796
|
+
send(ws, {
|
|
4797
|
+
type: "brain.status",
|
|
4798
|
+
payload: { maxAutoRisk: brainSettings.maxAutoRisk, log: brainLog }
|
|
4799
|
+
});
|
|
4800
|
+
break;
|
|
4801
|
+
}
|
|
4802
|
+
case "brain.ask": {
|
|
4803
|
+
const question = msg.payload?.question?.trim();
|
|
4804
|
+
if (!question) {
|
|
4805
|
+
sendResult(ws, false, "Usage: /brain ask <question>");
|
|
4806
|
+
break;
|
|
4807
|
+
}
|
|
4808
|
+
try {
|
|
4809
|
+
const decision = await brain.decide({
|
|
4810
|
+
id: `brain-ask-${Date.now().toString(36)}`,
|
|
4811
|
+
source: "user",
|
|
4812
|
+
question,
|
|
4813
|
+
risk: "medium",
|
|
4814
|
+
fallback: "ask_human"
|
|
4815
|
+
});
|
|
4816
|
+
send(ws, { type: "brain.answer", payload: { question, decision } });
|
|
4817
|
+
} catch (err) {
|
|
4818
|
+
sendResult(ws, false, `Brain consultation failed: ${errMessage(err)}`);
|
|
4819
|
+
}
|
|
4820
|
+
break;
|
|
4821
|
+
}
|
|
3303
4822
|
default:
|
|
3304
4823
|
if (msg.type.startsWith("autophase.")) {
|
|
3305
4824
|
await autoPhaseHandler.handleMessage(
|
|
@@ -3319,14 +4838,16 @@ async function startWebUI(opts = {}) {
|
|
|
3319
4838
|
getConfigWriteLock: () => configWriteLock,
|
|
3320
4839
|
setConfigWriteLock: (p) => {
|
|
3321
4840
|
configWriteLock = p;
|
|
3322
|
-
}
|
|
4841
|
+
},
|
|
4842
|
+
broadcast,
|
|
4843
|
+
clients
|
|
3323
4844
|
});
|
|
3324
4845
|
const httpServer = createHttpServer({
|
|
3325
4846
|
host: wsHost,
|
|
3326
|
-
distDir:
|
|
4847
|
+
distDir: path8.resolve(import.meta.dirname, "../../dist"),
|
|
3327
4848
|
wsPort
|
|
3328
4849
|
});
|
|
3329
|
-
const registryBaseDir =
|
|
4850
|
+
const registryBaseDir = path8.dirname(globalConfigPath);
|
|
3330
4851
|
httpServer.listen(httpPort, wsHost, () => {
|
|
3331
4852
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
3332
4853
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -3338,12 +4859,17 @@ async function startWebUI(opts = {}) {
|
|
|
3338
4859
|
wsPort,
|
|
3339
4860
|
host: wsHost,
|
|
3340
4861
|
projectRoot,
|
|
3341
|
-
projectName:
|
|
4862
|
+
projectName: path8.basename(projectRoot) || projectRoot,
|
|
3342
4863
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3343
4864
|
url: `http://${wsHost}:${httpPort}`
|
|
3344
4865
|
},
|
|
3345
4866
|
registryBaseDir
|
|
3346
|
-
).catch((err) => console.warn(
|
|
4867
|
+
).catch((err) => console.warn(JSON.stringify({
|
|
4868
|
+
level: "warn",
|
|
4869
|
+
event: "webui.instance_record_failed",
|
|
4870
|
+
message: errMessage(err),
|
|
4871
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4872
|
+
})));
|
|
3347
4873
|
});
|
|
3348
4874
|
registerShutdownHandlers({
|
|
3349
4875
|
flushSession: async () => {
|
|
@@ -3358,7 +4884,10 @@ async function startWebUI(opts = {}) {
|
|
|
3358
4884
|
servers: [httpServer, wssPrimary, wssSecondary],
|
|
3359
4885
|
// Drop this instance from the registry on a clean exit so the file reflects
|
|
3360
4886
|
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
3361
|
-
onShutdown: () =>
|
|
4887
|
+
onShutdown: () => {
|
|
4888
|
+
brainMonitor.stop();
|
|
4889
|
+
return unregisterInstance(process.pid, registryBaseDir);
|
|
4890
|
+
}
|
|
3362
4891
|
});
|
|
3363
4892
|
}
|
|
3364
4893
|
|
|
@@ -3369,7 +4898,12 @@ if (argv.includes("--list") || argv.includes("-l") || argv[0] === "ls") {
|
|
|
3369
4898
|
console.log(formatInstances(instances));
|
|
3370
4899
|
process.exit(0);
|
|
3371
4900
|
}).catch((err) => {
|
|
3372
|
-
console.error(
|
|
4901
|
+
console.error(JSON.stringify({
|
|
4902
|
+
level: "fatal",
|
|
4903
|
+
event: "webui.instance_registry_read_failed",
|
|
4904
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4905
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4906
|
+
}));
|
|
3373
4907
|
process.exit(1);
|
|
3374
4908
|
});
|
|
3375
4909
|
} else {
|
|
@@ -3378,7 +4912,12 @@ if (argv.includes("--list") || argv.includes("-l") || argv[0] === "ls") {
|
|
|
3378
4912
|
const open = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
|
|
3379
4913
|
console.log(`[WebUI] Starting standalone server on ${wsHost}:${wsPort}...`);
|
|
3380
4914
|
startWebUI({ wsPort, wsHost, open }).catch((err) => {
|
|
3381
|
-
console.error(
|
|
4915
|
+
console.error(JSON.stringify({
|
|
4916
|
+
level: "fatal",
|
|
4917
|
+
event: "webui.startup_failed",
|
|
4918
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4919
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4920
|
+
}));
|
|
3382
4921
|
process.exit(1);
|
|
3383
4922
|
});
|
|
3384
4923
|
}
|