@wrongstack/webui-server 0.285.0 → 0.287.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.
Files changed (61) hide show
  1. package/dist/index.js +1538 -508
  2. package/dist/index.js.map +4 -4
  3. package/dist/server/autophase-ws-handler.d.ts +17 -1
  4. package/dist/server/autophase-ws-handler.d.ts.map +1 -1
  5. package/dist/server/backend-services.d.ts +12 -23
  6. package/dist/server/backend-services.d.ts.map +1 -1
  7. package/dist/server/brain-routes.d.ts +2 -0
  8. package/dist/server/brain-routes.d.ts.map +1 -1
  9. package/dist/server/collaboration-ws-handler.d.ts +27 -19
  10. package/dist/server/collaboration-ws-handler.d.ts.map +1 -1
  11. package/dist/server/context-meta.d.ts.map +1 -1
  12. package/dist/server/entry.js +1513 -506
  13. package/dist/server/entry.js.map +4 -4
  14. package/dist/server/handlers.js.map +2 -2
  15. package/dist/server/http-server.d.ts.map +1 -1
  16. package/dist/server/index.d.ts +6 -5
  17. package/dist/server/index.d.ts.map +1 -1
  18. package/dist/server/instance-registry.d.ts +3 -1
  19. package/dist/server/instance-registry.d.ts.map +1 -1
  20. package/dist/server/kanban-routes.d.ts +3 -0
  21. package/dist/server/kanban-routes.d.ts.map +1 -1
  22. package/dist/server/mailbox-handlers.d.ts +12 -0
  23. package/dist/server/mailbox-handlers.d.ts.map +1 -1
  24. package/dist/server/mailbox-routes.d.ts +1 -0
  25. package/dist/server/mailbox-routes.d.ts.map +1 -1
  26. package/dist/server/mcp-handlers.d.ts +10 -1
  27. package/dist/server/mcp-handlers.d.ts.map +1 -1
  28. package/dist/server/mcp-routes.d.ts +4 -0
  29. package/dist/server/mcp-routes.d.ts.map +1 -1
  30. package/dist/server/memory-handlers.d.ts +41 -9
  31. package/dist/server/memory-handlers.d.ts.map +1 -1
  32. package/dist/server/message-dispatcher.d.ts.map +1 -1
  33. package/dist/server/mode-handlers.d.ts.map +1 -1
  34. package/dist/server/port-utils.d.ts +48 -10
  35. package/dist/server/port-utils.d.ts.map +1 -1
  36. package/dist/server/pre-context-services.d.ts +3 -1
  37. package/dist/server/pre-context-services.d.ts.map +1 -1
  38. package/dist/server/pref-helpers.d.ts +1 -1
  39. package/dist/server/pref-helpers.d.ts.map +1 -1
  40. package/dist/server/provider-handlers.d.ts +13 -0
  41. package/dist/server/provider-handlers.d.ts.map +1 -1
  42. package/dist/server/provider-routes.d.ts +2 -0
  43. package/dist/server/provider-routes.d.ts.map +1 -1
  44. package/dist/server/routes.d.ts +23 -20
  45. package/dist/server/routes.d.ts.map +1 -1
  46. package/dist/server/sdd-wizard-wiring.d.ts +31 -1
  47. package/dist/server/sdd-wizard-wiring.d.ts.map +1 -1
  48. package/dist/server/server-runtime.d.ts +1 -0
  49. package/dist/server/server-runtime.d.ts.map +1 -1
  50. package/dist/server/session-handlers.d.ts +17 -4
  51. package/dist/server/session-handlers.d.ts.map +1 -1
  52. package/dist/server/session-history.d.ts +29 -0
  53. package/dist/server/session-history.d.ts.map +1 -0
  54. package/dist/server/setup-events.d.ts +1 -1
  55. package/dist/server/setup-events.d.ts.map +1 -1
  56. package/dist/server/standalone-session-identity.d.ts +35 -0
  57. package/dist/server/standalone-session-identity.d.ts.map +1 -0
  58. package/dist/server/start-webui.d.ts.map +1 -1
  59. package/dist/server/ws-payload-validation.d.ts +4 -0
  60. package/dist/server/ws-payload-validation.d.ts.map +1 -1
  61. package/package.json +9 -9
package/dist/index.js CHANGED
@@ -38,12 +38,13 @@ function commitsSince(cwd, baseSha, branch) {
38
38
  }
39
39
  }
40
40
  var AutoPhaseWebSocketHandler = class {
41
- constructor(agent, context, logger, storeDir, events, projectRoot) {
41
+ constructor(agent, context, logger, storeDir, events, projectRoot, onBoardState) {
42
42
  this.agent = agent;
43
43
  this.context = context;
44
44
  this.logger = logger;
45
45
  this.events = events;
46
46
  this.projectRoot = projectRoot;
47
+ this.onBoardState = onBoardState;
47
48
  this.store = new PhaseStore({ baseDir: storeDir });
48
49
  }
49
50
  agent;
@@ -51,6 +52,7 @@ var AutoPhaseWebSocketHandler = class {
51
52
  logger;
52
53
  events;
53
54
  projectRoot;
55
+ onBoardState;
54
56
  orchestrator = null;
55
57
  graph = null;
56
58
  store;
@@ -395,6 +397,15 @@ Type: ${task.type}`;
395
397
  if (!this.graph) return;
396
398
  const state = this.buildState(activePhaseId);
397
399
  this.broadcast({ type: "autophase.state", payload: state });
400
+ if (this.onBoardState) {
401
+ try {
402
+ this.onBoardState(this.graph.id, state);
403
+ } catch (err) {
404
+ this.logger.error(
405
+ `[AutoPhase] board-state tap failed: ${err instanceof Error ? err.message : String(err)}`
406
+ );
407
+ }
408
+ }
398
409
  }
399
410
  buildState(activePhaseId) {
400
411
  if (!this.graph) {
@@ -2618,10 +2629,7 @@ function cspSourceFromUrl(rawUrl) {
2618
2629
  return void 0;
2619
2630
  }
2620
2631
  }
2621
- var ALLOWED_INLINE_SCRIPT_HASHES = [
2622
- "'sha256-6PXDy0zrpXa6mvYOl11bZ8nubNUL7ushPUhGDZtaexg='",
2623
- "'sha256-6sIdwbEBx7jj0drqSHHm7MqvmoYD3CQ4lp8Zp8blcb0='"
2624
- ];
2632
+ var EXTRA_SCRIPT_SOURCES = ["'wasm-unsafe-eval'"];
2625
2633
  function buildCspHeader(wsPort, requestHost, publicWsUrl) {
2626
2634
  const connect = /* @__PURE__ */ new Set([
2627
2635
  "'self'",
@@ -2635,7 +2643,7 @@ function buildCspHeader(wsPort, requestHost, publicWsUrl) {
2635
2643
  }
2636
2644
  const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
2637
2645
  if (publicWsSource) connect.add(publicWsSource);
2638
- const scriptSrc = ["'self'", ...ALLOWED_INLINE_SCRIPT_HASHES].join(" ");
2646
+ const scriptSrc = ["'self'", ...EXTRA_SCRIPT_SOURCES].join(" ");
2639
2647
  return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; connect-src ${Array.from(connect).join(" ")}; img-src 'self' data:; font-src 'self' data:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
2640
2648
  }
2641
2649
  function isInsideDist(candidate, distDir) {
@@ -2996,7 +3004,7 @@ function mapStatus(raw) {
2996
3004
  return "stopped";
2997
3005
  }
2998
3006
  }
2999
- function toView(info) {
3007
+ function toView(info, health) {
3000
3008
  const view = {
3001
3009
  name: info.name,
3002
3010
  transport: info.transport,
@@ -3008,6 +3016,7 @@ function toView(info) {
3008
3016
  };
3009
3017
  if (info.description !== void 0) view.description = info.description;
3010
3018
  if (info.lazy !== void 0) view.lazy = info.lazy;
3019
+ if (health !== void 0) view.health = health;
3011
3020
  return view;
3012
3021
  }
3013
3022
  function deps(ws, globalConfigPath, registry) {
@@ -3033,7 +3042,13 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
3033
3042
  registry: mcpRegistry,
3034
3043
  presets: allServers()
3035
3044
  });
3036
- send(ws, { type: "mcp.list", payload: { servers: servers.map(toView) } });
3045
+ const health = new Map(
3046
+ (typeof mcpRegistry.operationalHealth === "function" ? mcpRegistry.operationalHealth() : []).map((item) => [item.name, item])
3047
+ );
3048
+ send(ws, {
3049
+ type: "mcp.list",
3050
+ payload: { servers: servers.map((server) => toView(server, health.get(server.name))) }
3051
+ });
3037
3052
  }
3038
3053
  async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
3039
3054
  const d = deps(ws, globalConfigPath, mcpRegistry);
@@ -3183,10 +3198,119 @@ async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
3183
3198
  payload: { success: result.ok, message: result.message }
3184
3199
  });
3185
3200
  }
3201
+ async function handleMcpResources(ws, msg, _globalConfigPath, mcpRegistry) {
3202
+ if (!mcpRegistry) return sendContentError(ws, "resources", "", "MCP registry is not available.");
3203
+ const payload = payloadRecord(msg);
3204
+ let serverName = "";
3205
+ try {
3206
+ serverName = requiredPayloadString(payload, "name");
3207
+ const refresh = payload["refresh"] === true;
3208
+ const [resources, resourceTemplates] = await Promise.all([
3209
+ mcpRegistry.listResources(serverName, { refresh }),
3210
+ mcpRegistry.listResourceTemplates(serverName, { refresh })
3211
+ ]);
3212
+ send(ws, {
3213
+ type: "mcp.resources",
3214
+ payload: { name: serverName, resources, resourceTemplates }
3215
+ });
3216
+ } catch (err) {
3217
+ sendContentError(ws, "resources", serverName, errorMessage(err));
3218
+ }
3219
+ }
3220
+ async function handleMcpPrompts(ws, msg, _globalConfigPath, mcpRegistry) {
3221
+ if (!mcpRegistry) return sendContentError(ws, "prompts", "", "MCP registry is not available.");
3222
+ const payload = payloadRecord(msg);
3223
+ let serverName = "";
3224
+ try {
3225
+ serverName = requiredPayloadString(payload, "name");
3226
+ const prompts = await mcpRegistry.listPrompts(serverName, {
3227
+ refresh: payload["refresh"] === true
3228
+ });
3229
+ send(ws, { type: "mcp.prompts", payload: { name: serverName, prompts } });
3230
+ } catch (err) {
3231
+ sendContentError(ws, "prompts", serverName, errorMessage(err));
3232
+ }
3233
+ }
3234
+ async function handleMcpResourceRead(ws, msg, _globalConfigPath, mcpRegistry) {
3235
+ if (!mcpRegistry)
3236
+ return sendContentError(ws, "resource.read", "", "MCP registry is not available.");
3237
+ const payload = payloadRecord(msg);
3238
+ let serverName = "";
3239
+ try {
3240
+ serverName = requiredPayloadString(payload, "name");
3241
+ const insertion = await mcpRegistry.selectResourceForInsertion(
3242
+ serverName,
3243
+ requiredPayloadString(payload, "uri")
3244
+ );
3245
+ send(ws, { type: "mcp.content.selected", payload: insertion });
3246
+ } catch (err) {
3247
+ sendContentError(ws, "resource.read", serverName, errorMessage(err));
3248
+ }
3249
+ }
3250
+ async function handleMcpPromptGet(ws, msg, _globalConfigPath, mcpRegistry) {
3251
+ if (!mcpRegistry) return sendContentError(ws, "prompt.get", "", "MCP registry is not available.");
3252
+ const payload = payloadRecord(msg);
3253
+ let serverName = "";
3254
+ try {
3255
+ serverName = requiredPayloadString(payload, "name");
3256
+ const insertion = await mcpRegistry.selectPromptForInsertion(
3257
+ serverName,
3258
+ requiredPayloadString(payload, "prompt"),
3259
+ promptArguments(payload["arguments"])
3260
+ );
3261
+ send(ws, { type: "mcp.content.selected", payload: insertion });
3262
+ } catch (err) {
3263
+ sendContentError(ws, "prompt.get", serverName, errorMessage(err));
3264
+ }
3265
+ }
3266
+ function payloadRecord(msg) {
3267
+ return msg.payload && typeof msg.payload === "object" && !Array.isArray(msg.payload) ? msg.payload : {};
3268
+ }
3269
+ function requiredPayloadString(payload, field) {
3270
+ const value = payload[field];
3271
+ if (typeof value !== "string" || value.length === 0) {
3272
+ throw new Error(`MCP payload field "${field}" must be a non-empty string`);
3273
+ }
3274
+ return value;
3275
+ }
3276
+ function promptArguments(value) {
3277
+ if (value === void 0) return void 0;
3278
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3279
+ throw new Error("MCP prompt arguments must be an object");
3280
+ }
3281
+ const args = {};
3282
+ for (const [key, item] of Object.entries(value)) {
3283
+ if (typeof item !== "string") throw new Error(`MCP prompt argument "${key}" must be a string`);
3284
+ args[key] = item;
3285
+ }
3286
+ return args;
3287
+ }
3288
+ function sendContentError(ws, action, name2, error) {
3289
+ send(ws, { type: "mcp.content.error", payload: { action, name: name2, error } });
3290
+ }
3291
+ function errorMessage(err) {
3292
+ return err instanceof Error ? err.message : String(err);
3293
+ }
3186
3294
 
3187
3295
  // src/server/memory-handlers.ts
3296
+ function isSuperMemoryStore(store) {
3297
+ const s = store;
3298
+ return typeof s.stats === "function" && typeof s.listSuper === "function" && typeof s.getSuperMemory === "function" && typeof s.updateSuperMemory === "function" && typeof s.deleteSuperMemory === "function";
3299
+ }
3300
+ function requiresSuperMemory(command) {
3301
+ return `\`${command}\` requires the Super Memory backend (superMemory.enabled).`;
3302
+ }
3188
3303
  async function handleMemoryList(ws, memoryStore) {
3189
3304
  try {
3305
+ if (isSuperMemoryStore(memoryStore)) {
3306
+ const [stats, memories] = await Promise.all([
3307
+ memoryStore.stats(),
3308
+ memoryStore.listSuper()
3309
+ ]);
3310
+ const text2 = memories.length === 0 ? "\u{1F9E0} Super Memory is empty." : formatSuperMemoryText(stats, memories);
3311
+ send(ws, { type: "memory.list", payload: { text: text2 } });
3312
+ return;
3313
+ }
3190
3314
  const text = await memoryStore.readAll();
3191
3315
  send(ws, { type: "memory.list", payload: { text } });
3192
3316
  } catch (err) {
@@ -3196,26 +3320,128 @@ async function handleMemoryList(ws, memoryStore) {
3196
3320
  });
3197
3321
  }
3198
3322
  }
3199
- async function handleMemoryRemember(ws, msg, memoryStore) {
3200
- const { text, scope } = msg.payload;
3323
+ function formatSuperMemoryText(stats, memories) {
3324
+ const active = stats.byStatus["active"] ?? 0;
3325
+ const stale = stats.byStatus["stale"] ?? 0;
3326
+ const archived = stats.byStatus["archived"] ?? 0;
3327
+ const lines = [
3328
+ "## \u{1F9E0} Super Memory",
3329
+ "",
3330
+ `**Total:** ${stats.total} \xB7 \u{1F7E2} ${active} active \xB7 \u{1F7E1} ${stale} stale \xB7 \u{1F535} ${archived} archived \xB7 **edges:** ${stats.edges}`,
3331
+ ""
3332
+ ];
3333
+ for (const m of memories) {
3334
+ const tags = m.tags.length > 0 ? ` \`${m.tags.slice(0, 3).join("` `")}\`` : "";
3335
+ const icon = m.status === "active" ? "\u{1F7E2}" : m.status === "stale" ? "\u{1F7E1}" : m.status === "archived" ? "\u{1F535}" : "\u26AA";
3336
+ const preview = m.text.replace(/\s+/g, " ").trim().slice(0, 140);
3337
+ lines.push(`- ${icon} \`${m.id.slice(0, 12)}\u2026\` [${m.kind}] ${preview}${tags}`);
3338
+ }
3339
+ return lines.join("\n");
3340
+ }
3341
+ async function handleSuperMemoryList(ws, memoryStore) {
3342
+ if (!isSuperMemoryStore(memoryStore)) {
3343
+ send(ws, { type: "memory.super.list", payload: { error: requiresSuperMemory("memory.super.list") } });
3344
+ return;
3345
+ }
3201
3346
  try {
3202
- await memoryStore.remember(text, scope ?? "project-memory");
3203
- sendResult(ws, true, "Saved to memory");
3347
+ const [stats, memories] = await Promise.all([
3348
+ memoryStore.stats(),
3349
+ memoryStore.listSuper()
3350
+ ]);
3351
+ send(ws, { type: "memory.super.list", payload: { memories, stats } });
3204
3352
  } catch (err) {
3205
- sendResult(ws, false, errMessage(err));
3353
+ send(ws, { type: "memory.super.list", payload: { error: errMessage(err) } });
3206
3354
  }
3207
3355
  }
3208
- async function handleMemoryForget(ws, msg, memoryStore) {
3209
- const { text, scope } = msg.payload;
3356
+ async function handleSuperMemoryGet(ws, msg, memoryStore) {
3357
+ if (!isSuperMemoryStore(memoryStore)) {
3358
+ send(ws, { type: "memory.super.get", payload: { error: requiresSuperMemory("memory.super.get") } });
3359
+ return;
3360
+ }
3361
+ const { id } = msg.payload;
3362
+ if (!id) {
3363
+ send(ws, { type: "memory.super.get", payload: { error: "id is required" } });
3364
+ return;
3365
+ }
3210
3366
  try {
3211
- const removed = await memoryStore.forget(text, scope ?? "project-memory");
3212
- sendResult(
3213
- ws,
3214
- removed > 0,
3215
- removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
3216
- );
3367
+ const memory = await memoryStore.getSuperMemory(id);
3368
+ if (!memory) {
3369
+ send(ws, { type: "memory.super.get", payload: { error: `Memory "${id}" not found.` } });
3370
+ return;
3371
+ }
3372
+ send(ws, { type: "memory.super.get", payload: { memory } });
3217
3373
  } catch (err) {
3218
- sendResult(ws, false, errMessage(err));
3374
+ send(ws, { type: "memory.super.get", payload: { error: errMessage(err) } });
3375
+ }
3376
+ }
3377
+ async function handleSuperMemoryUpdate(ws, msg, memoryStore) {
3378
+ if (!isSuperMemoryStore(memoryStore)) {
3379
+ send(ws, { type: "memory.super.update", payload: { error: requiresSuperMemory("memory.super.update") } });
3380
+ return;
3381
+ }
3382
+ const payload = msg.payload;
3383
+ const id = payload["id"];
3384
+ if (!id) {
3385
+ send(ws, { type: "memory.super.update", payload: { error: "id is required" } });
3386
+ return;
3387
+ }
3388
+ const patch = { ...payload };
3389
+ delete patch["id"];
3390
+ if (Object.keys(patch).length === 0) {
3391
+ send(ws, { type: "memory.super.update", payload: { error: "No fields to update." } });
3392
+ return;
3393
+ }
3394
+ try {
3395
+ const memory = await memoryStore.updateSuperMemory(id, patch);
3396
+ send(ws, { type: "memory.super.update", payload: { memory } });
3397
+ } catch (err) {
3398
+ send(ws, { type: "memory.super.update", payload: { error: errMessage(err) } });
3399
+ }
3400
+ }
3401
+ async function handleSuperMemoryRemember(ws, msg, memoryStore) {
3402
+ if (!isSuperMemoryStore(memoryStore)) {
3403
+ send(ws, { type: "memory.super.remember", payload: { error: requiresSuperMemory("memory.super.remember") } });
3404
+ return;
3405
+ }
3406
+ const payload = msg.payload;
3407
+ const text = payload["text"];
3408
+ if (!text || !text.trim()) {
3409
+ send(ws, { type: "memory.super.remember", payload: { error: "text is required" } });
3410
+ return;
3411
+ }
3412
+ try {
3413
+ const memory = await memoryStore.rememberSuper({
3414
+ text: text.trim(),
3415
+ kind: payload["kind"],
3416
+ scope: payload["scope"],
3417
+ tags: payload["tags"],
3418
+ anchors: payload["anchors"],
3419
+ importance: payload["importance"],
3420
+ confidence: payload["confidence"],
3421
+ freshness: payload["freshness"],
3422
+ supersedes: payload["supersedes"],
3423
+ contradicts: payload["contradicts"]
3424
+ });
3425
+ send(ws, { type: "memory.super.remember", payload: { memory } });
3426
+ } catch (err) {
3427
+ send(ws, { type: "memory.super.remember", payload: { error: errMessage(err) } });
3428
+ }
3429
+ }
3430
+ async function handleSuperMemoryDelete(ws, msg, memoryStore) {
3431
+ if (!isSuperMemoryStore(memoryStore)) {
3432
+ send(ws, { type: "memory.super.delete", payload: { success: false, message: requiresSuperMemory("memory.super.delete") } });
3433
+ return;
3434
+ }
3435
+ const { id, reason } = msg.payload;
3436
+ if (!id) {
3437
+ send(ws, { type: "memory.super.delete", payload: { success: false, message: "id is required" } });
3438
+ return;
3439
+ }
3440
+ try {
3441
+ await memoryStore.deleteSuperMemory(id, reason);
3442
+ send(ws, { type: "memory.super.delete", payload: { success: true, message: `Deleted memory "${id}".` } });
3443
+ } catch (err) {
3444
+ send(ws, { type: "memory.super.delete", payload: { success: false, message: errMessage(err) } });
3219
3445
  }
3220
3446
  }
3221
3447
 
@@ -3265,6 +3491,16 @@ function openBrowser(url, platform = process.platform) {
3265
3491
  // src/server/port-utils.ts
3266
3492
  import * as net from "node:net";
3267
3493
  import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core";
3494
+ var SURFACE_DEFAULT_PORTS = {
3495
+ webui: { http: 3456, ws: 3457 },
3496
+ simpleui: { http: 3466, ws: 3467 }
3497
+ };
3498
+ function surfaceLabel(surface) {
3499
+ return surface === "webui" ? "WebUI" : "SimpleUI";
3500
+ }
3501
+ function getSurfaceDefaultPorts(surface) {
3502
+ return { ...SURFACE_DEFAULT_PORTS[surface] };
3503
+ }
3268
3504
  function isPortFree(host, port) {
3269
3505
  return new Promise((resolve10) => {
3270
3506
  const srv = net.createServer();
@@ -3821,6 +4057,8 @@ var SddBoardWebSocketHandler = class {
3821
4057
  import * as path9 from "node:path";
3822
4058
  import { spawnSync as spawnSync2 } from "node:child_process";
3823
4059
  import {
4060
+ DefaultTaskStore,
4061
+ TaskTracker,
3824
4062
  WorktreeManager as WorktreeManager2,
3825
4063
  ToolValidationError as ToolValidationError4
3826
4064
  } from "@wrongstack/core";
@@ -3836,6 +4074,63 @@ import {
3836
4074
  startSddRun,
3837
4075
  TaskGraphStore
3838
4076
  } from "@wrongstack/sdd";
4077
+ function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
4078
+ const runTracker = tracker ?? (() => {
4079
+ const t = new TaskTracker({ store: new DefaultTaskStore() });
4080
+ t.setGraph(graph);
4081
+ return t;
4082
+ })();
4083
+ const worktreesEnabled = config.worktrees ?? process.env["WRONGSTACK_SDD_WORKTREES"] !== "0";
4084
+ let worktrees;
4085
+ if (worktreesEnabled) {
4086
+ const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
4087
+ cwd: deps2.projectRoot,
4088
+ encoding: "utf8",
4089
+ windowsHide: true
4090
+ }).stdout?.trim() === "true";
4091
+ if (inGit) {
4092
+ void cleanupStaleSddWorktrees({
4093
+ projectRoot: deps2.projectRoot,
4094
+ boardsDir: deps2.projectSddBoards
4095
+ }).catch(() => void 0);
4096
+ worktrees = new WorktreeManager2({
4097
+ projectRoot: deps2.projectRoot,
4098
+ events: deps2.events,
4099
+ sessionId: () => deps2.agent.ctx.session?.id
4100
+ });
4101
+ }
4102
+ }
4103
+ const superviseFailure = deps2.brain && deps2.runIsolatedTurn ? new SddSupervisor({
4104
+ brain: deps2.brain,
4105
+ reassignModels: config.fallbackModels,
4106
+ generateSubtasks: makeLlmSubtaskGenerator({
4107
+ run: (prompt) => deps2.runIsolatedTurn(prompt, "Task Splitter")
4108
+ }),
4109
+ requestLlmVerdict: true
4110
+ }).superviseFailure : deps2.brain ? new SddSupervisor({
4111
+ brain: deps2.brain,
4112
+ reassignModels: config.fallbackModels,
4113
+ requestLlmVerdict: true
4114
+ }).superviseFailure : void 0;
4115
+ return startSddRun({
4116
+ tracker: runTracker,
4117
+ graph,
4118
+ agent: deps2.agent,
4119
+ projectRoot: deps2.projectRoot,
4120
+ events: deps2.events,
4121
+ sessionId: () => deps2.agent.ctx.session?.id,
4122
+ subagentFactory: deps2.subagentFactory,
4123
+ boardStore: new SddBoardStore2({ baseDir: deps2.projectSddBoards }),
4124
+ registry: deps2.registry ?? new SddRunRegistry(),
4125
+ verifyTask: makeCommandVerifier(),
4126
+ ...superviseFailure ? { superviseFailure } : {},
4127
+ ...config.parallelSlots !== void 0 ? { parallelSlots: config.parallelSlots } : {},
4128
+ ...config.defaultModel !== void 0 ? { defaultModel: config.defaultModel } : {},
4129
+ ...config.defaultProvider !== void 0 ? { defaultProvider: config.defaultProvider } : {},
4130
+ ...config.fallbackModels !== void 0 ? { fallbackModels: config.fallbackModels } : {},
4131
+ ...worktrees ? { worktrees } : {}
4132
+ });
4133
+ }
3839
4134
  var PLANNING_ONLY_GUARD = "SYSTEM: You are running a PLANNING-ONLY specification interview. Do NOT write, create, or edit any files, and do NOT run shell/terminal commands or use any code-editing tools \u2014 they are disabled here and any attempt will fail and waste the turn. Respond with TEXT ONLY: ask your question, or emit the requested spec / plan / task JSON. All code is written later, automatically, once the plan is approved and the multi-agent run starts.\n\n---\n\n";
3840
4135
  function buildSddWizardDeps(opts) {
3841
4136
  const registry = new SddRunRegistry();
@@ -3871,64 +4166,27 @@ function buildSddWizardDeps(opts) {
3871
4166
  field: "taskGraph"
3872
4167
  });
3873
4168
  }
3874
- const worktreesEnabled = useWorktrees ?? process.env["WRONGSTACK_SDD_WORKTREES"] !== "0";
3875
- let worktrees;
3876
- if (worktreesEnabled) {
3877
- const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
3878
- cwd: opts.projectRoot,
3879
- encoding: "utf8",
3880
- windowsHide: true
3881
- }).stdout?.trim() === "true";
3882
- if (inGit) {
3883
- await cleanupStaleSddWorktrees({
3884
- projectRoot: opts.projectRoot,
3885
- boardsDir: opts.paths.projectSddBoards
3886
- }).catch(() => void 0);
3887
- worktrees = new WorktreeManager2({
3888
- projectRoot: opts.projectRoot,
3889
- events: opts.events,
3890
- sessionId: () => opts.agent.ctx.session?.id
3891
- });
3892
- }
3893
- }
3894
- const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
3895
- const verifyTask = makeCommandVerifier();
3896
- const superviseFailure = opts.brain ? new SddSupervisor({
3897
- brain: opts.brain,
3898
- // The run-level fallback chain (chosen in the wizard) doubles as the
3899
- // supervisor's reassign options — a `reassign` verdict rotates the
3900
- // worker model on retry. Empty/undefined → reassign option dropped.
3901
- reassignModels: fallbackModels,
3902
- // LLM auto-split: decompose a retry-exhausted task into smaller
3903
- // sub-tasks on an isolated read-only turn. Heavily validated +
3904
- // bounded; an empty result degrades the split into a retry.
3905
- generateSubtasks: makeLlmSubtaskGenerator({
3906
- run: (prompt) => runIsolatedTurn(prompt, "Task Splitter")
3907
- }),
3908
- // The standalone brain is a tiered policy→LLM arbiter with NO
3909
- // human-escalation wrapper (see index.ts), so it never blocks on a
3910
- // human prompt — an unresolved verdict degrades to a bounded retry.
3911
- // Safe to let the LLM layer actually pick reassign/split.
3912
- requestLlmVerdict: true
3913
- }).superviseFailure : void 0;
3914
- const handle = startSddRun({
3915
- tracker,
4169
+ const handle = startSddRunFromGraph(
3916
4170
  graph,
3917
- agent: opts.agent,
3918
- projectRoot: opts.projectRoot,
3919
- events: opts.events,
3920
- sessionId: () => opts.agent.ctx.session?.id,
3921
- subagentFactory: opts.subagentFactory,
3922
- worktrees,
3923
- boardStore,
3924
- registry,
3925
- parallelSlots,
3926
- defaultModel,
3927
- defaultProvider,
3928
- fallbackModels,
3929
- verifyTask,
3930
- superviseFailure
3931
- });
4171
+ {
4172
+ agent: opts.agent,
4173
+ events: opts.events,
4174
+ projectRoot: opts.projectRoot,
4175
+ subagentFactory: opts.subagentFactory,
4176
+ projectSddBoards: opts.paths.projectSddBoards,
4177
+ registry,
4178
+ runIsolatedTurn,
4179
+ ...opts.brain ? { brain: opts.brain } : {}
4180
+ },
4181
+ {
4182
+ ...parallelSlots !== void 0 ? { parallelSlots } : {},
4183
+ ...defaultModel !== void 0 ? { defaultModel } : {},
4184
+ ...defaultProvider !== void 0 ? { defaultProvider } : {},
4185
+ ...fallbackModels !== void 0 ? { fallbackModels } : {},
4186
+ ...useWorktrees !== void 0 ? { worktrees: useWorktrees } : {}
4187
+ },
4188
+ tracker
4189
+ );
3932
4190
  void handle.completion.catch(() => {
3933
4191
  });
3934
4192
  return { runId: handle.runId };
@@ -4072,6 +4330,31 @@ var SddWizardWebSocketHandler = class {
4072
4330
  }
4073
4331
  };
4074
4332
 
4333
+ // src/server/session-history.ts
4334
+ function toSessionHistoryEntry(summary, currentSessionId) {
4335
+ return {
4336
+ id: summary.id,
4337
+ title: summary.title,
4338
+ ...summary.name !== void 0 ? { name: summary.name } : {},
4339
+ startedAt: summary.startedAt,
4340
+ ...summary.endedAt !== void 0 ? { endedAt: summary.endedAt } : {},
4341
+ model: summary.model,
4342
+ provider: summary.provider,
4343
+ tokenTotal: summary.tokenTotal,
4344
+ ...summary.iterationCount !== void 0 ? { iterationCount: summary.iterationCount } : {},
4345
+ ...summary.toolCallCount !== void 0 ? { toolCallCount: summary.toolCallCount } : {},
4346
+ ...summary.toolErrorCount !== void 0 ? { toolErrorCount: summary.toolErrorCount } : {},
4347
+ ...summary.fileChangeCount !== void 0 ? { fileChangeCount: summary.fileChangeCount } : {},
4348
+ ...summary.toolBreakdown !== void 0 ? { toolBreakdown: summary.toolBreakdown } : {},
4349
+ ...summary.compactionCount !== void 0 ? { compactionCount: summary.compactionCount } : {},
4350
+ ...summary.outcome !== void 0 ? { outcome: summary.outcome } : {},
4351
+ isCurrent: summary.id === currentSessionId
4352
+ };
4353
+ }
4354
+ function toSessionHistoryEntries(summaries, currentSessionId) {
4355
+ return summaries.map((summary) => toSessionHistoryEntry(summary, currentSessionId));
4356
+ }
4357
+
4075
4358
  // src/server/shell-open.ts
4076
4359
  import * as fs8 from "node:fs/promises";
4077
4360
  import * as path10 from "node:path";
@@ -4272,6 +4555,16 @@ function validateBrainAskPayload(payload) {
4272
4555
  }
4273
4556
  return { ok: true, value: { question: question.trim() } };
4274
4557
  }
