@wrongstack/webui 0.148.0 → 0.236.0

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