@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
@@ -42,12 +42,13 @@ function commitsSince(cwd, baseSha, branch) {
42
42
  }
43
43
  }
44
44
  var AutoPhaseWebSocketHandler = class {
45
- constructor(agent, context, logger, storeDir, events, projectRoot) {
45
+ constructor(agent, context, logger, storeDir, events, projectRoot, onBoardState) {
46
46
  this.agent = agent;
47
47
  this.context = context;
48
48
  this.logger = logger;
49
49
  this.events = events;
50
50
  this.projectRoot = projectRoot;
51
+ this.onBoardState = onBoardState;
51
52
  this.store = new PhaseStore({ baseDir: storeDir });
52
53
  }
53
54
  agent;
@@ -55,6 +56,7 @@ var AutoPhaseWebSocketHandler = class {
55
56
  logger;
56
57
  events;
57
58
  projectRoot;
59
+ onBoardState;
58
60
  orchestrator = null;
59
61
  graph = null;
60
62
  store;
@@ -399,6 +401,15 @@ Type: ${task.type}`;
399
401
  if (!this.graph) return;
400
402
  const state = this.buildState(activePhaseId);
401
403
  this.broadcast({ type: "autophase.state", payload: state });
404
+ if (this.onBoardState) {
405
+ try {
406
+ this.onBoardState(this.graph.id, state);
407
+ } catch (err) {
408
+ this.logger.error(
409
+ `[AutoPhase] board-state tap failed: ${err instanceof Error ? err.message : String(err)}`
410
+ );
411
+ }
412
+ }
402
413
  }
403
414
  buildState(activePhaseId) {
404
415
  if (!this.graph) {
@@ -2614,10 +2625,7 @@ function cspSourceFromUrl(rawUrl) {
2614
2625
  return void 0;
2615
2626
  }
2616
2627
  }
2617
- var ALLOWED_INLINE_SCRIPT_HASHES = [
2618
- "'sha256-6PXDy0zrpXa6mvYOl11bZ8nubNUL7ushPUhGDZtaexg='",
2619
- "'sha256-6sIdwbEBx7jj0drqSHHm7MqvmoYD3CQ4lp8Zp8blcb0='"
2620
- ];
2628
+ var EXTRA_SCRIPT_SOURCES = ["'wasm-unsafe-eval'"];
2621
2629
  function buildCspHeader(wsPort, requestHost, publicWsUrl) {
2622
2630
  const connect = /* @__PURE__ */ new Set([
2623
2631
  "'self'",
@@ -2631,7 +2639,7 @@ function buildCspHeader(wsPort, requestHost, publicWsUrl) {
2631
2639
  }
2632
2640
  const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
2633
2641
  if (publicWsSource) connect.add(publicWsSource);
2634
- const scriptSrc = ["'self'", ...ALLOWED_INLINE_SCRIPT_HASHES].join(" ");
2642
+ const scriptSrc = ["'self'", ...EXTRA_SCRIPT_SOURCES].join(" ");
2635
2643
  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'`;
2636
2644
  }
2637
2645
  function isInsideDist(candidate, distDir) {
@@ -2992,7 +3000,7 @@ function mapStatus(raw) {
2992
3000
  return "stopped";
2993
3001
  }
2994
3002
  }
2995
- function toView(info) {
3003
+ function toView(info, health) {
2996
3004
  const view = {
2997
3005
  name: info.name,
2998
3006
  transport: info.transport,
@@ -3004,6 +3012,7 @@ function toView(info) {
3004
3012
  };
3005
3013
  if (info.description !== void 0) view.description = info.description;
3006
3014
  if (info.lazy !== void 0) view.lazy = info.lazy;
3015
+ if (health !== void 0) view.health = health;
3007
3016
  return view;
3008
3017
  }
3009
3018
  function deps(ws, globalConfigPath, registry) {
@@ -3029,7 +3038,13 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
3029
3038
  registry: mcpRegistry,
3030
3039
  presets: allServers()
3031
3040
  });
3032
- send(ws, { type: "mcp.list", payload: { servers: servers.map(toView) } });
3041
+ const health = new Map(
3042
+ (typeof mcpRegistry.operationalHealth === "function" ? mcpRegistry.operationalHealth() : []).map((item) => [item.name, item])
3043
+ );
3044
+ send(ws, {
3045
+ type: "mcp.list",
3046
+ payload: { servers: servers.map((server) => toView(server, health.get(server.name))) }
3047
+ });
3033
3048
  }
3034
3049
  async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
3035
3050
  const d = deps(ws, globalConfigPath, mcpRegistry);
@@ -3179,10 +3194,119 @@ async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
3179
3194
  payload: { success: result.ok, message: result.message }
3180
3195
  });
3181
3196
  }
3197
+ async function handleMcpResources(ws, msg, _globalConfigPath, mcpRegistry) {
3198
+ if (!mcpRegistry) return sendContentError(ws, "resources", "", "MCP registry is not available.");
3199
+ const payload = payloadRecord(msg);
3200
+ let serverName = "";
3201
+ try {
3202
+ serverName = requiredPayloadString(payload, "name");
3203
+ const refresh = payload["refresh"] === true;
3204
+ const [resources, resourceTemplates] = await Promise.all([
3205
+ mcpRegistry.listResources(serverName, { refresh }),
3206
+ mcpRegistry.listResourceTemplates(serverName, { refresh })
3207
+ ]);
3208
+ send(ws, {
3209
+ type: "mcp.resources",
3210
+ payload: { name: serverName, resources, resourceTemplates }
3211
+ });
3212
+ } catch (err) {
3213
+ sendContentError(ws, "resources", serverName, errorMessage(err));
3214
+ }
3215
+ }
3216
+ async function handleMcpPrompts(ws, msg, _globalConfigPath, mcpRegistry) {
3217
+ if (!mcpRegistry) return sendContentError(ws, "prompts", "", "MCP registry is not available.");
3218
+ const payload = payloadRecord(msg);
3219
+ let serverName = "";
3220
+ try {
3221
+ serverName = requiredPayloadString(payload, "name");
3222
+ const prompts = await mcpRegistry.listPrompts(serverName, {
3223
+ refresh: payload["refresh"] === true
3224
+ });
3225
+ send(ws, { type: "mcp.prompts", payload: { name: serverName, prompts } });
3226
+ } catch (err) {
3227
+ sendContentError(ws, "prompts", serverName, errorMessage(err));
3228
+ }
3229
+ }
3230
+ async function handleMcpResourceRead(ws, msg, _globalConfigPath, mcpRegistry) {
3231
+ if (!mcpRegistry)
3232
+ return sendContentError(ws, "resource.read", "", "MCP registry is not available.");
3233
+ const payload = payloadRecord(msg);
3234
+ let serverName = "";
3235
+ try {
3236
+ serverName = requiredPayloadString(payload, "name");
3237
+ const insertion = await mcpRegistry.selectResourceForInsertion(
3238
+ serverName,
3239
+ requiredPayloadString(payload, "uri")
3240
+ );
3241
+ send(ws, { type: "mcp.content.selected", payload: insertion });
3242
+ } catch (err) {
3243
+ sendContentError(ws, "resource.read", serverName, errorMessage(err));
3244
+ }
3245
+ }
3246
+ async function handleMcpPromptGet(ws, msg, _globalConfigPath, mcpRegistry) {
3247
+ if (!mcpRegistry) return sendContentError(ws, "prompt.get", "", "MCP registry is not available.");
3248
+ const payload = payloadRecord(msg);
3249
+ let serverName = "";
3250
+ try {
3251
+ serverName = requiredPayloadString(payload, "name");
3252
+ const insertion = await mcpRegistry.selectPromptForInsertion(
3253
+ serverName,
3254
+ requiredPayloadString(payload, "prompt"),
3255
+ promptArguments(payload["arguments"])
3256
+ );
3257
+ send(ws, { type: "mcp.content.selected", payload: insertion });
3258
+ } catch (err) {
3259
+ sendContentError(ws, "prompt.get", serverName, errorMessage(err));
3260
+ }
3261
+ }
3262
+ function payloadRecord(msg) {
3263
+ return msg.payload && typeof msg.payload === "object" && !Array.isArray(msg.payload) ? msg.payload : {};
3264
+ }
3265
+ function requiredPayloadString(payload, field) {
3266
+ const value = payload[field];
3267
+ if (typeof value !== "string" || value.length === 0) {
3268
+ throw new Error(`MCP payload field "${field}" must be a non-empty string`);
3269
+ }
3270
+ return value;
3271
+ }
3272
+ function promptArguments(value) {
3273
+ if (value === void 0) return void 0;
3274
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3275
+ throw new Error("MCP prompt arguments must be an object");
3276
+ }
3277
+ const args = {};
3278
+ for (const [key, item] of Object.entries(value)) {
3279
+ if (typeof item !== "string") throw new Error(`MCP prompt argument "${key}" must be a string`);
3280
+ args[key] = item;
3281
+ }
3282
+ return args;
3283
+ }
3284
+ function sendContentError(ws, action, name2, error) {
3285
+ send(ws, { type: "mcp.content.error", payload: { action, name: name2, error } });
3286
+ }
3287
+ function errorMessage(err) {
3288
+ return err instanceof Error ? err.message : String(err);
3289
+ }
3182
3290
 
3183
3291
  // src/server/memory-handlers.ts
3292
+ function isSuperMemoryStore(store) {
3293
+ const s = store;
3294
+ return typeof s.stats === "function" && typeof s.listSuper === "function" && typeof s.getSuperMemory === "function" && typeof s.updateSuperMemory === "function" && typeof s.deleteSuperMemory === "function";
3295
+ }
3296
+ function requiresSuperMemory(command) {
3297
+ return `\`${command}\` requires the Super Memory backend (superMemory.enabled).`;
3298
+ }
3184
3299
  async function handleMemoryList(ws, memoryStore) {
3185
3300
  try {
3301
+ if (isSuperMemoryStore(memoryStore)) {
3302
+ const [stats, memories] = await Promise.all([
3303
+ memoryStore.stats(),
3304
+ memoryStore.listSuper()
3305
+ ]);
3306
+ const text2 = memories.length === 0 ? "\u{1F9E0} Super Memory is empty." : formatSuperMemoryText(stats, memories);
3307
+ send(ws, { type: "memory.list", payload: { text: text2 } });
3308
+ return;
3309
+ }
3186
3310
  const text = await memoryStore.readAll();
3187
3311
  send(ws, { type: "memory.list", payload: { text } });
3188
3312
  } catch (err) {
@@ -3192,26 +3316,128 @@ async function handleMemoryList(ws, memoryStore) {
3192
3316
  });
3193
3317
  }
3194
3318
  }
3195
- async function handleMemoryRemember(ws, msg, memoryStore) {
3196
- const { text, scope } = msg.payload;
3319
+ function formatSuperMemoryText(stats, memories) {
3320
+ const active = stats.byStatus["active"] ?? 0;
3321
+ const stale = stats.byStatus["stale"] ?? 0;
3322
+ const archived = stats.byStatus["archived"] ?? 0;
3323
+ const lines = [
3324
+ "## \u{1F9E0} Super Memory",
3325
+ "",
3326
+ `**Total:** ${stats.total} \xB7 \u{1F7E2} ${active} active \xB7 \u{1F7E1} ${stale} stale \xB7 \u{1F535} ${archived} archived \xB7 **edges:** ${stats.edges}`,
3327
+ ""
3328
+ ];
3329
+ for (const m of memories) {
3330
+ const tags = m.tags.length > 0 ? ` \`${m.tags.slice(0, 3).join("` `")}\`` : "";
3331
+ const icon = m.status === "active" ? "\u{1F7E2}" : m.status === "stale" ? "\u{1F7E1}" : m.status === "archived" ? "\u{1F535}" : "\u26AA";
3332
+ const preview = m.text.replace(/\s+/g, " ").trim().slice(0, 140);
3333
+ lines.push(`- ${icon} \`${m.id.slice(0, 12)}\u2026\` [${m.kind}] ${preview}${tags}`);
3334
+ }
3335
+ return lines.join("\n");
3336
+ }
3337
+ async function handleSuperMemoryList(ws, memoryStore) {
3338
+ if (!isSuperMemoryStore(memoryStore)) {
3339
+ send(ws, { type: "memory.super.list", payload: { error: requiresSuperMemory("memory.super.list") } });
3340
+ return;
3341
+ }
3197
3342
  try {
3198
- await memoryStore.remember(text, scope ?? "project-memory");
3199
- sendResult(ws, true, "Saved to memory");
3343
+ const [stats, memories] = await Promise.all([
3344
+ memoryStore.stats(),
3345
+ memoryStore.listSuper()
3346
+ ]);
3347
+ send(ws, { type: "memory.super.list", payload: { memories, stats } });
3200
3348
  } catch (err) {
3201
- sendResult(ws, false, errMessage(err));
3349
+ send(ws, { type: "memory.super.list", payload: { error: errMessage(err) } });
3202
3350
  }
3203
3351
  }
3204
- async function handleMemoryForget(ws, msg, memoryStore) {
3205
- const { text, scope } = msg.payload;
3352
+ async function handleSuperMemoryGet(ws, msg, memoryStore) {
3353
+ if (!isSuperMemoryStore(memoryStore)) {
3354
+ send(ws, { type: "memory.super.get", payload: { error: requiresSuperMemory("memory.super.get") } });
3355
+ return;
3356
+ }
3357
+ const { id } = msg.payload;
3358
+ if (!id) {
3359
+ send(ws, { type: "memory.super.get", payload: { error: "id is required" } });
3360
+ return;
3361
+ }
3206
3362
  try {
3207
- const removed = await memoryStore.forget(text, scope ?? "project-memory");
3208
- sendResult(
3209
- ws,
3210
- removed > 0,
3211
- removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
3212
- );
3363
+ const memory = await memoryStore.getSuperMemory(id);
3364
+ if (!memory) {
3365
+ send(ws, { type: "memory.super.get", payload: { error: `Memory "${id}" not found.` } });
3366
+ return;
3367
+ }
3368
+ send(ws, { type: "memory.super.get", payload: { memory } });
3213
3369
  } catch (err) {
3214
- sendResult(ws, false, errMessage(err));
3370
+ send(ws, { type: "memory.super.get", payload: { error: errMessage(err) } });
3371
+ }
3372
+ }
3373
+ async function handleSuperMemoryUpdate(ws, msg, memoryStore) {
3374
+ if (!isSuperMemoryStore(memoryStore)) {
3375
+ send(ws, { type: "memory.super.update", payload: { error: requiresSuperMemory("memory.super.update") } });
3376
+ return;
3377
+ }
3378
+ const payload = msg.payload;
3379
+ const id = payload["id"];
3380
+ if (!id) {
3381
+ send(ws, { type: "memory.super.update", payload: { error: "id is required" } });
3382
+ return;
3383
+ }
3384
+ const patch = { ...payload };
3385
+ delete patch["id"];
3386
+ if (Object.keys(patch).length === 0) {
3387
+ send(ws, { type: "memory.super.update", payload: { error: "No fields to update." } });
3388
+ return;
3389
+ }
3390
+ try {
3391
+ const memory = await memoryStore.updateSuperMemory(id, patch);
3392
+ send(ws, { type: "memory.super.update", payload: { memory } });
3393
+ } catch (err) {
3394
+ send(ws, { type: "memory.super.update", payload: { error: errMessage(err) } });
3395
+ }
3396
+ }
3397
+ async function handleSuperMemoryRemember(ws, msg, memoryStore) {
3398
+ if (!isSuperMemoryStore(memoryStore)) {
3399
+ send(ws, { type: "memory.super.remember", payload: { error: requiresSuperMemory("memory.super.remember") } });
3400
+ return;
3401
+ }
3402
+ const payload = msg.payload;
3403
+ const text = payload["text"];
3404
+ if (!text || !text.trim()) {
3405
+ send(ws, { type: "memory.super.remember", payload: { error: "text is required" } });
3406
+ return;
3407
+ }
3408
+ try {
3409
+ const memory = await memoryStore.rememberSuper({
3410
+ text: text.trim(),
3411
+ kind: payload["kind"],
3412
+ scope: payload["scope"],
3413
+ tags: payload["tags"],
3414
+ anchors: payload["anchors"],
3415
+ importance: payload["importance"],
3416
+ confidence: payload["confidence"],
3417
+ freshness: payload["freshness"],
3418
+ supersedes: payload["supersedes"],
3419
+ contradicts: payload["contradicts"]
3420
+ });
3421
+ send(ws, { type: "memory.super.remember", payload: { memory } });
3422
+ } catch (err) {
3423
+ send(ws, { type: "memory.super.remember", payload: { error: errMessage(err) } });
3424
+ }
3425
+ }
3426
+ async function handleSuperMemoryDelete(ws, msg, memoryStore) {
3427
+ if (!isSuperMemoryStore(memoryStore)) {
3428
+ send(ws, { type: "memory.super.delete", payload: { success: false, message: requiresSuperMemory("memory.super.delete") } });
3429
+ return;
3430
+ }
3431
+ const { id, reason } = msg.payload;
3432
+ if (!id) {
3433
+ send(ws, { type: "memory.super.delete", payload: { success: false, message: "id is required" } });
3434
+ return;
3435
+ }
3436
+ try {
3437
+ await memoryStore.deleteSuperMemory(id, reason);
3438
+ send(ws, { type: "memory.super.delete", payload: { success: true, message: `Deleted memory "${id}".` } });
3439
+ } catch (err) {
3440
+ send(ws, { type: "memory.super.delete", payload: { success: false, message: errMessage(err) } });
3215
3441
  }
3216
3442
  }
3217
3443
 
@@ -3803,6 +4029,8 @@ var SddBoardWebSocketHandler = class {
3803
4029
  import * as path8 from "node:path";
3804
4030
  import { spawnSync as spawnSync2 } from "node:child_process";
3805
4031
  import {
4032
+ DefaultTaskStore,
4033
+ TaskTracker,
3806
4034
  WorktreeManager as WorktreeManager2,
3807
4035
  ToolValidationError as ToolValidationError4
3808
4036
  } from "@wrongstack/core";
@@ -3818,6 +4046,63 @@ import {
3818
4046
  startSddRun,
3819
4047
  TaskGraphStore
3820
4048
  } from "@wrongstack/sdd";
4049
+ function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
4050
+ const runTracker = tracker ?? (() => {
4051
+ const t = new TaskTracker({ store: new DefaultTaskStore() });
4052
+ t.setGraph(graph);
4053
+ return t;
4054
+ })();
4055
+ const worktreesEnabled = config.worktrees ?? process.env["WRONGSTACK_SDD_WORKTREES"] !== "0";
4056
+ let worktrees;
4057
+ if (worktreesEnabled) {
4058
+ const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
4059
+ cwd: deps2.projectRoot,
4060
+ encoding: "utf8",
4061
+ windowsHide: true
4062
+ }).stdout?.trim() === "true";
4063
+ if (inGit) {
4064
+ void cleanupStaleSddWorktrees({
4065
+ projectRoot: deps2.projectRoot,
4066
+ boardsDir: deps2.projectSddBoards
4067
+ }).catch(() => void 0);
4068
+ worktrees = new WorktreeManager2({
4069
+ projectRoot: deps2.projectRoot,
4070
+ events: deps2.events,
4071
+ sessionId: () => deps2.agent.ctx.session?.id
4072
+ });
4073
+ }
4074
+ }
4075
+ const superviseFailure = deps2.brain && deps2.runIsolatedTurn ? new SddSupervisor({
4076
+ brain: deps2.brain,
4077
+ reassignModels: config.fallbackModels,
4078
+ generateSubtasks: makeLlmSubtaskGenerator({
4079
+ run: (prompt) => deps2.runIsolatedTurn(prompt, "Task Splitter")
4080
+ }),
4081
+ requestLlmVerdict: true
4082
+ }).superviseFailure : deps2.brain ? new SddSupervisor({
4083
+ brain: deps2.brain,
4084
+ reassignModels: config.fallbackModels,
4085
+ requestLlmVerdict: true
4086
+ }).superviseFailure : void 0;
4087
+ return startSddRun({
4088
+ tracker: runTracker,
4089
+ graph,
4090
+ agent: deps2.agent,
4091
+ projectRoot: deps2.projectRoot,
4092
+ events: deps2.events,
4093
+ sessionId: () => deps2.agent.ctx.session?.id,
4094
+ subagentFactory: deps2.subagentFactory,
4095
+ boardStore: new SddBoardStore2({ baseDir: deps2.projectSddBoards }),
4096
+ registry: deps2.registry ?? new SddRunRegistry(),
4097
+ verifyTask: makeCommandVerifier(),
4098
+ ...superviseFailure ? { superviseFailure } : {},
4099
+ ...config.parallelSlots !== void 0 ? { parallelSlots: config.parallelSlots } : {},
4100
+ ...config.defaultModel !== void 0 ? { defaultModel: config.defaultModel } : {},
4101
+ ...config.defaultProvider !== void 0 ? { defaultProvider: config.defaultProvider } : {},
4102
+ ...config.fallbackModels !== void 0 ? { fallbackModels: config.fallbackModels } : {},
4103
+ ...worktrees ? { worktrees } : {}
4104
+ });
4105
+ }
3821
4106
  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";