4558
+ function validateBrainConfigSetPayload(payload) {
4559
+ if (!isRecord(payload)) {
4560
+ return { ok: false, message: "brain.config.set payload must be an object with a patch object" };
4561
+ }
4562
+ const patch = payload["patch"];
4563
+ if (!isRecord(patch)) {
4564
+ return { ok: false, message: "brain.config.set payload.patch must be an object" };
4565
+ }
4566
+ return { ok: true, value: { patch } };
4567
+ }
4275
4568
  function validateAutonomySwitchPayload(payload) {
4276
4569
  if (!isRecord(payload)) {
4277
4570
  return { ok: false, message: "autonomy.switch payload must be an object with string mode" };
@@ -4333,7 +4626,9 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
4333
4626
  "hqEnabled",
4334
4627
  "hqRawContent",
4335
4628
  "fallbackAuto",
4336
- "favoriteModelsOnly"
4629
+ "favoriteModelsOnly",
4630
+ "breakerEnabled",
4631
+ "debugStream"
4337
4632
  ]);
4338
4633
  var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels", "favoriteModels"]);
4339
4634
  var STRING_ARRAY_RECORD_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackProfiles"]);
@@ -4344,9 +4639,18 @@ var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
4344
4639
  "maxIterations",
4345
4640
  "maxConcurrent",
4346
4641
  "enhanceDelayMs",
4347
- "tgLongToolMs"
4642
+ "tgLongToolMs",
4643
+ "breakerAutoKillResetMs"
4644
+ ]);
4645
+ var STRING_PREF_KEYS = /* @__PURE__ */ new Set([
4646
+ "hqUrl",
4647
+ "hqToken",
4648
+ "uiLocale",
4649
+ "thinkingWord",
4650
+ "refinerProvider",
4651
+ "refinerModel",
4652
+ "refinerFallbackProfile"
4348
4653
  ]);
