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