3822
4107
  function buildSddWizardDeps(opts) {
3823
4108
  const registry = new SddRunRegistry();
@@ -3853,64 +4138,27 @@ function buildSddWizardDeps(opts) {
3853
4138
  field: "taskGraph"
3854
4139
  });
3855
4140
  }
3856
- const worktreesEnabled = useWorktrees ?? process.env["WRONGSTACK_SDD_WORKTREES"] !== "0";
3857
- let worktrees;
3858
- if (worktreesEnabled) {
3859
- const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
3860
- cwd: opts.projectRoot,
3861
- encoding: "utf8",
3862
- windowsHide: true
3863
- }).stdout?.trim() === "true";
3864
- if (inGit) {
3865
- await cleanupStaleSddWorktrees({
3866
- projectRoot: opts.projectRoot,
3867
- boardsDir: opts.paths.projectSddBoards
3868
- }).catch(() => void 0);
3869
- worktrees = new WorktreeManager2({
3870
- projectRoot: opts.projectRoot,
3871
- events: opts.events,
3872
- sessionId: () => opts.agent.ctx.session?.id
3873
- });
3874
- }
3875
- }
3876
- const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
3877
- const verifyTask = makeCommandVerifier();
3878
- const superviseFailure = opts.brain ? new SddSupervisor({
3879
- brain: opts.brain,
3880
- // The run-level fallback chain (chosen in the wizard) doubles as the
3881
- // supervisor's reassign options — a `reassign` verdict rotates the
3882
- // worker model on retry. Empty/undefined → reassign option dropped.
3883
- reassignModels: fallbackModels,
3884
- // LLM auto-split: decompose a retry-exhausted task into smaller
3885
- // sub-tasks on an isolated read-only turn. Heavily validated +
3886
- // bounded; an empty result degrades the split into a retry.
3887
- generateSubtasks: makeLlmSubtaskGenerator({
3888
- run: (prompt) => runIsolatedTurn(prompt, "Task Splitter")
3889
- }),
3890
- // The standalone brain is a tiered policy→LLM arbiter with NO
3891
- // human-escalation wrapper (see index.ts), so it never blocks on a
3892
- // human prompt — an unresolved verdict degrades to a bounded retry.
3893
- // Safe to let the LLM layer actually pick reassign/split.
3894
- requestLlmVerdict: true
3895
- }).superviseFailure : void 0;
3896
- const handle = startSddRun({
3897
- tracker,
4141
+ const handle = startSddRunFromGraph(
3898
4142
  graph,
3899
- agent: opts.agent,
3900
- projectRoot: opts.projectRoot,
3901
- events: opts.events,
3902
- sessionId: () => opts.agent.ctx.session?.id,
3903
- subagentFactory: opts.subagentFactory,
3904
- worktrees,
3905
- boardStore,
3906
- registry,
3907
- parallelSlots,
3908
- defaultModel,
3909
- defaultProvider,
3910
- fallbackModels,
3911
- verifyTask,
3912
- superviseFailure
3913
- });
4143
+ {
4144
+ agent: opts.agent,
4145
+ events: opts.events,
4146
+ projectRoot: opts.projectRoot,
4147
+ subagentFactory: opts.subagentFactory,
4148
+ projectSddBoards: opts.paths.projectSddBoards,
4149
+ registry,
4150
+ runIsolatedTurn,
4151
+ ...opts.brain ? { brain: opts.brain } : {}
4152
+ },
4153
+ {
4154
+ ...parallelSlots !== void 0 ? { parallelSlots } : {},
4155
+ ...defaultModel !== void 0 ? { defaultModel } : {},
4156
+ ...defaultProvider !== void 0 ? { defaultProvider } : {},
4157
+ ...fallbackModels !== void 0 ? { fallbackModels } : {},
4158
+ ...useWorktrees !== void 0 ? { worktrees: useWorktrees } : {}
4159
+ },
4160
+ tracker
4161
+ );
3914
4162
  void handle.completion.catch(() => {
3915
4163
  });
3916
4164
  return { runId: handle.runId };
@@ -4054,6 +4302,31 @@ var SddWizardWebSocketHandler = class {
4054
4302
  }
4055
4303
  };
4056
4304
 
4305
+ // src/server/session-history.ts
4306
+ function toSessionHistoryEntry(summary, currentSessionId) {
4307
+ return {
4308
+ id: summary.id,
4309
+ title: summary.title,
4310
+ ...summary.name !== void 0 ? { name: summary.name } : {},
4311
+ startedAt: summary.startedAt,
4312
+ ...summary.endedAt !== void 0 ? { endedAt: summary.endedAt } : {},
4313
+ model: summary.model,
4314
+ provider: summary.provider,
4315
+ tokenTotal: summary.tokenTotal,
4316
+ ...summary.iterationCount !== void 0 ? { iterationCount: summary.iterationCount } : {},
4317
+ ...summary.toolCallCount !== void 0 ? { toolCallCount: summary.toolCallCount } : {},
4318
+ ...summary.toolErrorCount !== void 0 ? { toolErrorCount: summary.toolErrorCount } : {},
4319
+ ...summary.fileChangeCount !== void 0 ? { fileChangeCount: summary.fileChangeCount } : {},
4320
+ ...summary.toolBreakdown !== void 0 ? { toolBreakdown: summary.toolBreakdown } : {},
4321
+ ...summary.compactionCount !== void 0 ? { compactionCount: summary.compactionCount } : {},
4322
+ ...summary.outcome !== void 0 ? { outcome: summary.outcome } : {},
4323
+ isCurrent: summary.id === currentSessionId
4324
+ };
4325
+ }
4326
+ function toSessionHistoryEntries(summaries, currentSessionId) {
4327
+ return summaries.map((summary) => toSessionHistoryEntry(summary, currentSessionId));
4328
+ }
4329
+
4057
4330
  // src/server/shell-open.ts
4058
4331
  import * as fs8 from "node:fs/promises";
4059
4332
  import * as path9 from "node:path";
@@ -4254,6 +4527,16 @@ function validateBrainAskPayload(payload) {
4254
4527
  }
4255
4528
  return { ok: true, value: { question: question.trim() } };
4256
4529
  }
4530
+ function validateBrainConfigSetPayload(payload) {
4531
+ if (!isRecord(payload)) {
4532
+ return { ok: false, message: "brain.config.set payload must be an object with a patch object" };
4533
+ }
4534
+ const patch = payload["patch"];
4535
+ if (!isRecord(patch)) {
4536
+ return { ok: false, message: "brain.config.set payload.patch must be an object" };
4537
+ }
4538
+ return { ok: true, value: { patch } };
4539
+ }
4257
4540
  function validateAutonomySwitchPayload(payload) {
4258
4541
  if (!isRecord(payload)) {
4259
4542
  return { ok: false, message: "autonomy.switch payload must be an object with string mode" };
@@ -4315,7 +4598,9 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
4315
4598
  "hqEnabled",
4316
4599
  "hqRawContent",
4317
4600
  "fallbackAuto",
4318
- "favoriteModelsOnly"
4601
+ "favoriteModelsOnly",
4602
+ "breakerEnabled",
4603
+ "debugStream"
4319
4604
  ]);
4320
4605
  var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels", "favoriteModels"]);
4321
4606
  var STRING_ARRAY_RECORD_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackProfiles"]);
@@ -4326,9 +4611,18 @@ var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
4326
4611
  "maxIterations",
4327
4612
  "maxConcurrent",
4328
4613
  "enhanceDelayMs",
4329
- "tgLongToolMs"
4614
+ "tgLongToolMs",
4615
+ "breakerAutoKillResetMs"
4616
+ ]);
4617
+ var STRING_PREF_KEYS = /* @__PURE__ */ new Set([
4618
+ "hqUrl",
4619
+ "hqToken",
4620
+ "uiLocale",
4621
+ "thinkingWord",
4622
+ "refinerProvider",
4623
+ "refinerModel",
4624
+ "refinerFallbackProfile"
4330
4625
  ]);