4349
- var STRING_PREF_KEYS = /* @__PURE__ */ new Set(["hqUrl", "hqToken", "uiLocale"]);
4350
4654
  var ENUM_PREF_KEYS = {
4351
4655
  autonomy: AUTONOMY_VALUES,
4352
4656
  contextStrategy: CONTEXT_STRATEGY_VALUES,
@@ -4357,7 +4661,10 @@ var ENUM_PREF_KEYS = {
4357
4661
  auditLevel: AUDIT_LEVEL_VALUES,
4358
4662
  reasoningMode: REASONING_MODE_VALUES,
4359
4663
  reasoningEffort: REASONING_EFFORT_VALUES,
4360
- cacheTtl: CACHE_TTL_VALUES
4664
+ cacheTtl: CACHE_TTL_VALUES,
4665
+ statuslineMode: /* @__PURE__ */ new Set(["minimum", "detailed", "no-color"]),
4666
+ animationStyle: /* @__PURE__ */ new Set(["rainbow", "wave", "pulse", "dots", "breathe", "cycle"]),
4667
+ fsAccess: /* @__PURE__ */ new Set(["unrestricted", "project"])
4361
4668
  };
4362
4669
  function validateModelRuntimeValue(modelRuntime, path23) {
4363
4670
  const reasoning = modelRuntime["reasoning"];
@@ -5756,6 +6063,10 @@ function shouldLogWatcherStats() {
5756
6063
  function setupEvents(deps2) {
5757
6064
  const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
5758
6065
  const disposers = [];
6066
+ let disposed = false;
6067
+ const on = (event, listener) => {
6068
+ disposers.push(events.on(event, listener));
6069
+ };
5759
6070
  const currentSessionId = () => context.session?.id ?? "";
5760
6071
  const sessionPayload2 = (payload) => {
5761
6072
  const provided = payload["sessionId"];
@@ -5771,20 +6082,20 @@ function setupEvents(deps2) {
5771
6082
  sessionBridge?.append(event).catch(() => {
5772
6083
  });
5773
6084
  };
5774
- events.on("iteration.started", (e) => {
6085
+ on("iteration.started", (e) => {
5775
6086
  const maxIt = typeof context.meta["maxIterations"] === "number" ? context.meta["maxIterations"] : config.tools?.maxIterations ?? 100;
5776
6087
  broadcast2(clients, {
5777
6088
  type: "iteration.started",
5778
6089
  payload: sessionPayload2({ sessionId: e.sessionId, index: e.index, maxIterations: maxIt })
5779
6090
  });
5780
6091
  });
5781
- events.on("iteration.completed", (e) => {
6092
+ on("iteration.completed", (e) => {
5782
6093
  broadcast2(clients, {
5783
6094
  type: "iteration.completed",
5784
6095
  payload: sessionPayload2({ sessionId: e.sessionId, index: e.index, totalIterations: e.index + 1 })
5785
6096
  });
5786
6097
  });
5787
- events.on("iteration.limit_reached", (e) => {
6098
+ on("iteration.limit_reached", (e) => {
5788
6099
  broadcast2(clients, {
5789
6100
  type: "iteration.limit_reached",
5790
6101
  payload: sessionPayload2({
@@ -5794,19 +6105,19 @@ function setupEvents(deps2) {
5794
6105
  })
5795
6106
  });
5796
6107
  });
5797
- events.on("provider.text_delta", (e) => {
6108
+ on("provider.text_delta", (e) => {
5798
6109
  broadcast2(clients, { type: "provider.text_delta", payload: sessionPayload2({ sessionId: e.sessionId, text: e.text, messageId: "current" }) });
5799
6110
  });
5800
- events.on("provider.thinking_delta", (e) => {
6111
+ on("provider.thinking_delta", (e) => {
5801
6112
  broadcast2(clients, { type: "provider.thinking_delta", payload: sessionPayload2({ sessionId: e.sessionId, text: e.text }) });
5802
6113
  });
5803
- events.on("provider.stream_error", (e) => {
6114
+ on("provider.stream_error", (e) => {
5804
6115
  broadcast2(clients, {
5805
6116
  type: "provider.stream_error",
5806
6117
  payload: sessionPayload2({ sessionId: e.sessionId, eventType: e.eventType, message: e.msg })
5807
6118
  });
5808
6119
  });
5809
- events.on("tool.started", (e) => {
6120
+ on("tool.started", (e) => {
5810
6121
  broadcast2(clients, {
5811
6122
  type: "tool.started",
5812
6123
  payload: sessionPayload2({ sessionId: e.sessionId, id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` })
@@ -5819,7 +6130,7 @@ function setupEvents(deps2) {
5819
6130
  input: e.input
5820
6131
  });
5821
6132
  });
5822
- events.on("tool.progress", (e) => {
6133
+ on("tool.progress", (e) => {
5823
6134
  broadcast2(clients, {
5824
6135
  type: "tool.progress",
5825
6136
  // Nested `event` shape — the client handler reads `payload.event?.text`
@@ -5840,7 +6151,7 @@ function setupEvents(deps2) {
5840
6151
  }
5841
6152
  });
5842
6153
  });
5843
- events.on("tool.executed", (e) => {
6154
+ on("tool.executed", (e) => {
5844
6155
  broadcast2(clients, {
5845
6156
  type: "tool.executed",
5846
6157
  payload: sessionPayload2({ sessionId: e.sessionId, id: e.id, name: e.name, durationMs: e.durationMs, ok: e.ok, input: e.input, output: e.output })
@@ -5898,7 +6209,7 @@ function setupEvents(deps2) {
5898
6209
  })();
5899
6210
  }
5900
6211
  });
5901
- events.on("tool.loop_detected", (e) => {
6212
+ on("tool.loop_detected", (e) => {
5902
6213
  broadcast2(clients, {
5903
6214
  type: "tool.loop_detected",
5904
6215
  payload: sessionPayload2({
@@ -5910,19 +6221,19 @@ function setupEvents(deps2) {
5910
6221
  })
5911
6222
  });
5912
6223
  });
5913
- events.on("trust.persisted", (e) => {
6224
+ on("trust.persisted", (e) => {
5914
6225
  broadcast2(clients, {
5915
6226
  type: "trust.persisted",
5916
6227
  payload: sessionPayload2({ sessionId: e.sessionId, tool: e.tool, pattern: e.pattern, decision: e.decision })
5917
6228
  });
5918
6229
  });
5919
- events.on("delegate.started", (e) => {
6230
+ on("delegate.started", (e) => {
5920
6231
  broadcast2(clients, {
5921
6232
  type: "delegate.started",
5922
6233
  payload: sessionPayload2({ sessionId: e.sessionId, target: e.target, task: e.task })
5923
6234
  });
5924
6235
  });
5925
- events.on("delegate.completed", (e) => {
6236
+ on("delegate.completed", (e) => {
5926
6237
  broadcast2(clients, {
5927
6238
  type: "delegate.completed",
5928
6239
  payload: sessionPayload2({
@@ -5940,7 +6251,7 @@ function setupEvents(deps2) {
5940
6251
  })
5941
6252
  });
5942
6253
  });
5943
- events.on("provider.response", (e) => {
6254
+ on("provider.response", (e) => {
5944
6255
  broadcast2(clients, {
5945
6256
  type: "provider.response",
5946
6257
  payload: sessionPayload2({
@@ -5952,7 +6263,7 @@ function setupEvents(deps2) {
5952
6263
  })
5953
6264
  });
5954
6265
  });
5955
- events.on("ctx.pct", (e) => {
6266
+ on("ctx.pct", (e) => {
5956
6267
  broadcast2(clients, {
5957
6268
  type: "ctx.pct",
5958
6269
  payload: sessionPayload2({ sessionId: e.sessionId, load: e.load, tokens: e.tokens, maxContext: e.maxContext })
@@ -5969,28 +6280,28 @@ function setupEvents(deps2) {
5969
6280
  })
5970
6281
  });
5971
6282
  });
5972
- events.on("ctx.max_context", (e) => {
6283
+ on("ctx.max_context", (e) => {
5973
6284
  broadcast2(clients, {
5974
6285
  type: "ctx.max_context",
5975
6286
  payload: sessionPayload2({ sessionId: e.sessionId, providerId: e.providerId, modelId: e.modelId, maxContext: e.maxContext })
5976
6287
  });
5977
6288
  });
5978
- events.on("token.threshold", (e) => {
6289
+ on("token.threshold", (e) => {
5979
6290
  broadcast2(clients, {
5980
6291
  type: "token.threshold",
5981
6292
  payload: sessionPayload2({ sessionId: e.sessionId, used: e.used, limit: e.limit })
5982
6293
  });
5983
6294
  });
5984
- events.on("token.cost_estimate_unavailable", (e) => {
6295
+ on("token.cost_estimate_unavailable", (e) => {
5985
6296
  broadcast2(clients, {
5986
6297
  type: "token.cost_estimate_unavailable",
5987
6298
  payload: sessionPayload2({ sessionId: e.sessionId, model: e.model })
5988
6299
  });
5989
6300
  });
5990
- events.on("context.repaired", (e) => {
6301
+ on("context.repaired", (e) => {
5991
6302
  broadcast2(clients, { type: "context.repaired", payload: sessionPayload2({ sessionId: e.sessionId, removedToolUses: e.removedToolUses, removedToolResults: e.removedToolResults, removedMessages: e.removedMessages }) });
5992
6303
  });
5993
- events.on("tool.confirm_needed", (e) => {
6304
+ on("tool.confirm_needed", (e) => {
5994
6305
  const id = e.toolUseId ?? `confirm_${Date.now()}`;
5995
6306
  const payload = sessionPayload2({ sessionId: e.sessionId, id, toolName: e.tool?.name ?? "unknown", input: e.input, suggestedPattern: e.suggestedPattern, decisionSource: e.decisionSource, riskTier: e.riskTier });
5996
6307
  pendingConfirms.set(id, {
@@ -6001,7 +6312,7 @@ function setupEvents(deps2) {
6001
6312
  });
6002
6313
  broadcast2(clients, { type: "tool.confirm_needed", payload });
6003
6314
  });
6004
- events.on("error", (e) => {
6315
+ on("error", (e) => {
6005
6316
  broadcast2(clients, { type: "error", payload: sessionPayload2({ sessionId: e.sessionId, phase: e.phase, message: e.err instanceof Error ? e.err.message : String(e.err) }) });
6006
6317
  appendForCurrentSession(e.sessionId, {
6007
6318
  type: "error",
@@ -6010,13 +6321,13 @@ function setupEvents(deps2) {
6010
6321
  phase: e.phase
6011
6322
  });
6012
6323
  });
6013
- events.on("session.damaged", (e) => {
6324
+ on("session.damaged", (e) => {
6014
6325
  broadcast2(clients, {
6015
6326
  type: "session.damaged",
6016
6327
  payload: { sessionId: e.sessionId, detail: e.detail }
6017
6328
  });
6018
6329
  });
6019
- events.on("session.rewound", (e) => {
6330
+ on("session.rewound", (e) => {
6020
6331
  broadcast2(clients, {
6021
6332
  type: "session.rewound",
6022
6333
  payload: sessionPayload2({
@@ -6027,7 +6338,7 @@ function setupEvents(deps2) {
6027
6338
  })
6028
6339
  });
6029
6340
  });
6030
- events.on("checkpoint.written", (e) => {
6341
+ on("checkpoint.written", (e) => {
6031
6342
  broadcast2(clients, {
6032
6343
  type: "checkpoint.written",
6033
6344
  payload: sessionPayload2({
@@ -6039,19 +6350,19 @@ function setupEvents(deps2) {
6039
6350
  })
6040
6351
  });
6041
6352
  });
6042
- events.on("in_flight.started", (e) => {
6353
+ on("in_flight.started", (e) => {
6043
6354
  broadcast2(clients, {
6044
6355
  type: "in_flight.started",
6045
6356
  payload: sessionPayload2({ sessionId: e.sessionId, context: e.context, ts: e.ts })
6046
6357
  });
6047
6358
  });
6048
- events.on("in_flight.ended", (e) => {
6359
+ on("in_flight.ended", (e) => {
6049
6360
  broadcast2(clients, {
6050
6361
  type: "in_flight.ended",
6051
6362
  payload: sessionPayload2({ sessionId: e.sessionId, reason: e.reason, ts: e.ts })
6052
6363
  });
6053
6364
  });
6054
- events.on("provider.retry", (e) => {
6365
+ on("provider.retry", (e) => {
6055
6366
  broadcast2(clients, {
6056
6367
  type: "provider.retry",
6057
6368
  payload: sessionPayload2({
@@ -6073,7 +6384,7 @@ function setupEvents(deps2) {
6073
6384
  description: e.description
6074
6385
  });
6075
6386
  });
6076
- events.on("provider.error", (e) => {
6387
+ on("provider.error", (e) => {
6077
6388
  broadcast2(clients, {
6078
6389
  type: "provider.error",
6079
6390
  payload: sessionPayload2({
@@ -6093,7 +6404,7 @@ function setupEvents(deps2) {
6093
6404
  retryable: e.retryable
6094
6405
  });
6095
6406
  });
6096
- events.on("provider.fallback", (e) => {
6407
+ on("provider.fallback", (e) => {
6097
6408
  broadcast2(clients, {
6098
6409
  type: "provider.fallback",
6099
6410
  payload: sessionPayload2({
@@ -6105,7 +6416,7 @@ function setupEvents(deps2) {
6105
6416
  })
6106
6417
  });
6107
6418
  });
6108
- events.on("compaction.fired", (e) => {
6419
+ on("compaction.fired", (e) => {
6109
6420
  broadcast2(clients, {
6110
6421
  type: "context.compacted",
6111
6422
  payload: sessionPayload2({
@@ -6117,7 +6428,7 @@ function setupEvents(deps2) {
6117
6428
  })
6118
6429
  });
6119
6430
  });
6120
- events.on("compaction.failed", (e) => {
6431
+ on("compaction.failed", (e) => {
6121
6432
  broadcast2(clients, {
6122
6433
  type: "compaction.failed",
6123
6434
  payload: sessionPayload2({
@@ -6132,25 +6443,25 @@ function setupEvents(deps2) {
6132
6443
  })
6133
6444
  });
6134
6445
  });
6135
- events.on("mcp.server.connected", (e) => {
6446
+ on("mcp.server.connected", (e) => {
6136
6447
  broadcast2(clients, {
6137
6448
  type: "mcp.server.connected",
6138
6449
  payload: { name: e.name, toolCount: e.toolCount }
6139
6450
  });
6140
6451
  });
6141
- events.on("mcp.server.reconnected", (e) => {
6452
+ on("mcp.server.reconnected", (e) => {
6142
6453
  broadcast2(clients, {
6143
6454
  type: "mcp.server.reconnected",
6144
6455
  payload: { name: e.name, toolCount: e.toolCount }
6145
6456
  });
6146
6457
  });
6147
- events.on("mcp.server.disconnected", (e) => {
6458
+ on("mcp.server.disconnected", (e) => {
6148
6459
  broadcast2(clients, {
6149
6460
  type: "mcp.server.disconnected",
6150
6461
  payload: { name: e.name, reason: e.reason }
6151
6462
  });
6152
6463
  });
6153
- events.on("coordinator.stats", (e) => {
6464
+ on("coordinator.stats", (e) => {
6154
6465
  broadcast2(clients, {
6155
6466
  type: "coordinator.stats",
6156
6467
  payload: sessionPayload2({
@@ -6178,16 +6489,16 @@ function setupEvents(deps2) {
6178
6489
  broadcast2(clients, { type: "mailbox.agent_registered", payload });
6179
6490
  });
6180
6491
  const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: sessionPayload2({ kind, ...payload }) });
6181
- events.on("subagent.spawned", (e) => forwardSubagent("spawned", { sessionId: e.sessionId, subagentId: e.subagentId, taskId: e.taskId, name: e.name, provider: e.provider, model: e.model, description: e.description }));
6182
- events.on("subagent.task_started", (e) => forwardSubagent("task_started", { sessionId: e.sessionId, subagentId: e.subagentId, taskId: e.taskId, description: e.description }));
6183
- events.on("subagent.tool_executed", (e) => forwardSubagent("tool_executed", { sessionId: e.sessionId, subagentId: e.subagentId, toolName: e.name, durationMs: e.durationMs, ok: e.ok }));
6184
- events.on("subagent.iteration_summary", (e) => forwardSubagent("iteration_summary", { sessionId: e.sessionId, subagentId: e.subagentId, iteration: e.iteration, toolCalls: e.toolCalls, costUsd: e.costUsd, currentTool: e.currentTool, partialText: e.partialText }));
6185
- events.on("subagent.budget_warning", (e) => forwardSubagent("budget_warning", { sessionId: e.sessionId, subagentId: e.subagentId, budgetKind: e.kind, used: e.used, limit: e.limit }));
6186
- events.on("subagent.budget_extended", (e) => forwardSubagent("budget_extended", { sessionId: e.sessionId, subagentId: e.subagentId, budgetKind: e.kind, newLimit: e.newLimit, totalExtensions: e.totalExtensions }));
6187
- events.on("subagent.ctx_pct", (e) => forwardSubagent("ctx_pct", { sessionId: e.sessionId, subagentId: e.subagentId, load: e.load, tokens: e.tokens, maxContext: e.maxContext }));
6188
- events.on("subagent.task_completed", (e) => forwardSubagent("task_completed", { sessionId: e.sessionId, subagentId: e.subagentId, status: e.status, iterations: e.iterations, toolCalls: e.toolCalls, finalText: e.finalText, failureReason: e.error?.kind, error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0 }));
6189
- events.on("subagent.removed", (e) => forwardSubagent("removed", { sessionId: e.sessionId, subagentId: e.subagentId, reason: e.reason }));
6190
- events.on("agent.timeline.message", (e) => {
6492
+ on("subagent.spawned", (e) => forwardSubagent("spawned", { sessionId: e.sessionId, subagentId: e.subagentId, taskId: e.taskId, name: e.name, provider: e.provider, model: e.model, description: e.description }));
6493
+ on("subagent.task_started", (e) => forwardSubagent("task_started", { sessionId: e.sessionId, subagentId: e.subagentId, taskId: e.taskId, description: e.description }));
6494
+ on("subagent.tool_executed", (e) => forwardSubagent("tool_executed", { sessionId: e.sessionId, subagentId: e.subagentId, toolName: e.name, durationMs: e.durationMs, ok: e.ok }));
6495
+ on("subagent.iteration_summary", (e) => forwardSubagent("iteration_summary", { sessionId: e.sessionId, subagentId: e.subagentId, iteration: e.iteration, toolCalls: e.toolCalls, costUsd: e.costUsd, currentTool: e.currentTool, partialText: e.partialText }));
6496
+ on("subagent.budget_warning", (e) => forwardSubagent("budget_warning", { sessionId: e.sessionId, subagentId: e.subagentId, budgetKind: e.kind, used: e.used, limit: e.limit }));
6497
+ on("subagent.budget_extended", (e) => forwardSubagent("budget_extended", { sessionId: e.sessionId, subagentId: e.subagentId, budgetKind: e.kind, newLimit: e.newLimit, totalExtensions: e.totalExtensions }));
6498
+ on("subagent.ctx_pct", (e) => forwardSubagent("ctx_pct", { sessionId: e.sessionId, subagentId: e.subagentId, load: e.load, tokens: e.tokens, maxContext: e.maxContext }));
6499
+ on("subagent.task_completed", (e) => forwardSubagent("task_completed", { sessionId: e.sessionId, subagentId: e.subagentId, status: e.status, iterations: e.iterations, toolCalls: e.toolCalls, finalText: e.finalText, failureReason: e.error?.kind, error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0 }));
6500
+ on("subagent.removed", (e) => forwardSubagent("removed", { sessionId: e.sessionId, subagentId: e.subagentId, reason: e.reason }));
6501
+ on("agent.timeline.message", (e) => {
6191
6502
  broadcast2(clients, {
6192
6503
  type: "agent.timeline.message",
6193
6504
  payload: sessionPayload2({
@@ -6203,7 +6514,7 @@ function setupEvents(deps2) {
6203
6514
  })
6204
6515
  });
6205
6516
  });
6206
- events.on("agent.status_changed", (e) => {
6517
+ on("agent.status_changed", (e) => {
6207
6518
  broadcast2(clients, {
6208
6519
  type: "agent.status_changed",
6209
6520
  payload: sessionPayload2({
@@ -6218,7 +6529,7 @@ function setupEvents(deps2) {
6218
6529
  });
6219
6530
  });
6220
6531
  let leaderSpawned = false;
6221
- events.on("iteration.started", (e) => {
6532
+ on("iteration.started", (e) => {
6222
6533
  if (!leaderSpawned) {
6223
6534
  leaderSpawned = true;
6224
6535
  const provider = context.provider?.id ?? "unknown";
@@ -6232,7 +6543,7 @@ function setupEvents(deps2) {
6232
6543
  });
6233
6544
  }
6234
6545
  });
6235
- events.on("tool.executed", (e) => {
6546
+ on("tool.executed", (e) => {
6236
6547
  forwardSubagent("tool_executed", {
6237
6548
  sessionId: e.sessionId,
6238
6549
  subagentId: "leader",
@@ -6241,7 +6552,7 @@ function setupEvents(deps2) {
6241
6552
  ok: e.ok
6242
6553
  });
6243
6554
  });
6244
- events.on("provider.response", (e) => {
6555
+ on("provider.response", (e) => {
6245
6556
  if (e.usage?.input != null) {
6246
6557
  const maxCtx = context.provider.capabilities.maxContext;
6247
6558
  const rawLoad = maxCtx > 0 ? e.usage.input / maxCtx : 0;
@@ -6258,7 +6569,7 @@ function setupEvents(deps2) {
6258
6569
  });
6259
6570
  }
6260
6571
  });
6261
- events.on("iteration.completed", (e) => {
6572
+ on("iteration.completed", (e) => {
6262
6573
  if (!leaderSpawned) {
6263
6574
  leaderSpawned = true;
6264
6575
  const provider = context.provider?.id ?? "unknown";
@@ -6290,7 +6601,19 @@ function setupEvents(deps2) {
6290
6601
  payload: sessionPayload2({ event: eventName, ...payload })
6291
6602
  });
6292
6603
  });
6293
- events.on("client.status", async (e) => {
6604
+ events.onPattern("cron:state_snapshot", (_eventName, payload) => {
6605
+ broadcast2(clients, {
6606
+ type: "cron.snapshot",
6607
+ payload
6608
+ });
6609
+ });
6610
+ events.onPattern("cron:job_fired", (_eventName, payload) => {
6611
+ broadcast2(clients, {
6612
+ type: "cron.job_fired",
6613
+ payload
6614
+ });
6615
+ });
6616
+ on("client.status", async (e) => {
6294
6617
  broadcast2(clients, { type: "client.status_update", payload: e });
6295
6618
  if (wpaths?.projectStatus) {
6296
6619
  try {
@@ -6376,6 +6699,7 @@ function setupEvents(deps2) {
6376
6699
  const startWatcher = async () => {
6377
6700
  try {
6378
6701
  await fs10.mkdir(projectsDir, { recursive: true });
6702
+ if (disposed) return;
6379
6703
  watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
6380
6704
  if (eventType !== "change" && eventType !== "rename") return;
6381
6705
  if (filename == null) return;
@@ -6406,7 +6730,7 @@ function setupEvents(deps2) {
6406
6730
  );
6407
6731
  }
6408
6732
  };
6409
- events.on("client.status", (e) => {
6733
+ on("client.status", (e) => {
6410
6734
  if (e.projectHash) {
6411
6735
  const hash = String(e.projectHash);
6412
6736
  if (!knownProjectHashes.has(hash)) {
@@ -6446,7 +6770,9 @@ function setupEvents(deps2) {
6446
6770
  const registry = new SessionRegistry(globalRoot);
6447
6771
  const sessions = await registry.list();
6448
6772
  const mySlug = sessions.find((s) => s.pid === process.pid)?.projectSlug;
6449
- const live = sessions.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true).map((s) => ({
6773
+ const live = sessions.filter(
6774
+ (s) => s.status === "active" || s.status === "idle"
6775
+ ).filter((s) => mySlug ? s.projectSlug === mySlug : true).map((s) => ({
6450
6776
  sessionId: s.sessionId,
6451
6777
  projectName: s.projectName,
6452
6778
  projectSlug: s.projectSlug,
@@ -6500,6 +6826,8 @@ function setupEvents(deps2) {
6500
6826
  void broadcastSessions();
6501
6827
  }
6502
6828
  return () => {
6829
+ if (disposed) return;
6830
+ disposed = true;
6503
6831
  for (const dispose of disposers) {
6504
6832
  try {
6505
6833
  dispose();
@@ -6634,9 +6962,11 @@ function positiveNumber(value) {
6634
6962
  // src/server/server-runtime.ts
6635
6963
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
6636
6964
  async function resolvePorts(opts) {
6637
- const requestedWsPort = opts.wsPort ?? 3457;
6965
+ const surface = opts.surface ?? "webui";
6966
+ const surfaceDefaults = surface === "simpleui" ? { http: 3466, ws: 3467 } : { http: 3456, ws: 3457 };
6967
+ const requestedWsPort = opts.wsPort ?? surfaceDefaults.ws;
6638
6968
  const wsHost = opts.wsHost ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
6639
- const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? "3456", 10);
6969
+ const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? String(surfaceDefaults.http), 10);
6640
6970
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
6641
6971
  const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
6642
6972
  const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
@@ -6726,7 +7056,7 @@ function createWsServers(ports, accessToken) {
6726
7056
  allowedHostnames: publicHostnames,
6727
7057
  allowBrowserUrlToken: Boolean(ports.publicWsUrl)
6728
7058
  });
6729
- const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
7059
+ const WS_MAX_PAYLOAD = 20 * 1024 * 1024;
6730
7060
  const wssPrimary = new WebSocketServer({
6731
7061
  port: ports.wsPort,
6732
7062
  host: ports.wsHost,
@@ -6794,8 +7124,8 @@ function resolveWebuiDistDir(fromUrl, explicitDistDir) {
6794
7124
  if (explicitDistDir) return path13.resolve(explicitDistDir);
6795
7125
  try {
6796
7126
  const requireFromHere2 = createRequire(fromUrl);
6797
- const serverEntry = requireFromHere2.resolve("@wrongstack/webui/server");
6798
- return path13.resolve(path13.dirname(serverEntry), "..");
7127
+ const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
7128
+ return path13.dirname(serverEntry);
6799
7129
  } catch {
6800
7130
  return path13.resolve(path13.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
6801
7131
  }
@@ -6818,7 +7148,7 @@ function startHttpServer(opts) {
6818
7148
  console.log(`[WebUI] HTTP server running on ${openUrl}`);
6819
7149
  if (opts.openBrowser) openBrowser(openUrl);
6820
7150
  void registerInstance(
6821
- { pid: process.pid, httpPort: opts.httpPort, wsPort: opts.wsPort, host: opts.wsHost, projectRoot: opts.projectRoot, projectName: path13.basename(opts.projectRoot) || opts.projectRoot, startedAt: (/* @__PURE__ */ new Date()).toISOString(), url: buildWebUIAccessUrl({ host: opts.wsHost, port: opts.httpPort, publicUrl: opts.publicUrl }) },
7151
+ { pid: process.pid, surface: "webui", httpPort: opts.httpPort, wsPort: opts.wsPort, host: opts.wsHost, projectRoot: opts.projectRoot, projectName: path13.basename(opts.projectRoot) || opts.projectRoot, startedAt: (/* @__PURE__ */ new Date()).toISOString(), url: buildWebUIAccessUrl({ host: opts.wsHost, port: opts.httpPort, publicUrl: opts.publicUrl }) },
6822
7152
  registryBaseDir
6823
7153
  ).catch((err) => console.warn(JSON.stringify({ level: "warn", event: "webui.instance_record_failed", message: errMessage(err), timestamp: (/* @__PURE__ */ new Date()).toISOString() })));
6824
7154
  });
@@ -6834,12 +7164,10 @@ function registerShutdown(deps2) {
6834
7164
  }
6835
7165
 
6836
7166
  // src/server/pre-context-services.ts
6837
- import * as path15 from "node:path";
7167
+ import * as path16 from "node:path";
6838
7168
  import { createRequire as createRequire2 } from "node:module";
6839
- import { WebSocket as WebSocket2 } from "ws";
6840
7169
  import { DefaultTokenCounter } from "@wrongstack/core/infrastructure";
6841
7170
  import {
6842
- AgentStatusTracker,
6843
7171
  AnnotationsStore,
6844
7172
  DEFAULT_SESSION_PRUNE_DAYS,
6845
7173
  DefaultModelsRegistry,
@@ -6850,7 +7178,6 @@ import {
6850
7178
  DefaultSkillLoader,
6851
7179
  DefaultSystemPromptBuilder,
6852
7180
  EventBus,
6853
- FleetNotifier,
6854
7181
  GlobalMailbox,
6855
7182
  PromptUsageStore,
6856
7183
  ProviderRegistry,
@@ -6861,8 +7188,8 @@ import {
6861
7188
  applyToolDescriptionModes,
6862
7189
  applyToolResultRenderModes,
6863
7190
  configureChildEnvGitIdentity,
7191
+ getSessionRegistry as getSessionRegistry2,
6864
7192
  resolveContextWindowPolicy,
6865
- getSessionRegistry,
6866
7193
  makeMailboxTool,
6867
7194
  makeMailInboxTool,
6868
7195
  makeFleetStatusTool,
@@ -6871,7 +7198,12 @@ import {
6871
7198
  import {
6872
7199
  createSuperMemoryTools
6873
7200
  } from "@wrongstack/super-memory";
6874
- import { MCPRegistry } from "@wrongstack/mcp";
7201
+ import {
7202
+ MCPAuthorizationManager,
7203
+ MCPRegistry,
7204
+ MCPVaultTokenStore,
7205
+ createVaultBackedMcpAuthorizationProviderFactory
7206
+ } from "@wrongstack/mcp";
6875
7207
  import { buildProviderFactoriesFromRegistry } from "@wrongstack/providers";
6876
7208
  import { createDefaultContainer } from "@wrongstack/runtime";
6877
7209
  import {
@@ -6883,7 +7215,11 @@ import {
6883
7215
  rememberTool,
6884
7216
  searchMemoryTool
6885
7217
  } from "@wrongstack/tools";
6886
- import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
7218
+ import {
7219
+ attachSessionKanbanMirror,
7220
+ hydrateSessionKanban
7221
+ } from "@wrongstack/tools/session-kanban";
7222
+ import { sessionScopedPath, toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
6887
7223
 
6888
7224
  // src/server/setup-screen.ts
6889
7225
  import { expectDefined as expectDefined2 } from "@wrongstack/core";
@@ -7021,6 +7357,21 @@ function seedContextMeta(config, context) {
7021
7357
  meta["reasoningPreserve"] = mr.reasoning?.preserve === true;
7022
7358
  meta["cacheTtl"] = mr.cache?.ttl ?? "default";
7023
7359
  }
7360
+ meta["refinerProvider"] = autonomyCfg["refinerProvider"] ?? "";
7361
+ meta["refinerModel"] = autonomyCfg["refinerModel"] ?? "";
7362
+ meta["refinerFallbackProfile"] = autonomyCfg["refinerFallbackProfile"] ?? "";
7363
+ meta["thinkingWord"] = autonomyCfg["thinkingWord"] ?? "thinking";
7364
+ meta["statuslineMode"] = autonomyCfg["statuslineMode"] ?? "detailed";
7365
+ meta["animationStyle"] = autonomyCfg["animationStyle"] ?? "rainbow";
7366
+ meta["breakerEnabled"] = config.circuitBreaker?.enabled === true;
7367
+ meta["breakerAutoKillResetMs"] = config.circuitBreaker?.autoKillResetMs ?? 6e4;
7368
+ {
7369
+ const featuresAllow = config.features?.allowOutsideProjectRoot;
7370
+ const toolsRestrict = config.tools?.restrictToProjectRoot;
7371
+ const allow = featuresAllow !== void 0 ? featuresAllow : toolsRestrict !== void 0 ? !toolsRestrict : true;
7372
+ meta["fsAccess"] = allow ? "unrestricted" : "project";
7373
+ }
7374
+ meta["debugStream"] = config.debugStream === true;
7024
7375
  const hqConfig = config.hq;
7025
7376
  meta["hqEnabled"] = hqConfig?.enabled === true;
7026
7377
  meta["hqUrl"] = hqConfig?.url ?? "";
@@ -7117,10 +7468,188 @@ async function discoverAndMergeWebuiProviders(opts) {
7117
7468
  }
7118
7469
  }
7119
7470
 
7471
+ // src/server/standalone-session-identity.ts
7472
+ import * as path15 from "node:path";
7473
+ import {
7474
+ AgentStatusTracker,
7475
+ FleetNotifier,
7476
+ getSessionRegistry,
7477
+ RecoveryLock
7478
+ } from "@wrongstack/core";
7479
+ import { WebSocket as WebSocket2 } from "ws";
7480
+ async function createStandaloneSessionIdentityLifecycle(opts) {
7481
+ const { paths, events, logger } = opts;
7482
+ const registry = opts.sessionRegistry ?? getSessionRegistry(paths.globalRoot);
7483
+ const fleetNotifier = new FleetNotifier({
7484
+ baseDir: paths.globalRoot,
7485
+ projectRoot: paths.projectRoot,
7486
+ selfPid: process.pid
7487
+ });
7488
+ let activeSessionId = opts.initialSessionId;
7489
+ let stopped = false;
7490
+ let transition = Promise.resolve();
7491
+ const statusTracker = new AgentStatusTracker({
7492
+ events,
7493
+ registry,
7494
+ sessionId: () => activeSessionId,
7495
+ onUpdate: () => fleetNotifier.notify()
7496
+ });
7497
+ const register = async (sessionId) => {
7498
+ try {
7499
+ await registry.register({
7500
+ sessionId,
7501
+ projectSlug: paths.projectSlug,
7502
+ projectRoot: paths.projectRoot,
7503
+ projectName: path15.basename(paths.projectRoot),
7504
+ workingDir: opts.workingDir,
7505
+ clientType: "webui",
7506
+ pid: process.pid,
7507
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7508
+ agents: statusTracker.getAgents()
7509
+ });
7510
+ fleetNotifier.notify();
7511
+ } catch (err) {
7512
+ logger.debug?.(`WebUI session registry update failed: ${errorMessage2(err)}`);
7513
+ }
7514
+ };
7515
+ await register(activeSessionId);
7516
+ statusTracker.start();
7517
+ const recoveryLock = opts.manageRecoveryLock === false ? void 0 : new RecoveryLock({ dir: paths.projectSessions, sessionStore: opts.sessionStore });
7518
+ let ownsRecoveryLock = false;
7519
+ if (recoveryLock) {
7520
+ try {
7521
+ await recoveryLock.write(activeSessionId);
7522
+ ownsRecoveryLock = true;
7523
+ } catch {
7524
+ }
7525
+ }
7526
+ let stopHqBridges = () => {
7527
+ };
7528
+ let closeHqPublisher = () => {
7529
+ };
7530
+ let restartHqBridges = (_sessionId) => {
7531
+ };
7532
+ if (opts.enableHqTelemetry !== false) {
7533
+ try {
7534
+ const core = await import("@wrongstack/core");
7535
+ const publisher = core.createHqPublisherFromEnv({
7536
+ clientKind: "webui",
7537
+ projectRoot: paths.projectRoot,
7538
+ projectName: path15.basename(paths.projectRoot),
7539
+ appConfig: opts.config,
7540
+ socketFactory: (url) => new WebSocket2(url)
7541
+ });
7542
+ if (publisher) {
7543
+ publisher.connect();
7544
+ closeHqPublisher = () => publisher.close();
7545
+ restartHqBridges = (sessionId) => {
7546
+ stopHqBridges();
7547
+ const stops = [];
7548
+ const add = (start) => {
7549
+ try {
7550
+ stops.push(start());
7551
+ } catch {
7552
+ }
7553
+ };
7554
+ add(
7555
+ () => core.startSessionTelemetryBridge({
7556
+ publisher,
7557
+ events,
7558
+ sessionId,
7559
+ projectRoot: paths.projectRoot,
7560
+ projectName: path15.basename(paths.projectRoot),
7561
+ globalRoot: paths.globalRoot,
7562
+ initialAgents: statusTracker.getAgents(),
7563
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
7564
+ })
7565
+ );
7566
+ add(
7567
+ () => core.startFleetTelemetryBridge({
7568
+ events,
7569
+ publisher,
7570
+ runId: sessionId,
7571
+ sessionId
7572
+ })
7573
+ );
7574
+ add(() => core.startBrainTelemetryBridge({ events, publisher, sessionId }));
7575
+ add(() => core.startWorktreeTelemetryBridge({ events, publisher, sessionId }));
7576
+ add(
7577
+ () => core.startToolTelemetryBridge({
7578
+ events,
7579
+ publisher,
7580
+ projectRoot: paths.projectRoot,
7581
+ sessionId
7582
+ })
7583
+ );
7584
+ add(() => core.startCostTelemetryBridge({ events, publisher, sessionId }));
7585
+ stopHqBridges = () => {
7586
+ for (const stop2 of stops.splice(0)) {
7587
+ try {
7588
+ stop2();
7589
+ } catch {
7590
+ }
7591
+ }
7592
+ };
7593
+ };
7594
+ restartHqBridges(activeSessionId);
7595
+ }
7596
+ } catch (err) {
7597
+ logger.debug?.(`WebUI HQ telemetry unavailable: ${errorMessage2(err)}`);
7598
+ }
7599
+ }
7600
+ const repointRecovery = async (sessionId) => {
7601
+ if (!recoveryLock) return;
7602
+ try {
7603
+ if (ownsRecoveryLock) await recoveryLock.clear();
7604
+ await recoveryLock.write(sessionId);
7605
+ ownsRecoveryLock = true;
7606
+ } catch {
7607
+ ownsRecoveryLock = false;
7608
+ }
7609
+ };
7610
+ const activate = async (sessionId) => {
7611
+ if (stopped || sessionId === activeSessionId) return;
7612
+ transition = transition.then(async () => {
7613
+ if (stopped || sessionId === activeSessionId) return;
7614
+ activeSessionId = sessionId;
7615
+ await register(sessionId);
7616
+ await repointRecovery(sessionId);
7617
+ try {
7618
+ restartHqBridges(sessionId);
7619
+ } catch (err) {
7620
+ logger.debug?.(`WebUI HQ session swap failed: ${errorMessage2(err)}`);
7621
+ }
7622
+ });
7623
+ await transition;
7624
+ };
7625
+ const stop = async () => {
7626
+ if (stopped) return;
7627
+ stopped = true;
7628
+ await transition.catch(() => void 0);
7629
+ statusTracker.stop();
7630
+ stopHqBridges();
7631
+ closeHqPublisher();
7632
+ fleetNotifier.dispose();
7633
+ try {
7634
+ await registry.markClosing();
7635
+ await registry.unregister();
7636
+ } catch {
7637
+ }
7638
+ if (recoveryLock && ownsRecoveryLock) {
7639
+ await recoveryLock.clear().catch(() => void 0);
7640
+ ownsRecoveryLock = false;
7641
+ }
7642
+ };
7643
+ return { statusTracker, activate, stop };
7644
+ }
7645
+ function errorMessage2(err) {
7646
+ return err instanceof Error ? err.message : String(err);
7647
+ }
7648
+
7120
7649
  // src/server/pre-context-services.ts
7121
7650
  var GITHUB_PROVIDERS_OVERLAY_URL = "https://raw.githubusercontent.com/WrongStack/WrongStack/main/packages/cli/data/providers.json";
7122
7651
  async function createPreContextServices(input) {
7123
- const { config, wpaths, logger, opts, projectRoot, workingDir, needsProvider } = input;
7652
+ const { config, wpaths, logger, opts, vault, projectRoot, workingDir, needsProvider } = input;
7124
7653
  const modelsRegistry = opts.services?.modelsRegistry ?? new DefaultModelsRegistry({
7125
7654
  cacheFile: wpaths.modelsCache,
7126
7655
  ttlSeconds: 0,
@@ -7139,7 +7668,7 @@ async function createPreContextServices(input) {
7139
7668
  await discoverAndMergeWebuiProviders({
7140
7669
  config,
7141
7670
  registry: modelsRegistry,
7142
- cacheDir: path15.dirname(wpaths.modelsCache),
7671
+ cacheDir: path16.dirname(wpaths.modelsCache),
7143
7672
  logger
7144
7673
  });
7145
7674
  } catch (err) {
@@ -7164,12 +7693,13 @@ async function createPreContextServices(input) {
7164
7693
  })();
7165
7694
  const memoryStore = container.resolve(TOKENS.MemoryStore);
7166
7695
  if (config.features.memory) {
7167
- toolRegistry.register(rememberTool(memoryStore));
7168
- toolRegistry.register(forgetTool(memoryStore));
7169
- toolRegistry.register(searchMemoryTool(memoryStore));
7170
- toolRegistry.register(relatedMemoryTool(memoryStore));
7171
7696
  if (isSuperMemoryService(memoryStore)) {
7172
7697
  for (const tool of createSuperMemoryTools(memoryStore)) toolRegistry.register(tool);
7698
+ } else {
7699
+ toolRegistry.register(rememberTool(memoryStore));
7700
+ toolRegistry.register(forgetTool(memoryStore));
7701
+ toolRegistry.register(searchMemoryTool(memoryStore));
7702
+ toolRegistry.register(relatedMemoryTool(memoryStore));
7173
7703
  }
7174
7704
  }
7175
7705
  toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
@@ -7182,10 +7712,23 @@ async function createPreContextServices(input) {
7182
7712
  configureDangerBypass(config.tools?.exec?.danger ?? {});
7183
7713
  configureChildEnvGitIdentity(config.git?.identity ?? null);
7184
7714
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
7185
- const mcpRegistry = new MCPRegistry({ toolRegistry, events, log: logger, cacheDir: wpaths.cacheDir });
7715
+ const mcpTokenStore = new MCPVaultTokenStore(
7716
+ path16.join(wpaths.projectDir, "mcp-auth.json"),
7717
+ vault
7718
+ );
7719
+ const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
7720
+ const mcpRegistry = new MCPRegistry({
7721
+ toolRegistry,
7722
+ events,
7723
+ log: logger,
7724
+ cacheDir: wpaths.cacheDir,
7725
+ authorizationProviderFactory: createVaultBackedMcpAuthorizationProviderFactory({
7726
+ store: mcpTokenStore
7727
+ }),
7728
+ authorizationManager: mcpAuthorizationManager
7729
+ });
7186
7730
  if (config.features.mcp && config.mcpServers) {
7187
7731
  for (const [name2, cfg] of Object.entries(config.mcpServers)) {
7188
- if (cfg.enabled === false) continue;
7189
7732
  void mcpRegistry.start({ ...cfg, name: name2 }).catch((err) => {
7190
7733
  logger.warn(`MCP server "${name2}" failed to start at boot`, err);
7191
7734
  });
@@ -7200,7 +7743,7 @@ async function createPreContextServices(input) {
7200
7743
  // surface via the SessionRegistry.
7201
7744
  isSessionInUse: async (sessionId) => {
7202
7745
  try {
7203
- const registry = getSessionRegistry(wpaths.globalRoot);
7746
+ const registry = getSessionRegistry2(wpaths.globalRoot);
7204
7747
  const live = await registry.listByProject(wpaths.projectSlug);
7205
7748
  const hit = live.find((e) => e.sessionId === sessionId);
7206
7749
  if (hit) {
@@ -7225,155 +7768,57 @@ async function createPreContextServices(input) {
7225
7768
  await input.touchProject(projectRoot, workingDir);
7226
7769
  } catch {
7227
7770
  }
7228
- let statusTracker;
7229
- try {
7230
- const registry = getSessionRegistry(wpaths.globalRoot);
7231
- await registry.register({
7232
- sessionId: session.id,
7233
- projectSlug: wpaths.projectSlug,
7771
+ const sessionIdentity = await createStandaloneSessionIdentityLifecycle({
7772
+ config,
7773
+ events,
7774
+ logger,
7775
+ paths: {
7776
+ globalRoot: wpaths.globalRoot,
7234
7777
  projectRoot,
7235
- projectName: path15.basename(projectRoot),
7236
- workingDir,
7237
- clientType: "webui",
7238
- pid: process.pid,
7239
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
7240
- });
7241
- const fleetNotifier = new FleetNotifier({ baseDir: wpaths.globalRoot, projectRoot, selfPid: process.pid });
7242
- statusTracker = new AgentStatusTracker({
7243
- events,
7244
- registry,
7245
- sessionId: () => session.id,
7246
- onUpdate: () => fleetNotifier.notify()
7247
- });
7248
- statusTracker.start();
7249
- let stopHqSessionBridge;
7250
- let hqTelemetryPublisher;
7251
- const stopHqAuxBridges = [];
7252
- try {
7253
- const {
7254
- createHqPublisherFromEnv,
7255
- startSessionTelemetryBridge,
7256
- startFleetTelemetryBridge,
7257
- startBrainTelemetryBridge,
7258
- startWorktreeTelemetryBridge,
7259
- startToolTelemetryBridge,
7260
- startCostTelemetryBridge
7261
- } = await import("@wrongstack/core");
7262
- const hqTelemetry = createHqPublisherFromEnv({
7263
- clientKind: "webui",
7264
- projectRoot,
7265
- projectName: path15.basename(projectRoot),
7266
- appConfig: config,
7267
- socketFactory: (url) => new WebSocket2(url)
7268
- });
7269
- if (hqTelemetry) {
7270
- hqTelemetry.connect();
7271
- hqTelemetryPublisher = hqTelemetry;
7272
- stopHqSessionBridge = startSessionTelemetryBridge({
7273
- publisher: hqTelemetry,
7274
- events,
7275
- sessionId: session.id,
7276
- projectRoot,
7277
- projectName: path15.basename(projectRoot),
7278
- globalRoot: wpaths.globalRoot,
7279
- initialAgents: statusTracker?.getAgents(),
7280
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
7281
- });
7282
- try {
7283
- stopHqAuxBridges.push(
7284
- startFleetTelemetryBridge({ events, publisher: hqTelemetry, runId: session.id, sessionId: session.id })
7285
- );
7286
- } catch {
7287
- }
7288
- try {
7289
- stopHqAuxBridges.push(
7290
- startBrainTelemetryBridge({ events, publisher: hqTelemetry, sessionId: session.id })
7291
- );
7292
- } catch {
7293
- }
7294
- try {
7295
- stopHqAuxBridges.push(
7296
- startWorktreeTelemetryBridge({ events, publisher: hqTelemetry, sessionId: session.id })
7297
- );
7298
- } catch {
7299
- }
7300
- try {
7301
- stopHqAuxBridges.push(
7302
- startToolTelemetryBridge({ events, publisher: hqTelemetry, projectRoot, sessionId: session.id })
7303
- );
7304
- } catch {
7305
- }
7306
- try {
7307
- stopHqAuxBridges.push(
7308
- startCostTelemetryBridge({ events, publisher: hqTelemetry, sessionId: session.id })
7309
- );
7310
- } catch {
7311
- }
7312
- }
7313
- } catch {
7314
- }
7315
- const stopTracking = async () => {
7316
- try {
7317
- fleetNotifier.dispose();
7318
- await registry.markClosing();
7319
- statusTracker?.stop();
7320
- stopHqSessionBridge?.();
7321
- for (const stop of stopHqAuxBridges) {
7322
- try {
7323
- stop();
7324
- } catch {
7325
- }
7326
- }
7327
- hqTelemetryPublisher?.close();
7328
- } catch {
7329
- }
7330
- };
7331
- process.once("beforeExit", () => {
7332
- void stopTracking();
7333
- });
7334
- process.once("SIGINT", () => {
7335
- void stopTracking();
7336
- });
7337
- process.once("SIGTERM", () => {
7338
- void stopTracking();
7339
- });
7340
- } catch {
7341
- }
7342
- let context;
7343
- const tokenCounter = new DefaultTokenCounter({
7344
- registry: modelsRegistry,
7345
- providerId: config.provider,
7346
- events,
7347
- sessionId: () => context?.session?.id ?? session.id
7348
- });
7349
- const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
7350
- const activeMode = await modeStore.getActiveMode();
7351
- const modeId = activeMode?.id ?? "default";
7352
- const modePrompt = activeMode?.prompt ?? "";
7353
- const customModeStore = createCustomModeStore(wpaths.configDir);
7354
- await customModeStore.load();
7355
- console.log("[WebUI] Custom context modes loaded:", customModeStore.list().filter((m) => m.custom).length, "custom");
7356
- const resolvedModel = await resolveProviderModelMetadata(
7357
- modelsRegistry,
7358
- config.provider,
7359
- config.model,
7360
- config.providers?.[config.provider]
7361
- );
7362
- const modelCapabilities = resolvedModel?.capabilities ? { maxContextTokens: resolvedModel.capabilities.maxContext, supportsTools: resolvedModel.capabilities.tools, supportsVision: resolvedModel.capabilities.vision, supportsReasoning: resolvedModel.capabilities.reasoning } : void 0;
7363
- const modelCapabilitiesRef = { current: modelCapabilities };
7364
- const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
7365
- const skillInstaller = config.features.skills ? new SkillInstaller({
7366
- manifestPath: path15.join(wpaths.globalRoot, "installed-skills.json"),
7367
- projectSkillsDir: wpaths.inProjectSkills,
7368
- globalSkillsDir: wpaths.globalSkills,
7369
- projectHash: wpaths.projectHash,
7370
- skillLoader
7371
- }) : void 0;
7372
- const promptsEnabled = config.features.prompts !== false;
7373
- const bundledPromptsDir = promptsEnabled ? (() => {
7778
+ projectSlug: wpaths.projectSlug,
7779
+ projectSessions: wpaths.projectSessions
7780
+ },
7781
+ workingDir,
7782
+ initialSessionId: session.id,
7783
+ sessionStore,
7784
+ manageRecoveryLock: !opts.services?.session
7785
+ });
7786
+ const statusTracker = sessionIdentity.statusTracker;
7787
+ let context;
7788
+ const tokenCounter = new DefaultTokenCounter({
7789
+ registry: modelsRegistry,
7790
+ providerId: config.provider,
7791
+ events,
7792
+ sessionId: () => context?.session?.id ?? session.id
7793
+ });
7794
+ const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
7795
+ const activeMode = await modeStore.getActiveMode();
7796
+ const modeId = activeMode?.id ?? "default";
7797
+ const modePrompt = activeMode?.prompt ?? "";
7798
+ const customModeStore = createCustomModeStore(wpaths.configDir);
7799
+ await customModeStore.load();
7800
+ console.log("[WebUI] Custom context modes loaded:", customModeStore.list().filter((m) => m.custom).length, "custom");
7801
+ const resolvedModel = await resolveProviderModelMetadata(
7802
+ modelsRegistry,
7803
+ config.provider,
7804
+ config.model,
7805
+ config.providers?.[config.provider]
7806
+ );
7807
+ const modelCapabilities = resolvedModel?.capabilities ? { maxContextTokens: resolvedModel.capabilities.maxContext, supportsTools: resolvedModel.capabilities.tools, supportsVision: resolvedModel.capabilities.vision, supportsReasoning: resolvedModel.capabilities.reasoning } : void 0;
7808
+ const modelCapabilitiesRef = { current: modelCapabilities };
7809
+ const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
7810
+ const skillInstaller = config.features.skills ? new SkillInstaller({
7811
+ manifestPath: path16.join(wpaths.globalRoot, "installed-skills.json"),
7812
+ projectSkillsDir: wpaths.inProjectSkills,
7813
+ globalSkillsDir: wpaths.globalSkills,
7814
+ projectHash: wpaths.projectHash,
7815
+ skillLoader
7816
+ }) : void 0;
7817
+ const promptsEnabled = config.features.prompts !== false;
7818
+ const bundledPromptsDir = promptsEnabled ? (() => {
7374
7819
  try {
7375
7820
  const req = createRequire2(import.meta.url);
7376
- return path15.join(path15.dirname(req.resolve("@wrongstack/core/package.json")), "data", "prompts");
7821
+ return path16.join(path16.dirname(req.resolve("@wrongstack/core/package.json")), "data", "prompts");
7377
7822
  } catch {
7378
7823
  return void 0;
7379
7824
  }
@@ -7383,6 +7828,9 @@ async function createPreContextServices(input) {
7383
7828
  const promptsCtx = { promptLoader, promptUsage };
7384
7829
  const systemPromptBuilder = new DefaultSystemPromptBuilder({
7385
7830
  memoryStore,
7831
+ // Super Memory's turn middleware is the single memory-injection channel;
7832
+ // don't also inject a static memory section here (avoids double injection).
7833
+ injectMemory: false,
7386
7834
  skillLoader,
7387
7835
  modeStore,
7388
7836
  modeId,
@@ -7425,6 +7873,16 @@ async function createPreContextServices(input) {
7425
7873
  const initialContextPolicy = resolveContextWindowPolicy(config.context);
7426
7874
  context.meta["contextWindowMode"] = initialContextPolicy.id;
7427
7875
  context.meta["contextWindowPolicy"] = initialContextPolicy;
7876
+ context.state.setMeta(
7877
+ "plan.path",
7878
+ sessionScopedPath(wpaths.projectSessions, session.id, ".plan.json")
7879
+ );
7880
+ context.state.setMeta(
7881
+ "task.path",
7882
+ sessionScopedPath(wpaths.projectSessions, session.id, ".tasks.json")
7883
+ );
7884
+ await hydrateSessionKanban(context);
7885
+ attachSessionKanbanMirror(context);
7428
7886
  seedContextMeta(config, context);
7429
7887
  return {
7430
7888
  modelsRegistry,
@@ -7441,6 +7899,7 @@ async function createPreContextServices(input) {
7441
7899
  session,
7442
7900
  sessionStartedAt,
7443
7901
  statusTracker,
7902
+ sessionIdentity,
7444
7903
  tokenCounter,
7445
7904
  modeStore,
7446
7905
  modeId,
@@ -7490,6 +7949,7 @@ function patchConfig(config, updates) {
7490
7949
  }
7491
7950
 
7492
7951
  // src/server/backend-services.ts
7952
+ import { join as join13 } from "node:path";
7493
7953
  import {
7494
7954
  Agent,
7495
7955
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
@@ -7503,9 +7963,10 @@ import {
7503
7963
  installDesignStudioMiddleware,
7504
7964
  ObservableBrainArbiter as ObservableBrainArbiterCtor,
7505
7965
  resolveContextWindowPolicy as resolveContextWindowPolicy2,
7506
- createAutonomyBrain,
7507
- createTieredBrainArbiter,
7508
- DefaultBrainArbiter,
7966
+ BrainDecisionLedger,
7967
+ createBrainRuntime,
7968
+ resolveBrainConfigDefaults,
7969
+ EscalationRoutingBrainArbiter,
7509
7970
  GlobalMailbox as GlobalMailbox2,
7510
7971
  mailboxSessionTag,
7511
7972
  SessionMemoryConsolidator,
@@ -7519,12 +7980,13 @@ import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
7519
7980
  var REPLAY_LIMIT = 50;
7520
7981
  var PAUSE_TIMEOUT_MS = 6e4;
7521
7982
  var CollaborationWebSocketHandler = class {
7522
- constructor(events, logger, reader, annotations, bus) {
7983
+ constructor(events, logger, reader, annotations, bus, options = {}) {
7523
7984
  this.events = events;
7524
7985
  this.logger = logger;
7525
7986
  this.reader = reader;
7526
7987
  this.annotations = annotations;
7527
7988
  this.bus = bus;
7989
+ this.options = options;
7528
7990
  this.subscribe();
7529
7991
  this.bus?.onInjectionConsumed((info) => this.broadcastInjectionConsumed(info));
7530
7992
  }
@@ -7533,6 +7995,7 @@ var CollaborationWebSocketHandler = class {
7533
7995
  reader;
7534
7996
  annotations;
7535
7997
  bus;
7998
+ options;
7536
7999
  clients = /* @__PURE__ */ new Set();
7537
8000
  /** sessionId → participants currently watching it. */
7538
8001
  bySession = /* @__PURE__ */ new Map();
@@ -7560,11 +8023,16 @@ var CollaborationWebSocketHandler = class {
7560
8023
  handleMessage(ws, msg) {
7561
8024
  if (msg.type === "collab.join") {
7562
8025
  const payload = msg.payload;
7563
- if (!payload?.sessionId) {
8026
+ if (typeof payload?.sessionId !== "string" || payload.sessionId.trim().length === 0) {
7564
8027
  this.send(ws, this.errorMessage("collab.join requires sessionId"));
7565
8028
  return true;
7566
8029
  }
7567
- this.join(ws, payload.sessionId, payload.role ?? "observer");
8030
+ const role = payload.role ?? "observer";
8031
+ if (role !== "observer" && role !== "annotator" && role !== "controller") {
8032
+ this.send(ws, this.errorMessage(`unknown collaboration role '${String(role)}'`));
8033
+ return true;
8034
+ }
8035
+ this.join(ws, payload.sessionId, role);
7568
8036
  return true;
7569
8037
  }
7570
8038
  if (msg.type === "collab.leave") {
@@ -7599,6 +8067,20 @@ var CollaborationWebSocketHandler = class {
7599
8067
  }
7600
8068
  // ── Join / leave flow ──────────────────────────────────────────────────
7601
8069
  join(ws, sessionId, role) {
8070
+ const activeSessionId = this.options.getActiveSessionId?.();
8071
+ if (activeSessionId !== void 0 && sessionId !== activeSessionId) {
8072
+ this.send(
8073
+ ws,
8074
+ this.errorMessage(
8075
+ `collab.join sessionId mismatch (active: ${activeSessionId})`
8076
+ )
8077
+ );
8078
+ return;
8079
+ }
8080
+ if (this.findParticipant(ws)) {
8081
+ this.send(ws, this.errorMessage("collab.join requires leaving the current session first"));
8082
+ return;
8083
+ }
7602
8084
  if (role === "controller" && !this.bus) {
7603
8085
  this.send(
7604
8086
  ws,
@@ -7617,6 +8099,13 @@ var CollaborationWebSocketHandler = class {
7617
8099
  );
7618
8100
  return;
7619
8101
  }
8102
+ if (role !== "observer" && this.options.authorizeRole?.({ ws, sessionId, requestedRole: role }) !== true) {
8103
+ this.send(
8104
+ ws,
8105
+ this.errorMessage(`role '${role}' requires explicit server authorization`)
8106
+ );
8107
+ return;
8108
+ }
7620
8109
  const participant = {
7621
8110
  participantId: randomUUID(),
7622
8111
  ws,
@@ -7870,17 +8359,13 @@ var CollaborationWebSocketHandler = class {
7870
8359
  type: "collab.event",
7871
8360
  payload: { kind, payload, at: (/* @__PURE__ */ new Date()).toISOString() }
7872
8361
  };
7873
- const data = JSON.stringify(msg);
7874
- for (const bucket of this.bySession.values()) {
7875
- for (const p of bucket) {
7876
- try {
7877
- if (p.ws.readyState === 1) p.ws.send(data);
7878
- } catch (err) {
7879
- this.logger.debug?.(
7880
- `collab broadcast failed: ${toErrorMessage6(err)}`
7881
- );
7882
- }
7883
- }
8362
+ const activeSessionId = this.options.getActiveSessionId?.();
8363
+ if (activeSessionId !== void 0) {
8364
+ this.broadcast(activeSessionId, msg);
8365
+ return;
8366
+ }
8367
+ for (const sessionId of this.bySession.keys()) {
8368
+ this.broadcast(sessionId, msg);
7884
8369
  }
7885
8370
  }
7886
8371
  /**
@@ -8204,7 +8689,8 @@ var CollaborationWebSocketHandler = class {
8204
8689
  * Bus callback: a queued injection was spliced into a real tool call. Re-emit
8205
8690
  * `collab.injection.granted` with phase `'consumed'` and the now-known tool
8206
8691
  * name. The injection carries no sessionId, so resolve it from the author's
8207
- * current session; if they've already left, fall back to every live session.
8692
+ * current participant. If the author has left, fail closed rather than leak
8693
+ * the payload to unrelated sessions.
8208
8694
  */
8209
8695
  broadcastInjectionConsumed(info) {
8210
8696
  let sessionId = null;
@@ -8233,14 +8719,16 @@ var CollaborationWebSocketHandler = class {
8233
8719
  if (sessionId) {
8234
8720
  this.broadcast(sessionId, message(sessionId));
8235
8721
  } else {
8236
- for (const sid of this.bySession.keys()) this.broadcast(sid, message(sid));
8722
+ this.logger.debug?.(
8723
+ `collab: consumed injection ${info.toolUseId} has no live author; broadcast suppressed`
8724
+ );
8237
8725
  }
8238
8726
  }
8239
8727
  };
8240
8728
 
8241
8729
  // src/server/codebase-indexing.ts
8242
8730
  import * as fs12 from "node:fs";
8243
- import * as path16 from "node:path";
8731
+ import * as path17 from "node:path";
8244
8732
  import {
8245
8733
  cancelPendingReindexes,
8246
8734
  enqueueReindex,
@@ -8293,7 +8781,7 @@ function setupWebUICodebaseIndexing(deps2) {
8293
8781
  if (!filename) return;
8294
8782
  const rel = filename.toString();
8295
8783
  if (isIgnored(rel)) return;
8296
- const abs = path16.resolve(deps2.projectRoot, rel);
8784
+ const abs = path17.resolve(deps2.projectRoot, rel);
8297
8785
  enqueueFile(abs);
8298
8786
  });
8299
8787
  watcher.on("error", (err) => deps2.logger.debug(`webui codebase index watcher error: ${err}`));
@@ -8306,7 +8794,7 @@ function setupWebUICodebaseIndexing(deps2) {
8306
8794
  }
8307
8795
  function enqueueFile(filePath) {
8308
8796
  if (!idx.onEdit && !idx.watchExternal) return;
8309
- const abs = path16.isAbsolute(filePath) ? path16.normalize(filePath) : path16.resolve(deps2.projectRoot, filePath);
8797
+ const abs = path17.isAbsolute(filePath) ? path17.normalize(filePath) : path17.resolve(deps2.projectRoot, filePath);
8310
8798
  if (!isInside2(deps2.projectRoot, abs) || !isIndexableFile(abs)) return;
8311
8799
  enqueueReindex({
8312
8800
  projectRoot: deps2.projectRoot,
@@ -8343,9 +8831,9 @@ function isIgnored(rel) {
8343
8831
  return rel.split(/[/\\]/).some((seg) => IGNORE_DIRS.has(seg));
8344
8832
  }
8345
8833
  function isInside2(root, target) {
8346
- const normalizedRoot = path16.resolve(root);
8347
- const normalizedTarget = path16.resolve(target);
8348
- return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(normalizedRoot + path16.sep);
8834
+ const normalizedRoot = path17.resolve(root);
8835
+ const normalizedTarget = path17.resolve(target);
8836
+ return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(normalizedRoot + path17.sep);
8349
8837
  }
8350
8838
 
8351
8839
  // src/server/discover-mailbox-bridge.ts
@@ -8878,21 +9366,59 @@ async function createAgentServices(input) {
8878
9366
  agent.extensions.register(new SessionMemoryConsolidator({ memoryStore }));
8879
9367
  }
8880
9368
  console.log("[WebUI] Agent initialized");
8881
- const brainSettings = { maxAutoRisk: "medium" };
8882
- const autonomousBrain = {
8883
- decide: (request) => createAutonomyBrain({
8884
- provider,
8885
- model: context.model,
8886
- maxAutoRisk: "all"
8887
- // the tiered ceiling gates risk — keep inner permissive
8888
- }).decide(request)
9369
+ const brainCfg = resolveBrainConfigDefaults(config.brain, {
9370
+ fallbackModels: config.fallbackModels
9371
+ });
9372
+ const brainLedgerPath = join13(wpaths.projectDir, "brain-ledger.jsonl");
9373
+ let brainLedgerEnabled = brainCfg.ledger?.enabled !== false;
9374
+ let brainLedger;
9375
+ const startBrainLedger = () => {
9376
+ if (brainLedger) return;
9377
+ brainLedger = new BrainDecisionLedger({ events, filePath: brainLedgerPath });
9378
+ void brainLedger.start();
9379
+ };
9380
+ if (brainLedgerEnabled) startBrainLedger();
9381
+ const brainRuntime = createBrainRuntime({
9382
+ initialConfig: brainCfg,
9383
+ defaultProviderId: config.provider,
9384
+ sessionProvider: () => provider,
9385
+ sessionModel: () => context.model,
9386
+ resolveProvider: (providerId) => {
9387
+ const savedCfg = config.providers?.[providerId] ?? {};
9388
+ return providerRegistry.create({
9389
+ ...savedCfg,
9390
+ apiKey: savedCfg.apiKey ?? config.apiKey,
9391
+ baseUrl: savedCfg.baseUrl ?? config.baseUrl,
9392
+ type: providerId
9393
+ });
9394
+ },
9395
+ ledger: {
9396
+ getPath: () => brainLedgerEnabled ? brainLedgerPath : void 0,
9397
+ isEnabled: () => brainLedgerEnabled,
9398
+ setEnabled: (on) => {
9399
+ brainLedgerEnabled = on;
9400
+ if (on) {
9401
+ startBrainLedger();
9402
+ } else {
9403
+ void brainLedger?.stop();
9404
+ brainLedger = void 0;
9405
+ }
9406
+ },
9407
+ failureStreakFor: (request) => brainLedger?.failureStreakFor(request) ?? 0,
9408
+ getDecisionDigest: (request) => brainLedger?.digestFor(request)
9409
+ },
9410
+ persist: input.persistBrainConfig
9411
+ });
9412
+ const brainSettings = {
9413
+ get maxAutoRisk() {
9414
+ return brainRuntime.getMaxAutoRisk();
9415
+ },
9416
+ set maxAutoRisk(level) {
9417
+ void brainRuntime.apply({ maxAutoRisk: level }).persisted;
9418
+ }
8889
9419
  };
8890
9420
  const brain = new ObservableBrainArbiterCtor(
8891
- createTieredBrainArbiter({
8892
- policy: new DefaultBrainArbiter(),
8893
- autonomous: autonomousBrain,
8894
- getMaxAutoRisk: () => brainSettings.maxAutoRisk
8895
- }),
9421
+ new EscalationRoutingBrainArbiter(brainRuntime.arbiter, void 0, () => "headless"),
8896
9422
  events
8897
9423
  );
8898
9424
  container.bind(TOKENS2.BrainArbiter, () => brain);
@@ -8932,6 +9458,12 @@ async function createAgentServices(input) {
8932
9458
  const brainMonitor = new BrainMonitor({
8933
9459
  events,
8934
9460
  brain,
9461
+ toolFailureStreak: brainCfg.monitor?.toolFailureStreak,
9462
+ errorStormCount: brainCfg.monitor?.errorStormCount,
9463
+ stallMs: brainCfg.monitor?.stallMs,
9464
+ fileChurnThreshold: brainCfg.monitor?.fileChurnThreshold,
9465
+ fileChurnWindowMs: brainCfg.monitor?.fileChurnWindowMs,
9466
+ cooldownMs: brainCfg.monitor?.cooldownMs,
8935
9467
  sessionId: () => context.session?.id,
8936
9468
  intervene: async ({ subject, body }) => {
8937
9469
  const tag = mailboxSessionTag(input.sessionGetter().id);
@@ -8996,7 +9528,10 @@ async function createAgentServices(input) {
8996
9528
  logger,
8997
9529
  input.sessionReader,
8998
9530
  input.annotationsStore,
8999
- collabBus
9531
+ collabBus,
9532
+ {
9533
+ getActiveSessionId: () => context.session.id
9534
+ }
9000
9535
  );
9001
9536
  return {
9002
9537
  collabBus,
@@ -9008,8 +9543,13 @@ async function createAgentServices(input) {
9008
9543
  pipelines,
9009
9544
  brain,
9010
9545
  brainSettings,
9546
+ brainRuntime,
9011
9547
  brainLog,
9012
9548
  brainMonitor,
9549
+ // Getter: ledger toggles swap the instance, shutdown must stop the LIVE one.
9550
+ get brainLedger() {
9551
+ return brainLedger;
9552
+ },
9013
9553
  codebaseIndexing,
9014
9554
  autoPhaseHandler,
9015
9555
  specsHandler,
@@ -9185,7 +9725,18 @@ function createConnectionHandler(opts) {
9185
9725
  }
9186
9726
 
9187
9727
  // src/server/message-dispatcher.ts
9188
- import path17 from "node:path";
9728
+ import path18 from "node:path";
9729
+ import {
9730
+ buildUserContentBlocks,
9731
+ IncomingImageError,
9732
+ parseIncomingImages
9733
+ } from "@wrongstack/core/utils";
9734
+ import {
9735
+ createToolVisionAdapters,
9736
+ ImageInputUnsupportedError,
9737
+ routeImagesForModel,
9738
+ VisionUrlBlockedError
9739
+ } from "@wrongstack/runtime/vision";
9189
9740
 
9190
9741
  // src/server/autophase-routes.ts
9191
9742
  async function handleAutoPhaseRoute(_ws, msg, handlers) {
@@ -9206,6 +9757,12 @@ async function handleBrainRoute(ws, msg, handlers) {
9206
9757
  case "brain.ask":
9207
9758
  await handlers.ask(ws, msg);
9208
9759
  return true;
9760
+ case "brain.config.get":
9761
+ await handlers.configGet(ws, msg);
9762
+ return true;
9763
+ case "brain.config.set":
9764
+ await handlers.configSet(ws, msg);
9765
+ return true;
9209
9766
  default:
9210
9767
  return false;
9211
9768
  }
@@ -9459,8 +10016,10 @@ import {
9459
10016
  serializeTaskGraph
9460
10017
  } from "@wrongstack/core";
9461
10018
  import {
10019
+ addCheckToTask,
9462
10020
  addColumn,
9463
10021
  addGoalMetricToTask,
10022
+ addNoteToTask,
9464
10023
  addTask,
9465
10024
  assignTask,
9466
10025
  claimReadyTask,
@@ -9474,29 +10033,47 @@ import {
9474
10033
  getKanbanQueueHealth,
9475
10034
  getTask,
9476
10035
  getTaskChain,
9477
- listReadyTasks,
9478
10036
  listBoards,
10037
+ listReadyTasks,
9479
10038
  mergeTasks,
9480
10039
  moveTask,
9481
10040
  parseLinesIntoTasks,
10041
+ reconcileKanbanBoard,
10042
+ recoverStaleTaskAssignments,
10043
+ releaseTaskClaim,
9482
10044
  removeBoard,
9483
10045
  removeColumn,
9484
10046
  removeTask,
9485
- releaseTaskClaim,
9486
10047
  setTaskChain,
9487
10048
  splitTask,
9488
10049
  syncBoardFromTaskGraph,
9489
10050
  transferTaskToBoard,
9490
10051
  updateBoard,
10052
+ updateCheckOnTask,
9491
10053
  updateGoalMetricOnTask,
9492
10054
  updateTask
9493
10055
  } from "@wrongstack/kanban";
10056
+ import { applySessionKanbanTaskToSource } from "@wrongstack/tools/session-kanban";
10057
+ async function syncSessionSource(ctx, task, remove = false) {
10058
+ if (!ctx.context) return;
10059
+ const update = await applySessionKanbanTaskToSource(ctx.context, task, { remove });
10060
+ const sessionId = ctx.context.session?.id ?? "";
10061
+ if (update.todos)
10062
+ ctx.broadcast?.({ type: "todos.updated", payload: { sessionId, todos: update.todos } });
10063
+ if (update.tasks)
10064
+ ctx.broadcast?.({ type: "tasks.updated", payload: { sessionId, tasks: update.tasks.tasks } });
10065
+ if (update.plan)
10066
+ ctx.broadcast?.({ type: "plan.updated", payload: { sessionId, plan: update.plan } });
10067
+ }
9494
10068
  function ok(ws, type, data) {
9495
10069
  send(ws, { type, payload: { success: true, data: data ?? null } });
9496
10070
  }
9497
10071
  function fail(ws, type, message) {
9498
10072
  send(ws, { type, payload: { success: false, error: message } });
9499
10073
  }
10074
+ function has(payload, key) {
10075
+ return payload !== void 0 && Object.hasOwn(payload, key);
10076
+ }
9500
10077
  async function handleKanbanRoute(ws, msg, ctx) {
9501
10078
  if (!msg.type.startsWith("kanban.")) return false;
9502
10079
  const payload = msg.payload;
@@ -9525,6 +10102,38 @@ async function handleKanbanRoute(ws, msg, ctx) {
9525
10102
  ok(ws, type, await getKanbanQueueHealth(ctx.projectRoot, { boardId: hBoardId }));
9526
10103
  return true;
9527
10104
  }
10105
+ case "kanban.supervisor.status":
10106
+ case "kanban.supervisor.audit": {
10107
+ const boardId = payload?.boardId;
10108
+ if (!boardId) {
10109
+ fail(ws, type, "boardId required");
10110
+ return true;
10111
+ }
10112
+ const board = await getBoard(ctx.projectRoot, boardId);
10113
+ if (!board) {
10114
+ fail(ws, type, `Board not found: ${boardId}`);
10115
+ return true;
10116
+ }
10117
+ const reconciled = await reconcileKanbanBoard(ctx.projectRoot, boardId);
10118
+ let health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
10119
+ const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments(ctx.projectRoot, boardId, {
10120
+ mode: board.supervisor?.recoveryMode ?? "auto",
10121
+ reason: "On-demand standalone Kanban supervisor audit."
10122
+ }) : null;
10123
+ if (recovered) health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
10124
+ const anomalyCount = health.staleAssignments.count + health.dependencyBlocked.count + health.failedRetryable.count + health.counts.failed + health.counts.blocked;
10125
+ ok(ws, type, {
10126
+ boardId,
10127
+ status: board.supervisor?.enabled === false ? "disabled" : anomalyCount ? "attention" : "healthy",
10128
+ mode: board.supervisor?.mode ?? "deterministic",
10129
+ lastAuditAt: (/* @__PURE__ */ new Date()).toISOString(),
10130
+ reconciledTaskIds: reconciled?.tasks.map((task) => task.id) ?? [],
10131
+ staleRecoveredTaskIds: recovered?.tasks.map((task) => task.id) ?? [],
10132
+ anomalyCount,
10133
+ summary: `${health.counts.running} running \xB7 ${health.counts.ready} ready \xB7 ${health.counts.review} review \xB7 ${health.counts.blocked} blocked \xB7 ${health.counts.failed} failed`
10134
+ });
10135
+ return true;
10136
+ }
9528
10137
  case "kanban.create": {
9529
10138
  const title = payload?.title;
9530
10139
  if (!title) {
@@ -9553,7 +10162,10 @@ async function handleKanbanRoute(ws, msg, ctx) {
9553
10162
  ...payload?.title ? { title: payload.title } : {},
9554
10163
  ...payload?.description ? { description: payload.description } : {},
9555
10164
  ...payload?.tags ? { tags: payload.tags } : {},
9556
- ...payload?.columns ? { columns: payload.columns } : {}
10165
+ ...payload?.columns ? { columns: payload.columns } : {},
10166
+ ...has(payload, "supervisor") ? {
10167
+ supervisor: payload?.supervisor ?? null
10168
+ } : {}
9557
10169
  });
9558
10170
  board ? ok(ws, type, board) : fail(ws, type, `Board not found: ${boardId}`);
9559
10171
  return true;
@@ -9580,6 +10192,11 @@ async function handleKanbanRoute(ws, msg, ctx) {
9580
10192
  return true;
9581
10193
  }
9582
10194
  const board = await getBoard(ctx.projectRoot, boardId);
10195
+ const activeSessionId = ctx.context?.session?.id;
10196
+ if (activeSessionId && board?.tags?.includes(`session:${activeSessionId}`)) {
10197
+ fail(ws, type, "The active session Kanban board cannot be deleted.");
10198
+ return true;
10199
+ }
9583
10200
  ok(ws, type, {
9584
10201
  removed: board ? await removeBoard(ctx.projectRoot, board.id) : false,
9585
10202
  boardId: board?.id ?? boardId
@@ -9760,15 +10377,31 @@ async function handleKanbanRoute(ws, msg, ctx) {
9760
10377
  return true;
9761
10378
  }
9762
10379
  const board = await updateTask(ctx.projectRoot, boardId, taskId, {
9763
- ...payload?.title ? { title: payload.title } : {},
9764
- ...payload?.description ? { description: payload.description } : {},
9765
- ...payload?.columnId ? { columnId: payload.columnId } : {},
9766
- ...payload?.priority ? { priority: payload.priority } : {},
9767
- ...payload?.status ? { status: payload.status } : {},
9768
- ...payload?.assignedAgent ? { assignedAgent: payload.assignedAgent } : {},
9769
- ...payload?.labels ? { labels: payload.labels } : {}
10380
+ ...has(payload, "title") ? { title: payload?.title } : {},
10381
+ ...has(payload, "description") ? { description: payload?.description ?? "" } : {},
10382
+ ...has(payload, "columnId") ? { columnId: payload?.columnId } : {},
10383
+ ...has(payload, "priority") ? { priority: payload?.priority } : {},
10384
+ ...has(payload, "type") ? { type: payload?.type } : {},
10385
+ ...has(payload, "status") ? { status: payload?.status } : {},
10386
+ ...has(payload, "dependsOn") ? { dependsOn: payload?.dependsOn ?? [] } : {},
10387
+ ...has(payload, "chain") ? { chain: payload?.chain ?? null } : {},
10388
+ ...has(payload, "labels") ? { labels: payload?.labels ?? [] } : {},
10389
+ ...has(payload, "estimatedHours") ? { estimatedHours: Number(payload?.estimatedHours ?? 0) } : {},
10390
+ ...has(payload, "actualHours") ? { actualHours: Number(payload?.actualHours ?? 0) } : {},
10391
+ ...has(payload, "retryPolicy") ? {
10392
+ retryPolicy: payload?.retryPolicy ?? null
10393
+ } : {},
10394
+ ...has(payload, "costCeilingUsd") ? {
10395
+ costCeilingUsd: payload?.costCeilingUsd === null || payload?.costCeilingUsd === "" ? null : Number(payload?.costCeilingUsd)
10396
+ } : {}
9770
10397
  });
9771
- board ? ok(ws, type, findTask(board.tasks, taskId)) : fail(ws, type, "Board or task not found");
10398
+ if (!board) {
10399
+ fail(ws, type, "Board or task not found");
10400
+ return true;
10401
+ }
10402
+ const task = findTask(board.tasks, taskId);
10403
+ if (task) await syncSessionSource(ctx, task);
10404
+ ok(ws, type, task);
9772
10405
  return true;
9773
10406
  }
9774
10407
  case "kanban.task.move": {
@@ -9786,7 +10419,13 @@ async function handleKanbanRoute(ws, msg, ctx) {
9786
10419
  columnId,
9787
10420
  payload?.order
9788
10421
  );
9789
- board ? ok(ws, type, findTask(board.tasks, taskId)) : fail(ws, type, "Move failed");
10422
+ if (!board) {
10423
+ fail(ws, type, "Move failed");
10424
+ return true;
10425
+ }
10426
+ const task = findTask(board.tasks, taskId);
10427
+ if (task) await syncSessionSource(ctx, task);
10428
+ ok(ws, type, task);
9790
10429
  return true;
9791
10430
  }
9792
10431
  case "kanban.task.copy":
@@ -9845,8 +10484,10 @@ async function handleKanbanRoute(ws, msg, ctx) {
9845
10484
  ...payload?.role ? { role: payload.role } : {},
9846
10485
  ...payload?.provider ? { provider: payload.provider } : {},
9847
10486
  ...payload?.model ? { model: payload.model } : {},
10487
+ ...payload?.modelRouting ? { modelRouting: payload.modelRouting } : {},
9848
10488
  ...payload?.fallbackProfile ? { fallbackProfile: payload.fallbackProfile } : {},
9849
10489
  ...payload?.fallbackModels ? { fallbackModels: payload.fallbackModels } : {},
10490
+ ...payload?.skills ? { skills: payload.skills } : {},
9850
10491
  ...payload?.tools ? { tools: payload.tools } : {},
9851
10492
  ...payload?.allowedCapabilities ? { allowedCapabilities: payload.allowedCapabilities } : {},
9852
10493
  ...payload?.assignee ? { assignee: payload.assignee } : {},
@@ -9865,7 +10506,10 @@ async function handleKanbanRoute(ws, msg, ctx) {
9865
10506
  const board = await releaseTaskClaim(ctx.projectRoot, boardId, taskId, {
9866
10507
  ...payload?.status ? { status: payload.status } : {},
9867
10508
  ...payload?.reason ? { reason: payload.reason } : {},
9868
- ...typeof payload?.clearAssignee === "boolean" ? { clearAssignee: payload.clearAssignee } : {}
10509
+ ...typeof payload?.clearAssignee === "boolean" ? { clearAssignee: payload.clearAssignee } : {},
10510
+ ...typeof payload?.maxAttempts === "number" ? { maxAttempts: payload.maxAttempts } : {},
10511
+ ...typeof payload?.costCeilingUsd === "number" ? { costCeilingUsd: payload.costCeilingUsd } : {},
10512
+ ...payload?.retryPolicy ? { retryPolicy: payload.retryPolicy } : {}
9869
10513
  });
9870
10514
  board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
9871
10515
  return true;
@@ -9921,14 +10565,66 @@ async function handleKanbanRoute(ws, msg, ctx) {
9921
10565
  ...payload?.role ? { role: payload.role } : {},
9922
10566
  ...payload?.provider ? { provider: payload.provider } : {},
9923
10567
  ...payload?.model ? { model: payload.model } : {},
10568
+ ...payload?.modelRouting ? { modelRouting: payload.modelRouting } : {},
9924
10569
  ...payload?.fallbackProfile ? { fallbackProfile: payload.fallbackProfile } : {},
9925
10570
  ...payload?.fallbackModels ? { fallbackModels: payload.fallbackModels } : {},
10571
+ ...payload?.skills ? { skills: payload.skills } : {},
9926
10572
  ...payload?.tools ? { tools: payload.tools } : {},
9927
- ...payload?.allowedCapabilities ? { allowedCapabilities: payload.allowedCapabilities } : {}
10573
+ ...payload?.allowedCapabilities ? { allowedCapabilities: payload.allowedCapabilities } : {},
10574
+ ...payload?.assignee ? { assignee: payload.assignee } : {},
10575
+ ...payload?.maxAttempts !== void 0 ? { maxAttempts: Number(payload.maxAttempts) } : {},
10576
+ ...payload?.costCeilingUsd !== void 0 ? { costCeilingUsd: Number(payload.costCeilingUsd) } : {},
10577
+ ...payload?.retryPolicy ? { retryPolicy: payload.retryPolicy } : {}
9928
10578
  });
9929
10579
  board ? ok(ws, type, findTask(board.tasks, taskId)) : fail(ws, type, "Board or task not found");
9930
10580
  return true;
9931
10581
  }
10582
+ case "kanban.task.check.add": {
10583
+ const boardId = payload?.boardId;
10584
+ const taskId = payload?.taskId;
10585
+ const description = payload?.description;
10586
+ if (!boardId || !taskId || !description) {
10587
+ fail(ws, type, "boardId, taskId, and description required");
10588
+ return true;
10589
+ }
10590
+ const board = await addCheckToTask(ctx.projectRoot, boardId, taskId, {
10591
+ description,
10592
+ type: payload?.checkType ?? "manual",
10593
+ status: payload?.status ?? "pending"
10594
+ });
10595
+ board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
10596
+ return true;
10597
+ }
10598
+ case "kanban.task.check.update": {
10599
+ const boardId = payload?.boardId;
10600
+ const taskId = payload?.taskId;
10601
+ const checkId = payload?.checkId;
10602
+ if (!boardId || !taskId || !checkId) {
10603
+ fail(ws, type, "boardId, taskId, and checkId required");
10604
+ return true;
10605
+ }
10606
+ const board = await updateCheckOnTask(ctx.projectRoot, boardId, taskId, checkId, {
10607
+ ...has(payload, "status") ? { status: payload?.status } : {}
10608
+ });
10609
+ if (!board) fail(ws, type, "Check not found");
10610
+ else ok(ws, type, (await reconcileKanbanBoard(ctx.projectRoot, boardId))?.board ?? board);
10611
+ return true;
10612
+ }
10613
+ case "kanban.task.note.add": {
10614
+ const boardId = payload?.boardId;
10615
+ const taskId = payload?.taskId;
10616
+ const content = payload?.content;
10617
+ if (!boardId || !taskId || !content) {
10618
+ fail(ws, type, "boardId, taskId, and content required");
10619
+ return true;
10620
+ }
10621
+ const board = await addNoteToTask(ctx.projectRoot, boardId, taskId, {
10622
+ author: payload?.author ?? "webui",
10623
+ content
10624
+ });
10625
+ board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
10626
+ return true;
10627
+ }
9932
10628
  case "kanban.task.dispatch":
9933
10629
  fail(
9934
10630
  ws,
@@ -9936,6 +10632,9 @@ async function handleKanbanRoute(ws, msg, ctx) {
9936
10632
  "Kanban agent dispatch is only available from the CLI-hosted WebUI runtime."
9937
10633
  );
9938
10634
  return true;
10635
+ case "kanban.capabilities":
10636
+ ok(ws, type, { dispatchSupported: false });
10637
+ return true;
9939
10638
  case "kanban.task.remove": {
9940
10639
  const boardId = payload?.boardId;
9941
10640
  const taskId = payload?.taskId;
@@ -9949,7 +10648,12 @@ async function handleKanbanRoute(ws, msg, ctx) {
9949
10648
  return true;
9950
10649
  }
9951
10650
  const board = await removeTask(ctx.projectRoot, boardId, taskId);
9952
- board ? ok(ws, type, { removed: true, boardId: board.id, taskId: task.id, board }) : fail(ws, type, "Board or task not found");
10651
+ if (!board) {
10652
+ fail(ws, type, "Board or task not found");
10653
+ return true;
10654
+ }
10655
+ await syncSessionSource(ctx, task, true);
10656
+ ok(ws, type, { removed: true, boardId: board.id, taskId: task.id, board });
9953
10657
  return true;
9954
10658
  }
9955
10659
  case "kanban.task.get": {
@@ -10015,6 +10719,9 @@ async function handleMailboxRoute(ws, msg, handlers) {
10015
10719
  case "mailbox.purge":
10016
10720
  await handlers.purge(ws, msg);
10017
10721
  return true;
10722
+ case "mailbox.compact":
10723
+ await handlers.compact(ws, msg);
10724
+ return true;
10018
10725
  default:
10019
10726
  return false;
10020
10727
  }
@@ -10053,6 +10760,18 @@ async function handleMcpRoute(ws, msg, handlers) {
10053
10760
  case "mcp.discover":
10054
10761
  await handlers.discover(ws, msg);
10055
10762
  return true;
10763
+ case "mcp.resources":
10764
+ await handlers.resources(ws, msg);
10765
+ return true;
10766
+ case "mcp.prompts":
10767
+ await handlers.prompts(ws, msg);
10768
+ return true;
10769
+ case "mcp.resource.read":
10770
+ await handlers.resourceRead(ws, msg);
10771
+ return true;
10772
+ case "mcp.prompt.get":
10773
+ await handlers.promptGet(ws, msg);
10774
+ return true;
10056
10775
  default:
10057
10776
  return false;
10058
10777
  }
@@ -10246,6 +10965,7 @@ async function handleProviderRoute(ws, msg, routes) {
10246
10965
  baseUrl,
10247
10966
  apiKey
10248
10967
  });
10968
+ await routes.adoptDefaultProviderIfUnset(id);
10249
10969
  return true;
10250
10970
  }
10251
10971
  case "provider.remove": {
@@ -10442,7 +11162,7 @@ function createMessageDispatcher(opts) {
10442
11162
  skillLoader: deps2.skillLoader,
10443
11163
  skillInstaller: deps2.skillInstaller,
10444
11164
  projectRoot,
10445
- projectSkillsDir: path17.join(projectRoot, ".wrongstack", "skills"),
11165
+ projectSkillsDir: path18.join(projectRoot, ".wrongstack", "skills"),
10446
11166
  globalSkillsDir: deps2.wpaths.globalSkills
10447
11167
  };
10448
11168
  }
@@ -10484,7 +11204,12 @@ function createMessageDispatcher(opts) {
10484
11204
  if (await handleSpecsRoute(ws, msg, routes.specsRoutes)) return;
10485
11205
  if (await handleSddBoardRoute(ws, msg, routes.sddBoardRoutes)) return;
10486
11206
  if (await handleSddWizardRoute(ws, msg, routes.sddWizardRoutes)) return;
10487
- if (await handleKanbanRoute(ws, msg, { projectRoot: state.getProjectRoot() })) return;
11207
+ if (await handleKanbanRoute(ws, msg, {
11208
+ projectRoot: state.getProjectRoot(),
11209
+ context: deps2.context,
11210
+ broadcast: (message) => broadcast(state.getClients(), message)
11211
+ }))
11212
+ return;
10488
11213
  if (msg.type.startsWith("worktree.") && await deps2.worktreeHandler.handleMessage(
10489
11214
  msg
10490
11215
  ))
@@ -10515,7 +11240,8 @@ function createMessageDispatcher(opts) {
10515
11240
  }
10516
11241
  case "user_message": {
10517
11242
  if (!ensureCurrentSession(ws, msg, "user_message")) return;
10518
- const content = msg.payload.content;
11243
+ const userPayload = msg.payload;
11244
+ const content = userPayload.content;
10519
11245
  if (runLock.get()) {
10520
11246
  send(ws, {
10521
11247
  type: "error",
@@ -10529,8 +11255,21 @@ function createMessageDispatcher(opts) {
10529
11255
  const thisRun = new AbortController();
10530
11256
  runLock.set(thisRun);
10531
11257
  try {
11258
+ let input = content;
11259
+ const imageBlocks = parseIncomingImages(userPayload.images, userPayload.imageBase64);
11260
+ if (imageBlocks.length > 0) {
11261
+ const routed = await routeImagesForModel(buildUserContentBlocks(content, imageBlocks), {
11262
+ supportsVision: deps2.agent.ctx.provider.capabilities.vision,
11263
+ adapters: () => createToolVisionAdapters(deps2.agent.tools),
11264
+ ctx: deps2.agent.ctx,
11265
+ signal: thisRun.signal,
11266
+ providerId: deps2.agent.ctx.provider.id,
11267
+ model: deps2.agent.ctx.model
11268
+ });
11269
+ input = routed.blocks;
11270
+ }
10532
11271
  const maxIt = typeof deps2.context.meta["maxIterations"] === "number" ? deps2.context.meta["maxIterations"] : void 0;
10533
- const result = await deps2.agent.run(content, {
11272
+ const result = await deps2.agent.run(input, {
10534
11273
  signal: thisRun.signal,
10535
11274
  maxIterations: maxIt
10536
11275
  });
@@ -10548,13 +11287,24 @@ function createMessageDispatcher(opts) {
10548
11287
  })
10549
11288
  });
10550
11289
  } catch (err) {
10551
- send(ws, {
10552
- type: "error",
10553
- payload: sessionPayload2({
10554
- phase: "agent.run",
10555
- message: errMessage(err)
10556
- })
10557
- });
11290
+ if (err instanceof IncomingImageError || err instanceof ImageInputUnsupportedError || err instanceof VisionUrlBlockedError) {
11291
+ send(ws, {
11292
+ type: "error",
11293
+ payload: sessionPayload2({
11294
+ phase: "user_message",
11295
+ ...err instanceof ImageInputUnsupportedError ? { code: "vision_unsupported" } : {},
11296
+ message: err.message
11297
+ })
11298
+ });
11299
+ } else {
11300
+ send(ws, {
11301
+ type: "error",
11302
+ payload: sessionPayload2({
11303
+ phase: "agent.run",
11304
+ message: errMessage(err)
11305
+ })
11306
+ });
11307
+ }
10558
11308
  } finally {
10559
11309
  if (runLock.get() === thisRun) {
10560
11310
  runLock.set(null);
@@ -10602,7 +11352,7 @@ function createMessageDispatcher(opts) {
10602
11352
  break;
10603
11353
  }
10604
11354
  case "tool.disable": {
10605
- const name2 = (msg.payload ?? {}).name;
11355
+ const name2 = msg.payload?.name;
10606
11356
  if (!name2) {
10607
11357
  send(ws, { type: "error", payload: { message: "tool.disable requires a name" } });
10608
11358
  break;
@@ -10611,13 +11361,15 @@ function createMessageDispatcher(opts) {
10611
11361
  const currentCfg = deps2.configStore.get().tools ?? {};
10612
11362
  const currentDisabled = currentCfg.disabledTools ?? [];
10613
11363
  if (ok2 && !currentDisabled.includes(name2)) {
10614
- deps2.configStore.update({ tools: { ...currentCfg, disabledTools: [...currentDisabled, name2] } });
11364
+ deps2.configStore.update({
11365
+ tools: { ...currentCfg, disabledTools: [...currentDisabled, name2] }
11366
+ });
10615
11367
  }
10616
11368
  send(ws, { type: "tool.disabled", payload: { name: name2, ok: ok2 } });
10617
11369
  break;
10618
11370
  }
10619
11371
  case "tool.enable": {
10620
- const name2 = (msg.payload ?? {}).name;
11372
+ const name2 = msg.payload?.name;
10621
11373
  if (!name2) {
10622
11374
  send(ws, { type: "error", payload: { message: "tool.enable requires a name" } });
10623
11375
  break;
@@ -10627,19 +11379,31 @@ function createMessageDispatcher(opts) {
10627
11379
  const currentCfg = deps2.configStore.get().tools ?? {};
10628
11380
  const currentDisabled = currentCfg.disabledTools ?? [];
10629
11381
  deps2.configStore.update({
10630
- tools: { ...currentCfg, disabledTools: currentDisabled.filter((n) => n !== name2) }
11382
+ tools: {
11383
+ ...currentCfg,
11384
+ disabledTools: currentDisabled.filter((n) => n !== name2)
11385
+ }
10631
11386
  });
10632
11387
  }
10633
11388
  send(ws, { type: "tool.enabled", payload: { name: name2, ok: ok2 } });
10634
11389
  break;
10635
11390
  }
10636
11391
  // ── Memory operations — delegated to shared handlers (memory-handlers.ts) ──
11392
+ // `memory.list` renders the single Super Memory store; all writes go
11393
+ // through the structured `memory.super.*` operations below.
10637
11394
  case "memory.list":
10638
11395
  return handleMemoryList(ws, deps2.memoryStore);
10639
- case "memory.remember":
10640
- return handleMemoryRemember(ws, msg, deps2.memoryStore);
10641
- case "memory.forget":
10642
- return handleMemoryForget(ws, msg, deps2.memoryStore);
11396
+ // ── SuperMemory operations ──
11397
+ case "memory.super.list":
11398
+ return handleSuperMemoryList(ws, deps2.memoryStore);
11399
+ case "memory.super.get":
11400
+ return handleSuperMemoryGet(ws, msg, deps2.memoryStore);
11401
+ case "memory.super.update":
11402
+ return handleSuperMemoryUpdate(ws, msg, deps2.memoryStore);
11403
+ case "memory.super.delete":
11404
+ return handleSuperMemoryDelete(ws, msg, deps2.memoryStore);
11405
+ case "memory.super.remember":
11406
+ return handleSuperMemoryRemember(ws, msg, deps2.memoryStore);
10643
11407
  // ── MCP tripwires — handleMcpRoute claims these upstream. ──
10644
11408
  case "mcp.list":
10645
11409
  throw new Error("handleMcpRoute did not claim mcp.list \u2014 check chain order");
@@ -10661,6 +11425,14 @@ function createMessageDispatcher(opts) {
10661
11425
  throw new Error("handleMcpRoute did not claim mcp.restart \u2014 check chain order");
10662
11426
  case "mcp.discover":
10663
11427
  throw new Error("handleMcpRoute did not claim mcp.discover \u2014 check chain order");
11428
+ case "mcp.resources":
11429
+ throw new Error("handleMcpRoute did not claim mcp.resources \u2014 check chain order");
11430
+ case "mcp.prompts":
11431
+ throw new Error("handleMcpRoute did not claim mcp.prompts \u2014 check chain order");
11432
+ case "mcp.resource.read":
11433
+ throw new Error("handleMcpRoute did not claim mcp.resource.read \u2014 check chain order");
11434
+ case "mcp.prompt.get":
11435
+ throw new Error("handleMcpRoute did not claim mcp.prompt.get \u2014 check chain order");
10664
11436
  // Skills — full request→response cycle lives in skills-handlers.ts.
10665
11437
  case "skills.list":
10666
11438
  await handleSkillsList(ws, makeSkillsContext());
@@ -10961,7 +11733,20 @@ var PREF_KEYS = [
10961
11733
  "favoriteModels",
10962
11734
  "favoriteModelsOnly",
10963
11735
  "modelMatrix",
10964
- "fallbackAuto"
11736
+ "fallbackAuto",
11737
+ // Refiner + TUI visual prefs (parity with the CLI's embedded server —
11738
+ // these were browser-editable there but rejected as unknown keys here).
11739
+ "refinerProvider",
11740
+ "refinerModel",
11741
+ "refinerFallbackProfile",
11742
+ "thinkingWord",
11743
+ "statuslineMode",
11744
+ "animationStyle",
11745
+ // Safety / system prefs (parity with /settings breaker, fs-access, debug-stream).
11746
+ "breakerEnabled",
11747
+ "breakerAutoKillResetMs",
11748
+ "fsAccess",
11749
+ "debugStream"
10965
11750
  ];
10966
11751
  function prefSnapshot(contextMeta) {
10967
11752
  const snapshot = {};
@@ -11032,6 +11817,18 @@ async function persistPrefsToConfig(deps2, holder, payload) {
11032
11817
  setAutonomy("enhanceDelayMs", payload["enhanceDelayMs"]);
11033
11818
  if (typeof payload["enhanceLanguage"] === "string")
11034
11819
  setAutonomy("enhanceLanguage", payload["enhanceLanguage"]);
11820
+ if (typeof payload["refinerProvider"] === "string")
11821
+ setAutonomy("refinerProvider", payload["refinerProvider"]);
11822
+ if (typeof payload["refinerModel"] === "string")
11823
+ setAutonomy("refinerModel", payload["refinerModel"]);
11824
+ if (typeof payload["refinerFallbackProfile"] === "string")
11825
+ setAutonomy("refinerFallbackProfile", payload["refinerFallbackProfile"]);
11826
+ if (typeof payload["thinkingWord"] === "string")
11827
+ setAutonomy("thinkingWord", payload["thinkingWord"]);
11828
+ if (typeof payload["statuslineMode"] === "string")
11829
+ setAutonomy("statuslineMode", payload["statuslineMode"]);
11830
+ if (typeof payload["animationStyle"] === "string")
11831
+ setAutonomy("animationStyle", payload["animationStyle"]);
11035
11832
  if (autonomyTouched) decrypted.autonomy = autonomyCfg;
11036
11833
  if (typeof payload["nextPrediction"] === "boolean")
11037
11834
  decrypted.nextPrediction = payload["nextPrediction"];
@@ -11149,16 +11946,34 @@ async function persistPrefsToConfig(deps2, holder, payload) {
11149
11946
  }
11150
11947
  decrypted.modelRuntime = mr;
11151
11948
  }
11949
+ if (typeof payload["breakerEnabled"] === "boolean" || typeof payload["breakerAutoKillResetMs"] === "number") {
11950
+ const cb = decrypted.circuitBreaker ?? {};
11951
+ if (typeof payload["breakerEnabled"] === "boolean") cb.enabled = payload["breakerEnabled"];
11952
+ if (typeof payload["breakerAutoKillResetMs"] === "number")
11953
+ cb.autoKillResetMs = payload["breakerAutoKillResetMs"];
11954
+ decrypted.circuitBreaker = cb;
11955
+ }
11956
+ if (payload["fsAccess"] === "unrestricted" || payload["fsAccess"] === "project") {
11957
+ const restrict = payload["fsAccess"] === "project";
11958
+ const toolsCfg = decrypted.tools ?? {};
11959
+ toolsCfg.restrictToProjectRoot = restrict;
11960
+ decrypted.tools = toolsCfg;
11961
+ const featsCfg = decrypted.features ?? {};
11962
+ featsCfg.allowOutsideProjectRoot = !restrict;
11963
+ decrypted.features = featsCfg;
11964
+ }
11965
+ if (typeof payload["debugStream"] === "boolean")
11966
+ decrypted.debugStream = payload["debugStream"];
11152
11967
  }, "prefs");
11153
11968
  }
11154
11969
 
11155
11970
  // src/server/projects-manifest.ts
11156
11971
  import * as fs14 from "node:fs/promises";
11157
- import * as path18 from "node:path";
11972
+ import * as path19 from "node:path";
11158
11973
  import { projectSlug } from "@wrongstack/core";
11159
11974
  function projectsJsonPath(globalConfigPath) {
11160
- const base = path18.dirname(globalConfigPath);
11161
- return path18.join(base, "projects.json");
11975
+ const base = path19.dirname(globalConfigPath);
11976
+ return path19.join(base, "projects.json");
11162
11977
  }
11163
11978
  async function loadManifest(globalConfigPath) {
11164
11979
  try {
@@ -11171,15 +11986,15 @@ async function loadManifest(globalConfigPath) {
11171
11986
  }
11172
11987
  async function saveManifest(manifest, globalConfigPath) {
11173
11988
  const file = projectsJsonPath(globalConfigPath);
11174
- await fs14.mkdir(path18.dirname(file), { recursive: true });
11989
+ await fs14.mkdir(path19.dirname(file), { recursive: true });
11175
11990
  await fs14.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
11176
11991
  }
11177
11992
  function generateProjectSlug(rootPath) {
11178
11993
  return projectSlug(rootPath);
11179
11994
  }
11180
11995
  async function ensureProjectDataDir(slug, globalConfigPath) {
11181
- const base = path18.dirname(globalConfigPath);
11182
- const dir = path18.join(base, "projects", slug);
11996
+ const base = path19.dirname(globalConfigPath);
11997
+ const dir = path19.join(base, "projects", slug);
11183
11998
  await fs14.mkdir(dir, { recursive: true });
11184
11999
  return dir;
11185
12000
  }
@@ -11213,6 +12028,23 @@ function projectSavedProviders(providers) {
11213
12028
  });
11214
12029
  }
11215
12030
  var probeScrubber = new DefaultSecretScrubber();
12031
+ async function probeModelDescriptors(cfg) {
12032
+ if (!cfg.baseUrl) return [];
12033
+ try {
12034
+ const keys = normalizeKeys(cfg);
12035
+ const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
12036
+ const result = await probeLocalLlm({
12037
+ baseUrl: cfg.baseUrl,
12038
+ apiKey: active?.apiKey,
12039
+ noAuth: false,
12040
+ scrubber: probeScrubber
12041
+ });
12042
+ if (!result.ok || !result.modelIds) return [];
12043
+ return result.modelIds.map((id) => ({ id, name: id, capabilities: [] }));
12044
+ } catch {
12045
+ return [];
12046
+ }
12047
+ }
11216
12048
  function createProviderHandlers(deps2) {
11217
12049
  const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
11218
12050
  let configWriteLock = deps2.getConfigWriteLock();
@@ -11499,7 +12331,9 @@ import path21 from "node:path";
11499
12331
  import {
11500
12332
  enhanceUserPrompt,
11501
12333
  gatedEnhancerReasoning,
12334
+ nextEnhanceTimeout,
11502
12335
  recentTextTurns,
12336
+ resolveEnhanceFallbackRef,
11503
12337
  resolveProviderModelList
11504
12338
  } from "@wrongstack/core";
11505
12339
  import { makeProviderFromConfig as makeProviderFromConfig2 } from "@wrongstack/providers";
@@ -11596,6 +12430,16 @@ async function handleMailboxPurge(ws, deps2, opts) {
11596
12430
  send(ws, { type: "mailbox.purged", payload: { error: errMessage(err) } });
11597
12431
  }
11598
12432
  }
12433
+ async function handleMailboxCompact(ws, deps2, opts) {
12434
+ try {
12435
+ const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
12436
+ const mb = new GlobalMailbox3(dir);
12437
+ const result = await mb.autoCompact(opts);
12438
+ send(ws, { type: "mailbox.compacted", payload: result });
12439
+ } catch (err) {
12440
+ send(ws, { type: "mailbox.compacted", payload: { error: errMessage(err) } });
12441
+ }
12442
+ }
11599
12443
 
11600
12444
  // src/server/mode-handlers.ts
11601
12445
  import {
@@ -11654,6 +12498,8 @@ function createModeHandlers(ctx) {
11654
12498
  const paths = resolveWstackPaths2({ projectRoot: ctx.projectRoot, globalRoot: ctx.globalRoot });
11655
12499
  const freshBuilder = new DefaultSystemPromptBuilder2({
11656
12500
  memoryStore: ctx.memoryStore,
12501
+ // Single injection channel: Super Memory turn middleware, not a static section.
12502
+ injectMemory: false,
11657
12503
  skillLoader: ctx.skillLoader,
11658
12504
  modeStore: ctx.modeStore,
11659
12505
  modeId: id,
@@ -11684,7 +12530,7 @@ function createModeHandlers(ctx) {
11684
12530
  }
11685
12531
 
11686
12532
  // src/server/project-handlers.ts
11687
- import * as path19 from "node:path";
12533
+ import * as path20 from "node:path";
11688
12534
  function createProjectHandlers(ctx) {
11689
12535
  return {
11690
12536
  listProjects: async (ws) => {
@@ -11710,7 +12556,7 @@ function createProjectHandlers(ctx) {
11710
12556
  selectProject: async (ws, msg) => {
11711
12557
  const payload = msg.payload;
11712
12558
  const root = typeof payload?.root === "string" ? payload.root : "";
11713
- const name2 = typeof payload?.name === "string" ? payload.name : root ? path19.basename(root) : "";
12559
+ const name2 = typeof payload?.name === "string" ? payload.name : root ? path20.basename(root) : "";
11714
12560
  send(ws, {
11715
12561
  type: "projects.selected",
11716
12562
  payload: {
@@ -11745,12 +12591,12 @@ function createProjectHandlers(ctx) {
11745
12591
  }
11746
12592
 
11747
12593
  // src/server/session-handlers.ts
11748
- import * as path20 from "node:path";
11749
12594
  import {
11750
12595
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
11751
12596
  repairToolUseAdjacency,
11752
12597
  resolveContextWindowPolicy as resolveContextWindowPolicy3
11753
12598
  } from "@wrongstack/core";
12599
+ import { sessionScopedPath as sessionScopedPath2 } from "@wrongstack/core/utils";
11754
12600
  function createSessionHandlers(ctx) {
11755
12601
  const currentSessionId = () => ctx.getSession().id;
11756
12602
  const sessionPayload2 = (payload) => {
@@ -11776,33 +12622,46 @@ function createSessionHandlers(ctx) {
11776
12622
  });
11777
12623
  return false;
11778
12624
  };
12625
+ const finalizeSession = async (writer) => {
12626
+ await writer.append({
12627
+ type: "session_end",
12628
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
12629
+ usage: ctx.tokenCounter.total()
12630
+ }).catch(() => void 0);
12631
+ await writer.close().catch(() => void 0);
12632
+ };
12633
+ const activateSession = async (next, messages, usage) => {
12634
+ const current = ctx.getSession();
12635
+ if (current !== next) await finalizeSession(current);
12636
+ ctx.setSession(next);
12637
+ ctx.context.session = next;
12638
+ ctx.context.state.replaceMessages(messages);
12639
+ ctx.context.state.replaceTodos([]);
12640
+ ctx.context.readFiles.clear();
12641
+ ctx.context.fileMtimes.clear();
12642
+ ctx.context.state.setMeta(
12643
+ "plan.path",
12644
+ sessionScopedPath2(ctx.sessionsDir, next.id, ".plan.json")
12645
+ );
12646
+ ctx.context.state.setMeta(
12647
+ "task.path",
12648
+ sessionScopedPath2(ctx.sessionsDir, next.id, ".tasks.json")
12649
+ );
12650
+ ctx.tokenCounter.reset();
12651
+ if (usage) ctx.tokenCounter.account(usage, ctx.config.model);
12652
+ ctx.setSessionStartedAt(Date.now());
12653
+ await ctx.onSessionSwapped(next.id);
12654
+ };
11779
12655
  return {
11780
12656
  newSession: async (ws, msg) => {
11781
12657
  if (!ensureCurrentSession(ws, msg, "session.new")) return;
11782
- const session = ctx.getSession();
11783
- try {
11784
- await session.append({
11785
- type: "session_end",
11786
- ts: (/* @__PURE__ */ new Date()).toISOString(),
11787
- usage: ctx.tokenCounter.total()
11788
- });
11789
- await session.close();
11790
- } catch {
11791
- }
11792
12658
  const next = await ctx.getSessionStore().create({
11793
12659
  id: "",
11794
12660
  title: "",
11795
12661
  model: ctx.config.model,
11796
12662
  provider: ctx.config.provider
11797
12663
  });
11798
- ctx.setSession(next);
11799
- ctx.context.session = next;
11800
- ctx.context.state.replaceMessages([]);
11801
- ctx.context.state.replaceTodos([]);
11802
- ctx.context.readFiles.clear();
11803
- ctx.context.fileMtimes.clear();
11804
- ctx.tokenCounter.reset();
11805
- ctx.setSessionStartedAt(Date.now());
12664
+ await activateSession(next, []);
11806
12665
  broadcast(ctx.clients, { type: "session.start", payload: await ctx.sessionStartPayload() });
11807
12666
  },
11808
12667
  clearContext: async (ws, msg) => {
@@ -11883,7 +12742,9 @@ function createSessionHandlers(ctx) {
11883
12742
  },
11884
12743
  listContextModes: async (ws, msg) => {
11885
12744
  if (!ensureCurrentSession(ws, msg, "context.modes.list")) return;
11886
- const active = String(ctx.context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID);
12745
+ const active = String(
12746
+ ctx.context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID
12747
+ );
11887
12748
  const allModes = ctx.customModeStore.list().map((m) => ({
11888
12749
  id: m.id,
11889
12750
  name: m.name,
@@ -11894,7 +12755,10 @@ function createSessionHandlers(ctx) {
11894
12755
  eliseThreshold: m.eliseThreshold,
11895
12756
  custom: m.custom === true
11896
12757
  }));
11897
- send(ws, { type: "context.modes.list", payload: sessionPayload2({ activeId: active, modes: allModes }) });
12758
+ send(ws, {
12759
+ type: "context.modes.list",
12760
+ payload: sessionPayload2({ activeId: active, modes: allModes })
12761
+ });
11898
12762
  },
11899
12763
  switchContextMode: async (ws, msg) => {
11900
12764
  if (!ensureCurrentSession(ws, msg, "context.mode.switch")) return;
@@ -11976,7 +12840,10 @@ function createSessionHandlers(ctx) {
11976
12840
  const { id } = parsed.value;
11977
12841
  if (String(ctx.context.meta["contextWindowMode"] ?? "") === id) {
11978
12842
  ctx.context.meta["contextWindowMode"] = DEFAULT_CONTEXT_WINDOW_MODE_ID;
11979
- ctx.context.meta["contextWindowPolicy"] = resolveContextWindowPolicy3({}, DEFAULT_CONTEXT_WINDOW_MODE_ID);
12843
+ ctx.context.meta["contextWindowPolicy"] = resolveContextWindowPolicy3(
12844
+ {},
12845
+ DEFAULT_CONTEXT_WINDOW_MODE_ID
12846
+ );
11980
12847
  }
11981
12848
  const result = ctx.customModeStore.remove(id);
11982
12849
  sendResult(ws, result.ok, result.error ?? `Mode "${id}" deleted`);
@@ -11989,15 +12856,7 @@ function createSessionHandlers(ctx) {
11989
12856
  send(ws, {
11990
12857
  type: "sessions.list",
11991
12858
  payload: {
11992
- sessions: list.map((s) => ({
11993
- id: s.id,
11994
- title: s.title,
11995
- startedAt: s.startedAt,
11996
- model: s.model,
11997
- provider: s.provider,
11998
- tokenTotal: s.tokenTotal,
11999
- isCurrent: s.id === currentId
12000
- }))
12859
+ sessions: toSessionHistoryEntries(list, currentId)
12001
12860
  }
12002
12861
  });
12003
12862
  } catch (err) {
@@ -12011,8 +12870,17 @@ function createSessionHandlers(ctx) {
12011
12870
  sendResult(ws, false, "Cannot delete the active session");
12012
12871
  return;
12013
12872
  }
12014
- await ctx.getSessionStore().delete(id);
12873
+ const store = ctx.getSessionStore();
12874
+ await store.delete(id);
12015
12875
  sendResult(ws, true, `Session ${id} deleted`);
12876
+ try {
12877
+ const list = await store.list(200);
12878
+ broadcast(ctx.clients, {
12879
+ type: "sessions.list",
12880
+ payload: { sessions: toSessionHistoryEntries(list, ctx.getSession().id) }
12881
+ });
12882
+ } catch {
12883
+ }
12016
12884
  } catch (err) {
12017
12885
  sendResult(ws, false, errMessage(err));
12018
12886
  }
@@ -12028,23 +12896,17 @@ function createSessionHandlers(ctx) {
12028
12896
  try {
12029
12897
  await ctx.getSessionStore().rename(id, name2);
12030
12898
  sendResult(ws, true, name2 ? `Renamed session to "${name2}"` : `Cleared session name`);
12031
- const list = await ctx.getSessionStore().list(50);
12032
- const currentId = ctx.getSession().id;
12033
- broadcast(ctx.clients, {
12034
- type: "sessions.list",
12035
- payload: {
12036
- sessions: list.map((s) => ({
12037
- id: s.id,
12038
- name: s.name,
12039
- title: s.title,
12040
- startedAt: s.startedAt,
12041
- model: s.model,
12042
- provider: s.provider,
12043
- tokenTotal: s.tokenTotal,
12044
- isCurrent: s.id === currentId
12045
- }))
12046
- }
12047
- });
12899
+ try {
12900
+ const list = await ctx.getSessionStore().list(200);
12901
+ const currentId = ctx.getSession().id;
12902
+ broadcast(ctx.clients, {
12903
+ type: "sessions.list",
12904
+ payload: {
12905
+ sessions: toSessionHistoryEntries(list, currentId)
12906
+ }
12907
+ });
12908
+ } catch {
12909
+ }
12048
12910
  } catch (err) {
12049
12911
  sendResult(ws, false, errMessage(err));
12050
12912
  }
@@ -12058,23 +12920,7 @@ function createSessionHandlers(ctx) {
12058
12920
  return;
12059
12921
  }
12060
12922
  const resumed = await ctx.getSessionStore().resume(id);
12061
- try {
12062
- await current.append({
12063
- type: "session_end",
12064
- ts: (/* @__PURE__ */ new Date()).toISOString(),
12065
- usage: ctx.tokenCounter.total()
12066
- });
12067
- await current.close();
12068
- } catch {
12069
- }
12070
- ctx.setSession(resumed.writer);
12071
- ctx.context.session = resumed.writer;
12072
- ctx.context.state.replaceMessages(resumed.data.messages);
12073
- ctx.context.readFiles.clear();
12074
- ctx.context.fileMtimes.clear();
12075
- ctx.tokenCounter.reset();
12076
- ctx.tokenCounter.account(resumed.data.usage, ctx.config.model);
12077
- ctx.setSessionStartedAt(Date.now());
12923
+ await activateSession(resumed.writer, resumed.data.messages, resumed.data.usage);
12078
12924
  broadcast(ctx.clients, {
12079
12925
  type: "session.start",
12080
12926
  payload: {
@@ -12098,10 +12944,7 @@ function createSessionHandlers(ctx) {
12098
12944
  try {
12099
12945
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
12100
12946
  const projectRoot = ctx.getProjectRoot();
12101
- const rewinder = new DefaultSessionRewinder(
12102
- path20.join(projectRoot, ".wrongstack", "sessions"),
12103
- projectRoot
12104
- );
12947
+ const rewinder = new DefaultSessionRewinder(ctx.sessionsDir, projectRoot);
12105
12948
  const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
12106
12949
  send(ws, { type: "session.checkpoints", payload: sessionPayload2({ checkpoints }) });
12107
12950
  } catch {
@@ -12114,10 +12957,7 @@ function createSessionHandlers(ctx) {
12114
12957
  try {
12115
12958
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
12116
12959
  const projectRoot = ctx.getProjectRoot();
12117
- const rewinder = new DefaultSessionRewinder(
12118
- path20.join(projectRoot, ".wrongstack", "sessions"),
12119
- projectRoot
12120
- );
12960
+ const rewinder = new DefaultSessionRewinder(ctx.sessionsDir, projectRoot);
12121
12961
  await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
12122
12962
  await ctx.context.session.truncateToCheckpoint(checkpointIndex);
12123
12963
  sendResult(ws, true, `Rewound to checkpoint ${checkpointIndex}`);
@@ -12166,6 +13006,25 @@ function buildRoutes(state, deps2, cb) {
12166
13006
  clients: state.getClients(),
12167
13007
  modelsRegistry: deps2.modelsRegistry
12168
13008
  });
13009
+ async function applyModelSwitchCore(newProvider, newModel) {
13010
+ const cur = state.getConfig();
13011
+ state.setConfig(patchConfig(cur, { provider: newProvider, model: newModel }));
13012
+ deps2.configStore.update({ provider: newProvider, model: newModel });
13013
+ deps2.context.model = newModel;
13014
+ const newCfg = state.getConfig();
13015
+ const providerCfg = newCfg.providers?.[newProvider] ?? { type: newProvider };
13016
+ const newProv = deps2.providerRegistry.has(newProvider) ? deps2.providerRegistry.create({ ...providerCfg, type: newProvider }) : makeProviderFromConfig2(newProvider, providerCfg);
13017
+ deps2.context.provider = newProv;
13018
+ await cb.updateAutoCompactionMaxContext(newProv, newProvider, providerCfg);
13019
+ await cb.updateGlobalConfig((config) => {
13020
+ config.provider = newProvider;
13021
+ config.model = newModel;
13022
+ }, "model.switch");
13023
+ broadcast(state.getClients(), {
13024
+ type: "session.start",
13025
+ payload: await cb.sessionStartPayload()
13026
+ });
13027
+ }
12169
13028
  const providerRoutes = {
12170
13029
  providerHandlers,
12171
13030
  listProviders: async (ws) => {
@@ -12202,11 +13061,20 @@ function buildRoutes(state, deps2, cb) {
12202
13061
  const provider = await resolveProviderCatalogForModels(deps2.modelsRegistry, providerId, cfg);
12203
13062
  const siblingId = SIBLING_CATALOG[providerId];
12204
13063
  const siblingCatalog = siblingId && siblingId !== providerId ? await deps2.modelsRegistry.getProvider(siblingId).catch(() => void 0) : void 0;
13064
+ let resolved = resolveProviderModelList(
13065
+ cfg?.models,
13066
+ provider,
13067
+ cfg?.type ?? providerId,
13068
+ siblingCatalog
13069
+ );
13070
+ if (resolved.length === 0 && cfg?.baseUrl) {
13071
+ resolved = await probeModelDescriptors(cfg);
13072
+ }
12205
13073
  const models = await enrichProviderModelDescriptors(
12206
13074
  deps2.modelsRegistry,
12207
13075
  providerId,
12208
13076
  cfg,
12209
- resolveProviderModelList(cfg?.models, provider, cfg?.type ?? providerId, siblingCatalog)
13077
+ resolved
12210
13078
  );
12211
13079
  send(ws, {
12212
13080
  type: "provider.models",
@@ -12224,21 +13092,7 @@ function buildRoutes(state, deps2, cb) {
12224
13092
  }
12225
13093
  const { provider: newProvider, model: newModel } = parsed.value;
12226
13094
  try {
12227
- const cur = state.getConfig();
12228
- state.setConfig(patchConfig(cur, { provider: newProvider, model: newModel }));
12229
- deps2.configStore.update({ provider: newProvider, model: newModel });
12230
- deps2.context.model = newModel;
12231
- const newCfg = state.getConfig();
12232
- const providerCfg = newCfg.providers?.[newProvider] ?? {
12233
- type: newProvider
12234
- };
12235
- const newProv = deps2.providerRegistry.has(newProvider) ? deps2.providerRegistry.create({ ...providerCfg, type: newProvider }) : makeProviderFromConfig2(newProvider, providerCfg);
12236
- deps2.context.provider = newProv;
12237
- await cb.updateAutoCompactionMaxContext(newProv, newProvider, providerCfg);
12238
- await cb.updateGlobalConfig((config) => {
12239
- config.provider = newProvider;
12240
- config.model = newModel;
12241
- }, "model.switch");
13095
+ await applyModelSwitchCore(newProvider, newModel);
12242
13096
  send(ws, {
12243
13097
  type: "key.operation_result",
12244
13098
  payload: { success: true, message: `Switched to ${newProvider} / ${newModel}` }
@@ -12251,45 +13105,100 @@ function buildRoutes(state, deps2, cb) {
12251
13105
  message: `Switch failed: ${errMessage(err)}`
12252
13106
  }
12253
13107
  });
12254
- return;
12255
13108
  }
12256
- broadcast(state.getClients(), {
12257
- type: "session.start",
12258
- payload: await cb.sessionStartPayload()
12259
- });
13109
+ },
13110
+ adoptDefaultProviderIfUnset: async (providerId) => {
13111
+ if (state.getConfig().model) return;
13112
+ const saved = await providerHandlers.loadConfigProviders();
13113
+ state.setConfig(patchConfig(state.getConfig(), { providers: saved }));
13114
+ const cfg = saved[providerId];
13115
+ if (!cfg) return;
13116
+ let model = cfg.models?.[0];
13117
+ if (!model) {
13118
+ const catalogId = cfg.type && cfg.type !== providerId ? cfg.type : providerId;
13119
+ const catalog = await deps2.modelsRegistry.getProvider(catalogId).catch(() => void 0);
13120
+ model = catalog?.models?.[0]?.id;
13121
+ }
13122
+ if (!model) {
13123
+ const probed = await probeModelDescriptors(cfg);
13124
+ model = probed[0]?.id;
13125
+ }
13126
+ if (!model) return;
13127
+ try {
13128
+ await applyModelSwitchCore(providerId, model);
13129
+ } catch {
13130
+ }
12260
13131
  },
12261
13132
  refineModel: async (ws, msg) => {
12262
- const { text } = msg.payload;
13133
+ const payload = msg.payload;
13134
+ const { text } = payload;
12263
13135
  if (!text?.trim()) {
12264
13136
  send(ws, {
12265
13137
  type: "model.refine_result",
12266
- payload: { refined: "", english: "", error: "Empty text" }
13138
+ payload: { refined: "", english: "", error: "Empty text", errorKind: "provider_error" }
12267
13139
  });
12268
13140
  return;
12269
13141
  }
13142
+ const cfg = state.getConfig();
13143
+ const fallbackRef = resolveEnhanceFallbackRef({
13144
+ ...cfg,
13145
+ provider: cfg.provider ?? "",
13146
+ model: cfg.model ?? ""
13147
+ });
13148
+ let provider = deps2.context.provider;
13149
+ let providerId = cfg.provider ?? "";
13150
+ let model = deps2.context.model;
13151
+ if (payload.provider && payload.model) {
13152
+ try {
13153
+ const providerCfg = cfg.providers?.[payload.provider] ?? {
13154
+ type: payload.provider
13155
+ };
13156
+ provider = deps2.providerRegistry.has(payload.provider) ? deps2.providerRegistry.create({ ...providerCfg, type: payload.provider }) : makeProviderFromConfig2(payload.provider, providerCfg);
13157
+ providerId = payload.provider;
13158
+ model = payload.model;
13159
+ } catch (err) {
13160
+ send(ws, {
13161
+ type: "model.refine_result",
13162
+ payload: {
13163
+ refined: text,
13164
+ english: text,
13165
+ error: `Cannot use ${payload.provider}/${payload.model}: ${errMessage(err)}`,
13166
+ errorKind: "provider_error",
13167
+ ...fallbackRef ? { fallbackRef } : {}
13168
+ }
13169
+ });
13170
+ return;
13171
+ }
13172
+ }
13173
+ const baseTimeout = 9e4;
13174
+ const timeoutMs = typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : baseTimeout;
12270
13175
  try {
12271
13176
  const history = recentTextTurns(deps2.context.messages);
12272
- const cfg = state.getConfig();
12273
13177
  const resolved = await resolveProviderModelMetadata(
12274
13178
  deps2.modelsRegistry,
12275
- cfg.provider ?? "",
12276
- cfg.model ?? "",
12277
- cfg.providers?.[cfg.provider ?? ""]
13179
+ providerId,
13180
+ model,
13181
+ cfg.providers?.[providerId]
12278
13182
  ).catch(() => void 0);
12279
13183
  const reasoning = gatedEnhancerReasoning(resolved?.capabilities?.reasoningConfig);
13184
+ let failureKind;
12280
13185
  const result = await enhanceUserPrompt({
12281
- provider: deps2.context.provider,
12282
- model: deps2.context.model,
13186
+ provider,
13187
+ model,
12283
13188
  text,
12284
13189
  history,
12285
- timeoutMs: 9e4,
13190
+ timeoutMs,
12286
13191
  ...reasoning ? { reasoning } : {},
12287
- onError: (reason) => {
13192
+ onError: (reason, kind) => {
13193
+ failureKind = kind;
12288
13194
  console.warn(
12289
13195
  JSON.stringify({
12290
13196
  level: "warn",
12291
13197
  event: "model.refine_failed",
12292
13198
  reason,
13199
+ kind,
13200
+ provider: providerId,
13201
+ model,
12293
13202
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
12294
13203
  })
12295
13204
  );
@@ -12298,12 +13207,23 @@ function buildRoutes(state, deps2, cb) {
12298
13207
  if (result) {
12299
13208
  send(ws, {
12300
13209
  type: "model.refine_result",
12301
- payload: { refined: result.refined, english: result.english }
13210
+ payload: {
13211
+ refined: result.refined,
13212
+ english: result.english,
13213
+ refinedWith: { provider: providerId, model }
13214
+ }
12302
13215
  });
12303
13216
  } else {
12304
13217
  send(ws, {
12305
13218
  type: "model.refine_result",
12306
- payload: { refined: text, english: text, error: "Refinement returned no result" }
13219
+ payload: {
13220
+ refined: text,
13221
+ english: text,
13222
+ error: "Refinement returned no result",
13223
+ errorKind: failureKind ?? "empty",
13224
+ ...failureKind === "timeout" ? { retryTimeoutMs: nextEnhanceTimeout(timeoutMs, cfg.autonomy) } : {},
13225
+ ...fallbackRef ? { fallbackRef } : {}
13226
+ }
12307
13227
  });
12308
13228
  }
12309
13229
  } catch (err) {
@@ -12317,7 +13237,13 @@ function buildRoutes(state, deps2, cb) {
12317
13237
  );
12318
13238
  send(ws, {
12319
13239
  type: "model.refine_result",
12320
- payload: { refined: text, english: text, error: errMessage(err) }
13240
+ payload: {
13241
+ refined: text,
13242
+ english: text,
13243
+ error: errMessage(err),
13244
+ errorKind: "provider_error",
13245
+ ...fallbackRef ? { fallbackRef } : {}
13246
+ }
12321
13247
  });
12322
13248
  }
12323
13249
  }
@@ -12333,8 +13259,10 @@ function buildRoutes(state, deps2, cb) {
12333
13259
  getProjectRoot: state.getProjectRoot,
12334
13260
  getSession: state.getSession,
12335
13261
  getSessionStore: state.getSessionStore,
13262
+ sessionsDir: deps2.wpaths.projectSessions,
12336
13263
  setSession: state.setSession,
12337
13264
  setSessionStartedAt: state.setSessionStartedAt,
13265
+ onSessionSwapped: cb.onSessionSwapped,
12338
13266
  sessionStartPayload: cb.sessionStartPayload
12339
13267
  });
12340
13268
  const projectRoutes = createProjectHandlers({
@@ -12429,6 +13357,17 @@ function buildRoutes(state, deps2, cb) {
12429
13357
  deps2.pipelines.contextWindow.remove("AutoCompaction", { optional: true });
12430
13358
  }
12431
13359
  }
13360
+ if (typeof payload["breakerEnabled"] === "boolean" || typeof payload["breakerAutoKillResetMs"] === "number") {
13361
+ const { getProcessRegistry } = await import("@wrongstack/tools");
13362
+ getProcessRegistry().setBreakerConfig({
13363
+ ...typeof payload["breakerEnabled"] === "boolean" ? { enabled: payload["breakerEnabled"] } : {},
13364
+ ...typeof payload["breakerAutoKillResetMs"] === "number" ? { autoKillResetMs: payload["breakerAutoKillResetMs"] } : {}
13365
+ });
13366
+ }
13367
+ if (typeof payload["debugStream"] === "boolean") {
13368
+ const { setDebugStreamEnabled } = await import("@wrongstack/providers");
13369
+ setDebugStreamEnabled(payload["debugStream"]);
13370
+ }
12432
13371
  if (typeof payload["logLevel"] === "string") {
12433
13372
  const valid = ["debug", "info", "warn", "error"];
12434
13373
  if (valid.includes(payload["logLevel"])) {
@@ -12506,6 +13445,13 @@ function buildRoutes(state, deps2, cb) {
12506
13445
  { projectRoot: state.getProjectRoot(), globalRoot: path21.dirname(deps2.globalConfigPath) },
12507
13446
  parsed.value
12508
13447
  );
13448
+ },
13449
+ compact: (ws, msg) => {
13450
+ return handleMailboxCompact(
13451
+ ws,
13452
+ { projectRoot: state.getProjectRoot(), globalRoot: path21.dirname(deps2.globalConfigPath) },
13453
+ msg.payload ?? {}
13454
+ );
12509
13455
  }
12510
13456
  };
12511
13457
  const mcpRoutes = {
@@ -12518,14 +13464,29 @@ function buildRoutes(state, deps2, cb) {
12518
13464
  sleep: (ws, msg) => handleMcpSleep(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
12519
13465
  wake: (ws, msg) => handleMcpWake(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
12520
13466
  restart: (ws, msg) => handleMcpRestart(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
12521
- discover: (ws, msg) => handleMcpDiscover(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry)
13467
+ discover: (ws, msg) => handleMcpDiscover(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
13468
+ resources: (ws, msg) => handleMcpResources(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
13469
+ prompts: (ws, msg) => handleMcpPrompts(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
13470
+ resourceRead: (ws, msg) => handleMcpResourceRead(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
13471
+ promptGet: (ws, msg) => handleMcpPromptGet(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry)
13472
+ };
13473
+ const sendBrainStatus = (ws) => {
13474
+ const snapshot = deps2.brainRuntime.getSnapshot();
13475
+ send(ws, {
13476
+ type: "brain.status",
13477
+ payload: {
13478
+ maxAutoRisk: deps2.brainSettings.maxAutoRisk,
13479
+ log: deps2.brainLog,
13480
+ mode: snapshot.mode,
13481
+ poolLabels: snapshot.poolLabels,
13482
+ councilLabels: snapshot.councilLabels,
13483
+ ledgerPath: snapshot.ledger.path
13484
+ }
13485
+ });
12522
13486
  };
12523
13487
  const brainRoutes = {
12524
13488
  status: (ws) => {
12525
- send(ws, {
12526
- type: "brain.status",
12527
- payload: { maxAutoRisk: deps2.brainSettings.maxAutoRisk, log: deps2.brainLog }
12528
- });
13489
+ sendBrainStatus(ws);
12529
13490
  },
12530
13491
  risk: (ws, msg) => {
12531
13492
  const parsed = validateBrainRiskPayload(msg.payload);
@@ -12535,11 +13496,43 @@ function buildRoutes(state, deps2, cb) {
12535
13496
  }
12536
13497
  const { level } = parsed.value;
12537
13498
  deps2.brainSettings.maxAutoRisk = level;
13499
+ sendBrainStatus(ws);
13500
+ },
13501
+ configGet: (ws) => {
12538
13502
  send(ws, {
12539
- type: "brain.status",
12540
- payload: { maxAutoRisk: deps2.brainSettings.maxAutoRisk, log: deps2.brainLog }
13503
+ type: "brain.config",
13504
+ payload: { config: deps2.brainRuntime.getSnapshot(), persisted: true }
12541
13505
  });
12542
13506
  },
13507
+ configSet: async (ws, msg) => {
13508
+ const parsed = validateBrainConfigSetPayload(msg.payload);
13509
+ if (!parsed.ok) {
13510
+ sendResult(ws, false, parsed.message);
13511
+ return;
13512
+ }
13513
+ try {
13514
+ const { persisted } = deps2.brainRuntime.apply(parsed.value.patch);
13515
+ const result = await persisted;
13516
+ send(ws, {
13517
+ type: "brain.config",
13518
+ payload: {
13519
+ config: deps2.brainRuntime.getSnapshot(),
13520
+ persisted: result.ok,
13521
+ ...result.ok ? {} : { error: result.error ?? "Persist failed." }
13522
+ }
13523
+ });
13524
+ sendBrainStatus(ws);
13525
+ } catch (err) {
13526
+ send(ws, {
13527
+ type: "brain.config",
13528
+ payload: {
13529
+ config: deps2.brainRuntime.getSnapshot(),
13530
+ persisted: false,
13531
+ error: `Invalid Brain setting: ${errMessage(err)}`
13532
+ }
13533
+ });
13534
+ }
13535
+ },
12543
13536
  ask: async (ws, msg) => {
12544
13537
  const parsed = validateBrainAskPayload(msg.payload);
12545
13538
  if (!parsed.ok) {
@@ -12612,8 +13605,16 @@ async function startWebUI(opts = {}) {
12612
13605
  console.log("[WebUI] Config loaded:", config.provider ?? "(none)", "/", config.model ?? "(none)");
12613
13606
  if (!config.provider && config.providers && typeof config.providers === "object" && config.providers !== null && !Array.isArray(config.providers) && Object.keys(config.providers).length > 0) {
12614
13607
  const firstKey = expectDefined3(Object.keys(config.providers)[0]);
12615
- config = patchConfig(config, { provider: firstKey });
12616
- console.log("[WebUI] No active provider \u2014 auto-selected:", firstKey);
13608
+ const adoptModel = !config.model ? config.providers[firstKey]?.models?.[0] : void 0;
13609
+ config = patchConfig(config, {
13610
+ provider: firstKey,
13611
+ ...adoptModel ? { model: adoptModel } : {}
13612
+ });
13613
+ console.log(
13614
+ "[WebUI] No active provider \u2014 auto-selected:",
13615
+ firstKey,
13616
+ adoptModel ? `/ ${adoptModel}` : ""
13617
+ );
12617
13618
  }
12618
13619
  const needsProvider = !config.provider || !config.model;
12619
13620
  const preContext = await createPreContextServices({
@@ -12647,7 +13648,8 @@ async function startWebUI(opts = {}) {
12647
13648
  promptsCtx,
12648
13649
  modelCapabilitiesRef,
12649
13650
  provider,
12650
- context
13651
+ context,
13652
+ sessionIdentity
12651
13653
  } = preContext;
12652
13654
  let sessionStore = preContext.sessionStore;
12653
13655
  let session = preContext.session;
@@ -12680,7 +13682,12 @@ async function startWebUI(opts = {}) {
12680
13682
  modelCapabilitiesRef,
12681
13683
  sessionGetter: () => session,
12682
13684
  sessionReader,
12683
- annotationsStore
13685
+ annotationsStore,
13686
+ // Brain settings persist to the GLOBAL config only (config.brain is on
13687
+ // the in-project deny list), serialized behind the shared write lock.
13688
+ persistBrainConfig: (brainConfig) => updateGlobalConfig2((decrypted) => {
13689
+ decrypted["brain"] = brainConfig;
13690
+ }, "brain.config")
12684
13691
  });
12685
13692
  const {
12686
13693
  compactor,
@@ -12690,6 +13697,7 @@ async function startWebUI(opts = {}) {
12690
13697
  pipelines,
12691
13698
  brain,
12692
13699
  brainSettings,
13700
+ brainRuntime,
12693
13701
  brainLog,
12694
13702
  brainMonitor,
12695
13703
  codebaseIndexing,
@@ -12860,10 +13868,16 @@ async function startWebUI(opts = {}) {
12860
13868
  terminalHandler,
12861
13869
  brain,
12862
13870
  brainSettings,
13871
+ brainRuntime,
12863
13872
  brainLog
12864
13873
  };
12865
13874
  const cb = {
12866
13875
  sessionStartPayload,
13876
+ onSessionSwapped: async (sessionId) => {
13877
+ await sessionIdentity.activate(sessionId);
13878
+ const { hydrateSessionKanban: hydrateSessionKanban2 } = await import("@wrongstack/tools/session-kanban");
13879
+ await hydrateSessionKanban2(deps2.context);
13880
+ },
12867
13881
  updateAutoCompactionMaxContext,
12868
13882
  updateGlobalConfig: updateGlobalConfig2,
12869
13883
  persistPrefsToConfig: persistPrefsToConfig2,
@@ -12989,7 +14003,10 @@ async function startWebUI(opts = {}) {
12989
14003
  onShutdown: async () => {
12990
14004
  credentialWatcherClose?.();
12991
14005
  brainMonitor.stop();
14006
+ await agentServices.brainLedger?.stop().catch(() => {
14007
+ });
12992
14008
  await mcpRegistry.stopAll().catch(() => void 0);
14009
+ await sessionIdentity.stop();
12993
14010
  eventArming.getDispose()?.();
12994
14011
  if (eternalSubscription) {
12995
14012
  eternalSubscription.dispose();
@@ -13100,6 +14117,7 @@ export {
13100
14117
  AutoPhaseWebSocketHandler,
13101
14118
  CollaborationWebSocketHandler,
13102
14119
  SKIP_DIRS,
14120
+ SURFACE_DEFAULT_PORTS,
13103
14121
  SddBoardWebSocketHandler,
13104
14122
  SddWizardWebSocketHandler,
13105
14123
  SpecsWebSocketHandler,
@@ -13139,6 +14157,7 @@ export {
13139
14157
  generateAuthToken,
13140
14158
  getAnalyticsBuffer,
13141
14159
  getCostRates,
14160
+ getSurfaceDefaultPorts,
13142
14161
  handleApiAnalyticsGet,
13143
14162
  handleApiAnalyticsPost,
13144
14163
  handleApiAnalyticsSummary,
@@ -13166,15 +14185,17 @@ export {
13166
14185
  handleMcpDiscover,
13167
14186
  handleMcpEnable,
13168
14187
  handleMcpList,
14188
+ handleMcpPromptGet,
14189
+ handleMcpPrompts,
13169
14190
  handleMcpRemove,
14191
+ handleMcpResourceRead,
14192
+ handleMcpResources,
13170
14193
  handleMcpRestart,
13171
14194
  handleMcpRoute,
13172
14195
  handleMcpSleep,
13173
14196
  handleMcpUpdate,
13174
14197
  handleMcpWake,
13175
- handleMemoryForget,
13176
14198
  handleMemoryList,
13177
- handleMemoryRemember,
13178
14199
  handleModeRoute,
13179
14200
  handlePrefsRoute,
13180
14201
  handleProcessKill,
@@ -13203,6 +14224,11 @@ export {
13203
14224
  handleSkillsUninstall,
13204
14225
  handleSkillsUpdate,
13205
14226
  handleSpecsRoute,
14227
+ handleSuperMemoryDelete,
14228
+ handleSuperMemoryGet,
14229
+ handleSuperMemoryList,
14230
+ handleSuperMemoryRemember,
14231
+ handleSuperMemoryUpdate,
13206
14232
  handleWorklistMessage,
13207
14233
  hostForBrowserUrl,
13208
14234
  hostHeaderOk,
@@ -13247,9 +14273,13 @@ export {
13247
14273
  setActiveKey,
13248
14274
  setupEvents,
13249
14275
  setupWebUICodebaseIndexing,
14276
+ startSddRunFromGraph,
13250
14277
  startWebUI,
13251
14278
  statusProjectHashFromWatchFilename,
13252
14279
  stringifyContent,
14280
+ surfaceLabel,
14281
+ toSessionHistoryEntries,
14282
+ toSessionHistoryEntry,
13253
14283
  tokenMatches,
13254
14284
  unregisterInstance,
13255
14285
  upsertKey,