4331
- var STRING_PREF_KEYS = /* @__PURE__ */ new Set(["hqUrl", "hqToken", "uiLocale"]);
4332
4626
  var ENUM_PREF_KEYS = {
4333
4627
  autonomy: AUTONOMY_VALUES,
4334
4628
  contextStrategy: CONTEXT_STRATEGY_VALUES,
@@ -4339,7 +4633,10 @@ var ENUM_PREF_KEYS = {
4339
4633
  auditLevel: AUDIT_LEVEL_VALUES,
4340
4634
  reasoningMode: REASONING_MODE_VALUES,
4341
4635
  reasoningEffort: REASONING_EFFORT_VALUES,
4342
- cacheTtl: CACHE_TTL_VALUES
4636
+ cacheTtl: CACHE_TTL_VALUES,
4637
+ statuslineMode: /* @__PURE__ */ new Set(["minimum", "detailed", "no-color"]),
4638
+ animationStyle: /* @__PURE__ */ new Set(["rainbow", "wave", "pulse", "dots", "breathe", "cycle"]),
4639
+ fsAccess: /* @__PURE__ */ new Set(["unrestricted", "project"])
4343
4640
  };
4344
4641
  function validateModelRuntimeValue(modelRuntime, path22) {
4345
4642
  const reasoning = modelRuntime["reasoning"];
@@ -5662,6 +5959,10 @@ function shouldLogWatcherStats() {
5662
5959
  function setupEvents(deps2) {
5663
5960
  const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
5664
5961
  const disposers = [];
5962
+ let disposed = false;
5963
+ const on = (event, listener) => {
5964
+ disposers.push(events.on(event, listener));
5965
+ };
5665
5966
  const currentSessionId = () => context.session?.id ?? "";
5666
5967
  const sessionPayload2 = (payload) => {
5667
5968
  const provided = payload["sessionId"];
@@ -5677,20 +5978,20 @@ function setupEvents(deps2) {
5677
5978
  sessionBridge?.append(event).catch(() => {
5678
5979
  });
5679
5980
  };
5680
- events.on("iteration.started", (e) => {
5981
+ on("iteration.started", (e) => {
5681
5982
  const maxIt = typeof context.meta["maxIterations"] === "number" ? context.meta["maxIterations"] : config.tools?.maxIterations ?? 100;
5682
5983
  broadcast2(clients, {
5683
5984
  type: "iteration.started",
5684
5985
  payload: sessionPayload2({ sessionId: e.sessionId, index: e.index, maxIterations: maxIt })
5685
5986
  });
5686
5987
  });
5687
- events.on("iteration.completed", (e) => {
5988
+ on("iteration.completed", (e) => {
5688
5989
  broadcast2(clients, {
5689
5990
  type: "iteration.completed",
5690
5991
  payload: sessionPayload2({ sessionId: e.sessionId, index: e.index, totalIterations: e.index + 1 })
5691
5992
  });
5692
5993
  });
5693
- events.on("iteration.limit_reached", (e) => {
5994
+ on("iteration.limit_reached", (e) => {
5694
5995
  broadcast2(clients, {
5695
5996
  type: "iteration.limit_reached",
5696
5997
  payload: sessionPayload2({
@@ -5700,19 +6001,19 @@ function setupEvents(deps2) {
5700
6001
  })
5701
6002
  });
5702
6003
  });
5703
- events.on("provider.text_delta", (e) => {
6004
+ on("provider.text_delta", (e) => {
5704
6005
  broadcast2(clients, { type: "provider.text_delta", payload: sessionPayload2({ sessionId: e.sessionId, text: e.text, messageId: "current" }) });
5705
6006
  });
5706
- events.on("provider.thinking_delta", (e) => {
6007
+ on("provider.thinking_delta", (e) => {
5707
6008
  broadcast2(clients, { type: "provider.thinking_delta", payload: sessionPayload2({ sessionId: e.sessionId, text: e.text }) });
5708
6009
  });
5709
- events.on("provider.stream_error", (e) => {
6010
+ on("provider.stream_error", (e) => {
5710
6011
  broadcast2(clients, {
5711
6012
  type: "provider.stream_error",
5712
6013
  payload: sessionPayload2({ sessionId: e.sessionId, eventType: e.eventType, message: e.msg })
5713
6014
  });
5714
6015
  });
5715
- events.on("tool.started", (e) => {
6016
+ on("tool.started", (e) => {
5716
6017
  broadcast2(clients, {
5717
6018
  type: "tool.started",
5718
6019
  payload: sessionPayload2({ sessionId: e.sessionId, id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` })
@@ -5725,7 +6026,7 @@ function setupEvents(deps2) {
5725
6026
  input: e.input
5726
6027
  });
5727
6028
  });
5728
- events.on("tool.progress", (e) => {
6029
+ on("tool.progress", (e) => {
5729
6030
  broadcast2(clients, {
5730
6031
  type: "tool.progress",
5731
6032
  // Nested `event` shape — the client handler reads `payload.event?.text`
@@ -5746,7 +6047,7 @@ function setupEvents(deps2) {
5746
6047
  }
5747
6048
  });
5748
6049
  });
5749
- events.on("tool.executed", (e) => {
6050
+ on("tool.executed", (e) => {
5750
6051
  broadcast2(clients, {
5751
6052
  type: "tool.executed",
5752
6053
  payload: sessionPayload2({ sessionId: e.sessionId, id: e.id, name: e.name, durationMs: e.durationMs, ok: e.ok, input: e.input, output: e.output })
@@ -5804,7 +6105,7 @@ function setupEvents(deps2) {
5804
6105
  })();
5805
6106
  }
5806
6107
  });
5807
- events.on("tool.loop_detected", (e) => {
6108
+ on("tool.loop_detected", (e) => {
5808
6109
  broadcast2(clients, {
5809
6110
  type: "tool.loop_detected",
5810
6111
  payload: sessionPayload2({
@@ -5816,19 +6117,19 @@ function setupEvents(deps2) {
5816
6117
  })
5817
6118
  });
5818
6119
  });
5819
- events.on("trust.persisted", (e) => {
6120
+ on("trust.persisted", (e) => {
5820
6121
  broadcast2(clients, {
5821
6122
  type: "trust.persisted",
5822
6123
  payload: sessionPayload2({ sessionId: e.sessionId, tool: e.tool, pattern: e.pattern, decision: e.decision })
5823
6124
  });
5824
6125
  });
5825
- events.on("delegate.started", (e) => {
6126
+ on("delegate.started", (e) => {
5826
6127
  broadcast2(clients, {
5827
6128
  type: "delegate.started",
5828
6129
  payload: sessionPayload2({ sessionId: e.sessionId, target: e.target, task: e.task })
5829
6130
  });
5830
6131
  });
5831
- events.on("delegate.completed", (e) => {
6132
+ on("delegate.completed", (e) => {
5832
6133
  broadcast2(clients, {
5833
6134
  type: "delegate.completed",
5834
6135
  payload: sessionPayload2({
@@ -5846,7 +6147,7 @@ function setupEvents(deps2) {
5846
6147
  })
5847
6148
  });
5848
6149
  });
5849
- events.on("provider.response", (e) => {
6150
+ on("provider.response", (e) => {
5850
6151
  broadcast2(clients, {
5851
6152
  type: "provider.response",
5852
6153
  payload: sessionPayload2({
@@ -5858,7 +6159,7 @@ function setupEvents(deps2) {
5858
6159
  })
5859
6160
  });
5860
6161
  });
5861
- events.on("ctx.pct", (e) => {
6162
+ on("ctx.pct", (e) => {
5862
6163
  broadcast2(clients, {
5863
6164
  type: "ctx.pct",
5864
6165
  payload: sessionPayload2({ sessionId: e.sessionId, load: e.load, tokens: e.tokens, maxContext: e.maxContext })
@@ -5875,28 +6176,28 @@ function setupEvents(deps2) {
5875
6176
  })
5876
6177
  });
5877
6178
  });
5878
- events.on("ctx.max_context", (e) => {
6179
+ on("ctx.max_context", (e) => {
5879
6180
  broadcast2(clients, {
5880
6181
  type: "ctx.max_context",
5881
6182
  payload: sessionPayload2({ sessionId: e.sessionId, providerId: e.providerId, modelId: e.modelId, maxContext: e.maxContext })
5882
6183
  });
5883
6184
  });
5884
- events.on("token.threshold", (e) => {
6185
+ on("token.threshold", (e) => {
5885
6186
  broadcast2(clients, {
5886
6187
  type: "token.threshold",
5887
6188
  payload: sessionPayload2({ sessionId: e.sessionId, used: e.used, limit: e.limit })
5888
6189
  });
5889
6190
  });
5890
- events.on("token.cost_estimate_unavailable", (e) => {
6191
+ on("token.cost_estimate_unavailable", (e) => {
5891
6192
  broadcast2(clients, {
5892
6193
  type: "token.cost_estimate_unavailable",
5893
6194
  payload: sessionPayload2({ sessionId: e.sessionId, model: e.model })
5894
6195
  });
5895
6196
  });
5896
- events.on("context.repaired", (e) => {
6197
+ on("context.repaired", (e) => {
5897
6198
  broadcast2(clients, { type: "context.repaired", payload: sessionPayload2({ sessionId: e.sessionId, removedToolUses: e.removedToolUses, removedToolResults: e.removedToolResults, removedMessages: e.removedMessages }) });
5898
6199
  });
5899
- events.on("tool.confirm_needed", (e) => {
6200
+ on("tool.confirm_needed", (e) => {
5900
6201
  const id = e.toolUseId ?? `confirm_${Date.now()}`;
5901
6202
  const payload = sessionPayload2({ sessionId: e.sessionId, id, toolName: e.tool?.name ?? "unknown", input: e.input, suggestedPattern: e.suggestedPattern, decisionSource: e.decisionSource, riskTier: e.riskTier });
5902
6203
  pendingConfirms.set(id, {
@@ -5907,7 +6208,7 @@ function setupEvents(deps2) {
5907
6208
  });
5908
6209
  broadcast2(clients, { type: "tool.confirm_needed", payload });
5909
6210
  });
5910
- events.on("error", (e) => {
6211
+ on("error", (e) => {
5911
6212
  broadcast2(clients, { type: "error", payload: sessionPayload2({ sessionId: e.sessionId, phase: e.phase, message: e.err instanceof Error ? e.err.message : String(e.err) }) });
5912
6213
  appendForCurrentSession(e.sessionId, {
5913
6214
  type: "error",
@@ -5916,13 +6217,13 @@ function setupEvents(deps2) {
5916
6217
  phase: e.phase
5917
6218
  });
5918
6219
  });
5919
- events.on("session.damaged", (e) => {
6220
+ on("session.damaged", (e) => {
5920
6221
  broadcast2(clients, {
5921
6222
  type: "session.damaged",
5922
6223
  payload: { sessionId: e.sessionId, detail: e.detail }
5923
6224
  });
5924
6225
  });
5925
- events.on("session.rewound", (e) => {
6226
+ on("session.rewound", (e) => {
5926
6227
  broadcast2(clients, {
5927
6228
  type: "session.rewound",
5928
6229
  payload: sessionPayload2({
@@ -5933,7 +6234,7 @@ function setupEvents(deps2) {
5933
6234
  })
5934
6235
  });
5935
6236
  });
5936
- events.on("checkpoint.written", (e) => {
6237
+ on("checkpoint.written", (e) => {
5937
6238
  broadcast2(clients, {
5938
6239
  type: "checkpoint.written",
5939
6240
  payload: sessionPayload2({
@@ -5945,19 +6246,19 @@ function setupEvents(deps2) {
5945
6246
  })
5946
6247
  });
5947
6248
  });
5948
- events.on("in_flight.started", (e) => {
6249
+ on("in_flight.started", (e) => {
5949
6250
  broadcast2(clients, {
5950
6251
  type: "in_flight.started",
5951
6252
  payload: sessionPayload2({ sessionId: e.sessionId, context: e.context, ts: e.ts })
5952
6253
  });
5953
6254
  });
5954
- events.on("in_flight.ended", (e) => {
6255
+ on("in_flight.ended", (e) => {
5955
6256
  broadcast2(clients, {
5956
6257
  type: "in_flight.ended",
5957
6258
  payload: sessionPayload2({ sessionId: e.sessionId, reason: e.reason, ts: e.ts })
5958
6259
  });
5959
6260
  });
5960
- events.on("provider.retry", (e) => {
6261
+ on("provider.retry", (e) => {
5961
6262
  broadcast2(clients, {
5962
6263
  type: "provider.retry",
5963
6264
  payload: sessionPayload2({
@@ -5979,7 +6280,7 @@ function setupEvents(deps2) {
5979
6280
  description: e.description
5980
6281
  });
5981
6282
  });
5982
- events.on("provider.error", (e) => {
6283
+ on("provider.error", (e) => {
5983
6284
  broadcast2(clients, {
5984
6285
  type: "provider.error",
5985
6286
  payload: sessionPayload2({
@@ -5999,7 +6300,7 @@ function setupEvents(deps2) {
5999
6300
  retryable: e.retryable
6000
6301
  });
6001
6302
  });
6002
- events.on("provider.fallback", (e) => {
6303
+ on("provider.fallback", (e) => {
6003
6304
  broadcast2(clients, {
6004
6305
  type: "provider.fallback",
6005
6306
  payload: sessionPayload2({
@@ -6011,7 +6312,7 @@ function setupEvents(deps2) {
6011
6312
  })
6012
6313
  });
6013
6314
  });
6014
- events.on("compaction.fired", (e) => {
6315
+ on("compaction.fired", (e) => {
6015
6316
  broadcast2(clients, {
6016
6317
  type: "context.compacted",
6017
6318
  payload: sessionPayload2({
@@ -6023,7 +6324,7 @@ function setupEvents(deps2) {
6023
6324
  })
6024
6325
  });
6025
6326
  });
6026
- events.on("compaction.failed", (e) => {
6327
+ on("compaction.failed", (e) => {
6027
6328
  broadcast2(clients, {
6028
6329
  type: "compaction.failed",
6029
6330
  payload: sessionPayload2({
@@ -6038,25 +6339,25 @@ function setupEvents(deps2) {
6038
6339
  })
6039
6340
  });
6040
6341
  });
6041
- events.on("mcp.server.connected", (e) => {
6342
+ on("mcp.server.connected", (e) => {
6042
6343
  broadcast2(clients, {
6043
6344
  type: "mcp.server.connected",
6044
6345
  payload: { name: e.name, toolCount: e.toolCount }
6045
6346
  });
6046
6347
  });
6047
- events.on("mcp.server.reconnected", (e) => {
6348
+ on("mcp.server.reconnected", (e) => {
6048
6349
  broadcast2(clients, {
6049
6350
  type: "mcp.server.reconnected",
6050
6351
  payload: { name: e.name, toolCount: e.toolCount }
6051
6352
  });
6052
6353
  });
6053
- events.on("mcp.server.disconnected", (e) => {
6354
+ on("mcp.server.disconnected", (e) => {
6054
6355
  broadcast2(clients, {
6055
6356
  type: "mcp.server.disconnected",
6056
6357
  payload: { name: e.name, reason: e.reason }
6057
6358
  });
6058
6359
  });
6059
- events.on("coordinator.stats", (e) => {
6360
+ on("coordinator.stats", (e) => {
6060
6361
  broadcast2(clients, {
6061
6362
  type: "coordinator.stats",
6062
6363
  payload: sessionPayload2({
@@ -6084,16 +6385,16 @@ function setupEvents(deps2) {
6084
6385
  broadcast2(clients, { type: "mailbox.agent_registered", payload });
6085
6386
  });
6086
6387
  const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: sessionPayload2({ kind, ...payload }) });
6087
- 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 }));
6088
- events.on("subagent.task_started", (e) => forwardSubagent("task_started", { sessionId: e.sessionId, subagentId: e.subagentId, taskId: e.taskId, description: e.description }));
6089
- events.on("subagent.tool_executed", (e) => forwardSubagent("tool_executed", { sessionId: e.sessionId, subagentId: e.subagentId, toolName: e.name, durationMs: e.durationMs, ok: e.ok }));
6090
- 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 }));
6091
- events.on("subagent.budget_warning", (e) => forwardSubagent("budget_warning", { sessionId: e.sessionId, subagentId: e.subagentId, budgetKind: e.kind, used: e.used, limit: e.limit }));
6092
- events.on("subagent.budget_extended", (e) => forwardSubagent("budget_extended", { sessionId: e.sessionId, subagentId: e.subagentId, budgetKind: e.kind, newLimit: e.newLimit, totalExtensions: e.totalExtensions }));
6093
- events.on("subagent.ctx_pct", (e) => forwardSubagent("ctx_pct", { sessionId: e.sessionId, subagentId: e.subagentId, load: e.load, tokens: e.tokens, maxContext: e.maxContext }));
6094
- 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 }));
6095
- events.on("subagent.removed", (e) => forwardSubagent("removed", { sessionId: e.sessionId, subagentId: e.subagentId, reason: e.reason }));
6096
- events.on("agent.timeline.message", (e) => {
6388
+ 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 }));
6389
+ on("subagent.task_started", (e) => forwardSubagent("task_started", { sessionId: e.sessionId, subagentId: e.subagentId, taskId: e.taskId, description: e.description }));
6390
+ on("subagent.tool_executed", (e) => forwardSubagent("tool_executed", { sessionId: e.sessionId, subagentId: e.subagentId, toolName: e.name, durationMs: e.durationMs, ok: e.ok }));
6391
+ 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 }));
6392
+ on("subagent.budget_warning", (e) => forwardSubagent("budget_warning", { sessionId: e.sessionId, subagentId: e.subagentId, budgetKind: e.kind, used: e.used, limit: e.limit }));
6393
+ on("subagent.budget_extended", (e) => forwardSubagent("budget_extended", { sessionId: e.sessionId, subagentId: e.subagentId, budgetKind: e.kind, newLimit: e.newLimit, totalExtensions: e.totalExtensions }));
6394
+ on("subagent.ctx_pct", (e) => forwardSubagent("ctx_pct", { sessionId: e.sessionId, subagentId: e.subagentId, load: e.load, tokens: e.tokens, maxContext: e.maxContext }));
6395
+ 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 }));
6396
+ on("subagent.removed", (e) => forwardSubagent("removed", { sessionId: e.sessionId, subagentId: e.subagentId, reason: e.reason }));
6397
+ on("agent.timeline.message", (e) => {
6097
6398
  broadcast2(clients, {
6098
6399
  type: "agent.timeline.message",
6099
6400
  payload: sessionPayload2({
@@ -6109,7 +6410,7 @@ function setupEvents(deps2) {
6109
6410
  })
6110
6411
  });
6111
6412
  });
6112
- events.on("agent.status_changed", (e) => {
6413
+ on("agent.status_changed", (e) => {
6113
6414
  broadcast2(clients, {
6114
6415
  type: "agent.status_changed",
6115
6416
  payload: sessionPayload2({
@@ -6124,7 +6425,7 @@ function setupEvents(deps2) {
6124
6425
  });
6125
6426
  });
6126
6427
  let leaderSpawned = false;
6127
- events.on("iteration.started", (e) => {
6428
+ on("iteration.started", (e) => {
6128
6429
  if (!leaderSpawned) {
6129
6430
  leaderSpawned = true;
6130
6431
  const provider = context.provider?.id ?? "unknown";
@@ -6138,7 +6439,7 @@ function setupEvents(deps2) {
6138
6439
  });
6139
6440
  }
6140
6441
  });
6141
- events.on("tool.executed", (e) => {
6442
+ on("tool.executed", (e) => {
6142
6443
  forwardSubagent("tool_executed", {
6143
6444
  sessionId: e.sessionId,
6144
6445
  subagentId: "leader",
@@ -6147,7 +6448,7 @@ function setupEvents(deps2) {
6147
6448
  ok: e.ok
6148
6449
  });
6149
6450
  });
6150
- events.on("provider.response", (e) => {
6451
+ on("provider.response", (e) => {
6151
6452
  if (e.usage?.input != null) {
6152
6453
  const maxCtx = context.provider.capabilities.maxContext;
6153
6454
  const rawLoad = maxCtx > 0 ? e.usage.input / maxCtx : 0;
@@ -6164,7 +6465,7 @@ function setupEvents(deps2) {
6164
6465
  });
6165
6466
  }
6166
6467
  });
6167
- events.on("iteration.completed", (e) => {
6468
+ on("iteration.completed", (e) => {
6168
6469
  if (!leaderSpawned) {
6169
6470
  leaderSpawned = true;
6170
6471
  const provider = context.provider?.id ?? "unknown";
@@ -6196,7 +6497,19 @@ function setupEvents(deps2) {
6196
6497
  payload: sessionPayload2({ event: eventName, ...payload })
6197
6498
  });
6198
6499
  });
6199
- events.on("client.status", async (e) => {
6500
+ events.onPattern("cron:state_snapshot", (_eventName, payload) => {
6501
+ broadcast2(clients, {
6502
+ type: "cron.snapshot",
6503
+ payload
6504
+ });
6505
+ });
6506
+ events.onPattern("cron:job_fired", (_eventName, payload) => {
6507
+ broadcast2(clients, {
6508
+ type: "cron.job_fired",
6509
+ payload
6510
+ });
6511
+ });
6512
+ on("client.status", async (e) => {
6200
6513
  broadcast2(clients, { type: "client.status_update", payload: e });
6201
6514
  if (wpaths?.projectStatus) {
6202
6515
  try {
@@ -6282,6 +6595,7 @@ function setupEvents(deps2) {
6282
6595
  const startWatcher = async () => {
6283
6596
  try {
6284
6597
  await fs10.mkdir(projectsDir, { recursive: true });
6598
+ if (disposed) return;
6285
6599
  watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
6286
6600
  if (eventType !== "change" && eventType !== "rename") return;
6287
6601
  if (filename == null) return;
@@ -6312,7 +6626,7 @@ function setupEvents(deps2) {
6312
6626
  );
6313
6627
  }
6314
6628
  };
6315
- events.on("client.status", (e) => {
6629
+ on("client.status", (e) => {
6316
6630
  if (e.projectHash) {
6317
6631
  const hash = String(e.projectHash);
6318
6632
  if (!knownProjectHashes.has(hash)) {
@@ -6352,7 +6666,9 @@ function setupEvents(deps2) {
6352
6666
  const registry = new SessionRegistry(globalRoot);
6353
6667
  const sessions = await registry.list();
6354
6668
  const mySlug = sessions.find((s) => s.pid === process.pid)?.projectSlug;
6355
- const live = sessions.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true).map((s) => ({
6669
+ const live = sessions.filter(
6670
+ (s) => s.status === "active" || s.status === "idle"
6671
+ ).filter((s) => mySlug ? s.projectSlug === mySlug : true).map((s) => ({
6356
6672
  sessionId: s.sessionId,
6357
6673
  projectName: s.projectName,
6358
6674
  projectSlug: s.projectSlug,
@@ -6406,6 +6722,8 @@ function setupEvents(deps2) {
6406
6722
  void broadcastSessions();
6407
6723
  }
6408
6724
  return () => {
6725
+ if (disposed) return;
6726
+ disposed = true;
6409
6727
  for (const dispose of disposers) {
6410
6728
  try {
6411
6729
  dispose();
@@ -6540,9 +6858,11 @@ function positiveNumber(value) {
6540
6858
  // src/server/server-runtime.ts
6541
6859
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
6542
6860
  async function resolvePorts(opts) {
6543
- const requestedWsPort = opts.wsPort ?? 3457;
6861
+ const surface = opts.surface ?? "webui";
6862
+ const surfaceDefaults = surface === "simpleui" ? { http: 3466, ws: 3467 } : { http: 3456, ws: 3457 };
6863
+ const requestedWsPort = opts.wsPort ?? surfaceDefaults.ws;
6544
6864
  const wsHost = opts.wsHost ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
6545
- const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? "3456", 10);
6865
+ const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? String(surfaceDefaults.http), 10);
6546
6866
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
6547
6867
  const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
6548
6868
  const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
@@ -6632,7 +6952,7 @@ function createWsServers(ports, accessToken) {
6632
6952
  allowedHostnames: publicHostnames,
6633
6953
  allowBrowserUrlToken: Boolean(ports.publicWsUrl)
6634
6954
  });
6635
- const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
6955
+ const WS_MAX_PAYLOAD = 20 * 1024 * 1024;
6636
6956
  const wssPrimary = new WebSocketServer({
6637
6957
  port: ports.wsPort,
6638
6958
  host: ports.wsHost,
@@ -6700,8 +7020,8 @@ function resolveWebuiDistDir(fromUrl, explicitDistDir) {
6700
7020
  if (explicitDistDir) return path12.resolve(explicitDistDir);
6701
7021
  try {
6702
7022
  const requireFromHere2 = createRequire(fromUrl);
6703
- const serverEntry = requireFromHere2.resolve("@wrongstack/webui/server");
6704
- return path12.resolve(path12.dirname(serverEntry), "..");
7023
+ const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
7024
+ return path12.dirname(serverEntry);
6705
7025
  } catch {
6706
7026
  return path12.resolve(path12.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
6707
7027
  }
@@ -6724,7 +7044,7 @@ function startHttpServer(opts) {
6724
7044
  console.log(`[WebUI] HTTP server running on ${openUrl}`);
6725
7045
  if (opts.openBrowser) openBrowser(openUrl);
6726
7046
  void registerInstance(
6727
- { pid: process.pid, httpPort: opts.httpPort, wsPort: opts.wsPort, host: opts.wsHost, projectRoot: opts.projectRoot, projectName: path12.basename(opts.projectRoot) || opts.projectRoot, startedAt: (/* @__PURE__ */ new Date()).toISOString(), url: buildWebUIAccessUrl({ host: opts.wsHost, port: opts.httpPort, publicUrl: opts.publicUrl }) },
7047
+ { pid: process.pid, surface: "webui", httpPort: opts.httpPort, wsPort: opts.wsPort, host: opts.wsHost, projectRoot: opts.projectRoot, projectName: path12.basename(opts.projectRoot) || opts.projectRoot, startedAt: (/* @__PURE__ */ new Date()).toISOString(), url: buildWebUIAccessUrl({ host: opts.wsHost, port: opts.httpPort, publicUrl: opts.publicUrl }) },
6728
7048
  registryBaseDir
6729
7049
  ).catch((err) => console.warn(JSON.stringify({ level: "warn", event: "webui.instance_record_failed", message: errMessage(err), timestamp: (/* @__PURE__ */ new Date()).toISOString() })));
6730
7050
  });
@@ -6740,12 +7060,10 @@ function registerShutdown(deps2) {
6740
7060
  }
6741
7061
 
6742
7062
  // src/server/pre-context-services.ts
6743
- import * as path14 from "node:path";
7063
+ import * as path15 from "node:path";
6744
7064
  import { createRequire as createRequire2 } from "node:module";
6745
- import { WebSocket as WebSocket2 } from "ws";
6746
7065
  import { DefaultTokenCounter } from "@wrongstack/core/infrastructure";
6747
7066
  import {
6748
- AgentStatusTracker,
6749
7067
  AnnotationsStore,
6750
7068
  DEFAULT_SESSION_PRUNE_DAYS,
6751
7069
  DefaultModelsRegistry,
@@ -6756,7 +7074,6 @@ import {
6756
7074
  DefaultSkillLoader,
6757
7075
  DefaultSystemPromptBuilder,
6758
7076
  EventBus,
6759
- FleetNotifier,
6760
7077
  GlobalMailbox,
6761
7078
  PromptUsageStore,
6762
7079
  ProviderRegistry,
@@ -6767,8 +7084,8 @@ import {
6767
7084
  applyToolDescriptionModes,
6768
7085
  applyToolResultRenderModes,
6769
7086
  configureChildEnvGitIdentity,
7087
+ getSessionRegistry as getSessionRegistry2,
6770
7088
  resolveContextWindowPolicy,
6771
- getSessionRegistry,
6772
7089
  makeMailboxTool,
6773
7090
  makeMailInboxTool,
6774
7091
  makeFleetStatusTool,
@@ -6777,7 +7094,12 @@ import {
6777
7094
  import {
6778
7095
  createSuperMemoryTools
6779
7096
  } from "@wrongstack/super-memory";
6780
- import { MCPRegistry } from "@wrongstack/mcp";
7097
+ import {
7098
+ MCPAuthorizationManager,
7099
+ MCPRegistry,
7100
+ MCPVaultTokenStore,
7101
+ createVaultBackedMcpAuthorizationProviderFactory
7102
+ } from "@wrongstack/mcp";
6781
7103
  import { buildProviderFactoriesFromRegistry } from "@wrongstack/providers";
6782
7104
  import { createDefaultContainer } from "@wrongstack/runtime";
6783
7105
  import {
@@ -6789,7 +7111,11 @@ import {
6789
7111
  rememberTool,
6790
7112
  searchMemoryTool
6791
7113
  } from "@wrongstack/tools";
6792
- import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
7114
+ import {
7115
+ attachSessionKanbanMirror,
7116
+ hydrateSessionKanban
7117
+ } from "@wrongstack/tools/session-kanban";
7118
+ import { sessionScopedPath, toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
6793
7119
 
6794
7120
  // src/server/setup-screen.ts
6795
7121
  import { expectDefined as expectDefined2 } from "@wrongstack/core";
@@ -6927,6 +7253,21 @@ function seedContextMeta(config, context) {
6927
7253
  meta["reasoningPreserve"] = mr.reasoning?.preserve === true;
6928
7254
  meta["cacheTtl"] = mr.cache?.ttl ?? "default";
6929
7255
  }
7256
+ meta["refinerProvider"] = autonomyCfg["refinerProvider"] ?? "";
7257
+ meta["refinerModel"] = autonomyCfg["refinerModel"] ?? "";
7258
+ meta["refinerFallbackProfile"] = autonomyCfg["refinerFallbackProfile"] ?? "";
7259
+ meta["thinkingWord"] = autonomyCfg["thinkingWord"] ?? "thinking";
7260
+ meta["statuslineMode"] = autonomyCfg["statuslineMode"] ?? "detailed";
7261
+ meta["animationStyle"] = autonomyCfg["animationStyle"] ?? "rainbow";
7262
+ meta["breakerEnabled"] = config.circuitBreaker?.enabled === true;
7263
+ meta["breakerAutoKillResetMs"] = config.circuitBreaker?.autoKillResetMs ?? 6e4;
7264
+ {
7265
+ const featuresAllow = config.features?.allowOutsideProjectRoot;
7266
+ const toolsRestrict = config.tools?.restrictToProjectRoot;
7267
+ const allow = featuresAllow !== void 0 ? featuresAllow : toolsRestrict !== void 0 ? !toolsRestrict : true;
7268
+ meta["fsAccess"] = allow ? "unrestricted" : "project";
7269
+ }
7270
+ meta["debugStream"] = config.debugStream === true;
6930
7271
  const hqConfig = config.hq;
6931
7272
  meta["hqEnabled"] = hqConfig?.enabled === true;
6932
7273
  meta["hqUrl"] = hqConfig?.url ?? "";
@@ -7023,10 +7364,188 @@ async function discoverAndMergeWebuiProviders(opts) {
7023
7364
  }
7024
7365
  }
7025
7366
 
7367
+ // src/server/standalone-session-identity.ts
7368
+ import * as path14 from "node:path";
7369
+ import {
7370
+ AgentStatusTracker,
7371
+ FleetNotifier,
7372
+ getSessionRegistry,
7373
+ RecoveryLock
7374
+ } from "@wrongstack/core";
7375
+ import { WebSocket as WebSocket2 } from "ws";
7376
+ async function createStandaloneSessionIdentityLifecycle(opts) {
7377
+ const { paths, events, logger } = opts;
7378
+ const registry = opts.sessionRegistry ?? getSessionRegistry(paths.globalRoot);
7379
+ const fleetNotifier = new FleetNotifier({
7380
+ baseDir: paths.globalRoot,
7381
+ projectRoot: paths.projectRoot,
7382
+ selfPid: process.pid
7383
+ });
7384
+ let activeSessionId = opts.initialSessionId;
7385
+ let stopped = false;
7386
+ let transition = Promise.resolve();
7387
+ const statusTracker = new AgentStatusTracker({
7388
+ events,
7389
+ registry,
7390
+ sessionId: () => activeSessionId,
7391
+ onUpdate: () => fleetNotifier.notify()
7392
+ });
7393
+ const register = async (sessionId) => {
7394
+ try {
7395
+ await registry.register({
7396
+ sessionId,
7397
+ projectSlug: paths.projectSlug,
7398
+ projectRoot: paths.projectRoot,
7399
+ projectName: path14.basename(paths.projectRoot),
7400
+ workingDir: opts.workingDir,
7401
+ clientType: "webui",
7402
+ pid: process.pid,
7403
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7404
+ agents: statusTracker.getAgents()
7405
+ });
7406
+ fleetNotifier.notify();
7407
+ } catch (err) {
7408
+ logger.debug?.(`WebUI session registry update failed: ${errorMessage2(err)}`);
7409
+ }
7410
+ };
7411
+ await register(activeSessionId);
7412
+ statusTracker.start();
7413
+ const recoveryLock = opts.manageRecoveryLock === false ? void 0 : new RecoveryLock({ dir: paths.projectSessions, sessionStore: opts.sessionStore });
7414
+ let ownsRecoveryLock = false;
7415
+ if (recoveryLock) {
7416
+ try {
7417
+ await recoveryLock.write(activeSessionId);
7418
+ ownsRecoveryLock = true;
7419
+ } catch {
7420
+ }
7421
+ }
7422
+ let stopHqBridges = () => {
7423
+ };
7424
+ let closeHqPublisher = () => {
7425
+ };
7426
+ let restartHqBridges = (_sessionId) => {
7427
+ };
7428
+ if (opts.enableHqTelemetry !== false) {
7429
+ try {
7430
+ const core = await import("@wrongstack/core");
7431
+ const publisher = core.createHqPublisherFromEnv({
7432
+ clientKind: "webui",
7433
+ projectRoot: paths.projectRoot,
7434
+ projectName: path14.basename(paths.projectRoot),
7435
+ appConfig: opts.config,
7436
+ socketFactory: (url) => new WebSocket2(url)
7437
+ });
7438
+ if (publisher) {
7439
+ publisher.connect();
7440
+ closeHqPublisher = () => publisher.close();
7441
+ restartHqBridges = (sessionId) => {
7442
+ stopHqBridges();
7443
+ const stops = [];
7444
+ const add = (start) => {
7445
+ try {
7446
+ stops.push(start());
7447
+ } catch {
7448
+ }
7449
+ };
7450
+ add(
7451
+ () => core.startSessionTelemetryBridge({
7452
+ publisher,
7453
+ events,
7454
+ sessionId,
7455
+ projectRoot: paths.projectRoot,
7456
+ projectName: path14.basename(paths.projectRoot),
7457
+ globalRoot: paths.globalRoot,
7458
+ initialAgents: statusTracker.getAgents(),
7459
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
7460
+ })
7461
+ );
7462
+ add(
7463
+ () => core.startFleetTelemetryBridge({
7464
+ events,
7465
+ publisher,
7466
+ runId: sessionId,
7467
+ sessionId
7468
+ })
7469
+ );
7470
+ add(() => core.startBrainTelemetryBridge({ events, publisher, sessionId }));
7471
+ add(() => core.startWorktreeTelemetryBridge({ events, publisher, sessionId }));
7472
+ add(
7473
+ () => core.startToolTelemetryBridge({
7474
+ events,
7475
+ publisher,
7476
+ projectRoot: paths.projectRoot,
7477
+ sessionId
7478
+ })
7479
+ );
7480
+ add(() => core.startCostTelemetryBridge({ events, publisher, sessionId }));
7481
+ stopHqBridges = () => {
7482
+ for (const stop2 of stops.splice(0)) {
7483
+ try {
7484
+ stop2();
7485
+ } catch {
7486
+ }
7487
+ }
7488
+ };
7489
+ };
7490
+ restartHqBridges(activeSessionId);
7491
+ }
7492
+ } catch (err) {
7493
+ logger.debug?.(`WebUI HQ telemetry unavailable: ${errorMessage2(err)}`);
7494
+ }
7495
+ }
7496
+ const repointRecovery = async (sessionId) => {
7497
+ if (!recoveryLock) return;
7498
+ try {
7499
+ if (ownsRecoveryLock) await recoveryLock.clear();
7500
+ await recoveryLock.write(sessionId);
7501
+ ownsRecoveryLock = true;
7502
+ } catch {
7503
+ ownsRecoveryLock = false;
7504
+ }
7505
+ };
7506
+ const activate = async (sessionId) => {
7507
+ if (stopped || sessionId === activeSessionId) return;
7508
+ transition = transition.then(async () => {
7509
+ if (stopped || sessionId === activeSessionId) return;
7510
+ activeSessionId = sessionId;
7511
+ await register(sessionId);
7512
+ await repointRecovery(sessionId);
7513
+ try {
7514
+ restartHqBridges(sessionId);
7515
+ } catch (err) {
7516
+ logger.debug?.(`WebUI HQ session swap failed: ${errorMessage2(err)}`);
7517
+ }
7518
+ });
7519
+ await transition;
7520
+ };
7521
+ const stop = async () => {
7522
+ if (stopped) return;
7523
+ stopped = true;
7524
+ await transition.catch(() => void 0);
7525
+ statusTracker.stop();
7526
+ stopHqBridges();
7527
+ closeHqPublisher();
7528
+ fleetNotifier.dispose();
7529
+ try {
7530
+ await registry.markClosing();
7531
+ await registry.unregister();
7532
+ } catch {
7533
+ }
7534
+ if (recoveryLock && ownsRecoveryLock) {
7535
+ await recoveryLock.clear().catch(() => void 0);
7536
+ ownsRecoveryLock = false;
7537
+ }
7538
+ };
7539
+ return { statusTracker, activate, stop };
7540
+ }
7541
+ function errorMessage2(err) {
7542
+ return err instanceof Error ? err.message : String(err);
7543
+ }
7544
+
7026
7545
  // src/server/pre-context-services.ts
7027
7546
  var GITHUB_PROVIDERS_OVERLAY_URL = "https://raw.githubusercontent.com/WrongStack/WrongStack/main/packages/cli/data/providers.json";
7028
7547
  async function createPreContextServices(input) {
7029
- const { config, wpaths, logger, opts, projectRoot, workingDir, needsProvider } = input;
7548
+ const { config, wpaths, logger, opts, vault, projectRoot, workingDir, needsProvider } = input;
7030
7549
  const modelsRegistry = opts.services?.modelsRegistry ?? new DefaultModelsRegistry({
7031
7550
  cacheFile: wpaths.modelsCache,
7032
7551
  ttlSeconds: 0,
@@ -7045,7 +7564,7 @@ async function createPreContextServices(input) {
7045
7564
  await discoverAndMergeWebuiProviders({
7046
7565
  config,
7047
7566
  registry: modelsRegistry,
7048
- cacheDir: path14.dirname(wpaths.modelsCache),
7567
+ cacheDir: path15.dirname(wpaths.modelsCache),
7049
7568
  logger
7050
7569
  });
7051
7570
  } catch (err) {
@@ -7070,12 +7589,13 @@ async function createPreContextServices(input) {
7070
7589
  })();
7071
7590
  const memoryStore = container.resolve(TOKENS.MemoryStore);
7072
7591
  if (config.features.memory) {
7073
- toolRegistry.register(rememberTool(memoryStore));
7074
- toolRegistry.register(forgetTool(memoryStore));
7075
- toolRegistry.register(searchMemoryTool(memoryStore));
7076
- toolRegistry.register(relatedMemoryTool(memoryStore));
7077
7592
  if (isSuperMemoryService(memoryStore)) {
7078
7593
  for (const tool of createSuperMemoryTools(memoryStore)) toolRegistry.register(tool);
7594
+ } else {
7595
+ toolRegistry.register(rememberTool(memoryStore));
7596
+ toolRegistry.register(forgetTool(memoryStore));
7597
+ toolRegistry.register(searchMemoryTool(memoryStore));
7598
+ toolRegistry.register(relatedMemoryTool(memoryStore));
7079
7599
  }
7080
7600
  }
7081
7601
  toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
@@ -7088,10 +7608,23 @@ async function createPreContextServices(input) {
7088
7608
  configureDangerBypass(config.tools?.exec?.danger ?? {});
7089
7609
  configureChildEnvGitIdentity(config.git?.identity ?? null);
7090
7610
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
7091
- const mcpRegistry = new MCPRegistry({ toolRegistry, events, log: logger, cacheDir: wpaths.cacheDir });
7611
+ const mcpTokenStore = new MCPVaultTokenStore(
7612
+ path15.join(wpaths.projectDir, "mcp-auth.json"),
7613
+ vault
7614
+ );
7615
+ const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
7616
+ const mcpRegistry = new MCPRegistry({
7617
+ toolRegistry,
7618
+ events,
7619
+ log: logger,
7620
+ cacheDir: wpaths.cacheDir,
7621
+ authorizationProviderFactory: createVaultBackedMcpAuthorizationProviderFactory({
7622
+ store: mcpTokenStore
7623
+ }),
7624
+ authorizationManager: mcpAuthorizationManager
7625
+ });
7092
7626
  if (config.features.mcp && config.mcpServers) {
7093
7627
  for (const [name2, cfg] of Object.entries(config.mcpServers)) {
7094
- if (cfg.enabled === false) continue;
7095
7628
  void mcpRegistry.start({ ...cfg, name: name2 }).catch((err) => {
7096
7629
  logger.warn(`MCP server "${name2}" failed to start at boot`, err);
7097
7630
  });
@@ -7106,7 +7639,7 @@ async function createPreContextServices(input) {
7106
7639
  // surface via the SessionRegistry.
7107
7640
  isSessionInUse: async (sessionId) => {
7108
7641
  try {
7109
- const registry = getSessionRegistry(wpaths.globalRoot);
7642
+ const registry = getSessionRegistry2(wpaths.globalRoot);
7110
7643
  const live = await registry.listByProject(wpaths.projectSlug);
7111
7644
  const hit = live.find((e) => e.sessionId === sessionId);
7112
7645
  if (hit) {
@@ -7131,155 +7664,57 @@ async function createPreContextServices(input) {
7131
7664
  await input.touchProject(projectRoot, workingDir);
7132
7665
  } catch {
7133
7666
  }
7134
- let statusTracker;
7135
- try {
7136
- const registry = getSessionRegistry(wpaths.globalRoot);
7137
- await registry.register({
7138
- sessionId: session.id,
7139
- projectSlug: wpaths.projectSlug,
7667
+ const sessionIdentity = await createStandaloneSessionIdentityLifecycle({
7668
+ config,
7669
+ events,
7670
+ logger,
7671
+ paths: {
7672
+ globalRoot: wpaths.globalRoot,
7140
7673
  projectRoot,
7141
- projectName: path14.basename(projectRoot),
7142
- workingDir,
7143
- clientType: "webui",
7144
- pid: process.pid,
7145
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
7146
- });
7147
- const fleetNotifier = new FleetNotifier({ baseDir: wpaths.globalRoot, projectRoot, selfPid: process.pid });
7148
- statusTracker = new AgentStatusTracker({
7149
- events,
7150
- registry,
7151
- sessionId: () => session.id,
7152
- onUpdate: () => fleetNotifier.notify()
7153
- });
7154
- statusTracker.start();
7155
- let stopHqSessionBridge;
7156
- let hqTelemetryPublisher;
7157
- const stopHqAuxBridges = [];
7158
- try {
7159
- const {
7160
- createHqPublisherFromEnv,
7161
- startSessionTelemetryBridge,
7162
- startFleetTelemetryBridge,
7163
- startBrainTelemetryBridge,
7164
- startWorktreeTelemetryBridge,
7165
- startToolTelemetryBridge,
7166
- startCostTelemetryBridge
7167
- } = await import("@wrongstack/core");
7168
- const hqTelemetry = createHqPublisherFromEnv({
7169
- clientKind: "webui",
7170
- projectRoot,
7171
- projectName: path14.basename(projectRoot),
7172
- appConfig: config,
7173
- socketFactory: (url) => new WebSocket2(url)
7174
- });
7175
- if (hqTelemetry) {
7176
- hqTelemetry.connect();
7177
- hqTelemetryPublisher = hqTelemetry;
7178
- stopHqSessionBridge = startSessionTelemetryBridge({
7179
- publisher: hqTelemetry,
7180
- events,
7181
- sessionId: session.id,
7182
- projectRoot,
7183
- projectName: path14.basename(projectRoot),
7184
- globalRoot: wpaths.globalRoot,
7185
- initialAgents: statusTracker?.getAgents(),
7186
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
7187
- });
7188
- try {
7189
- stopHqAuxBridges.push(
7190
- startFleetTelemetryBridge({ events, publisher: hqTelemetry, runId: session.id, sessionId: session.id })
7191
- );
7192
- } catch {
7193
- }
7194
- try {
7195
- stopHqAuxBridges.push(
7196
- startBrainTelemetryBridge({ events, publisher: hqTelemetry, sessionId: session.id })
7197
- );
7198
- } catch {
7199
- }
7200
- try {
7201
- stopHqAuxBridges.push(
7202
- startWorktreeTelemetryBridge({ events, publisher: hqTelemetry, sessionId: session.id })
7203
- );
7204
- } catch {
7205
- }
7206
- try {
7207
- stopHqAuxBridges.push(
7208
- startToolTelemetryBridge({ events, publisher: hqTelemetry, projectRoot, sessionId: session.id })
7209
- );
7210
- } catch {
7211
- }
7212
- try {
7213
- stopHqAuxBridges.push(
7214
- startCostTelemetryBridge({ events, publisher: hqTelemetry, sessionId: session.id })
7215
- );
7216
- } catch {
7217
- }
7218
- }
7219
- } catch {
7220
- }
7221
- const stopTracking = async () => {
7222
- try {
7223
- fleetNotifier.dispose();
7224
- await registry.markClosing();
7225
- statusTracker?.stop();
7226
- stopHqSessionBridge?.();
7227
- for (const stop of stopHqAuxBridges) {
7228
- try {
7229
- stop();
7230
- } catch {
7231
- }
7232
- }
7233
- hqTelemetryPublisher?.close();
7234
- } catch {
7235
- }
7236
- };
7237
- process.once("beforeExit", () => {
7238
- void stopTracking();
7239
- });
7240
- process.once("SIGINT", () => {
7241
- void stopTracking();
7242
- });
7243
- process.once("SIGTERM", () => {
7244
- void stopTracking();
7245
- });
7246
- } catch {
7247
- }
7248
- let context;
7249
- const tokenCounter = new DefaultTokenCounter({
7250
- registry: modelsRegistry,
7251
- providerId: config.provider,
7252
- events,
7253
- sessionId: () => context?.session?.id ?? session.id
7254
- });
7255
- const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
7256
- const activeMode = await modeStore.getActiveMode();
7257
- const modeId = activeMode?.id ?? "default";
7258
- const modePrompt = activeMode?.prompt ?? "";
7259
- const customModeStore = createCustomModeStore(wpaths.configDir);
7260
- await customModeStore.load();
7261
- console.log("[WebUI] Custom context modes loaded:", customModeStore.list().filter((m) => m.custom).length, "custom");
7262
- const resolvedModel = await resolveProviderModelMetadata(
7263
- modelsRegistry,
7264
- config.provider,
7265
- config.model,
7266
- config.providers?.[config.provider]
7267
- );
7268
- const modelCapabilities = resolvedModel?.capabilities ? { maxContextTokens: resolvedModel.capabilities.maxContext, supportsTools: resolvedModel.capabilities.tools, supportsVision: resolvedModel.capabilities.vision, supportsReasoning: resolvedModel.capabilities.reasoning } : void 0;
7269
- const modelCapabilitiesRef = { current: modelCapabilities };
7270
- const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
7271
- const skillInstaller = config.features.skills ? new SkillInstaller({
7272
- manifestPath: path14.join(wpaths.globalRoot, "installed-skills.json"),
7273
- projectSkillsDir: wpaths.inProjectSkills,
7274
- globalSkillsDir: wpaths.globalSkills,
7275
- projectHash: wpaths.projectHash,
7276
- skillLoader
7277
- }) : void 0;
7278
- const promptsEnabled = config.features.prompts !== false;
7279
- const bundledPromptsDir = promptsEnabled ? (() => {
7674
+ projectSlug: wpaths.projectSlug,
7675
+ projectSessions: wpaths.projectSessions
7676
+ },
7677
+ workingDir,
7678
+ initialSessionId: session.id,
7679
+ sessionStore,
7680
+ manageRecoveryLock: !opts.services?.session
7681
+ });
7682
+ const statusTracker = sessionIdentity.statusTracker;
7683
+ let context;
7684
+ const tokenCounter = new DefaultTokenCounter({
7685
+ registry: modelsRegistry,
7686
+ providerId: config.provider,
7687
+ events,
7688
+ sessionId: () => context?.session?.id ?? session.id
7689
+ });
7690
+ const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
7691
+ const activeMode = await modeStore.getActiveMode();
7692
+ const modeId = activeMode?.id ?? "default";
7693
+ const modePrompt = activeMode?.prompt ?? "";
7694
+ const customModeStore = createCustomModeStore(wpaths.configDir);
7695
+ await customModeStore.load();
7696
+ console.log("[WebUI] Custom context modes loaded:", customModeStore.list().filter((m) => m.custom).length, "custom");
7697
+ const resolvedModel = await resolveProviderModelMetadata(
7698
+ modelsRegistry,
7699
+ config.provider,
7700
+ config.model,
7701
+ config.providers?.[config.provider]
7702
+ );
7703
+ const modelCapabilities = resolvedModel?.capabilities ? { maxContextTokens: resolvedModel.capabilities.maxContext, supportsTools: resolvedModel.capabilities.tools, supportsVision: resolvedModel.capabilities.vision, supportsReasoning: resolvedModel.capabilities.reasoning } : void 0;
7704
+ const modelCapabilitiesRef = { current: modelCapabilities };
7705
+ const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
7706
+ const skillInstaller = config.features.skills ? new SkillInstaller({
7707
+ manifestPath: path15.join(wpaths.globalRoot, "installed-skills.json"),
7708
+ projectSkillsDir: wpaths.inProjectSkills,
7709
+ globalSkillsDir: wpaths.globalSkills,
7710
+ projectHash: wpaths.projectHash,
7711
+ skillLoader
7712
+ }) : void 0;
7713
+ const promptsEnabled = config.features.prompts !== false;
7714
+ const bundledPromptsDir = promptsEnabled ? (() => {
7280
7715
  try {
7281
7716
  const req = createRequire2(import.meta.url);
7282
- return path14.join(path14.dirname(req.resolve("@wrongstack/core/package.json")), "data", "prompts");
7717
+ return path15.join(path15.dirname(req.resolve("@wrongstack/core/package.json")), "data", "prompts");
7283
7718
  } catch {
7284
7719
  return void 0;
7285
7720
  }
@@ -7289,6 +7724,9 @@ async function createPreContextServices(input) {
7289
7724
  const promptsCtx = { promptLoader, promptUsage };
7290
7725
  const systemPromptBuilder = new DefaultSystemPromptBuilder({
7291
7726
  memoryStore,
7727
+ // Super Memory's turn middleware is the single memory-injection channel;
7728
+ // don't also inject a static memory section here (avoids double injection).
7729
+ injectMemory: false,
7292
7730
  skillLoader,
7293
7731
  modeStore,
7294
7732
  modeId,
@@ -7331,6 +7769,16 @@ async function createPreContextServices(input) {
7331
7769
  const initialContextPolicy = resolveContextWindowPolicy(config.context);
7332
7770
  context.meta["contextWindowMode"] = initialContextPolicy.id;
7333
7771
  context.meta["contextWindowPolicy"] = initialContextPolicy;
7772
+ context.state.setMeta(
7773
+ "plan.path",
7774
+ sessionScopedPath(wpaths.projectSessions, session.id, ".plan.json")
7775
+ );
7776
+ context.state.setMeta(
7777
+ "task.path",
7778
+ sessionScopedPath(wpaths.projectSessions, session.id, ".tasks.json")
7779
+ );
7780
+ await hydrateSessionKanban(context);
7781
+ attachSessionKanbanMirror(context);
7334
7782
  seedContextMeta(config, context);
7335
7783
  return {
7336
7784
  modelsRegistry,
@@ -7347,6 +7795,7 @@ async function createPreContextServices(input) {
7347
7795
  session,
7348
7796
  sessionStartedAt,
7349
7797
  statusTracker,
7798
+ sessionIdentity,
7350
7799
  tokenCounter,
7351
7800
  modeStore,
7352
7801
  modeId,
@@ -7396,6 +7845,7 @@ function patchConfig(config, updates) {
7396
7845
  }
7397
7846
 
7398
7847
  // src/server/backend-services.ts
7848
+ import { join as join12 } from "node:path";
7399
7849
  import {
7400
7850
  Agent,
7401
7851
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
@@ -7409,9 +7859,10 @@ import {
7409
7859
  installDesignStudioMiddleware,
7410
7860
  ObservableBrainArbiter as ObservableBrainArbiterCtor,
7411
7861
  resolveContextWindowPolicy as resolveContextWindowPolicy2,
7412
- createAutonomyBrain,
7413
- createTieredBrainArbiter,
7414
- DefaultBrainArbiter,
7862
+ BrainDecisionLedger,
7863
+ createBrainRuntime,
7864
+ resolveBrainConfigDefaults,
7865
+ EscalationRoutingBrainArbiter,
7415
7866
  GlobalMailbox as GlobalMailbox2,
7416
7867
  mailboxSessionTag,
7417
7868
  SessionMemoryConsolidator,
@@ -7425,12 +7876,13 @@ import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
7425
7876
  var REPLAY_LIMIT = 50;
7426
7877
  var PAUSE_TIMEOUT_MS = 6e4;
7427
7878
  var CollaborationWebSocketHandler = class {
7428
- constructor(events, logger, reader, annotations, bus) {
7879
+ constructor(events, logger, reader, annotations, bus, options = {}) {
7429
7880
  this.events = events;
7430
7881
  this.logger = logger;
7431
7882
  this.reader = reader;
7432
7883
  this.annotations = annotations;
7433
7884
  this.bus = bus;
7885
+ this.options = options;
7434
7886
  this.subscribe();
7435
7887
  this.bus?.onInjectionConsumed((info) => this.broadcastInjectionConsumed(info));
7436
7888
  }
@@ -7439,6 +7891,7 @@ var CollaborationWebSocketHandler = class {
7439
7891
  reader;
7440
7892
  annotations;
7441
7893
  bus;
7894
+ options;
7442
7895
  clients = /* @__PURE__ */ new Set();
7443
7896
  /** sessionId → participants currently watching it. */
7444
7897
  bySession = /* @__PURE__ */ new Map();
@@ -7466,11 +7919,16 @@ var CollaborationWebSocketHandler = class {
7466
7919
  handleMessage(ws, msg) {
7467
7920
  if (msg.type === "collab.join") {
7468
7921
  const payload = msg.payload;
7469
- if (!payload?.sessionId) {
7922
+ if (typeof payload?.sessionId !== "string" || payload.sessionId.trim().length === 0) {
7470
7923
  this.send(ws, this.errorMessage("collab.join requires sessionId"));
7471
7924
  return true;
7472
7925
  }
7473
- this.join(ws, payload.sessionId, payload.role ?? "observer");
7926
+ const role = payload.role ?? "observer";
7927
+ if (role !== "observer" && role !== "annotator" && role !== "controller") {
7928
+ this.send(ws, this.errorMessage(`unknown collaboration role '${String(role)}'`));
7929
+ return true;
7930
+ }
7931
+ this.join(ws, payload.sessionId, role);
7474
7932
  return true;
7475
7933
  }
7476
7934
  if (msg.type === "collab.leave") {
@@ -7505,6 +7963,20 @@ var CollaborationWebSocketHandler = class {
7505
7963
  }
7506
7964
  // ── Join / leave flow ──────────────────────────────────────────────────
7507
7965
  join(ws, sessionId, role) {
7966
+ const activeSessionId = this.options.getActiveSessionId?.();
7967
+ if (activeSessionId !== void 0 && sessionId !== activeSessionId) {
7968
+ this.send(
7969
+ ws,
7970
+ this.errorMessage(
7971
+ `collab.join sessionId mismatch (active: ${activeSessionId})`
7972
+ )
7973
+ );
7974
+ return;
7975
+ }
7976
+ if (this.findParticipant(ws)) {
7977
+ this.send(ws, this.errorMessage("collab.join requires leaving the current session first"));
7978
+ return;
7979
+ }
7508
7980
  if (role === "controller" && !this.bus) {
7509
7981
  this.send(
7510
7982
  ws,
@@ -7523,6 +7995,13 @@ var CollaborationWebSocketHandler = class {
7523
7995
  );
7524
7996
  return;
7525
7997
  }
7998
+ if (role !== "observer" && this.options.authorizeRole?.({ ws, sessionId, requestedRole: role }) !== true) {
7999
+ this.send(
8000
+ ws,
8001
+ this.errorMessage(`role '${role}' requires explicit server authorization`)
8002
+ );
8003
+ return;
8004
+ }
7526
8005
  const participant = {
7527
8006
  participantId: randomUUID(),
7528
8007
  ws,
@@ -7776,17 +8255,13 @@ var CollaborationWebSocketHandler = class {
7776
8255
  type: "collab.event",
7777
8256
  payload: { kind, payload, at: (/* @__PURE__ */ new Date()).toISOString() }
7778
8257
  };
7779
- const data = JSON.stringify(msg);
7780
- for (const bucket of this.bySession.values()) {
7781
- for (const p of bucket) {
7782
- try {
7783
- if (p.ws.readyState === 1) p.ws.send(data);
7784
- } catch (err) {
7785
- this.logger.debug?.(
7786
- `collab broadcast failed: ${toErrorMessage6(err)}`
7787
- );
7788
- }
7789
- }
8258
+ const activeSessionId = this.options.getActiveSessionId?.();
8259
+ if (activeSessionId !== void 0) {
8260
+ this.broadcast(activeSessionId, msg);
8261
+ return;
8262
+ }
8263
+ for (const sessionId of this.bySession.keys()) {
8264
+ this.broadcast(sessionId, msg);
7790
8265
  }
7791
8266
  }
7792
8267
  /**
@@ -8110,7 +8585,8 @@ var CollaborationWebSocketHandler = class {
8110
8585
  * Bus callback: a queued injection was spliced into a real tool call. Re-emit
8111
8586
  * `collab.injection.granted` with phase `'consumed'` and the now-known tool
8112
8587
  * name. The injection carries no sessionId, so resolve it from the author's
8113
- * current session; if they've already left, fall back to every live session.
8588
+ * current participant. If the author has left, fail closed rather than leak
8589
+ * the payload to unrelated sessions.
8114
8590
  */
8115
8591
  broadcastInjectionConsumed(info) {
8116
8592
  let sessionId = null;
@@ -8139,14 +8615,16 @@ var CollaborationWebSocketHandler = class {
8139
8615
  if (sessionId) {
8140
8616
  this.broadcast(sessionId, message(sessionId));
8141
8617
  } else {
8142
- for (const sid of this.bySession.keys()) this.broadcast(sid, message(sid));
8618
+ this.logger.debug?.(
8619
+ `collab: consumed injection ${info.toolUseId} has no live author; broadcast suppressed`
8620
+ );
8143
8621
  }
8144
8622
  }
8145
8623
  };
8146
8624
 
8147
8625
  // src/server/codebase-indexing.ts
8148
8626
  import * as fs12 from "node:fs";
8149
- import * as path15 from "node:path";
8627
+ import * as path16 from "node:path";
8150
8628
  import {
8151
8629
  cancelPendingReindexes,
8152
8630
  enqueueReindex,
@@ -8199,7 +8677,7 @@ function setupWebUICodebaseIndexing(deps2) {
8199
8677
  if (!filename) return;
8200
8678
  const rel = filename.toString();
8201
8679
  if (isIgnored(rel)) return;
8202
- const abs = path15.resolve(deps2.projectRoot, rel);
8680
+ const abs = path16.resolve(deps2.projectRoot, rel);
8203
8681
  enqueueFile(abs);
8204
8682
  });
8205
8683
  watcher.on("error", (err) => deps2.logger.debug(`webui codebase index watcher error: ${err}`));
@@ -8212,7 +8690,7 @@ function setupWebUICodebaseIndexing(deps2) {
8212
8690
  }
8213
8691
  function enqueueFile(filePath) {
8214
8692
  if (!idx.onEdit && !idx.watchExternal) return;
8215
- const abs = path15.isAbsolute(filePath) ? path15.normalize(filePath) : path15.resolve(deps2.projectRoot, filePath);
8693
+ const abs = path16.isAbsolute(filePath) ? path16.normalize(filePath) : path16.resolve(deps2.projectRoot, filePath);
8216
8694
  if (!isInside2(deps2.projectRoot, abs) || !isIndexableFile(abs)) return;
8217
8695
  enqueueReindex({
8218
8696
  projectRoot: deps2.projectRoot,
@@ -8249,9 +8727,9 @@ function isIgnored(rel) {
8249
8727
  return rel.split(/[/\\]/).some((seg) => IGNORE_DIRS.has(seg));
8250
8728
  }
8251
8729
  function isInside2(root, target) {
8252
- const normalizedRoot = path15.resolve(root);
8253
- const normalizedTarget = path15.resolve(target);
8254
- return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(normalizedRoot + path15.sep);
8730
+ const normalizedRoot = path16.resolve(root);
8731
+ const normalizedTarget = path16.resolve(target);
8732
+ return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(normalizedRoot + path16.sep);
8255
8733
  }
8256
8734
 
8257
8735
  // src/server/discover-mailbox-bridge.ts
@@ -8784,21 +9262,59 @@ async function createAgentServices(input) {
8784
9262
  agent.extensions.register(new SessionMemoryConsolidator({ memoryStore }));
8785
9263
  }
8786
9264
  console.log("[WebUI] Agent initialized");
8787
- const brainSettings = { maxAutoRisk: "medium" };
8788
- const autonomousBrain = {
8789
- decide: (request) => createAutonomyBrain({
8790
- provider,
8791
- model: context.model,
8792
- maxAutoRisk: "all"
8793
- // the tiered ceiling gates risk — keep inner permissive
8794
- }).decide(request)
9265
+ const brainCfg = resolveBrainConfigDefaults(config.brain, {
9266
+ fallbackModels: config.fallbackModels
9267
+ });
9268
+ const brainLedgerPath = join12(wpaths.projectDir, "brain-ledger.jsonl");
9269
+ let brainLedgerEnabled = brainCfg.ledger?.enabled !== false;
9270
+ let brainLedger;
9271
+ const startBrainLedger = () => {
9272
+ if (brainLedger) return;
9273
+ brainLedger = new BrainDecisionLedger({ events, filePath: brainLedgerPath });
9274
+ void brainLedger.start();
9275
+ };
9276
+ if (brainLedgerEnabled) startBrainLedger();
9277
+ const brainRuntime = createBrainRuntime({
9278
+ initialConfig: brainCfg,
9279
+ defaultProviderId: config.provider,
9280
+ sessionProvider: () => provider,
9281
+ sessionModel: () => context.model,
9282
+ resolveProvider: (providerId) => {
9283
+ const savedCfg = config.providers?.[providerId] ?? {};
9284
+ return providerRegistry.create({
9285
+ ...savedCfg,
9286
+ apiKey: savedCfg.apiKey ?? config.apiKey,
9287
+ baseUrl: savedCfg.baseUrl ?? config.baseUrl,
9288
+ type: providerId
9289
+ });
9290
+ },
9291
+ ledger: {
9292
+ getPath: () => brainLedgerEnabled ? brainLedgerPath : void 0,
9293
+ isEnabled: () => brainLedgerEnabled,
9294
+ setEnabled: (on) => {
9295
+ brainLedgerEnabled = on;
9296
+ if (on) {
9297
+ startBrainLedger();
9298
+ } else {
9299
+ void brainLedger?.stop();
9300
+ brainLedger = void 0;
9301
+ }
9302
+ },
9303
+ failureStreakFor: (request) => brainLedger?.failureStreakFor(request) ?? 0,
9304
+ getDecisionDigest: (request) => brainLedger?.digestFor(request)
9305
+ },
9306
+ persist: input.persistBrainConfig
9307
+ });
9308
+ const brainSettings = {
9309
+ get maxAutoRisk() {
9310
+ return brainRuntime.getMaxAutoRisk();
9311
+ },
9312
+ set maxAutoRisk(level) {
9313
+ void brainRuntime.apply({ maxAutoRisk: level }).persisted;
9314
+ }
8795
9315
  };
8796
9316
  const brain = new ObservableBrainArbiterCtor(
8797
- createTieredBrainArbiter({
8798
- policy: new DefaultBrainArbiter(),
8799
- autonomous: autonomousBrain,
8800
- getMaxAutoRisk: () => brainSettings.maxAutoRisk
8801
- }),
9317
+ new EscalationRoutingBrainArbiter(brainRuntime.arbiter, void 0, () => "headless"),
8802
9318
  events
8803
9319
  );
8804
9320
  container.bind(TOKENS2.BrainArbiter, () => brain);
@@ -8838,6 +9354,12 @@ async function createAgentServices(input) {
8838
9354
  const brainMonitor = new BrainMonitor({
8839
9355
  events,
8840
9356
  brain,
9357
+ toolFailureStreak: brainCfg.monitor?.toolFailureStreak,
9358
+ errorStormCount: brainCfg.monitor?.errorStormCount,
9359
+ stallMs: brainCfg.monitor?.stallMs,
9360
+ fileChurnThreshold: brainCfg.monitor?.fileChurnThreshold,
9361
+ fileChurnWindowMs: brainCfg.monitor?.fileChurnWindowMs,
9362
+ cooldownMs: brainCfg.monitor?.cooldownMs,
8841
9363
  sessionId: () => context.session?.id,
8842
9364
  intervene: async ({ subject, body }) => {
8843
9365
  const tag = mailboxSessionTag(input.sessionGetter().id);
@@ -8902,7 +9424,10 @@ async function createAgentServices(input) {
8902
9424
  logger,
8903
9425
  input.sessionReader,
8904
9426
  input.annotationsStore,
8905
- collabBus
9427
+ collabBus,
9428
+ {
9429
+ getActiveSessionId: () => context.session.id
9430
+ }
8906
9431
  );
8907
9432
  return {
8908
9433
  collabBus,
@@ -8914,8 +9439,13 @@ async function createAgentServices(input) {
8914
9439
  pipelines,
8915
9440
  brain,
8916
9441
  brainSettings,
9442
+ brainRuntime,
8917
9443
  brainLog,
8918
9444
  brainMonitor,
9445
+ // Getter: ledger toggles swap the instance, shutdown must stop the LIVE one.
9446
+ get brainLedger() {
9447
+ return brainLedger;
9448
+ },
8919
9449
  codebaseIndexing,
8920
9450
  autoPhaseHandler,
8921
9451
  specsHandler,
@@ -9091,7 +9621,18 @@ function createConnectionHandler(opts) {
9091
9621
  }
9092
9622
 
9093
9623
  // src/server/message-dispatcher.ts
9094
- import path16 from "node:path";
9624
+ import path17 from "node:path";
9625
+ import {
9626
+ buildUserContentBlocks,
9627
+ IncomingImageError,
9628
+ parseIncomingImages
9629
+ } from "@wrongstack/core/utils";
9630
+ import {
9631
+ createToolVisionAdapters,
9632
+ ImageInputUnsupportedError,
9633
+ routeImagesForModel,
9634
+ VisionUrlBlockedError
9635
+ } from "@wrongstack/runtime/vision";
9095
9636
 
9096
9637
  // src/server/autophase-routes.ts
9097
9638
  async function handleAutoPhaseRoute(_ws, msg, handlers) {
@@ -9112,6 +9653,12 @@ async function handleBrainRoute(ws, msg, handlers) {
9112
9653
  case "brain.ask":
9113
9654
  await handlers.ask(ws, msg);
9114
9655
  return true;
9656
+ case "brain.config.get":
9657
+ await handlers.configGet(ws, msg);
9658
+ return true;
9659
+ case "brain.config.set":
9660
+ await handlers.configSet(ws, msg);
9661
+ return true;
9115
9662
  default:
9116
9663
  return false;
9117
9664
  }
@@ -9365,8 +9912,10 @@ import {
9365
9912
  serializeTaskGraph
9366
9913
  } from "@wrongstack/core";
9367
9914
  import {
9915
+ addCheckToTask,
9368
9916
  addColumn,
9369
9917
  addGoalMetricToTask,
9918
+ addNoteToTask,
9370
9919
  addTask,
9371
9920
  assignTask,
9372
9921
  claimReadyTask,
@@ -9380,29 +9929,47 @@ import {
9380
9929
  getKanbanQueueHealth,
9381
9930
  getTask,
9382
9931
  getTaskChain,
9383
- listReadyTasks,
9384
9932
  listBoards,
9933
+ listReadyTasks,
9385
9934
  mergeTasks,
9386
9935
  moveTask,
9387
9936
  parseLinesIntoTasks,
9937
+ reconcileKanbanBoard,
9938
+ recoverStaleTaskAssignments,
9939
+ releaseTaskClaim,
9388
9940
  removeBoard,
9389
9941
  removeColumn,
9390
9942
  removeTask,
9391
- releaseTaskClaim,
9392
9943
  setTaskChain,
9393
9944
  splitTask,
9394
9945
  syncBoardFromTaskGraph,
9395
9946
  transferTaskToBoard,
9396
9947
  updateBoard,
9948
+ updateCheckOnTask,
9397
9949
  updateGoalMetricOnTask,
9398
9950
  updateTask
9399
9951
  } from "@wrongstack/kanban";
9952
+ import { applySessionKanbanTaskToSource } from "@wrongstack/tools/session-kanban";
9953
+ async function syncSessionSource(ctx, task, remove = false) {
9954
+ if (!ctx.context) return;
9955
+ const update = await applySessionKanbanTaskToSource(ctx.context, task, { remove });
9956
+ const sessionId = ctx.context.session?.id ?? "";
9957
+ if (update.todos)
9958
+ ctx.broadcast?.({ type: "todos.updated", payload: { sessionId, todos: update.todos } });
9959
+ if (update.tasks)
9960
+ ctx.broadcast?.({ type: "tasks.updated", payload: { sessionId, tasks: update.tasks.tasks } });
9961
+ if (update.plan)
9962
+ ctx.broadcast?.({ type: "plan.updated", payload: { sessionId, plan: update.plan } });
9963
+ }
9400
9964
  function ok(ws, type, data) {
9401
9965
  send(ws, { type, payload: { success: true, data: data ?? null } });
9402
9966
  }
9403
9967
  function fail(ws, type, message) {
9404
9968
  send(ws, { type, payload: { success: false, error: message } });
9405
9969
  }
9970
+ function has(payload, key) {
9971
+ return payload !== void 0 && Object.hasOwn(payload, key);
9972
+ }
9406
9973
  async function handleKanbanRoute(ws, msg, ctx) {
9407
9974
  if (!msg.type.startsWith("kanban.")) return false;
9408
9975
  const payload = msg.payload;
@@ -9431,6 +9998,38 @@ async function handleKanbanRoute(ws, msg, ctx) {
9431
9998
  ok(ws, type, await getKanbanQueueHealth(ctx.projectRoot, { boardId: hBoardId }));
9432
9999
  return true;
9433
10000
  }
10001
+ case "kanban.supervisor.status":
10002
+ case "kanban.supervisor.audit": {
10003
+ const boardId = payload?.boardId;
10004
+ if (!boardId) {
10005
+ fail(ws, type, "boardId required");
10006
+ return true;
10007
+ }
10008
+ const board = await getBoard(ctx.projectRoot, boardId);
10009
+ if (!board) {
10010
+ fail(ws, type, `Board not found: ${boardId}`);
10011
+ return true;
10012
+ }
10013
+ const reconciled = await reconcileKanbanBoard(ctx.projectRoot, boardId);
10014
+ let health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
10015
+ const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments(ctx.projectRoot, boardId, {
10016
+ mode: board.supervisor?.recoveryMode ?? "auto",
10017
+ reason: "On-demand standalone Kanban supervisor audit."
10018
+ }) : null;
10019
+ if (recovered) health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
10020
+ const anomalyCount = health.staleAssignments.count + health.dependencyBlocked.count + health.failedRetryable.count + health.counts.failed + health.counts.blocked;
10021
+ ok(ws, type, {
10022
+ boardId,
10023
+ status: board.supervisor?.enabled === false ? "disabled" : anomalyCount ? "attention" : "healthy",
10024
+ mode: board.supervisor?.mode ?? "deterministic",
10025
+ lastAuditAt: (/* @__PURE__ */ new Date()).toISOString(),
10026
+ reconciledTaskIds: reconciled?.tasks.map((task) => task.id) ?? [],
10027
+ staleRecoveredTaskIds: recovered?.tasks.map((task) => task.id) ?? [],
10028
+ anomalyCount,
10029
+ 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`
10030
+ });
10031
+ return true;
10032
+ }
9434
10033
  case "kanban.create": {
9435
10034
  const title = payload?.title;
9436
10035
  if (!title) {
@@ -9459,7 +10058,10 @@ async function handleKanbanRoute(ws, msg, ctx) {
9459
10058
  ...payload?.title ? { title: payload.title } : {},
9460
10059
  ...payload?.description ? { description: payload.description } : {},
9461
10060
  ...payload?.tags ? { tags: payload.tags } : {},
9462
- ...payload?.columns ? { columns: payload.columns } : {}
10061
+ ...payload?.columns ? { columns: payload.columns } : {},
10062
+ ...has(payload, "supervisor") ? {
10063
+ supervisor: payload?.supervisor ?? null
10064
+ } : {}
9463
10065
  });
9464
10066
  board ? ok(ws, type, board) : fail(ws, type, `Board not found: ${boardId}`);
9465
10067
  return true;
@@ -9486,6 +10088,11 @@ async function handleKanbanRoute(ws, msg, ctx) {
9486
10088
  return true;
9487
10089
  }
9488
10090
  const board = await getBoard(ctx.projectRoot, boardId);
10091
+ const activeSessionId = ctx.context?.session?.id;
10092
+ if (activeSessionId && board?.tags?.includes(`session:${activeSessionId}`)) {
10093
+ fail(ws, type, "The active session Kanban board cannot be deleted.");
10094
+ return true;
10095
+ }
9489
10096
  ok(ws, type, {
9490
10097
  removed: board ? await removeBoard(ctx.projectRoot, board.id) : false,
9491
10098
  boardId: board?.id ?? boardId
@@ -9666,15 +10273,31 @@ async function handleKanbanRoute(ws, msg, ctx) {
9666
10273
  return true;
9667
10274
  }
9668
10275
  const board = await updateTask(ctx.projectRoot, boardId, taskId, {
9669
- ...payload?.title ? { title: payload.title } : {},
9670
- ...payload?.description ? { description: payload.description } : {},
9671
- ...payload?.columnId ? { columnId: payload.columnId } : {},
9672
- ...payload?.priority ? { priority: payload.priority } : {},
9673
- ...payload?.status ? { status: payload.status } : {},
9674
- ...payload?.assignedAgent ? { assignedAgent: payload.assignedAgent } : {},
9675
- ...payload?.labels ? { labels: payload.labels } : {}
10276
+ ...has(payload, "title") ? { title: payload?.title } : {},
10277
+ ...has(payload, "description") ? { description: payload?.description ?? "" } : {},
10278
+ ...has(payload, "columnId") ? { columnId: payload?.columnId } : {},
10279
+ ...has(payload, "priority") ? { priority: payload?.priority } : {},
10280
+ ...has(payload, "type") ? { type: payload?.type } : {},
10281
+ ...has(payload, "status") ? { status: payload?.status } : {},
10282
+ ...has(payload, "dependsOn") ? { dependsOn: payload?.dependsOn ?? [] } : {},
10283
+ ...has(payload, "chain") ? { chain: payload?.chain ?? null } : {},
10284
+ ...has(payload, "labels") ? { labels: payload?.labels ?? [] } : {},
10285
+ ...has(payload, "estimatedHours") ? { estimatedHours: Number(payload?.estimatedHours ?? 0) } : {},
10286
+ ...has(payload, "actualHours") ? { actualHours: Number(payload?.actualHours ?? 0) } : {},
10287
+ ...has(payload, "retryPolicy") ? {
10288
+ retryPolicy: payload?.retryPolicy ?? null
10289
+ } : {},
10290
+ ...has(payload, "costCeilingUsd") ? {
10291
+ costCeilingUsd: payload?.costCeilingUsd === null || payload?.costCeilingUsd === "" ? null : Number(payload?.costCeilingUsd)
10292
+ } : {}
9676
10293
  });
9677
- board ? ok(ws, type, findTask(board.tasks, taskId)) : fail(ws, type, "Board or task not found");
10294
+ if (!board) {
10295
+ fail(ws, type, "Board or task not found");
10296
+ return true;
10297
+ }
10298
+ const task = findTask(board.tasks, taskId);
10299
+ if (task) await syncSessionSource(ctx, task);
10300
+ ok(ws, type, task);
9678
10301
  return true;
9679
10302
  }
9680
10303
  case "kanban.task.move": {
@@ -9692,7 +10315,13 @@ async function handleKanbanRoute(ws, msg, ctx) {
9692
10315
  columnId,
9693
10316
  payload?.order
9694
10317
  );
9695
- board ? ok(ws, type, findTask(board.tasks, taskId)) : fail(ws, type, "Move failed");
10318
+ if (!board) {
10319
+ fail(ws, type, "Move failed");
10320
+ return true;
10321
+ }
10322
+ const task = findTask(board.tasks, taskId);
10323
+ if (task) await syncSessionSource(ctx, task);
10324
+ ok(ws, type, task);
9696
10325
  return true;
9697
10326
  }
9698
10327
  case "kanban.task.copy":
@@ -9751,8 +10380,10 @@ async function handleKanbanRoute(ws, msg, ctx) {
9751
10380
  ...payload?.role ? { role: payload.role } : {},
9752
10381
  ...payload?.provider ? { provider: payload.provider } : {},
9753
10382
  ...payload?.model ? { model: payload.model } : {},
10383
+ ...payload?.modelRouting ? { modelRouting: payload.modelRouting } : {},
9754
10384
  ...payload?.fallbackProfile ? { fallbackProfile: payload.fallbackProfile } : {},
9755
10385
  ...payload?.fallbackModels ? { fallbackModels: payload.fallbackModels } : {},
10386
+ ...payload?.skills ? { skills: payload.skills } : {},
9756
10387
  ...payload?.tools ? { tools: payload.tools } : {},
9757
10388
  ...payload?.allowedCapabilities ? { allowedCapabilities: payload.allowedCapabilities } : {},
9758
10389
  ...payload?.assignee ? { assignee: payload.assignee } : {},
@@ -9771,7 +10402,10 @@ async function handleKanbanRoute(ws, msg, ctx) {
9771
10402
  const board = await releaseTaskClaim(ctx.projectRoot, boardId, taskId, {
9772
10403
  ...payload?.status ? { status: payload.status } : {},
9773
10404
  ...payload?.reason ? { reason: payload.reason } : {},
9774
- ...typeof payload?.clearAssignee === "boolean" ? { clearAssignee: payload.clearAssignee } : {}
10405
+ ...typeof payload?.clearAssignee === "boolean" ? { clearAssignee: payload.clearAssignee } : {},
10406
+ ...typeof payload?.maxAttempts === "number" ? { maxAttempts: payload.maxAttempts } : {},
10407
+ ...typeof payload?.costCeilingUsd === "number" ? { costCeilingUsd: payload.costCeilingUsd } : {},
10408
+ ...payload?.retryPolicy ? { retryPolicy: payload.retryPolicy } : {}
9775
10409
  });
9776
10410
  board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
9777
10411
  return true;
@@ -9827,14 +10461,66 @@ async function handleKanbanRoute(ws, msg, ctx) {
9827
10461
  ...payload?.role ? { role: payload.role } : {},
9828
10462
  ...payload?.provider ? { provider: payload.provider } : {},
9829
10463
  ...payload?.model ? { model: payload.model } : {},
10464
+ ...payload?.modelRouting ? { modelRouting: payload.modelRouting } : {},
9830
10465
  ...payload?.fallbackProfile ? { fallbackProfile: payload.fallbackProfile } : {},
9831
10466
  ...payload?.fallbackModels ? { fallbackModels: payload.fallbackModels } : {},
10467
+ ...payload?.skills ? { skills: payload.skills } : {},
9832
10468
  ...payload?.tools ? { tools: payload.tools } : {},
9833
- ...payload?.allowedCapabilities ? { allowedCapabilities: payload.allowedCapabilities } : {}
10469
+ ...payload?.allowedCapabilities ? { allowedCapabilities: payload.allowedCapabilities } : {},
10470
+ ...payload?.assignee ? { assignee: payload.assignee } : {},
10471
+ ...payload?.maxAttempts !== void 0 ? { maxAttempts: Number(payload.maxAttempts) } : {},
10472
+ ...payload?.costCeilingUsd !== void 0 ? { costCeilingUsd: Number(payload.costCeilingUsd) } : {},
10473
+ ...payload?.retryPolicy ? { retryPolicy: payload.retryPolicy } : {}
9834
10474
  });
9835
10475
  board ? ok(ws, type, findTask(board.tasks, taskId)) : fail(ws, type, "Board or task not found");
9836
10476
  return true;
9837
10477
  }
10478
+ case "kanban.task.check.add": {
10479
+ const boardId = payload?.boardId;
10480
+ const taskId = payload?.taskId;
10481
+ const description = payload?.description;
10482
+ if (!boardId || !taskId || !description) {
10483
+ fail(ws, type, "boardId, taskId, and description required");
10484
+ return true;
10485
+ }
10486
+ const board = await addCheckToTask(ctx.projectRoot, boardId, taskId, {
10487
+ description,
10488
+ type: payload?.checkType ?? "manual",
10489
+ status: payload?.status ?? "pending"
10490
+ });
10491
+ board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
10492
+ return true;
10493
+ }
10494
+ case "kanban.task.check.update": {
10495
+ const boardId = payload?.boardId;
10496
+ const taskId = payload?.taskId;
10497
+ const checkId = payload?.checkId;
10498
+ if (!boardId || !taskId || !checkId) {
10499
+ fail(ws, type, "boardId, taskId, and checkId required");
10500
+ return true;
10501
+ }
10502
+ const board = await updateCheckOnTask(ctx.projectRoot, boardId, taskId, checkId, {
10503
+ ...has(payload, "status") ? { status: payload?.status } : {}
10504
+ });
10505
+ if (!board) fail(ws, type, "Check not found");
10506
+ else ok(ws, type, (await reconcileKanbanBoard(ctx.projectRoot, boardId))?.board ?? board);
10507
+ return true;
10508
+ }
10509
+ case "kanban.task.note.add": {
10510
+ const boardId = payload?.boardId;
10511
+ const taskId = payload?.taskId;
10512
+ const content = payload?.content;
10513
+ if (!boardId || !taskId || !content) {
10514
+ fail(ws, type, "boardId, taskId, and content required");
10515
+ return true;
10516
+ }
10517
+ const board = await addNoteToTask(ctx.projectRoot, boardId, taskId, {
10518
+ author: payload?.author ?? "webui",
10519
+ content
10520
+ });
10521
+ board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
10522
+ return true;
10523
+ }
9838
10524
  case "kanban.task.dispatch":
9839
10525
  fail(
9840
10526
  ws,
@@ -9842,6 +10528,9 @@ async function handleKanbanRoute(ws, msg, ctx) {
9842
10528
  "Kanban agent dispatch is only available from the CLI-hosted WebUI runtime."
9843
10529
  );
9844
10530
  return true;
10531
+ case "kanban.capabilities":
10532
+ ok(ws, type, { dispatchSupported: false });
10533
+ return true;
9845
10534
  case "kanban.task.remove": {
9846
10535
  const boardId = payload?.boardId;
9847
10536
  const taskId = payload?.taskId;
@@ -9855,7 +10544,12 @@ async function handleKanbanRoute(ws, msg, ctx) {
9855
10544
  return true;
9856
10545
  }
9857
10546
  const board = await removeTask(ctx.projectRoot, boardId, taskId);
9858
- board ? ok(ws, type, { removed: true, boardId: board.id, taskId: task.id, board }) : fail(ws, type, "Board or task not found");
10547
+ if (!board) {
10548
+ fail(ws, type, "Board or task not found");
10549
+ return true;
10550
+ }
10551
+ await syncSessionSource(ctx, task, true);
10552
+ ok(ws, type, { removed: true, boardId: board.id, taskId: task.id, board });
9859
10553
  return true;
9860
10554
  }
9861
10555
  case "kanban.task.get": {
@@ -9921,6 +10615,9 @@ async function handleMailboxRoute(ws, msg, handlers) {
9921
10615
  case "mailbox.purge":
9922
10616
  await handlers.purge(ws, msg);
9923
10617
  return true;
10618
+ case "mailbox.compact":
10619
+ await handlers.compact(ws, msg);
10620
+ return true;
9924
10621
  default:
9925
10622
  return false;
9926
10623
  }
@@ -9959,6 +10656,18 @@ async function handleMcpRoute(ws, msg, handlers) {
9959
10656
  case "mcp.discover":
9960
10657
  await handlers.discover(ws, msg);
9961
10658
  return true;
10659
+ case "mcp.resources":
10660
+ await handlers.resources(ws, msg);
10661
+ return true;
10662
+ case "mcp.prompts":
10663
+ await handlers.prompts(ws, msg);
10664
+ return true;
10665
+ case "mcp.resource.read":
10666
+ await handlers.resourceRead(ws, msg);
10667
+ return true;
10668
+ case "mcp.prompt.get":
10669
+ await handlers.promptGet(ws, msg);
10670
+ return true;
9962
10671
  default:
9963
10672
  return false;
9964
10673
  }
@@ -10152,6 +10861,7 @@ async function handleProviderRoute(ws, msg, routes) {
10152
10861
  baseUrl,
10153
10862
  apiKey
10154
10863
  });
10864
+ await routes.adoptDefaultProviderIfUnset(id);
10155
10865
  return true;
10156
10866
  }
10157
10867
  case "provider.remove": {
@@ -10348,7 +11058,7 @@ function createMessageDispatcher(opts) {
10348
11058
  skillLoader: deps2.skillLoader,
10349
11059
  skillInstaller: deps2.skillInstaller,
10350
11060
  projectRoot,
10351
- projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
11061
+ projectSkillsDir: path17.join(projectRoot, ".wrongstack", "skills"),
10352
11062
  globalSkillsDir: deps2.wpaths.globalSkills
10353
11063
  };
10354
11064
  }
@@ -10390,7 +11100,12 @@ function createMessageDispatcher(opts) {
10390
11100
  if (await handleSpecsRoute(ws, msg, routes.specsRoutes)) return;
10391
11101
  if (await handleSddBoardRoute(ws, msg, routes.sddBoardRoutes)) return;
10392
11102
  if (await handleSddWizardRoute(ws, msg, routes.sddWizardRoutes)) return;
10393
- if (await handleKanbanRoute(ws, msg, { projectRoot: state.getProjectRoot() })) return;
11103
+ if (await handleKanbanRoute(ws, msg, {
11104
+ projectRoot: state.getProjectRoot(),
11105
+ context: deps2.context,
11106
+ broadcast: (message) => broadcast(state.getClients(), message)
11107
+ }))
11108
+ return;
10394
11109
  if (msg.type.startsWith("worktree.") && await deps2.worktreeHandler.handleMessage(
10395
11110
  msg
10396
11111
  ))
@@ -10421,7 +11136,8 @@ function createMessageDispatcher(opts) {
10421
11136
  }
10422
11137
  case "user_message": {
10423
11138
  if (!ensureCurrentSession(ws, msg, "user_message")) return;
10424
- const content = msg.payload.content;
11139
+ const userPayload = msg.payload;
11140
+ const content = userPayload.content;
10425
11141
  if (runLock.get()) {
10426
11142
  send(ws, {
10427
11143
  type: "error",
@@ -10435,8 +11151,21 @@ function createMessageDispatcher(opts) {
10435
11151
  const thisRun = new AbortController();
10436
11152
  runLock.set(thisRun);
10437
11153
  try {
11154
+ let input = content;
11155
+ const imageBlocks = parseIncomingImages(userPayload.images, userPayload.imageBase64);
11156
+ if (imageBlocks.length > 0) {
11157
+ const routed = await routeImagesForModel(buildUserContentBlocks(content, imageBlocks), {
11158
+ supportsVision: deps2.agent.ctx.provider.capabilities.vision,
11159
+ adapters: () => createToolVisionAdapters(deps2.agent.tools),
11160
+ ctx: deps2.agent.ctx,
11161
+ signal: thisRun.signal,
11162
+ providerId: deps2.agent.ctx.provider.id,
11163
+ model: deps2.agent.ctx.model
11164
+ });
11165
+ input = routed.blocks;
11166
+ }
10438
11167
  const maxIt = typeof deps2.context.meta["maxIterations"] === "number" ? deps2.context.meta["maxIterations"] : void 0;
10439
- const result = await deps2.agent.run(content, {
11168
+ const result = await deps2.agent.run(input, {
10440
11169
  signal: thisRun.signal,
10441
11170
  maxIterations: maxIt
10442
11171
  });
@@ -10454,13 +11183,24 @@ function createMessageDispatcher(opts) {
10454
11183
  })
10455
11184
  });
10456
11185
  } catch (err) {
10457
- send(ws, {
10458
- type: "error",
10459
- payload: sessionPayload2({
10460
- phase: "agent.run",
10461
- message: errMessage(err)
10462
- })
10463
- });
11186
+ if (err instanceof IncomingImageError || err instanceof ImageInputUnsupportedError || err instanceof VisionUrlBlockedError) {
11187
+ send(ws, {
11188
+ type: "error",
11189
+ payload: sessionPayload2({
11190
+ phase: "user_message",
11191
+ ...err instanceof ImageInputUnsupportedError ? { code: "vision_unsupported" } : {},
11192
+ message: err.message
11193
+ })
11194
+ });
11195
+ } else {
11196
+ send(ws, {
11197
+ type: "error",
11198
+ payload: sessionPayload2({
11199
+ phase: "agent.run",
11200
+ message: errMessage(err)
11201
+ })
11202
+ });
11203
+ }
10464
11204
  } finally {
10465
11205
  if (runLock.get() === thisRun) {
10466
11206
  runLock.set(null);
@@ -10508,7 +11248,7 @@ function createMessageDispatcher(opts) {
10508
11248
  break;
10509
11249
  }
10510
11250
  case "tool.disable": {
10511
- const name2 = (msg.payload ?? {}).name;
11251
+ const name2 = msg.payload?.name;
10512
11252
  if (!name2) {
10513
11253
  send(ws, { type: "error", payload: { message: "tool.disable requires a name" } });
10514
11254
  break;
@@ -10517,13 +11257,15 @@ function createMessageDispatcher(opts) {
10517
11257
  const currentCfg = deps2.configStore.get().tools ?? {};
10518
11258
  const currentDisabled = currentCfg.disabledTools ?? [];
10519
11259
  if (ok2 && !currentDisabled.includes(name2)) {
10520
- deps2.configStore.update({ tools: { ...currentCfg, disabledTools: [...currentDisabled, name2] } });
11260
+ deps2.configStore.update({
11261
+ tools: { ...currentCfg, disabledTools: [...currentDisabled, name2] }
11262
+ });
10521
11263
  }
10522
11264
  send(ws, { type: "tool.disabled", payload: { name: name2, ok: ok2 } });
10523
11265
  break;
10524
11266
  }
10525
11267
  case "tool.enable": {
10526
- const name2 = (msg.payload ?? {}).name;
11268
+ const name2 = msg.payload?.name;
10527
11269
  if (!name2) {
10528
11270
  send(ws, { type: "error", payload: { message: "tool.enable requires a name" } });
10529
11271
  break;
@@ -10533,19 +11275,31 @@ function createMessageDispatcher(opts) {
10533
11275
  const currentCfg = deps2.configStore.get().tools ?? {};
10534
11276
  const currentDisabled = currentCfg.disabledTools ?? [];
10535
11277
  deps2.configStore.update({
10536
- tools: { ...currentCfg, disabledTools: currentDisabled.filter((n) => n !== name2) }
11278
+ tools: {
11279
+ ...currentCfg,
11280
+ disabledTools: currentDisabled.filter((n) => n !== name2)
11281
+ }
10537
11282
  });
10538
11283
  }
10539
11284
  send(ws, { type: "tool.enabled", payload: { name: name2, ok: ok2 } });
10540
11285
  break;
10541
11286
  }
10542
11287
  // ── Memory operations — delegated to shared handlers (memory-handlers.ts) ──
11288
+ // `memory.list` renders the single Super Memory store; all writes go
11289
+ // through the structured `memory.super.*` operations below.
10543
11290
  case "memory.list":
10544
11291
  return handleMemoryList(ws, deps2.memoryStore);
10545
- case "memory.remember":
10546
- return handleMemoryRemember(ws, msg, deps2.memoryStore);
10547
- case "memory.forget":
10548
- return handleMemoryForget(ws, msg, deps2.memoryStore);
11292
+ // ── SuperMemory operations ──
11293
+ case "memory.super.list":
11294
+ return handleSuperMemoryList(ws, deps2.memoryStore);
11295
+ case "memory.super.get":
11296
+ return handleSuperMemoryGet(ws, msg, deps2.memoryStore);
11297
+ case "memory.super.update":
11298
+ return handleSuperMemoryUpdate(ws, msg, deps2.memoryStore);
11299
+ case "memory.super.delete":
11300
+ return handleSuperMemoryDelete(ws, msg, deps2.memoryStore);
11301
+ case "memory.super.remember":
11302
+ return handleSuperMemoryRemember(ws, msg, deps2.memoryStore);
10549
11303
  // ── MCP tripwires — handleMcpRoute claims these upstream. ──
10550
11304
  case "mcp.list":
10551
11305
  throw new Error("handleMcpRoute did not claim mcp.list \u2014 check chain order");
@@ -10567,6 +11321,14 @@ function createMessageDispatcher(opts) {
10567
11321
  throw new Error("handleMcpRoute did not claim mcp.restart \u2014 check chain order");
10568
11322
  case "mcp.discover":
10569
11323
  throw new Error("handleMcpRoute did not claim mcp.discover \u2014 check chain order");
11324
+ case "mcp.resources":
11325
+ throw new Error("handleMcpRoute did not claim mcp.resources \u2014 check chain order");
11326
+ case "mcp.prompts":
11327
+ throw new Error("handleMcpRoute did not claim mcp.prompts \u2014 check chain order");
11328
+ case "mcp.resource.read":
11329
+ throw new Error("handleMcpRoute did not claim mcp.resource.read \u2014 check chain order");
11330
+ case "mcp.prompt.get":
11331
+ throw new Error("handleMcpRoute did not claim mcp.prompt.get \u2014 check chain order");
10570
11332
  // Skills — full request→response cycle lives in skills-handlers.ts.
10571
11333
  case "skills.list":
10572
11334
  await handleSkillsList(ws, makeSkillsContext());
@@ -10867,7 +11629,20 @@ var PREF_KEYS = [
10867
11629
  "favoriteModels",
10868
11630
  "favoriteModelsOnly",
10869
11631
  "modelMatrix",
10870
- "fallbackAuto"
11632
+ "fallbackAuto",
11633
+ // Refiner + TUI visual prefs (parity with the CLI's embedded server —
11634
+ // these were browser-editable there but rejected as unknown keys here).
11635
+ "refinerProvider",
11636
+ "refinerModel",
11637
+ "refinerFallbackProfile",
11638
+ "thinkingWord",
11639
+ "statuslineMode",
11640
+ "animationStyle",
11641
+ // Safety / system prefs (parity with /settings breaker, fs-access, debug-stream).
11642
+ "breakerEnabled",
11643
+ "breakerAutoKillResetMs",
11644
+ "fsAccess",
11645
+ "debugStream"
10871
11646
  ];
10872
11647
  function prefSnapshot(contextMeta) {
10873
11648
  const snapshot = {};
@@ -10938,6 +11713,18 @@ async function persistPrefsToConfig(deps2, holder, payload) {
10938
11713
  setAutonomy("enhanceDelayMs", payload["enhanceDelayMs"]);
10939
11714
  if (typeof payload["enhanceLanguage"] === "string")
10940
11715
  setAutonomy("enhanceLanguage", payload["enhanceLanguage"]);
11716
+ if (typeof payload["refinerProvider"] === "string")
11717
+ setAutonomy("refinerProvider", payload["refinerProvider"]);
11718
+ if (typeof payload["refinerModel"] === "string")
11719
+ setAutonomy("refinerModel", payload["refinerModel"]);
11720
+ if (typeof payload["refinerFallbackProfile"] === "string")
11721
+ setAutonomy("refinerFallbackProfile", payload["refinerFallbackProfile"]);
11722
+ if (typeof payload["thinkingWord"] === "string")
11723
+ setAutonomy("thinkingWord", payload["thinkingWord"]);
11724
+ if (typeof payload["statuslineMode"] === "string")
11725
+ setAutonomy("statuslineMode", payload["statuslineMode"]);
11726
+ if (typeof payload["animationStyle"] === "string")
11727
+ setAutonomy("animationStyle", payload["animationStyle"]);
10941
11728
  if (autonomyTouched) decrypted.autonomy = autonomyCfg;
10942
11729
  if (typeof payload["nextPrediction"] === "boolean")
10943
11730
  decrypted.nextPrediction = payload["nextPrediction"];
@@ -11055,16 +11842,34 @@ async function persistPrefsToConfig(deps2, holder, payload) {
11055
11842
  }
11056
11843
  decrypted.modelRuntime = mr;
11057
11844
  }
11845
+ if (typeof payload["breakerEnabled"] === "boolean" || typeof payload["breakerAutoKillResetMs"] === "number") {
11846
+ const cb = decrypted.circuitBreaker ?? {};
11847
+ if (typeof payload["breakerEnabled"] === "boolean") cb.enabled = payload["breakerEnabled"];
11848
+ if (typeof payload["breakerAutoKillResetMs"] === "number")
11849
+ cb.autoKillResetMs = payload["breakerAutoKillResetMs"];
11850
+ decrypted.circuitBreaker = cb;
11851
+ }
11852
+ if (payload["fsAccess"] === "unrestricted" || payload["fsAccess"] === "project") {
11853
+ const restrict = payload["fsAccess"] === "project";
11854
+ const toolsCfg = decrypted.tools ?? {};
11855
+ toolsCfg.restrictToProjectRoot = restrict;
11856
+ decrypted.tools = toolsCfg;
11857
+ const featsCfg = decrypted.features ?? {};
11858
+ featsCfg.allowOutsideProjectRoot = !restrict;
11859
+ decrypted.features = featsCfg;
11860
+ }
11861
+ if (typeof payload["debugStream"] === "boolean")
11862
+ decrypted.debugStream = payload["debugStream"];
11058
11863
  }, "prefs");
11059
11864
  }
11060
11865
 
11061
11866
  // src/server/projects-manifest.ts
11062
11867
  import * as fs14 from "node:fs/promises";
11063
- import * as path17 from "node:path";
11868
+ import * as path18 from "node:path";
11064
11869
  import { projectSlug } from "@wrongstack/core";
11065
11870
  function projectsJsonPath(globalConfigPath) {
11066
- const base = path17.dirname(globalConfigPath);
11067
- return path17.join(base, "projects.json");
11871
+ const base = path18.dirname(globalConfigPath);
11872
+ return path18.join(base, "projects.json");
11068
11873
  }
11069
11874
  async function loadManifest(globalConfigPath) {
11070
11875
  try {
@@ -11077,15 +11882,15 @@ async function loadManifest(globalConfigPath) {
11077
11882
  }
11078
11883
  async function saveManifest(manifest, globalConfigPath) {
11079
11884
  const file = projectsJsonPath(globalConfigPath);
11080
- await fs14.mkdir(path17.dirname(file), { recursive: true });
11885
+ await fs14.mkdir(path18.dirname(file), { recursive: true });
11081
11886
  await fs14.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
11082
11887
  }
11083
11888
  function generateProjectSlug(rootPath) {
11084
11889
  return projectSlug(rootPath);
11085
11890
  }
11086
11891
  async function ensureProjectDataDir(slug, globalConfigPath) {
11087
- const base = path17.dirname(globalConfigPath);
11088
- const dir = path17.join(base, "projects", slug);
11892
+ const base = path18.dirname(globalConfigPath);
11893
+ const dir = path18.join(base, "projects", slug);
11089
11894
  await fs14.mkdir(dir, { recursive: true });
11090
11895
  return dir;
11091
11896
  }
@@ -11119,6 +11924,23 @@ function projectSavedProviders(providers) {
11119
11924
  });
11120
11925
  }
11121
11926
  var probeScrubber = new DefaultSecretScrubber();
11927
+ async function probeModelDescriptors(cfg) {
11928
+ if (!cfg.baseUrl) return [];
11929
+ try {
11930
+ const keys = normalizeKeys(cfg);
11931
+ const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
11932
+ const result = await probeLocalLlm({
11933
+ baseUrl: cfg.baseUrl,
11934
+ apiKey: active?.apiKey,
11935
+ noAuth: false,
11936
+ scrubber: probeScrubber
11937
+ });
11938
+ if (!result.ok || !result.modelIds) return [];
11939
+ return result.modelIds.map((id) => ({ id, name: id, capabilities: [] }));
11940
+ } catch {
11941
+ return [];
11942
+ }
11943
+ }
11122
11944
  function createProviderHandlers(deps2) {
11123
11945
  const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
11124
11946
  let configWriteLock = deps2.getConfigWriteLock();
@@ -11405,7 +12227,9 @@ import path20 from "node:path";
11405
12227
  import {
11406
12228
  enhanceUserPrompt,
11407
12229
  gatedEnhancerReasoning,
12230
+ nextEnhanceTimeout,
11408
12231
  recentTextTurns,
12232
+ resolveEnhanceFallbackRef,
11409
12233
  resolveProviderModelList
11410
12234
  } from "@wrongstack/core";
11411
12235
  import { makeProviderFromConfig as makeProviderFromConfig2 } from "@wrongstack/providers";
@@ -11502,6 +12326,16 @@ async function handleMailboxPurge(ws, deps2, opts) {
11502
12326
  send(ws, { type: "mailbox.purged", payload: { error: errMessage(err) } });
11503
12327
  }
11504
12328
  }
12329
+ async function handleMailboxCompact(ws, deps2, opts) {
12330
+ try {
12331
+ const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
12332
+ const mb = new GlobalMailbox3(dir);
12333
+ const result = await mb.autoCompact(opts);
12334
+ send(ws, { type: "mailbox.compacted", payload: result });
12335
+ } catch (err) {
12336
+ send(ws, { type: "mailbox.compacted", payload: { error: errMessage(err) } });
12337
+ }
12338
+ }
11505
12339
 
11506
12340
  // src/server/mode-handlers.ts
11507
12341
  import {
@@ -11560,6 +12394,8 @@ function createModeHandlers(ctx) {
11560
12394
  const paths = resolveWstackPaths2({ projectRoot: ctx.projectRoot, globalRoot: ctx.globalRoot });
11561
12395
  const freshBuilder = new DefaultSystemPromptBuilder2({
11562
12396
  memoryStore: ctx.memoryStore,
12397
+ // Single injection channel: Super Memory turn middleware, not a static section.
12398
+ injectMemory: false,
11563
12399
  skillLoader: ctx.skillLoader,
11564
12400
  modeStore: ctx.modeStore,
11565
12401
  modeId: id,
@@ -11590,7 +12426,7 @@ function createModeHandlers(ctx) {
11590
12426
  }
11591
12427
 
11592
12428
  // src/server/project-handlers.ts
11593
- import * as path18 from "node:path";
12429
+ import * as path19 from "node:path";
11594
12430
  function createProjectHandlers(ctx) {
11595
12431
  return {
11596
12432
  listProjects: async (ws) => {
@@ -11616,7 +12452,7 @@ function createProjectHandlers(ctx) {
11616
12452
  selectProject: async (ws, msg) => {
11617
12453
  const payload = msg.payload;
11618
12454
  const root = typeof payload?.root === "string" ? payload.root : "";
11619
- const name2 = typeof payload?.name === "string" ? payload.name : root ? path18.basename(root) : "";
12455
+ const name2 = typeof payload?.name === "string" ? payload.name : root ? path19.basename(root) : "";
11620
12456
  send(ws, {
11621
12457
  type: "projects.selected",
11622
12458
  payload: {
@@ -11651,12 +12487,12 @@ function createProjectHandlers(ctx) {
11651
12487
  }
11652
12488
 
11653
12489
  // src/server/session-handlers.ts
11654
- import * as path19 from "node:path";
11655
12490
  import {
11656
12491
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
11657
12492
  repairToolUseAdjacency,
11658
12493
  resolveContextWindowPolicy as resolveContextWindowPolicy3
11659
12494
  } from "@wrongstack/core";
12495
+ import { sessionScopedPath as sessionScopedPath2 } from "@wrongstack/core/utils";
11660
12496
  function createSessionHandlers(ctx) {
11661
12497
  const currentSessionId = () => ctx.getSession().id;
11662
12498
  const sessionPayload2 = (payload) => {
@@ -11682,33 +12518,46 @@ function createSessionHandlers(ctx) {
11682
12518
  });
11683
12519
  return false;
11684
12520
  };
12521
+ const finalizeSession = async (writer) => {
12522
+ await writer.append({
12523
+ type: "session_end",
12524
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
12525
+ usage: ctx.tokenCounter.total()
12526
+ }).catch(() => void 0);
12527
+ await writer.close().catch(() => void 0);
12528
+ };
12529
+ const activateSession = async (next, messages, usage) => {
12530
+ const current = ctx.getSession();
12531
+ if (current !== next) await finalizeSession(current);
12532
+ ctx.setSession(next);
12533
+ ctx.context.session = next;
12534
+ ctx.context.state.replaceMessages(messages);
12535
+ ctx.context.state.replaceTodos([]);
12536
+ ctx.context.readFiles.clear();
12537
+ ctx.context.fileMtimes.clear();
12538
+ ctx.context.state.setMeta(
12539
+ "plan.path",
12540
+ sessionScopedPath2(ctx.sessionsDir, next.id, ".plan.json")
12541
+ );
12542
+ ctx.context.state.setMeta(
12543
+ "task.path",
12544
+ sessionScopedPath2(ctx.sessionsDir, next.id, ".tasks.json")
12545
+ );
12546
+ ctx.tokenCounter.reset();
12547
+ if (usage) ctx.tokenCounter.account(usage, ctx.config.model);
12548
+ ctx.setSessionStartedAt(Date.now());
12549
+ await ctx.onSessionSwapped(next.id);
12550
+ };
11685
12551
  return {
11686
12552
  newSession: async (ws, msg) => {
11687
12553
  if (!ensureCurrentSession(ws, msg, "session.new")) return;
11688
- const session = ctx.getSession();
11689
- try {
11690
- await session.append({
11691
- type: "session_end",
11692
- ts: (/* @__PURE__ */ new Date()).toISOString(),
11693
- usage: ctx.tokenCounter.total()
11694
- });
11695
- await session.close();
11696
- } catch {
11697
- }
11698
12554
  const next = await ctx.getSessionStore().create({
11699
12555
  id: "",
11700
12556
  title: "",
11701
12557
  model: ctx.config.model,
11702
12558
  provider: ctx.config.provider
11703
12559
  });
11704
- ctx.setSession(next);
11705
- ctx.context.session = next;
11706
- ctx.context.state.replaceMessages([]);
11707
- ctx.context.state.replaceTodos([]);
11708
- ctx.context.readFiles.clear();
11709
- ctx.context.fileMtimes.clear();
11710
- ctx.tokenCounter.reset();
11711
- ctx.setSessionStartedAt(Date.now());
12560
+ await activateSession(next, []);
11712
12561
  broadcast(ctx.clients, { type: "session.start", payload: await ctx.sessionStartPayload() });
11713
12562
  },
11714
12563
  clearContext: async (ws, msg) => {
@@ -11789,7 +12638,9 @@ function createSessionHandlers(ctx) {
11789
12638
  },
11790
12639
  listContextModes: async (ws, msg) => {
11791
12640
  if (!ensureCurrentSession(ws, msg, "context.modes.list")) return;
11792
- const active = String(ctx.context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID);
12641
+ const active = String(
12642
+ ctx.context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID
12643
+ );
11793
12644
  const allModes = ctx.customModeStore.list().map((m) => ({
11794
12645
  id: m.id,
11795
12646
  name: m.name,
@@ -11800,7 +12651,10 @@ function createSessionHandlers(ctx) {
11800
12651
  eliseThreshold: m.eliseThreshold,
11801
12652
  custom: m.custom === true
11802
12653
  }));
11803
- send(ws, { type: "context.modes.list", payload: sessionPayload2({ activeId: active, modes: allModes }) });
12654
+ send(ws, {
12655
+ type: "context.modes.list",
12656
+ payload: sessionPayload2({ activeId: active, modes: allModes })
12657
+ });
11804
12658
  },
11805
12659
  switchContextMode: async (ws, msg) => {
11806
12660
  if (!ensureCurrentSession(ws, msg, "context.mode.switch")) return;
@@ -11882,7 +12736,10 @@ function createSessionHandlers(ctx) {
11882
12736
  const { id } = parsed.value;
11883
12737
  if (String(ctx.context.meta["contextWindowMode"] ?? "") === id) {
11884
12738
  ctx.context.meta["contextWindowMode"] = DEFAULT_CONTEXT_WINDOW_MODE_ID;
11885
- ctx.context.meta["contextWindowPolicy"] = resolveContextWindowPolicy3({}, DEFAULT_CONTEXT_WINDOW_MODE_ID);
12739
+ ctx.context.meta["contextWindowPolicy"] = resolveContextWindowPolicy3(
12740
+ {},
12741
+ DEFAULT_CONTEXT_WINDOW_MODE_ID
12742
+ );
11886
12743
  }
11887
12744
  const result = ctx.customModeStore.remove(id);
11888
12745
  sendResult(ws, result.ok, result.error ?? `Mode "${id}" deleted`);
@@ -11895,15 +12752,7 @@ function createSessionHandlers(ctx) {
11895
12752
  send(ws, {
11896
12753
  type: "sessions.list",
11897
12754
  payload: {
11898
- sessions: list.map((s) => ({
11899
- id: s.id,
11900
- title: s.title,
11901
- startedAt: s.startedAt,
11902
- model: s.model,
11903
- provider: s.provider,
11904
- tokenTotal: s.tokenTotal,
11905
- isCurrent: s.id === currentId
11906
- }))
12755
+ sessions: toSessionHistoryEntries(list, currentId)
11907
12756
  }
11908
12757
  });
11909
12758
  } catch (err) {
@@ -11917,8 +12766,17 @@ function createSessionHandlers(ctx) {
11917
12766
  sendResult(ws, false, "Cannot delete the active session");
11918
12767
  return;
11919
12768
  }
11920
- await ctx.getSessionStore().delete(id);
12769
+ const store = ctx.getSessionStore();
12770
+ await store.delete(id);
11921
12771
  sendResult(ws, true, `Session ${id} deleted`);
12772
+ try {
12773
+ const list = await store.list(200);
12774
+ broadcast(ctx.clients, {
12775
+ type: "sessions.list",
12776
+ payload: { sessions: toSessionHistoryEntries(list, ctx.getSession().id) }
12777
+ });
12778
+ } catch {
12779
+ }
11922
12780
  } catch (err) {
11923
12781
  sendResult(ws, false, errMessage(err));
11924
12782
  }
@@ -11934,23 +12792,17 @@ function createSessionHandlers(ctx) {
11934
12792
  try {
11935
12793
  await ctx.getSessionStore().rename(id, name2);
11936
12794
  sendResult(ws, true, name2 ? `Renamed session to "${name2}"` : `Cleared session name`);
11937
- const list = await ctx.getSessionStore().list(50);
11938
- const currentId = ctx.getSession().id;
11939
- broadcast(ctx.clients, {
11940
- type: "sessions.list",
11941
- payload: {
11942
- sessions: list.map((s) => ({
11943
- id: s.id,
11944
- name: s.name,
11945
- title: s.title,
11946
- startedAt: s.startedAt,
11947
- model: s.model,
11948
- provider: s.provider,
11949
- tokenTotal: s.tokenTotal,
11950
- isCurrent: s.id === currentId
11951
- }))
11952
- }
11953
- });
12795
+ try {
12796
+ const list = await ctx.getSessionStore().list(200);
12797
+ const currentId = ctx.getSession().id;
12798
+ broadcast(ctx.clients, {
12799
+ type: "sessions.list",
12800
+ payload: {
12801
+ sessions: toSessionHistoryEntries(list, currentId)
12802
+ }
12803
+ });
12804
+ } catch {
12805
+ }
11954
12806
  } catch (err) {
11955
12807
  sendResult(ws, false, errMessage(err));
11956
12808
  }
@@ -11964,23 +12816,7 @@ function createSessionHandlers(ctx) {
11964
12816
  return;
11965
12817
  }
11966
12818
  const resumed = await ctx.getSessionStore().resume(id);
11967
- try {
11968
- await current.append({
11969
- type: "session_end",
11970
- ts: (/* @__PURE__ */ new Date()).toISOString(),
11971
- usage: ctx.tokenCounter.total()
11972
- });
11973
- await current.close();
11974
- } catch {
11975
- }
11976
- ctx.setSession(resumed.writer);
11977
- ctx.context.session = resumed.writer;
11978
- ctx.context.state.replaceMessages(resumed.data.messages);
11979
- ctx.context.readFiles.clear();
11980
- ctx.context.fileMtimes.clear();
11981
- ctx.tokenCounter.reset();
11982
- ctx.tokenCounter.account(resumed.data.usage, ctx.config.model);
11983
- ctx.setSessionStartedAt(Date.now());
12819
+ await activateSession(resumed.writer, resumed.data.messages, resumed.data.usage);
11984
12820
  broadcast(ctx.clients, {
11985
12821
  type: "session.start",
11986
12822
  payload: {
@@ -12004,10 +12840,7 @@ function createSessionHandlers(ctx) {
12004
12840
  try {
12005
12841
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
12006
12842
  const projectRoot = ctx.getProjectRoot();
12007
- const rewinder = new DefaultSessionRewinder(
12008
- path19.join(projectRoot, ".wrongstack", "sessions"),
12009
- projectRoot
12010
- );
12843
+ const rewinder = new DefaultSessionRewinder(ctx.sessionsDir, projectRoot);
12011
12844
  const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
12012
12845
  send(ws, { type: "session.checkpoints", payload: sessionPayload2({ checkpoints }) });
12013
12846
  } catch {
@@ -12020,10 +12853,7 @@ function createSessionHandlers(ctx) {
12020
12853
  try {
12021
12854
  const { DefaultSessionRewinder } = await import("@wrongstack/core");
12022
12855
  const projectRoot = ctx.getProjectRoot();
12023
- const rewinder = new DefaultSessionRewinder(
12024
- path19.join(projectRoot, ".wrongstack", "sessions"),
12025
- projectRoot
12026
- );
12856
+ const rewinder = new DefaultSessionRewinder(ctx.sessionsDir, projectRoot);
12027
12857
  await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
12028
12858
  await ctx.context.session.truncateToCheckpoint(checkpointIndex);
12029
12859
  sendResult(ws, true, `Rewound to checkpoint ${checkpointIndex}`);
@@ -12072,6 +12902,25 @@ function buildRoutes(state, deps2, cb) {
12072
12902
  clients: state.getClients(),
12073
12903
  modelsRegistry: deps2.modelsRegistry
12074
12904
  });
12905
+ async function applyModelSwitchCore(newProvider, newModel) {
12906
+ const cur = state.getConfig();
12907
+ state.setConfig(patchConfig(cur, { provider: newProvider, model: newModel }));
12908
+ deps2.configStore.update({ provider: newProvider, model: newModel });
12909
+ deps2.context.model = newModel;
12910
+ const newCfg = state.getConfig();
12911
+ const providerCfg = newCfg.providers?.[newProvider] ?? { type: newProvider };
12912
+ const newProv = deps2.providerRegistry.has(newProvider) ? deps2.providerRegistry.create({ ...providerCfg, type: newProvider }) : makeProviderFromConfig2(newProvider, providerCfg);
12913
+ deps2.context.provider = newProv;
12914
+ await cb.updateAutoCompactionMaxContext(newProv, newProvider, providerCfg);
12915
+ await cb.updateGlobalConfig((config) => {
12916
+ config.provider = newProvider;
12917
+ config.model = newModel;
12918
+ }, "model.switch");
12919
+ broadcast(state.getClients(), {
12920
+ type: "session.start",
12921
+ payload: await cb.sessionStartPayload()
12922
+ });
12923
+ }
12075
12924
  const providerRoutes = {
12076
12925
  providerHandlers,
12077
12926
  listProviders: async (ws) => {
@@ -12108,11 +12957,20 @@ function buildRoutes(state, deps2, cb) {
12108
12957
  const provider = await resolveProviderCatalogForModels(deps2.modelsRegistry, providerId, cfg);
12109
12958
  const siblingId = SIBLING_CATALOG[providerId];
12110
12959
  const siblingCatalog = siblingId && siblingId !== providerId ? await deps2.modelsRegistry.getProvider(siblingId).catch(() => void 0) : void 0;
12960
+ let resolved = resolveProviderModelList(
12961
+ cfg?.models,
12962
+ provider,
12963
+ cfg?.type ?? providerId,
12964
+ siblingCatalog
12965
+ );
12966
+ if (resolved.length === 0 && cfg?.baseUrl) {
12967
+ resolved = await probeModelDescriptors(cfg);
12968
+ }
12111
12969
  const models = await enrichProviderModelDescriptors(
12112
12970
  deps2.modelsRegistry,
12113
12971
  providerId,
12114
12972
  cfg,
12115
- resolveProviderModelList(cfg?.models, provider, cfg?.type ?? providerId, siblingCatalog)
12973
+ resolved
12116
12974
  );
12117
12975
  send(ws, {
12118
12976
  type: "provider.models",
@@ -12130,21 +12988,7 @@ function buildRoutes(state, deps2, cb) {
12130
12988
  }
12131
12989
  const { provider: newProvider, model: newModel } = parsed.value;
12132
12990
  try {
12133
- const cur = state.getConfig();
12134
- state.setConfig(patchConfig(cur, { provider: newProvider, model: newModel }));
12135
- deps2.configStore.update({ provider: newProvider, model: newModel });
12136
- deps2.context.model = newModel;
12137
- const newCfg = state.getConfig();
12138
- const providerCfg = newCfg.providers?.[newProvider] ?? {
12139
- type: newProvider
12140
- };
12141
- const newProv = deps2.providerRegistry.has(newProvider) ? deps2.providerRegistry.create({ ...providerCfg, type: newProvider }) : makeProviderFromConfig2(newProvider, providerCfg);
12142
- deps2.context.provider = newProv;
12143
- await cb.updateAutoCompactionMaxContext(newProv, newProvider, providerCfg);
12144
- await cb.updateGlobalConfig((config) => {
12145
- config.provider = newProvider;
12146
- config.model = newModel;
12147
- }, "model.switch");
12991
+ await applyModelSwitchCore(newProvider, newModel);
12148
12992
  send(ws, {
12149
12993
  type: "key.operation_result",
12150
12994
  payload: { success: true, message: `Switched to ${newProvider} / ${newModel}` }
@@ -12157,45 +13001,100 @@ function buildRoutes(state, deps2, cb) {
12157
13001
  message: `Switch failed: ${errMessage(err)}`
12158
13002
  }
12159
13003
  });
12160
- return;
12161
13004
  }
12162
- broadcast(state.getClients(), {
12163
- type: "session.start",
12164
- payload: await cb.sessionStartPayload()
12165
- });
13005
+ },
13006
+ adoptDefaultProviderIfUnset: async (providerId) => {
13007
+ if (state.getConfig().model) return;
13008
+ const saved = await providerHandlers.loadConfigProviders();
13009
+ state.setConfig(patchConfig(state.getConfig(), { providers: saved }));
13010
+ const cfg = saved[providerId];
13011
+ if (!cfg) return;
13012
+ let model = cfg.models?.[0];
13013
+ if (!model) {
13014
+ const catalogId = cfg.type && cfg.type !== providerId ? cfg.type : providerId;
13015
+ const catalog = await deps2.modelsRegistry.getProvider(catalogId).catch(() => void 0);
13016
+ model = catalog?.models?.[0]?.id;
13017
+ }
13018
+ if (!model) {
13019
+ const probed = await probeModelDescriptors(cfg);
13020
+ model = probed[0]?.id;
13021
+ }
13022
+ if (!model) return;
13023
+ try {
13024
+ await applyModelSwitchCore(providerId, model);
13025
+ } catch {
13026
+ }
12166
13027
  },
12167
13028
  refineModel: async (ws, msg) => {
12168
- const { text } = msg.payload;
13029
+ const payload = msg.payload;
13030
+ const { text } = payload;
12169
13031
  if (!text?.trim()) {
12170
13032
  send(ws, {
12171
13033
  type: "model.refine_result",
12172
- payload: { refined: "", english: "", error: "Empty text" }
13034
+ payload: { refined: "", english: "", error: "Empty text", errorKind: "provider_error" }
12173
13035
  });
12174
13036
  return;
12175
13037
  }
13038
+ const cfg = state.getConfig();
13039
+ const fallbackRef = resolveEnhanceFallbackRef({
13040
+ ...cfg,
13041
+ provider: cfg.provider ?? "",
13042
+ model: cfg.model ?? ""
13043
+ });
13044
+ let provider = deps2.context.provider;
13045
+ let providerId = cfg.provider ?? "";
13046
+ let model = deps2.context.model;
13047
+ if (payload.provider && payload.model) {
13048
+ try {
13049
+ const providerCfg = cfg.providers?.[payload.provider] ?? {
13050
+ type: payload.provider
13051
+ };
13052
+ provider = deps2.providerRegistry.has(payload.provider) ? deps2.providerRegistry.create({ ...providerCfg, type: payload.provider }) : makeProviderFromConfig2(payload.provider, providerCfg);
13053
+ providerId = payload.provider;
13054
+ model = payload.model;
13055
+ } catch (err) {
13056
+ send(ws, {
13057
+ type: "model.refine_result",
13058
+ payload: {
13059
+ refined: text,
13060
+ english: text,
13061
+ error: `Cannot use ${payload.provider}/${payload.model}: ${errMessage(err)}`,
13062
+ errorKind: "provider_error",
13063
+ ...fallbackRef ? { fallbackRef } : {}
13064
+ }
13065
+ });
13066
+ return;
13067
+ }
13068
+ }
13069
+ const baseTimeout = 9e4;
13070
+ const timeoutMs = typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : baseTimeout;
12176
13071
  try {
12177
13072
  const history = recentTextTurns(deps2.context.messages);
12178
- const cfg = state.getConfig();
12179
13073
  const resolved = await resolveProviderModelMetadata(
12180
13074
  deps2.modelsRegistry,
12181
- cfg.provider ?? "",
12182
- cfg.model ?? "",
12183
- cfg.providers?.[cfg.provider ?? ""]
13075
+ providerId,
13076
+ model,
13077
+ cfg.providers?.[providerId]
12184
13078
  ).catch(() => void 0);
12185
13079
  const reasoning = gatedEnhancerReasoning(resolved?.capabilities?.reasoningConfig);
13080
+ let failureKind;
12186
13081
  const result = await enhanceUserPrompt({
12187
- provider: deps2.context.provider,
12188
- model: deps2.context.model,
13082
+ provider,
13083
+ model,
12189
13084
  text,
12190
13085
  history,
12191
- timeoutMs: 9e4,
13086
+ timeoutMs,
12192
13087
  ...reasoning ? { reasoning } : {},
12193
- onError: (reason) => {
13088
+ onError: (reason, kind) => {
13089
+ failureKind = kind;
12194
13090
  console.warn(
12195
13091
  JSON.stringify({
12196
13092
  level: "warn",
12197
13093
  event: "model.refine_failed",
12198
13094
  reason,
13095
+ kind,
13096
+ provider: providerId,
13097
+ model,
12199
13098
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
12200
13099
  })
12201
13100
  );
@@ -12204,12 +13103,23 @@ function buildRoutes(state, deps2, cb) {
12204
13103
  if (result) {
12205
13104
  send(ws, {
12206
13105
  type: "model.refine_result",
12207
- payload: { refined: result.refined, english: result.english }
13106
+ payload: {
13107
+ refined: result.refined,
13108
+ english: result.english,
13109
+ refinedWith: { provider: providerId, model }
13110
+ }
12208
13111
  });
12209
13112
  } else {
12210
13113
  send(ws, {
12211
13114
  type: "model.refine_result",
12212
- payload: { refined: text, english: text, error: "Refinement returned no result" }
13115
+ payload: {
13116
+ refined: text,
13117
+ english: text,
13118
+ error: "Refinement returned no result",
13119
+ errorKind: failureKind ?? "empty",
13120
+ ...failureKind === "timeout" ? { retryTimeoutMs: nextEnhanceTimeout(timeoutMs, cfg.autonomy) } : {},
13121
+ ...fallbackRef ? { fallbackRef } : {}
13122
+ }
12213
13123
  });
12214
13124
  }
12215
13125
  } catch (err) {
@@ -12223,7 +13133,13 @@ function buildRoutes(state, deps2, cb) {
12223
13133
  );
12224
13134
  send(ws, {
12225
13135
  type: "model.refine_result",
12226
- payload: { refined: text, english: text, error: errMessage(err) }
13136
+ payload: {
13137
+ refined: text,
13138
+ english: text,
13139
+ error: errMessage(err),
13140
+ errorKind: "provider_error",
13141
+ ...fallbackRef ? { fallbackRef } : {}
13142
+ }
12227
13143
  });
12228
13144
  }
12229
13145
  }
@@ -12239,8 +13155,10 @@ function buildRoutes(state, deps2, cb) {
12239
13155
  getProjectRoot: state.getProjectRoot,
12240
13156
  getSession: state.getSession,
12241
13157
  getSessionStore: state.getSessionStore,
13158
+ sessionsDir: deps2.wpaths.projectSessions,
12242
13159
  setSession: state.setSession,
12243
13160
  setSessionStartedAt: state.setSessionStartedAt,
13161
+ onSessionSwapped: cb.onSessionSwapped,
12244
13162
  sessionStartPayload: cb.sessionStartPayload
12245
13163
  });
12246
13164
  const projectRoutes = createProjectHandlers({
@@ -12335,6 +13253,17 @@ function buildRoutes(state, deps2, cb) {
12335
13253
  deps2.pipelines.contextWindow.remove("AutoCompaction", { optional: true });
12336
13254
  }
12337
13255
  }
13256
+ if (typeof payload["breakerEnabled"] === "boolean" || typeof payload["breakerAutoKillResetMs"] === "number") {
13257
+ const { getProcessRegistry } = await import("@wrongstack/tools");
13258
+ getProcessRegistry().setBreakerConfig({
13259
+ ...typeof payload["breakerEnabled"] === "boolean" ? { enabled: payload["breakerEnabled"] } : {},
13260
+ ...typeof payload["breakerAutoKillResetMs"] === "number" ? { autoKillResetMs: payload["breakerAutoKillResetMs"] } : {}
13261
+ });
13262
+ }
13263
+ if (typeof payload["debugStream"] === "boolean") {
13264
+ const { setDebugStreamEnabled } = await import("@wrongstack/providers");
13265
+ setDebugStreamEnabled(payload["debugStream"]);
13266
+ }
12338
13267
  if (typeof payload["logLevel"] === "string") {
12339
13268
  const valid = ["debug", "info", "warn", "error"];
12340
13269
  if (valid.includes(payload["logLevel"])) {
@@ -12412,6 +13341,13 @@ function buildRoutes(state, deps2, cb) {
12412
13341
  { projectRoot: state.getProjectRoot(), globalRoot: path20.dirname(deps2.globalConfigPath) },
12413
13342
  parsed.value
12414
13343
  );
13344
+ },
13345
+ compact: (ws, msg) => {
13346
+ return handleMailboxCompact(
13347
+ ws,
13348
+ { projectRoot: state.getProjectRoot(), globalRoot: path20.dirname(deps2.globalConfigPath) },
13349
+ msg.payload ?? {}
13350
+ );
12415
13351
  }
12416
13352
  };
12417
13353
  const mcpRoutes = {
@@ -12424,14 +13360,29 @@ function buildRoutes(state, deps2, cb) {
12424
13360
  sleep: (ws, msg) => handleMcpSleep(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
12425
13361
  wake: (ws, msg) => handleMcpWake(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
12426
13362
  restart: (ws, msg) => handleMcpRestart(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
12427
- discover: (ws, msg) => handleMcpDiscover(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry)
13363
+ discover: (ws, msg) => handleMcpDiscover(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
13364
+ resources: (ws, msg) => handleMcpResources(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
13365
+ prompts: (ws, msg) => handleMcpPrompts(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
13366
+ resourceRead: (ws, msg) => handleMcpResourceRead(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry),
13367
+ promptGet: (ws, msg) => handleMcpPromptGet(ws, msg, deps2.globalConfigPath, deps2.mcpRegistry)
13368
+ };
13369
+ const sendBrainStatus = (ws) => {
13370
+ const snapshot = deps2.brainRuntime.getSnapshot();
13371
+ send(ws, {
13372
+ type: "brain.status",
13373
+ payload: {
13374
+ maxAutoRisk: deps2.brainSettings.maxAutoRisk,
13375
+ log: deps2.brainLog,
13376
+ mode: snapshot.mode,
13377
+ poolLabels: snapshot.poolLabels,
13378
+ councilLabels: snapshot.councilLabels,
13379
+ ledgerPath: snapshot.ledger.path
13380
+ }
13381
+ });
12428
13382
  };
12429
13383
  const brainRoutes = {
12430
13384
  status: (ws) => {
12431
- send(ws, {
12432
- type: "brain.status",
12433
- payload: { maxAutoRisk: deps2.brainSettings.maxAutoRisk, log: deps2.brainLog }
12434
- });
13385
+ sendBrainStatus(ws);
12435
13386
  },
12436
13387
  risk: (ws, msg) => {
12437
13388
  const parsed = validateBrainRiskPayload(msg.payload);
@@ -12441,11 +13392,43 @@ function buildRoutes(state, deps2, cb) {
12441
13392
  }
12442
13393
  const { level } = parsed.value;
12443
13394
  deps2.brainSettings.maxAutoRisk = level;
13395
+ sendBrainStatus(ws);
13396
+ },
13397
+ configGet: (ws) => {
12444
13398
  send(ws, {
12445
- type: "brain.status",
12446
- payload: { maxAutoRisk: deps2.brainSettings.maxAutoRisk, log: deps2.brainLog }
13399
+ type: "brain.config",
13400
+ payload: { config: deps2.brainRuntime.getSnapshot(), persisted: true }
12447
13401
  });
12448
13402
  },
13403
+ configSet: async (ws, msg) => {
13404
+ const parsed = validateBrainConfigSetPayload(msg.payload);
13405
+ if (!parsed.ok) {
13406
+ sendResult(ws, false, parsed.message);
13407
+ return;
13408
+ }
13409
+ try {
13410
+ const { persisted } = deps2.brainRuntime.apply(parsed.value.patch);
13411
+ const result = await persisted;
13412
+ send(ws, {
13413
+ type: "brain.config",
13414
+ payload: {
13415
+ config: deps2.brainRuntime.getSnapshot(),
13416
+ persisted: result.ok,
13417
+ ...result.ok ? {} : { error: result.error ?? "Persist failed." }
13418
+ }
13419
+ });
13420
+ sendBrainStatus(ws);
13421
+ } catch (err) {
13422
+ send(ws, {
13423
+ type: "brain.config",
13424
+ payload: {
13425
+ config: deps2.brainRuntime.getSnapshot(),
13426
+ persisted: false,
13427
+ error: `Invalid Brain setting: ${errMessage(err)}`
13428
+ }
13429
+ });
13430
+ }
13431
+ },
12449
13432
  ask: async (ws, msg) => {
12450
13433
  const parsed = validateBrainAskPayload(msg.payload);
12451
13434
  if (!parsed.ok) {
@@ -12518,8 +13501,16 @@ async function startWebUI(opts = {}) {
12518
13501
  console.log("[WebUI] Config loaded:", config.provider ?? "(none)", "/", config.model ?? "(none)");
12519
13502
  if (!config.provider && config.providers && typeof config.providers === "object" && config.providers !== null && !Array.isArray(config.providers) && Object.keys(config.providers).length > 0) {
12520
13503
  const firstKey = expectDefined3(Object.keys(config.providers)[0]);
12521
- config = patchConfig(config, { provider: firstKey });
12522
- console.log("[WebUI] No active provider \u2014 auto-selected:", firstKey);
13504
+ const adoptModel = !config.model ? config.providers[firstKey]?.models?.[0] : void 0;
13505
+ config = patchConfig(config, {
13506
+ provider: firstKey,
13507
+ ...adoptModel ? { model: adoptModel } : {}
13508
+ });
13509
+ console.log(
13510
+ "[WebUI] No active provider \u2014 auto-selected:",
13511
+ firstKey,
13512
+ adoptModel ? `/ ${adoptModel}` : ""
13513
+ );
12523
13514
  }
12524
13515
  const needsProvider = !config.provider || !config.model;
12525
13516
  const preContext = await createPreContextServices({
@@ -12553,7 +13544,8 @@ async function startWebUI(opts = {}) {
12553
13544
  promptsCtx,
12554
13545
  modelCapabilitiesRef,
12555
13546
  provider,
12556
- context
13547
+ context,
13548
+ sessionIdentity
12557
13549
  } = preContext;
12558
13550
  let sessionStore = preContext.sessionStore;
12559
13551
  let session = preContext.session;
@@ -12586,7 +13578,12 @@ async function startWebUI(opts = {}) {
12586
13578
  modelCapabilitiesRef,
12587
13579
  sessionGetter: () => session,
12588
13580
  sessionReader,
12589
- annotationsStore
13581
+ annotationsStore,
13582
+ // Brain settings persist to the GLOBAL config only (config.brain is on
13583
+ // the in-project deny list), serialized behind the shared write lock.
13584
+ persistBrainConfig: (brainConfig) => updateGlobalConfig2((decrypted) => {
13585
+ decrypted["brain"] = brainConfig;
13586
+ }, "brain.config")
12590
13587
  });
12591
13588
  const {
12592
13589
  compactor,
@@ -12596,6 +13593,7 @@ async function startWebUI(opts = {}) {
12596
13593
  pipelines,
12597
13594
  brain,
12598
13595
  brainSettings,
13596
+ brainRuntime,
12599
13597
  brainLog,
12600
13598
  brainMonitor,
12601
13599
  codebaseIndexing,
@@ -12766,10 +13764,16 @@ async function startWebUI(opts = {}) {
12766
13764
  terminalHandler,
12767
13765
  brain,
12768
13766
  brainSettings,
13767
+ brainRuntime,
12769
13768
  brainLog
12770
13769
  };
12771
13770
  const cb = {
12772
13771
  sessionStartPayload,
13772
+ onSessionSwapped: async (sessionId) => {
13773
+ await sessionIdentity.activate(sessionId);
13774
+ const { hydrateSessionKanban: hydrateSessionKanban2 } = await import("@wrongstack/tools/session-kanban");
13775
+ await hydrateSessionKanban2(deps2.context);
13776
+ },
12773
13777
  updateAutoCompactionMaxContext,
12774
13778
  updateGlobalConfig: updateGlobalConfig2,
12775
13779
  persistPrefsToConfig: persistPrefsToConfig2,
@@ -12895,7 +13899,10 @@ async function startWebUI(opts = {}) {
12895
13899
  onShutdown: async () => {
12896
13900
  credentialWatcherClose?.();
12897
13901
  brainMonitor.stop();
13902
+ await agentServices.brainLedger?.stop().catch(() => {
13903
+ });
12898
13904
  await mcpRegistry.stopAll().catch(() => void 0);
13905
+ await sessionIdentity.stop();
12899
13906
  eventArming.getDispose()?.();
12900
13907
  if (eternalSubscription) {
12901
13908
  eternalSubscription.dispose();