@wrongstack/webui-server 0.302.0 → 0.303.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1464,7 +1464,13 @@ function indexDbVersion(projectRoot, indexDir) {
1464
1464
  try {
1465
1465
  const dir = resolveIndexDir(projectRoot, indexDir);
1466
1466
  const st = fs.statSync(path.join(dir, DB_FILE));
1467
- return `${st.mtimeMs}:${st.size}`;
1467
+ let wal = "";
1468
+ try {
1469
+ const walSt = fs.statSync(path.join(dir, `${DB_FILE}-wal`));
1470
+ wal = `:${walSt.mtimeMs}:${walSt.size}`;
1471
+ } catch {
1472
+ }
1473
+ return `${st.mtimeMs}:${st.size}${wal}`;
1468
1474
  } catch {
1469
1475
  return "missing";
1470
1476
  }
@@ -4409,7 +4415,8 @@ function createConversationOperations(ctx) {
4409
4415
  userMessage: async (ws, msg) => {
4410
4416
  if (!ensureCurrentSession(ws, msg, "user_message")) return;
4411
4417
  const payload = msg.payload ?? {};
4412
- const controller = ctx.runControl.begin(ws);
4418
+ const originSessionId = ctx.getSessionId();
4419
+ const controller = ctx.runControl.begin(ws, originSessionId);
4413
4420
  if (!controller) {
4414
4421
  ctx.send(ws, {
4415
4422
  type: "error",
@@ -4420,7 +4427,6 @@ function createConversationOperations(ctx) {
4420
4427
  });
4421
4428
  return;
4422
4429
  }
4423
- const originSessionId = ctx.getSessionId();
4424
4430
  try {
4425
4431
  const agent = ctx.getAgent();
4426
4432
  if (payload.freshContext === true) await startFreshTopicContext(agent.ctx);
@@ -4479,12 +4485,13 @@ function createConversationOperations(ctx) {
4479
4485
  });
4480
4486
  }
4481
4487
  } finally {
4482
- ctx.runControl.end(ws, controller);
4488
+ ctx.runControl.end(ws, originSessionId, controller);
4483
4489
  }
4484
4490
  },
4485
4491
  abort: (ws, msg) => {
4486
4492
  if (!ensureCurrentSession(ws, msg, "abort")) return;
4487
- ctx.runControl.abort(ws);
4493
+ const sessionId = requestedSessionId(msg) ?? ctx.getSessionId();
4494
+ ctx.runControl.abort(ws, sessionId);
4488
4495
  ctx.notifyAbort(ws, {
4489
4496
  type: "error",
4490
4497
  payload: sessionPayload2({ phase: "abort", message: "User aborted" })
@@ -5536,6 +5543,7 @@ function createConnectionLifecycle(options) {
5536
5543
  }
5537
5544
 
5538
5545
  // src/server/connections-health-route.ts
5546
+ import * as net from "node:net";
5539
5547
  import {
5540
5548
  ChronicleProjectServerClient,
5541
5549
  createChronicleProjectAccess as createChronicleProjectAccess2,
@@ -5545,13 +5553,22 @@ import {
5545
5553
  isMailboxProjectServerAvailable,
5546
5554
  MailboxProjectServerConnection
5547
5555
  } from "@wrongstack/core/coordination";
5556
+ import { SessionCatalogProjectClient } from "@wrongstack/core/session-catalog";
5548
5557
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
5549
5558
  import {
5550
5559
  closeKanbanServerConnections,
5551
5560
  getKanbanServerConnection,
5552
5561
  isKanbanServerAvailable
5553
5562
  } from "@wrongstack/kanban";
5554
- import * as net from "node:net";
5563
+ import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
5564
+ import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
5565
+ import {
5566
+ checkCodebaseIndexServerHealth,
5567
+ ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
5568
+ getIndexState,
5569
+ resolveProjectIndexDaemonAvailability,
5570
+ shutdownCodebaseIndexServer
5571
+ } from "@wrongstack/tools";
5555
5572
 
5556
5573
  // src/server/privileged-actions.ts
5557
5574
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -5585,15 +5602,6 @@ async function authorizeWebUIAction(boundary, action, logger) {
5585
5602
  }
5586
5603
 
5587
5604
  // src/server/connections-health-route.ts
5588
- import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
5589
- import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
5590
- import {
5591
- checkCodebaseIndexServerHealth,
5592
- ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
5593
- getIndexState,
5594
- resolveProjectIndexDaemonAvailability,
5595
- shutdownCodebaseIndexServer
5596
- } from "@wrongstack/tools";
5597
5605
  async function handleConnectionsHealthRoute(context, ws, message) {
5598
5606
  if (message.type !== "connections.health") return false;
5599
5607
  try {
@@ -5614,6 +5622,7 @@ async function handleConnectionsHealthRoute(context, ws, message) {
5614
5622
  async function collectConnectionsHealth(options) {
5615
5623
  const services = await Promise.all([
5616
5624
  Promise.resolve(webuiHealth(options.backend)),
5625
+ sessionCatalogHealth(options.projectRoot),
5617
5626
  chronicleHealth(options.projectRoot),
5618
5627
  codebaseIndexHealth(options.projectRoot, options.indexDir),
5619
5628
  sageHealth(options.projectRoot),
@@ -5631,6 +5640,50 @@ async function collectConnectionsHealth(options) {
5631
5640
  services
5632
5641
  };
5633
5642
  }
5643
+ async function sessionCatalogHealth(projectRoot) {
5644
+ const startedAt = Date.now();
5645
+ try {
5646
+ const paths = resolveWstackPaths2({ projectRoot });
5647
+ const client = new SessionCatalogProjectClient({
5648
+ projectDir: paths.projectDir,
5649
+ projectRoot
5650
+ });
5651
+ try {
5652
+ const health = await client.ping();
5653
+ return {
5654
+ id: "session-catalog",
5655
+ label: "Session Catalog",
5656
+ status: health.damagedRows > 0 ? "degraded" : "healthy",
5657
+ required: true,
5658
+ mode: "project-daemon",
5659
+ detail: health.damagedRows > 0 ? `${health.damagedRows} damaged catalog row(s); rebuild is required.` : `${health.catalogRows} catalog session(s), ${health.liveLeases} live lease(s), ${health.reservations} reservation(s).`,
5660
+ ownerPid: health.pid,
5661
+ endpoint: health.endpoint,
5662
+ storage: health.databasePath,
5663
+ uptimeMs: health.uptimeMs,
5664
+ latencyMs: Date.now() - startedAt,
5665
+ clients: health.clients,
5666
+ activeRequests: health.activeRequests,
5667
+ queuedWork: health.reservations + health.maintenanceLeases,
5668
+ control: "none"
5669
+ };
5670
+ } finally {
5671
+ await client.close().catch(() => void 0);
5672
+ }
5673
+ } catch (error2) {
5674
+ return {
5675
+ id: "session-catalog",
5676
+ label: "Session Catalog",
5677
+ status: "error",
5678
+ required: true,
5679
+ mode: "project-daemon",
5680
+ detail: "Project-scoped session ownership and catalog are unavailable.",
5681
+ latencyMs: Date.now() - startedAt,
5682
+ lastError: error2 instanceof Error ? error2.message : String(error2),
5683
+ control: "none"
5684
+ };
5685
+ }
5686
+ }
5634
5687
  function webuiHealth(backend) {
5635
5688
  return {
5636
5689
  id: "webui",
@@ -6229,7 +6282,11 @@ async function restartSageServer(projectRoot) {
6229
6282
  });
6230
6283
  const verifyConn = new SageProjectServerConnection(projectRoot);
6231
6284
  try {
6232
- await verifyConn.call("ping", {}, { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } });
6285
+ await verifyConn.call(
6286
+ "ping",
6287
+ {},
6288
+ { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } }
6289
+ );
6233
6290
  return {
6234
6291
  serviceId: "sage",
6235
6292
  action: "restart",
@@ -7638,12 +7695,34 @@ function handleTodosGet(ctx, ws) {
7638
7695
  payload: sessionPayload(ctx, { todos: [...ctx.context.todos] })
7639
7696
  });
7640
7697
  }
7641
- function handleTodosClear(ctx, ws) {
7642
- ctx.replaceTodos?.([]);
7643
- sendResult3(ctx, ws, true, "Todos cleared");
7644
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: [] }) });
7698
+ async function commitTodos(ctx, todos) {
7699
+ if (ctx.mutateTodos) {
7700
+ const result = await ctx.mutateTodos(todos);
7701
+ return { todos: result.todos, warnings: result.warnings ?? [] };
7702
+ }
7703
+ ctx.replaceTodos?.(todos);
7704
+ return { todos: [...todos], warnings: [] };
7705
+ }
7706
+ function managedProjectionMessage() {
7707
+ return "Kanban-bound todos are task projections. Change or remove the task from Kanban.";
7645
7708
  }
7646
- function handleTodosRemove(ctx, ws, payload) {
7709
+ async function handleTodosClear(ctx, ws) {
7710
+ if (ctx.context.todos.some((todo) => todo.kanbanBoardId && todo.kanbanTaskId)) {
7711
+ sendResult3(ctx, ws, false, managedProjectionMessage());
7712
+ return;
7713
+ }
7714
+ try {
7715
+ const result = await commitTodos(ctx, []);
7716
+ sendResult3(ctx, ws, true, "Todos cleared");
7717
+ ctx.broadcast({
7718
+ type: "todos.updated",
7719
+ payload: sessionPayload(ctx, { todos: result.todos })
7720
+ });
7721
+ } catch (error2) {
7722
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7723
+ }
7724
+ }
7725
+ async function handleTodosRemove(ctx, ws, payload) {
7647
7726
  if (!payload) {
7648
7727
  sendResult3(ctx, ws, false, "Missing id or index");
7649
7728
  return;
@@ -7660,12 +7739,27 @@ function handleTodosRemove(ctx, ws, payload) {
7660
7739
  sendResult3(ctx, ws, false, "Todo not found");
7661
7740
  return;
7662
7741
  }
7742
+ if (removed.kanbanBoardId && removed.kanbanTaskId) {
7743
+ sendResult3(ctx, ws, false, managedProjectionMessage());
7744
+ return;
7745
+ }
7663
7746
  const next = [...todos.slice(0, targetIndex), ...todos.slice(targetIndex + 1)];
7664
- ctx.replaceTodos?.(next);
7665
- sendResult3(ctx, ws, true, `Removed: ${removed.content}`);
7666
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: next }) });
7747
+ try {
7748
+ const result = await commitTodos(ctx, next);
7749
+ sendResult3(ctx, ws, true, `Removed: ${removed.content}`);
7750
+ ctx.broadcast({
7751
+ type: "todos.updated",
7752
+ payload: sessionPayload(ctx, { todos: result.todos })
7753
+ });
7754
+ } catch (error2) {
7755
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7756
+ }
7667
7757
  }
7668
- function handleTodoUpdate(ctx, ws, payload) {
7758
+ async function handleTodoUpdate(ctx, ws, payload) {
7759
+ if (!payload || typeof payload.id !== "string" || payload.status !== void 0 && payload.status !== "pending" && payload.status !== "in_progress" && payload.status !== "completed" || payload.activeForm !== void 0 && typeof payload.activeForm !== "string") {
7760
+ sendResult3(ctx, ws, false, "Invalid todo update payload");
7761
+ return;
7762
+ }
7669
7763
  const index = ctx.context.todos.findIndex((todo) => todo.id === payload.id);
7670
7764
  const existing = ctx.context.todos[index];
7671
7765
  if (index === -1 || !existing) {
@@ -7678,9 +7772,25 @@ function handleTodoUpdate(ctx, ws, payload) {
7678
7772
  status: payload.status ?? existing.status,
7679
7773
  activeForm: payload.activeForm !== void 0 ? payload.activeForm : existing.activeForm
7680
7774
  };
7681
- ctx.replaceTodos?.(next);
7682
- sendResult3(ctx, ws, true, `Todo "${existing.content}" updated`);
7683
- ctx.broadcast({ type: "todos.updated", payload: sessionPayload(ctx, { todos: next }) });
7775
+ try {
7776
+ const result = await commitTodos(ctx, next);
7777
+ const projected = result.todos.find((todo) => todo.id === existing.id);
7778
+ const requestedStatus = payload.status ?? existing.status;
7779
+ const projectionRejected = Boolean(existing.kanbanBoardId && existing.kanbanTaskId) && projected?.status !== requestedStatus;
7780
+ const warning = result.warnings[0];
7781
+ sendResult3(
7782
+ ctx,
7783
+ ws,
7784
+ !projectionRejected,
7785
+ projectionRejected ? warning ?? `Kanban kept "${existing.content}" at ${projected?.status ?? "its current state"}.` : warning ? `Todo "${existing.content}" updated. ${warning}` : `Todo "${existing.content}" updated`
7786
+ );
7787
+ ctx.broadcast({
7788
+ type: "todos.updated",
7789
+ payload: sessionPayload(ctx, { todos: result.todos })
7790
+ });
7791
+ } catch (error2) {
7792
+ sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
7793
+ }
7684
7794
  }
7685
7795
  async function handleTasksGet(ctx, ws) {
7686
7796
  const taskPath = taskPathOf(ctx);
@@ -7708,14 +7818,32 @@ async function handleTaskUpdate(ctx, ws, payload) {
7708
7818
  return;
7709
7819
  }
7710
7820
  try {
7711
- const file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
7712
- const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
7713
- if (!task) return tasks;
7714
- task.status = payload.status;
7715
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7716
- return tasks;
7717
- });
7718
- sendResult3(ctx, ws, true, `Task status updated to "${payload.status}".`);
7821
+ let file;
7822
+ if (ctx.mutateTaskStatus) {
7823
+ const result = await ctx.mutateTaskStatus(payload.id, payload.status);
7824
+ if (!result.ok) {
7825
+ sendResult3(ctx, ws, false, result.message);
7826
+ return;
7827
+ }
7828
+ file = await loadTasks(taskPath);
7829
+ if (!file) throw new Error("Task mutation succeeded but its persisted snapshot is missing.");
7830
+ sendResult3(ctx, ws, true, result.message);
7831
+ } else {
7832
+ let matched = false;
7833
+ file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
7834
+ const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
7835
+ if (!task) return tasks;
7836
+ matched = true;
7837
+ task.status = payload.status;
7838
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7839
+ return tasks;
7840
+ });
7841
+ if (!matched) {
7842
+ sendResult3(ctx, ws, false, `Task "${payload.id}" not found.`);
7843
+ return;
7844
+ }
7845
+ sendResult3(ctx, ws, true, `Task status updated to "${payload.status}".`);
7846
+ }
7719
7847
  ctx.broadcast({
7720
7848
  type: "tasks.updated",
7721
7849
  payload: sessionPayload(ctx, { tasks: file.tasks })
@@ -7762,6 +7890,18 @@ async function handlePlanTemplateUse(ctx, ws, template) {
7762
7890
  return;
7763
7891
  }
7764
7892
  try {
7893
+ if (ctx.mutatePlan) {
7894
+ const result = await ctx.mutatePlan({ action: "template_use", template });
7895
+ if (!result.ok) {
7896
+ sendResult3(ctx, ws, false, result.message);
7897
+ return;
7898
+ }
7899
+ const plan2 = await loadPlan(planPath);
7900
+ if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
7901
+ sendResult3(ctx, ws, true, result.message);
7902
+ ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
7903
+ return;
7904
+ }
7765
7905
  const templateDefinition = getPlanTemplate(template);
7766
7906
  if (!templateDefinition) {
7767
7907
  sendResult3(ctx, ws, false, `Unknown template "${template}".`);
@@ -7790,6 +7930,22 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
7790
7930
  return;
7791
7931
  }
7792
7932
  try {
7933
+ if (ctx.mutatePlan) {
7934
+ const result = await ctx.mutatePlan({
7935
+ action: "status",
7936
+ target: payload.target,
7937
+ status: payload.status
7938
+ });
7939
+ if (!result.ok) {
7940
+ sendResult3(ctx, ws, false, result.message);
7941
+ return;
7942
+ }
7943
+ const plan2 = await loadPlan(planPath);
7944
+ if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
7945
+ sendResult3(ctx, ws, true, result.message);
7946
+ ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
7947
+ return;
7948
+ }
7793
7949
  let changed = false;
7794
7950
  const plan = await mutatePlan(planPath, currentSessionId(ctx), async (currentPlan) => {
7795
7951
  const before = currentPlan.updatedAt;
@@ -7813,13 +7969,17 @@ async function handleWorklistMessage(ctx, ws, message) {
7813
7969
  handleTodosGet(ctx, ws);
7814
7970
  return;
7815
7971
  case "todos.clear":
7816
- handleTodosClear(ctx, ws);
7972
+ await handleTodosClear(ctx, ws);
7817
7973
  return;
7818
7974
  case "todos.remove":
7819
- handleTodosRemove(ctx, ws, message.payload);
7975
+ await handleTodosRemove(
7976
+ ctx,
7977
+ ws,
7978
+ message.payload
7979
+ );
7820
7980
  return;
7821
7981
  case "todo.update":
7822
- handleTodoUpdate(
7982
+ await handleTodoUpdate(
7823
7983
  ctx,
7824
7984
  ws,
7825
7985
  message.payload
@@ -8248,8 +8408,8 @@ async function handleApiSessions(res, globalRoot) {
8248
8408
  return;
8249
8409
  }
8250
8410
  try {
8251
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8252
- const registry = new SessionRegistry(globalRoot);
8411
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8412
+ const registry = getSessionRegistry3(globalRoot);
8253
8413
  const sessions = await registry.list();
8254
8414
  const result = sessions.map((s) => ({
8255
8415
  sessionId: s.sessionId,
@@ -8286,8 +8446,8 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
8286
8446
  return;
8287
8447
  }
8288
8448
  try {
8289
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8290
- const registry = new SessionRegistry(globalRoot);
8449
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8450
+ const registry = getSessionRegistry3(globalRoot);
8291
8451
  const entry = await registry.get(sessionId);
8292
8452
  if (!entry) {
8293
8453
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8295,20 +8455,22 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
8295
8455
  return;
8296
8456
  }
8297
8457
  res.writeHead(200, { "Content-Type": "application/json" });
8298
- res.end(JSON.stringify({
8299
- sessionId: entry.sessionId,
8300
- projectName: entry.projectName,
8301
- status: entry.status,
8302
- agents: entry.agents.map((a) => ({
8303
- id: a.id,
8304
- name: a.name,
8305
- status: a.status,
8306
- currentTool: a.currentTool,
8307
- iterations: a.iterations,
8308
- toolCalls: a.toolCalls,
8309
- lastActivityAt: a.lastActivityAt
8310
- }))
8311
- }));
8458
+ res.end(
8459
+ JSON.stringify({
8460
+ sessionId: entry.sessionId,
8461
+ projectName: entry.projectName,
8462
+ status: entry.status,
8463
+ agents: entry.agents.map((a) => ({
8464
+ id: a.id,
8465
+ name: a.name,
8466
+ status: a.status,
8467
+ currentTool: a.currentTool,
8468
+ iterations: a.iterations,
8469
+ toolCalls: a.toolCalls,
8470
+ lastActivityAt: a.lastActivityAt
8471
+ }))
8472
+ })
8473
+ );
8312
8474
  } catch (err) {
8313
8475
  res.writeHead(500, { "Content-Type": "application/json" });
8314
8476
  res.end(JSON.stringify({ error: sanitizeApiError(err) }));
@@ -8426,9 +8588,9 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
8426
8588
  return;
8427
8589
  }
8428
8590
  try {
8429
- const { SessionRegistry, DefaultSessionStore: DefaultSessionStore4, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
8591
+ const { getSessionRegistry: getSessionRegistry3, DefaultSessionStore: DefaultSessionStore4, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
8430
8592
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8431
- const registry = new SessionRegistry(globalRoot);
8593
+ const registry = getSessionRegistry3(globalRoot);
8432
8594
  const entry = await registry.get(sessionId);
8433
8595
  if (!entry) {
8434
8596
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8436,7 +8598,10 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
8436
8598
  return;
8437
8599
  }
8438
8600
  const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
8439
- const store = new DefaultSessionStore4({ dir: paths.projectSessions });
8601
+ const store = new DefaultSessionStore4({
8602
+ dir: paths.projectSessions,
8603
+ projectRoot: entry.projectRoot
8604
+ });
8440
8605
  const reader = new DefaultSessionReader2({ store });
8441
8606
  const RING = Math.max(limit * 4, 2e3);
8442
8607
  const ring = [];
@@ -8528,10 +8693,10 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
8528
8693
  const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
8529
8694
  const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
8530
8695
  try {
8531
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8696
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8532
8697
  const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8533
8698
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8534
- const registry = new SessionRegistry(globalRoot);
8699
+ const registry = getSessionRegistry3(globalRoot);
8535
8700
  const entry = await registry.get(sessionId);
8536
8701
  if (!entry) {
8537
8702
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8556,10 +8721,10 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
8556
8721
  return;
8557
8722
  }
8558
8723
  try {
8559
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8724
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8560
8725
  const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8561
8726
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8562
- const registry = new SessionRegistry(globalRoot);
8727
+ const registry = getSessionRegistry3(globalRoot);
8563
8728
  const entry = await registry.get(sessionId);
8564
8729
  if (!entry) {
8565
8730
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8616,10 +8781,10 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
8616
8781
  const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
8617
8782
  const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
8618
8783
  try {
8619
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8784
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8620
8785
  const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8621
8786
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8622
- const registry = new SessionRegistry(globalRoot);
8787
+ const registry = getSessionRegistry3(globalRoot);
8623
8788
  const entry = await registry.get(sessionId);
8624
8789
  if (!entry) {
8625
8790
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8665,10 +8830,10 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
8665
8830
  }
8666
8831
  const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
8667
8832
  try {
8668
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8833
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8669
8834
  const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8670
8835
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8671
- const registry = new SessionRegistry(globalRoot);
8836
+ const registry = getSessionRegistry3(globalRoot);
8672
8837
  const all = await registry.list();
8673
8838
  const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
8674
8839
  const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
@@ -10872,9 +11037,9 @@ import {
10872
11037
  claimReadyTask,
10873
11038
  copyTaskToBoard,
10874
11039
  createBoard,
11040
+ createBoardFromText,
10875
11041
  duplicateBoard,
10876
11042
  exportBoardToTaskGraph,
10877
- createBoardFromText,
10878
11043
  getBoard,
10879
11044
  getKanbanOrchestrationSnapshot,
10880
11045
  getKanbanQueueHealth,
@@ -11062,8 +11227,35 @@ async function handleTaskVerification(ws, type, payload, ctx) {
11062
11227
  }
11063
11228
  }
11064
11229
 
11230
+ // src/server/kanban-route-pagination.ts
11231
+ function paginateKanbanBoards(boards, input) {
11232
+ const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
11233
+ const activeSessionIds = new Set(input.activeSessionIds ?? []);
11234
+ const isActive = (board) => board.presence?.some((entry) => entry.active) === true || board.tags?.some((tag) => tag.startsWith("session:") && activeSessionIds.has(tag.slice(8))) === true;
11235
+ const sorted = [...boards].sort((left, right) => {
11236
+ const activityOrder = Number(isActive(right)) - Number(isActive(left));
11237
+ return activityOrder || right.updatedAt.localeCompare(left.updatedAt);
11238
+ });
11239
+ const activeTotal = sorted.filter(isActive).length;
11240
+ const total = sorted.length;
11241
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
11242
+ const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page) : 1;
11243
+ const page = Math.min(totalPages, Math.max(1, requestedPage));
11244
+ const start = (page - 1) * pageSize;
11245
+ return {
11246
+ items: sorted.slice(start, start + pageSize),
11247
+ total,
11248
+ page,
11249
+ pageSize,
11250
+ totalPages,
11251
+ activeTotal,
11252
+ orphanedTotal: total - activeTotal
11253
+ };
11254
+ }
11255
+
11065
11256
  // src/server/kanban-task-routes.ts
11066
11257
  import {
11258
+ getKanbanWorkbench,
11067
11259
  getTask,
11068
11260
  listTaskActivity,
11069
11261
  recordTaskActivity,
@@ -11071,6 +11263,16 @@ import {
11071
11263
  } from "@wrongstack/kanban";
11072
11264
  async function handleKanbanTaskRoute(ws, type, payload, ctx) {
11073
11265
  switch (type) {
11266
+ case "kanban.workbench":
11267
+ ok(
11268
+ ws,
11269
+ type,
11270
+ await getKanbanWorkbench(ctx.projectRoot, {
11271
+ ...typeof payload?.limitPerLane === "number" ? { limitPerLane: payload.limitPerLane } : {},
11272
+ ...typeof payload?.alertLimit === "number" ? { alertLimit: payload.alertLimit } : {}
11273
+ })
11274
+ );
11275
+ return true;
11074
11276
  case "kanban.task.remove":
11075
11277
  await handleTaskRemove(ws, type, payload, ctx);
11076
11278
  return true;
@@ -11163,40 +11365,11 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
11163
11365
  outcome,
11164
11366
  ...typeof payload?.details === "string" && payload.details.trim() ? { details: payload.details.trim() } : {}
11165
11367
  },
11166
- activityContext(
11167
- ctx,
11168
- payload?.actor ?? ctx.context?.agentId ?? "webui"
11169
- )
11368
+ activityContext(ctx, payload?.actor ?? ctx.context?.agentId ?? "webui")
11170
11369
  );
11171
11370
  board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
11172
11371
  }
11173
11372
 
11174
- // src/server/kanban-route-pagination.ts
11175
- function paginateKanbanBoards(boards, input) {
11176
- const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
11177
- const activeSessionIds = new Set(input.activeSessionIds ?? []);
11178
- const isActive = (board) => board.presence?.some((entry) => entry.active) === true || board.tags?.some((tag) => tag.startsWith("session:") && activeSessionIds.has(tag.slice(8))) === true;
11179
- const sorted = [...boards].sort((left, right) => {
11180
- const activityOrder = Number(isActive(right)) - Number(isActive(left));
11181
- return activityOrder || right.updatedAt.localeCompare(left.updatedAt);
11182
- });
11183
- const activeTotal = sorted.filter(isActive).length;
11184
- const total = sorted.length;
11185
- const totalPages = Math.max(1, Math.ceil(total / pageSize));
11186
- const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page) : 1;
11187
- const page = Math.min(totalPages, Math.max(1, requestedPage));
11188
- const start = (page - 1) * pageSize;
11189
- return {
11190
- items: sorted.slice(start, start + pageSize),
11191
- total,
11192
- page,
11193
- pageSize,
11194
- totalPages,
11195
- activeTotal,
11196
- orphanedTotal: total - activeTotal
11197
- };
11198
- }
11199
-
11200
11373
  // src/server/kanban-route-protocol.ts
11201
11374
  var KANBAN_CLIENT_MESSAGE_TYPES = [
11202
11375
  "kanban.capabilities",
@@ -11241,7 +11414,8 @@ var KANBAN_CLIENT_MESSAGE_TYPES = [
11241
11414
  "kanban.task.verify",
11242
11415
  "kanban.taskgraph.export",
11243
11416
  "kanban.taskgraph.sync",
11244
- "kanban.update"
11417
+ "kanban.update",
11418
+ "kanban.workbench"
11245
11419
  ];
11246
11420
 
11247
11421
  // src/server/kanban-routes.ts
@@ -11304,6 +11478,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
11304
11478
  fail(ws, type, `Board not found: ${boardId}`);
11305
11479
  return true;
11306
11480
  }
11481
+ if (ctx.supervisor) {
11482
+ const snapshots = await ctx.supervisor.auditNow(boardId);
11483
+ const snapshot = snapshots[0];
11484
+ if (snapshot) {
11485
+ ok(ws, type, snapshot);
11486
+ return true;
11487
+ }
11488
+ }
11307
11489
  const reconciled = await reconcileKanbanBoard(ctx.projectRoot, boardId);
11308
11490
  let health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
11309
11491
  const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments(ctx.projectRoot, boardId, {
@@ -12014,7 +12196,9 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
12014
12196
  projectRoot,
12015
12197
  async (event) => {
12016
12198
  const family = event.event?.split(".")[0];
12017
- if (family !== "board" && family !== "task" && family !== "column") return;
12199
+ if (family !== "board" && family !== "task" && family !== "column" && family !== "contract") {
12200
+ return;
12201
+ }
12018
12202
  const evData = event.data;
12019
12203
  const boardId = evData?.boardId;
12020
12204
  if (!boardId) return;
@@ -12068,6 +12252,15 @@ function createShutdown(res) {
12068
12252
  } catch {
12069
12253
  }
12070
12254
  }
12255
+ if (res.onPreShutdown) {
12256
+ try {
12257
+ await res.onPreShutdown();
12258
+ } catch (e) {
12259
+ log(
12260
+ `[WebUI] Error during pre-shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`
12261
+ );
12262
+ }
12263
+ }
12071
12264
  for (const server of res.servers) server?.close();
12072
12265
  if (res.onShutdown) {
12073
12266
  try {
@@ -13322,14 +13515,17 @@ async function handleSageForFile(ws, msg, memoryStore) {
13322
13515
  send(ws, { type: "memory.sage.forFile", payload: { error: "filePath is required" } });
13323
13516
  return;
13324
13517
  }
13518
+ const includeSuperseded = typeof payload["showSuperseded"] === "boolean" ? payload["showSuperseded"] : typeof payload["includeSuperseded"] === "boolean" ? payload["includeSuperseded"] : void 0;
13519
+ const includeDeleted = payload["showDeleted"] === true || payload["includeDeleted"] === true;
13325
13520
  try {
13326
13521
  const response = await Sage.findMemoriesForFile(filePath, {
13327
13522
  ...typeof payload["lineStart"] === "number" ? { lineStart: payload["lineStart"] } : {},
13328
13523
  ...typeof payload["lineEnd"] === "number" ? { lineEnd: payload["lineEnd"] } : {},
13329
13524
  ...typeof payload["limit"] === "number" ? { limit: payload["limit"] } : {},
13330
- ...payload["includeDeleted"] === true ? { includeDeleted: true } : {}
13525
+ ...includeSuperseded !== void 0 ? { includeSuperseded } : {},
13526
+ ...includeDeleted ? { includeDeleted: true } : {}
13331
13527
  });
13332
- send(ws, { type: "memory.sage.forFile", payload: response });
13528
+ send(ws, { type: "memory.sage.forFile", payload: { response } });
13333
13529
  } catch (err) {
13334
13530
  send(ws, { type: "memory.sage.forFile", payload: { error: errMessage(err) } });
13335
13531
  }
@@ -14956,6 +15152,10 @@ import {
14956
15152
  recoverStaleTaskAssignments as recoverStaleTaskAssignments2,
14957
15153
  resolveGateEnforcement
14958
15154
  } from "@wrongstack/kanban";
15155
+ function resolveProjectRoot(deps2) {
15156
+ const root = deps2.projectRoot;
15157
+ return typeof root === "function" ? root() : root;
15158
+ }
14959
15159
  var DEFAULT_INTERVAL_MS = 1e4;
14960
15160
  var MIN_INTERVAL_MS = 2e3;
14961
15161
  var DEFAULT_AGENT_COOLDOWN_MS = 5 * 6e4;
@@ -15020,14 +15220,15 @@ function createKanbanSupervisor(deps2) {
15020
15220
  publish(snapshot2);
15021
15221
  return snapshot2;
15022
15222
  }
15023
- const reconciled = await reconcileKanbanBoard2(deps2.projectRoot, board.id);
15223
+ const reconciled = await reconcileKanbanBoard2(resolveProjectRoot(deps2), board.id);
15024
15224
  const gateSwept = await sweepGateParkedTasks(deps2, reconciled?.board ?? board);
15025
- let health = await getKanbanQueueHealth2(deps2.projectRoot, { boardId: board.id });
15026
- const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments2(deps2.projectRoot, board.id, {
15225
+ let health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
15226
+ const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments2(resolveProjectRoot(deps2), board.id, {
15027
15227
  mode: config.recoveryMode ?? "auto",
15028
15228
  reason: "Kanban supervisor found an expired worker lease."
15029
15229
  }) : null;
15030
- if (recovered) health = await getKanbanQueueHealth2(deps2.projectRoot, { boardId: board.id });
15230
+ if (recovered)
15231
+ health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
15031
15232
  const anomalyCount = countAnomalies(health);
15032
15233
  const snapshot = {
15033
15234
  boardId: board.id,
@@ -15046,7 +15247,7 @@ function createKanbanSupervisor(deps2) {
15046
15247
  await publishKanbanBoard(
15047
15248
  deps2.broadcast,
15048
15249
  changedBoard,
15049
- () => listBoards4(deps2.projectRoot)
15250
+ () => listBoards4(resolveProjectRoot(deps2))
15050
15251
  );
15051
15252
  }
15052
15253
  if (config.mode === "agentic" && anomalyCount > 0) {
@@ -15084,11 +15285,11 @@ function createKanbanSupervisor(deps2) {
15084
15285
  // tool-runtime boundary gate (`evaluateToolKanbanBoundary`) can resolve
15085
15286
  // the live board policy instead of failing open. Whole-board agentic
15086
15287
  // runs have no taskId, so only boardId is propagated.
15087
- context: { kanban: { boardId: board.id, projectRoot: deps2.projectRoot } },
15288
+ context: { kanban: { boardId: board.id, projectRoot: resolveProjectRoot(deps2) } },
15088
15289
  onDone: async (result) => {
15089
15290
  clearTimeout(watchdog);
15090
15291
  agentRunning.delete(board.id);
15091
- if (await getBoard3(deps2.projectRoot, board.id) === null) return;
15292
+ if (await getBoard3(resolveProjectRoot(deps2), board.id) === null) return;
15092
15293
  const current3 = snapshots.get(board.id) ?? snapshot;
15093
15294
  publish({
15094
15295
  ...current3,
@@ -15112,7 +15313,7 @@ function createKanbanSupervisor(deps2) {
15112
15313
  const auditNow = async (boardId) => {
15113
15314
  let boards;
15114
15315
  if (boardId) {
15115
- const board = await getBoard3(deps2.projectRoot, boardId);
15316
+ const board = await getBoard3(resolveProjectRoot(deps2), boardId);
15116
15317
  if (board === null) {
15117
15318
  forgetBoard(boardId);
15118
15319
  boards = [];
@@ -15120,9 +15321,11 @@ function createKanbanSupervisor(deps2) {
15120
15321
  boards = [board];
15121
15322
  }
15122
15323
  } else {
15123
- const summaries = await listBoards4(deps2.projectRoot);
15324
+ const summaries = await listBoards4(resolveProjectRoot(deps2));
15124
15325
  pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
15125
- boards = (await Promise.all(summaries.map((summary) => getBoard3(deps2.projectRoot, summary.id)))).filter((board) => Boolean(board));
15326
+ boards = (await Promise.all(
15327
+ summaries.map((summary) => getBoard3(resolveProjectRoot(deps2), summary.id))
15328
+ )).filter((board) => Boolean(board));
15126
15329
  }
15127
15330
  const results = [];
15128
15331
  for (const board of boards) results.push(await auditBoard(board));
@@ -15157,11 +15360,11 @@ function createKanbanSupervisor(deps2) {
15157
15360
  if (disposed) return;
15158
15361
  try {
15159
15362
  const now = Date.now();
15160
- const summaries = await listBoards4(deps2.projectRoot);
15363
+ const summaries = await listBoards4(resolveProjectRoot(deps2));
15161
15364
  pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
15162
15365
  for (const summary of summaries) {
15163
15366
  if ((nextDue.get(summary.id) ?? 0) > now) continue;
15164
- const board = await getBoard3(deps2.projectRoot, summary.id);
15367
+ const board = await getBoard3(resolveProjectRoot(deps2), summary.id);
15165
15368
  if (board) await auditBoard(board);
15166
15369
  }
15167
15370
  } catch (error2) {
@@ -15204,7 +15407,7 @@ async function sweepGateParkedTasks(deps2, board) {
15204
15407
  let lastBoard;
15205
15408
  for (const task of parked) {
15206
15409
  try {
15207
- const finalized = await finalizeTaskCompletion(deps2.projectRoot, board.id, task.id, {
15410
+ const finalized = await finalizeTaskCompletion(resolveProjectRoot(deps2), board.id, task.id, {
15208
15411
  eventContext: { actor: "kanban-supervisor" }
15209
15412
  });
15210
15413
  if (finalized) lastBoard = finalized.board;
@@ -17994,6 +18197,8 @@ function labelForEvent(e) {
17994
18197
  const count = e.messagesOmitted ?? e.messages.length;
17995
18198
  return `Messages replaced (${e.messagesOmitted ? "~" : ""}${count} msgs)`;
17996
18199
  }
18200
+ case "messages_dropped":
18201
+ return `Oldest ${e.count} message${e.count === 1 ? "" : "s"} evicted`;
17997
18202
  case "message_truncated":
17998
18203
  return `Message truncated: ${e.before} \u2192 ${e.after}`;
17999
18204
  case "file_event":
@@ -18081,6 +18286,8 @@ function detailForEvent(e) {
18081
18286
  return `at index ${e.index}`;
18082
18287
  case "messages_replaced":
18083
18288
  return `${e.messagesOmitted ?? e.messages.length} total`;
18289
+ case "messages_dropped":
18290
+ return `dropped ${e.count} from the front`;
18084
18291
  case "message_truncated":
18085
18292
  return `truncated to ${e.after} tokens`;
18086
18293
  case "mode_changed":
@@ -18298,7 +18505,7 @@ function createSessionHandlers(ctx) {
18298
18505
  const current2 = ctx.getSession();
18299
18506
  if (current2 !== next) {
18300
18507
  try {
18301
- ctx.abortActiveRun?.();
18508
+ ctx.abortActiveRun?.(current2.id);
18302
18509
  } catch {
18303
18510
  }
18304
18511
  await finalizeSession(current2);
@@ -18885,16 +19092,17 @@ function createEmbeddedConversationRoutes(ctx) {
18885
19092
  getAgent: () => ctx.agent,
18886
19093
  getSessionId: () => ctx.agent.ctx.session?.id ?? "",
18887
19094
  runControl: {
18888
- begin: (ws) => {
18889
- if (ctx.abortControllers.has(ws)) return void 0;
19095
+ begin: (_ws, sessionId) => {
19096
+ if (ctx.abortControllers.has(sessionId)) return void 0;
18890
19097
  const controller = new AbortController();
18891
- ctx.abortControllers.set(ws, controller);
19098
+ ctx.abortControllers.set(sessionId, controller);
18892
19099
  return controller;
18893
19100
  },
18894
- end: (ws, controller) => {
18895
- if (ctx.abortControllers.get(ws) === controller) ctx.abortControllers.delete(ws);
19101
+ end: (_ws, sessionId, controller) => {
19102
+ if (ctx.abortControllers.get(sessionId) === controller)
19103
+ ctx.abortControllers.delete(sessionId);
18896
19104
  },
18897
- abort: (ws) => ctx.abortControllers.get(ws)?.abort()
19105
+ abort: (_ws, sessionId) => ctx.abortControllers.get(sessionId)?.abort()
18898
19106
  },
18899
19107
  pendingConfirms: ctx.pendingConfirms,
18900
19108
  send: ctx.send,
@@ -18989,89 +19197,56 @@ function createEmbeddedProjectRoutes(ctx) {
18989
19197
 
18990
19198
  // src/server/embedded-message-router.ts
18991
19199
  import { makeProviderFromConfig as makeProviderFromConfig2 } from "@wrongstack/providers";
19200
+ import { planTool, taskTool, todoTool } from "@wrongstack/tools";
18992
19201
 
18993
19202
  // src/server/agent-roster-handlers.ts
18994
19203
  import {
18995
19204
  applyProjectAgentConfig,
18996
- buildConsolidationInstruction,
18997
19205
  captureLearnedFromAgentOutputDetailed,
18998
19206
  clearProjectAgentConsolidated,
19207
+ clearProjectSkillAugmentation,
18999
19208
  createProjectAgent,
19000
19209
  detectLearnedConflicts,
19210
+ evaluateAutoOptimize,
19001
19211
  FLEET_ROSTER,
19002
19212
  getProjectAgentLearnStats,
19003
19213
  isConsolidated,
19004
19214
  listProjectAgentLearnedEntries,
19005
19215
  listProjectAgentRoles,
19216
+ listProjectSkillAugmentations,
19006
19217
  loadConsolidationMetadata,
19007
19218
  loadProjectAgentConfig,
19008
19219
  loadProjectAgentConsolidated,
19009
19220
  loadProjectAgentIdentity,
19010
19221
  loadProjectAgentLearned,
19011
19222
  loadProjectAgentProfile,
19223
+ loadProjectSkillAugmentation,
19224
+ loadSkillAffinity,
19225
+ optimizeProjectAgentLearning,
19226
+ readRawLearnedEntries,
19012
19227
  resetProjectAgentIdentity,
19228
+ resolveAutoOptimizePolicy,
19229
+ resolveRoleSkillCandidates,
19013
19230
  saveProjectAgentConsolidated,
19231
+ saveProjectSkillAugmentation,
19232
+ setSkillPinned,
19014
19233
  slugifyProjectAgentRole,
19015
19234
  updateProjectAgentConfig,
19016
19235
  updateProjectAgentIdentity,
19017
19236
  updateProjectAgentLearned,
19018
19237
  updateProjectAgentLearningPolicy
19019
19238
  } from "@wrongstack/core/coordination";
19020
- import { isTextBlock } from "@wrongstack/core/types";
19021
- var CONSOLIDATION_MAX_TOKENS = 8e3;
19022
- var CONSOLIDATION_TIMEOUT_MS = 12e4;
19023
19239
  var AgentRosterWSHandler = class {
19024
19240
  getProjectRoot;
19025
19241
  getLlm;
19026
19242
  broadcast;
19243
+ getAutoOptimizeSettings;
19027
19244
  constructor(opts) {
19028
19245
  this.getProjectRoot = typeof opts.projectRoot === "function" ? opts.projectRoot : () => opts.projectRoot;
19029
19246
  this.getLlm = opts.getLlm ?? (() => void 0);
19030
19247
  this.broadcast = opts.broadcast ?? (() => {
19031
19248
  });
19032
- }
19033
- /**
19034
- * Run the consolidation LLM synthesis headlessly and return the cleaned
19035
- * document text. Returns undefined when no LLM is available so the caller
19036
- * can fall back to the instruction-only path.
19037
- */
19038
- async synthesizeConsolidation(instruction) {
19039
- const llm = this.getLlm();
19040
- if (!llm) return void 0;
19041
- const req = {
19042
- model: llm.model,
19043
- system: [
19044
- {
19045
- type: "text",
19046
- text: "You are a precise technical editor. You consolidate an AI agent role's captured learning entries into a single, durable, role-scoped instruction document. Output ONLY the consolidated markdown document \u2014 no preamble, no code fences, no commentary about the consolidation process."
19047
- }
19048
- ],
19049
- messages: [{ role: "user", content: instruction }],
19050
- maxTokens: CONSOLIDATION_MAX_TOKENS
19051
- };
19052
- const timer = new AbortController();
19053
- let timedOut = false;
19054
- const to = setTimeout(() => {
19055
- timedOut = true;
19056
- timer.abort(new Error("consolidation timeout"));
19057
- }, CONSOLIDATION_TIMEOUT_MS);
19058
- to.unref();
19059
- try {
19060
- const res = await llm.provider.complete(req, { signal: timer.signal });
19061
- const text2 = res.content.filter(isTextBlock).map((block) => block.text).join("\n").trim();
19062
- const wholeDocFence = /^```(?:markdown|md)?[^\n]*\n([\s\S]*?)\n?```\s*$/i;
19063
- const wrapped = wholeDocFence.exec(text2);
19064
- const inner = wrapped?.[1];
19065
- const unfenced = inner !== void 0 ? inner.trim() : text2;
19066
- return { content: unfenced, model: llm.model };
19067
- } catch (err) {
19068
- if (timedOut) {
19069
- throw new Error(`consolidation timed out after ${CONSOLIDATION_TIMEOUT_MS}ms`);
19070
- }
19071
- throw err;
19072
- } finally {
19073
- clearTimeout(to);
19074
- }
19249
+ this.getAutoOptimizeSettings = opts.getAutoOptimizeSettings;
19075
19250
  }
19076
19251
  /** Handle an incoming client message. Returns a response payload. */
19077
19252
  async handleMessage(_ws, type, payload) {
@@ -19276,70 +19451,47 @@ ${String(p.content ?? "")}`;
19276
19451
  const conflicts = detectLearnedConflicts(projectRoot);
19277
19452
  return { type, payload: { conflicts } };
19278
19453
  }
19279
- // ── Consolidate learned entries (headless LLM synthesis) ──────────
19280
- // Runs the whole optimization on the server: read raw entries
19281
- // synthesize with the active model write consolidated.md +
19282
- // consolidation.json. No chat round-trip. When no LLM is available the
19283
- // handler degrades to returning the instruction so a caller (e.g. the
19284
- // chat agent) can still perform the consolidation manually.
19285
- case "agent-roster.consolidate": {
19454
+ // ── Optimize: distil captures into skill addenda + a consolidated doc,
19455
+ // then archive and reset the raw buffer. Shared implementation with the
19456
+ // CLI (`optimizeProjectAgentLearning`) so both surfaces persist the same
19457
+ // artifacts instead of the CLI producing markdown nobody saved.
19458
+ case "agent-roster.consolidate":
19459
+ case "agent-roster.optimize": {
19286
19460
  if (!role) return { type, payload: { error: "role required" } };
19287
- const { instruction, rawEntries, hasExistingConsolidation } = buildConsolidationInstruction(role, projectRoot);
19288
- if (rawEntries.length === 0) {
19461
+ const hasExistingConsolidation = isConsolidated(role, projectRoot);
19462
+ const pending = readRawLearnedEntries(role, projectRoot);
19463
+ if (pending.length === 0) {
19289
19464
  return {
19290
19465
  type: "agent-roster.consolidate",
19291
19466
  payload: {
19292
19467
  role,
19293
19468
  consolidated: false,
19294
19469
  rawEntryCount: 0,
19470
+ skills: [],
19295
19471
  hasExistingConsolidation,
19296
19472
  currentStats: getProjectAgentLearnStats(role, projectRoot)
19297
19473
  }
19298
19474
  };
19299
19475
  }
19300
- let synth;
19301
- try {
19302
- synth = await this.synthesizeConsolidation(instruction);
19303
- } catch (err) {
19304
- return {
19305
- type: "agent-roster.consolidate",
19306
- payload: {
19307
- role,
19308
- consolidated: false,
19309
- rawEntryCount: rawEntries.length,
19310
- hasExistingConsolidation,
19311
- currentStats: getProjectAgentLearnStats(role, projectRoot),
19312
- error: err instanceof Error ? err.message : "consolidation failed"
19313
- }
19314
- };
19315
- }
19316
- if (synth && synth.content.length > 0) {
19317
- let stats;
19318
- let metadata;
19319
- try {
19320
- saveProjectAgentConsolidated(role, synth.content, projectRoot, {
19321
- trigger: "manual",
19322
- model: synth.model
19323
- });
19324
- stats = getProjectAgentLearnStats(role, projectRoot);
19325
- metadata = loadConsolidationMetadata(role, projectRoot);
19326
- } catch (err) {
19327
- return {
19328
- type: "agent-roster.consolidate",
19329
- payload: {
19330
- role,
19331
- consolidated: false,
19332
- rawEntryCount: rawEntries.length,
19333
- hasExistingConsolidation,
19334
- currentStats: getProjectAgentLearnStats(role, projectRoot),
19335
- error: err instanceof Error ? err.message : "failed to persist consolidation"
19336
- }
19337
- };
19338
- }
19476
+ const llm = this.getLlm();
19477
+ const result = await optimizeProjectAgentLearning(role, projectRoot, {
19478
+ ...llm ? { llm } : {},
19479
+ trigger: "manual"
19480
+ });
19481
+ const currentStats = getProjectAgentLearnStats(role, projectRoot);
19482
+ const metadata = loadConsolidationMetadata(role, projectRoot);
19483
+ const basePayload = {
19484
+ role,
19485
+ rawEntryCount: result.rawEntryCount,
19486
+ skills: result.skills,
19487
+ hasExistingConsolidation,
19488
+ currentStats
19489
+ };
19490
+ if (result.status === "optimized") {
19339
19491
  try {
19340
19492
  this.broadcast({
19341
19493
  type: "agent-roster.updated",
19342
- payload: { role, reason: "consolidated", currentStats: stats, metadata }
19494
+ payload: { role, reason: "consolidated", currentStats, metadata }
19343
19495
  });
19344
19496
  } catch (e) {
19345
19497
  console.warn(
@@ -19355,44 +19507,101 @@ ${String(p.content ?? "")}`;
19355
19507
  return {
19356
19508
  type: "agent-roster.consolidate",
19357
19509
  payload: {
19358
- role,
19510
+ ...basePayload,
19359
19511
  consolidated: true,
19360
- rawEntryCount: rawEntries.length,
19361
- content: synth.content,
19362
- model: synth.model,
19363
- currentStats: stats,
19512
+ content: result.content,
19513
+ model: result.model,
19514
+ pruned: result.pruned,
19364
19515
  metadata
19365
19516
  }
19366
19517
  };
19367
19518
  }
19368
- if (synth) {
19369
- return {
19370
- type: "agent-roster.consolidate",
19371
- payload: {
19372
- role,
19373
- consolidated: false,
19374
- emptySynthesis: true,
19375
- model: synth.model,
19376
- rawEntryCount: rawEntries.length,
19377
- hasExistingConsolidation,
19378
- currentStats: getProjectAgentLearnStats(role, projectRoot)
19379
- }
19380
- };
19381
- }
19382
19519
  return {
19383
19520
  type: "agent-roster.consolidate",
19384
19521
  payload: {
19385
- role,
19522
+ ...basePayload,
19386
19523
  consolidated: false,
19387
- instruction,
19388
- rawEntryCount: rawEntries.length,
19389
- hasExistingConsolidation,
19390
- currentStats: getProjectAgentLearnStats(role, projectRoot),
19391
- // Instruction for the leader agent to execute the consolidation
19392
- leaderInstruction: `Optimize what the "${role}" agent has learned. Read its raw learned entries, synthesize them into a single narrowly-scoped document preserving every fact, and save the result. The instruction text contains the full details and raw entries.`
19524
+ ...result.status === "empty-synthesis" ? { emptySynthesis: true, model: result.model } : {},
19525
+ ...result.status === "failed" ? { error: result.error } : {},
19526
+ ...result.status === "no-llm" ? {
19527
+ instruction: result.instruction,
19528
+ leaderInstruction: `Optimize what the "${role}" agent has learned. Read its raw learned entries, synthesize them into a single narrowly-scoped document preserving every fact, and save the result. The instruction text contains the full details and raw entries.`
19529
+ } : {}
19393
19530
  }
19394
19531
  };
19395
19532
  }
19533
+ // ── Automatic-optimization status ─────────────────────────────────
19534
+ // Read-only: says whether the background scheduler considers each role
19535
+ // eligible right now, and why not when it does not. Surfacing the reason
19536
+ // is what keeps "nothing happened" from looking like a broken feature.
19537
+ case "agent-roster.auto-optimize-status": {
19538
+ const policy = resolveAutoOptimizePolicy(
19539
+ this.getAutoOptimizeSettings?.() ?? void 0
19540
+ );
19541
+ const roles = role ? [role] : listProjectAgentRoles(projectRoot);
19542
+ return {
19543
+ type,
19544
+ payload: {
19545
+ policy,
19546
+ roles: roles.map((current2) => {
19547
+ try {
19548
+ const decision = evaluateAutoOptimize(current2, projectRoot, policy);
19549
+ return { role: current2, ...decision };
19550
+ } catch {
19551
+ return { role: current2, eligible: false, reason: "disabled" };
19552
+ }
19553
+ })
19554
+ }
19555
+ };
19556
+ }
19557
+ // ── Skill layer: what this project has developed for each role skill ──
19558
+ case "agent-roster.skills": {
19559
+ if (!role) return { type, payload: { error: "role required" } };
19560
+ const candidates = resolveRoleSkillCandidates(role, projectRoot);
19561
+ const developed = listProjectSkillAugmentations(role, projectRoot);
19562
+ const affinity = loadSkillAffinity(role, projectRoot);
19563
+ return {
19564
+ type,
19565
+ payload: {
19566
+ role,
19567
+ skills: candidates.map((skill) => ({
19568
+ skill,
19569
+ developed: developed.includes(skill),
19570
+ affinity: affinity.entries[skill] ?? null
19571
+ }))
19572
+ }
19573
+ };
19574
+ }
19575
+ case "agent-roster.read-skill": {
19576
+ const skill = typeof p.skill === "string" ? p.skill : "";
19577
+ if (!role || !skill) return { type, payload: { error: "role and skill required" } };
19578
+ return {
19579
+ type,
19580
+ payload: { role, skill, content: loadProjectSkillAugmentation(role, skill, projectRoot) }
19581
+ };
19582
+ }
19583
+ case "agent-roster.save-skill": {
19584
+ const skill = typeof p.skill === "string" ? p.skill : "";
19585
+ if (!role || !skill || typeof p.content !== "string") {
19586
+ return { type, payload: { error: "role, skill and content required" } };
19587
+ }
19588
+ const savedPath = saveProjectSkillAugmentation(role, skill, p.content, projectRoot);
19589
+ return { type, payload: { role, skill, path: savedPath, success: true } };
19590
+ }
19591
+ case "agent-roster.clear-skill": {
19592
+ const skill = typeof p.skill === "string" ? p.skill : "";
19593
+ if (!role) return { type, payload: { error: "role required" } };
19594
+ clearProjectSkillAugmentation(role, skill || void 0, projectRoot);
19595
+ return { type, payload: { role, skill: skill || null, success: true } };
19596
+ }
19597
+ case "agent-roster.pin-skill": {
19598
+ const skill = typeof p.skill === "string" ? p.skill : "";
19599
+ if (!role || !skill || typeof p.pinned !== "boolean") {
19600
+ return { type, payload: { error: "role, skill and boolean pinned required" } };
19601
+ }
19602
+ const affinity = setSkillPinned(role, skill, p.pinned, projectRoot);
19603
+ return { type, payload: { role, skill, pinned: p.pinned, affinity, success: true } };
19604
+ }
19396
19605
  // ── Save consolidated document ────────────────────────────────────
19397
19606
  case "agent-roster.save-consolidated": {
19398
19607
  if (!role || typeof p.content !== "string") {
@@ -20316,9 +20525,13 @@ function createEmbeddedMessageRouter(deps2) {
20316
20525
  // stream from the previous session would otherwise keep running in the
20317
20526
  // background after session.new/resume. The run's own end() cleanup
20318
20527
  // removes controllers from the map when it unwinds.
20319
- abortActiveRun: () => {
20320
- for (const controller of deps2.conversationCtx.abortControllers.values()) {
20321
- controller.abort();
20528
+ abortActiveRun: (sessionId) => {
20529
+ if (sessionId) {
20530
+ deps2.conversationCtx.abortControllers.get(sessionId)?.abort();
20531
+ } else {
20532
+ for (const controller of [...deps2.conversationCtx.abortControllers.values()]) {
20533
+ controller.abort();
20534
+ }
20322
20535
  }
20323
20536
  },
20324
20537
  isRunActive: () => deps2.conversationCtx.abortControllers.size > 0
@@ -20361,7 +20574,20 @@ function createEmbeddedMessageRouter(deps2) {
20361
20574
  },
20362
20575
  send: send2,
20363
20576
  broadcast: deps2.providerCtx.broadcast,
20364
- replaceTodos: (todos) => opts.agent.ctx.state.replaceTodos(todos)
20577
+ replaceTodos: (todos) => opts.agent.ctx.state.replaceTodos(todos),
20578
+ mutateTodos: async (todos) => {
20579
+ const result = await todoTool.execute({ todos }, opts.agent.ctx, {
20580
+ signal: AbortSignal.timeout(3e4)
20581
+ });
20582
+ return {
20583
+ todos: [...opts.agent.ctx.todos],
20584
+ ...result.kanban_warnings ? { warnings: result.kanban_warnings } : {}
20585
+ };
20586
+ },
20587
+ mutateTaskStatus: async (id, status) => taskTool.execute({ action: "status", id, status }, opts.agent.ctx, {
20588
+ signal: AbortSignal.timeout(3e4)
20589
+ }),
20590
+ mutatePlan: async (operation) => planTool.execute(operation, opts.agent.ctx, { signal: AbortSignal.timeout(3e4) })
20365
20591
  })
20366
20592
  });
20367
20593
  const processRoutes = {
@@ -20445,6 +20671,7 @@ function createEmbeddedMessageRouter(deps2) {
20445
20671
  agentRoster: {
20446
20672
  rosterHandler: new AgentRosterWSHandler({
20447
20673
  projectRoot,
20674
+ getAutoOptimizeSettings: () => deps2.agentConfigCtx.getConfig?.()?.fleet?.learning?.autoOptimize,
20448
20675
  getLlm: () => {
20449
20676
  const ctx = opts.agent.ctx;
20450
20677
  return ctx.provider && ctx.model ? { provider: ctx.provider, model: ctx.model } : void 0;
@@ -21496,7 +21723,6 @@ function registerSetupEventsClientStatusWriter(deps2) {
21496
21723
  }
21497
21724
 
21498
21725
  // src/server/setup-events-fleet-broadcaster.ts
21499
- import { watch as fsWatch } from "node:fs";
21500
21726
  import * as path25 from "node:path";
21501
21727
  function registerSetupEventsFleetBroadcaster(deps2) {
21502
21728
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
@@ -21505,8 +21731,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
21505
21731
  const disposers = [];
21506
21732
  const broadcastSessions = async () => {
21507
21733
  try {
21508
- const { SessionRegistry } = await import("@wrongstack/core/storage");
21509
- const registry = new SessionRegistry(globalRoot);
21734
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
21735
+ const registry = getSessionRegistry3(globalRoot);
21510
21736
  const sessions = await registry.list();
21511
21737
  const ownEntry = sessions.find((s) => s.pid === process.pid);
21512
21738
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
@@ -21553,7 +21779,7 @@ function registerSetupEventsFleetBroadcaster(deps2) {
21553
21779
  }
21554
21780
  };
21555
21781
  onFleetBroadcaster?.(broadcastSessions);
21556
- let regWatchLive = false;
21782
+ let subscriptionLive = false;
21557
21783
  let statusTimer;
21558
21784
  const scheduleStatusPoll = () => {
21559
21785
  if (isDisposed()) return;
@@ -21562,35 +21788,31 @@ function registerSetupEventsFleetBroadcaster(deps2) {
21562
21788
  void broadcastSessions();
21563
21789
  scheduleStatusPoll();
21564
21790
  },
21565
- regWatchLive ? 3e4 : 5e3
21791
+ subscriptionLive ? 3e4 : 5e3
21566
21792
  );
21567
21793
  if (statusTimer.unref) statusTimer.unref();
21568
21794
  };
21569
21795
  disposers.push(() => {
21570
21796
  if (statusTimer) clearTimeout(statusTimer);
21571
21797
  });
21572
- let regDebounce;
21573
- try {
21574
- const regWatcher = fsWatch(globalRoot, { persistent: false }, (_event, filename) => {
21575
- const name2 = filename ? String(filename) : "";
21576
- if (!name2.startsWith("session-registry.json") || name2.endsWith(".lock")) return;
21577
- if (regDebounce) clearTimeout(regDebounce);
21578
- regDebounce = setTimeout(() => void broadcastSessions(), 150);
21579
- });
21580
- regWatcher.on("error", () => {
21581
- regWatchLive = false;
21582
- try {
21583
- regWatcher.close();
21584
- } catch {
21585
- }
21586
- });
21587
- regWatchLive = true;
21588
- disposers.push(() => {
21589
- if (regDebounce) clearTimeout(regDebounce);
21590
- regWatcher.close();
21591
- });
21592
- } catch {
21593
- }
21798
+ let eventDebounce;
21799
+ let unsubscribe;
21800
+ void import("@wrongstack/core/storage").then(async ({ getSessionRegistry: getSessionRegistry3 }) => {
21801
+ const registry = getSessionRegistry3(globalRoot);
21802
+ const projectSlug2 = wpaths?.projectSlug;
21803
+ if (!projectSlug2 || isDisposed()) return;
21804
+ unsubscribe = await registry.subscribeProject(projectSlug2, context.projectRoot, () => {
21805
+ if (eventDebounce) clearTimeout(eventDebounce);
21806
+ eventDebounce = setTimeout(() => void broadcastSessions(), 25);
21807
+ });
21808
+ subscriptionLive = true;
21809
+ }).catch(() => {
21810
+ subscriptionLive = false;
21811
+ });
21812
+ disposers.push(() => {
21813
+ if (eventDebounce) clearTimeout(eventDebounce);
21814
+ void unsubscribe?.();
21815
+ });
21594
21816
  scheduleStatusPoll();
21595
21817
  void broadcastSessions();
21596
21818
  return () => {
@@ -21703,7 +21925,7 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
21703
21925
  }
21704
21926
 
21705
21927
  // src/server/setup-events-status-watcher.ts
21706
- import { watch as fsWatch2 } from "node:fs";
21928
+ import { watch as fsWatch } from "node:fs";
21707
21929
  import * as fs20 from "node:fs/promises";
21708
21930
  import * as path27 from "node:path";
21709
21931
 
@@ -21793,7 +22015,7 @@ function registerSetupEventsStatusWatcher(deps2) {
21793
22015
  try {
21794
22016
  await fs20.mkdir(projectsDir, { recursive: true });
21795
22017
  if (isDisposed()) return;
21796
- watcher = fsWatch2(
22018
+ watcher = fsWatch(
21797
22019
  projectsDir,
21798
22020
  { persistent: true, recursive: true },
21799
22021
  async (eventType, filename) => {
@@ -22955,7 +23177,6 @@ import {
22955
23177
  mailboxSessionTag,
22956
23178
  ObservableBrainArbiter as ObservableBrainArbiterCtor
22957
23179
  } from "@wrongstack/core/coordination";
22958
- import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
22959
23180
  import { installDesignStudioMiddleware } from "@wrongstack/core/design";
22960
23181
  import {
22961
23182
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
@@ -22967,6 +23188,7 @@ import {
22967
23188
  import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
22968
23189
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
22969
23190
  import {
23191
+ DEFAULT_TOOLS_CONFIG,
22970
23192
  resolveContextWindowPolicy as resolveContextWindowPolicy2
22971
23193
  } from "@wrongstack/core/types";
22972
23194
  import {
@@ -23848,6 +24070,11 @@ async function createAgentServices(input) {
23848
24070
  taskAware: config.Sage?.inject?.taskAware,
23849
24071
  minScore: config.Sage?.inject?.minScore,
23850
24072
  minImportance: config.Sage?.inject?.minImportance,
24073
+ // Forward the explicit relation floor so an operator-configured
24074
+ // `Sage.inject.relationFloor` is honored in WebUI sessions. Without
24075
+ // this we silently fall back to MIN_RELATION_STRENGTH (0.85), which
24076
+ // is the CLI default but masks operator overrides.
24077
+ relationFloor: config.Sage?.inject?.relationFloor,
23851
24078
  repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
23852
24079
  verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
23853
24080
  triggers: config.Sage?.inject?.triggers,
@@ -23869,6 +24096,10 @@ async function createAgentServices(input) {
23869
24096
  maxMemories: config.Sage?.inject?.maxTurnMemories,
23870
24097
  maxChars: config.Sage?.inject?.maxCharsPerTurn,
23871
24098
  minScore: config.Sage?.inject?.minScore,
24099
+ // CLI parity: honor `Sage.retrieval.metadataWeight` so the same config
24100
+ // value drives both runtimes instead of silently falling back to the
24101
+ // 0.3 default. The undefined case keeps the middleware's own default.
24102
+ metadataWeight: config.Sage?.retrieval?.metadataWeight,
23872
24103
  getSessionId: getSageSessionId,
23873
24104
  tracker: sageInjectionTracker
23874
24105
  })
@@ -23893,25 +24124,29 @@ async function createAgentServices(input) {
23893
24124
  strategy: config.context?.strategy,
23894
24125
  preserveK: config.context?.preserveK ?? 10,
23895
24126
  eliseThreshold: config.context?.eliseThreshold ?? 2e3,
24127
+ // Match the CLI/TUI runtime: keep corrections, errors and decisions
24128
+ // verbatim while collapsing routine assistant chatter/tool protocol.
24129
+ // Without this WebUI's hybrid strategy builds an ever-growing lossless
24130
+ // digest and eventually relies on blunt emergency head/tail trimming.
24131
+ smart: true,
23896
24132
  summarizerModel: config.context?.summarizerModel,
23897
24133
  llmSelector: config.context?.llmSelector
23898
24134
  });
23899
24135
  const initialContextPolicy = resolveContextWindowPolicy2(config.context);
23900
24136
  let autoCompactor;
23901
24137
  if (config.context?.autoCompact !== false) {
23902
- let effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
23903
- if (!effectiveMaxContext) {
23904
- try {
23905
- const m = await resolveProviderModelMetadata(
23906
- modelsRegistry,
23907
- config.provider,
23908
- context.model,
23909
- config.providers?.[config.provider]
23910
- );
23911
- effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
23912
- } catch {
23913
- }
24138
+ let effectiveMaxContext = 0;
24139
+ try {
24140
+ const m = await resolveProviderModelMetadata(
24141
+ modelsRegistry,
24142
+ config.provider,
24143
+ context.model,
24144
+ config.providers?.[config.provider]
24145
+ );
24146
+ effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
24147
+ } catch {
23914
24148
  }
24149
+ if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
23915
24150
  if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
23916
24151
  autoCompactor = new AutoCompactionMiddlewareCtor(
23917
24152
  compactor,
@@ -24432,6 +24667,7 @@ async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES)
24432
24667
 
24433
24668
  // src/server/message-dispatcher.ts
24434
24669
  import path28 from "node:path";
24670
+ import { planTool as planTool2, taskTool as taskTool2, todoTool as todoTool2 } from "@wrongstack/tools";
24435
24671
  function createMessageDispatcher(opts) {
24436
24672
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
24437
24673
  function makeWorklistContext() {
@@ -24443,7 +24679,20 @@ function createMessageDispatcher(opts) {
24443
24679
  },
24444
24680
  send: (w, m) => send(w, m),
24445
24681
  broadcast: (m) => broadcast(state.getClients(), m),
24446
- replaceTodos: (todos) => deps2.context.state.replaceTodos(todos)
24682
+ replaceTodos: (todos) => deps2.context.state.replaceTodos(todos),
24683
+ mutateTodos: async (todos) => {
24684
+ const result = await todoTool2.execute({ todos }, deps2.context, {
24685
+ signal: AbortSignal.timeout(3e4)
24686
+ });
24687
+ return {
24688
+ todos: [...deps2.context.todos],
24689
+ ...result.kanban_warnings ? { warnings: result.kanban_warnings } : {}
24690
+ };
24691
+ },
24692
+ mutateTaskStatus: async (id, status) => taskTool2.execute({ action: "status", id, status }, deps2.context, {
24693
+ signal: AbortSignal.timeout(3e4)
24694
+ }),
24695
+ mutatePlan: async (operation) => planTool2.execute(operation, deps2.context, { signal: AbortSignal.timeout(3e4) })
24447
24696
  };
24448
24697
  }
24449
24698
  function makeSkillsContext() {
@@ -24533,16 +24782,24 @@ function createMessageDispatcher(opts) {
24533
24782
  getAgent: () => deps2.agent,
24534
24783
  getSessionId: () => state.getSession().id,
24535
24784
  runControl: {
24536
- begin: () => {
24785
+ begin: (_ws, sessionId) => {
24537
24786
  if (runLock.get()) return void 0;
24538
24787
  const controller = new AbortController();
24539
24788
  runLock.set(controller);
24789
+ runLock.setSession(sessionId);
24540
24790
  return controller;
24541
24791
  },
24542
- end: (_ws, controller) => {
24543
- if (runLock.get() === controller) runLock.set(null);
24792
+ end: (_ws, _sessionId, controller) => {
24793
+ if (runLock.get() === controller) {
24794
+ runLock.set(null);
24795
+ runLock.setSession(null);
24796
+ }
24544
24797
  },
24545
- abort: () => runLock.get()?.abort()
24798
+ abort: (_ws, sessionId) => {
24799
+ if (runLock.getSession() === sessionId || !runLock.getSession()) {
24800
+ runLock.get()?.abort();
24801
+ }
24802
+ }
24546
24803
  },
24547
24804
  pendingConfirms,
24548
24805
  send,
@@ -24564,10 +24821,20 @@ function createMessageDispatcher(opts) {
24564
24821
  const goalSnapshotRoutes = {
24565
24822
  getSnapshot: () => handleGoalGet(state.getProjectRoot(), (message) => broadcast(state.getClients(), message))
24566
24823
  };
24824
+ const kanbanSupervisor = createKanbanSupervisor({
24825
+ projectRoot: () => state.getProjectRoot(),
24826
+ broadcast: (message) => broadcast(state.getClients(), message),
24827
+ log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
24828
+ });
24829
+ if (opts.onDispose) {
24830
+ const dispose = () => kanbanSupervisor.dispose();
24831
+ opts.onDispose(dispose);
24832
+ }
24567
24833
  const kanbanContext = () => ({
24568
24834
  projectRoot: state.getProjectRoot(),
24569
24835
  context: deps2.context,
24570
- broadcast: (message) => broadcast(state.getClients(), message)
24836
+ broadcast: (message) => broadcast(state.getClients(), message),
24837
+ supervisor: kanbanSupervisor
24571
24838
  });
24572
24839
  const kanbanHostRoutes = {
24573
24840
  meta: async (ws) => {
@@ -24637,6 +24904,7 @@ function createMessageDispatcher(opts) {
24637
24904
  agentRoster: {
24638
24905
  rosterHandler: new AgentRosterWSHandler({
24639
24906
  projectRoot: state.getProjectRoot,
24907
+ getAutoOptimizeSettings: () => state.getConfig().fleet?.learning?.autoOptimize,
24640
24908
  getLlm: () => {
24641
24909
  const ctx = deps2.agent.ctx;
24642
24910
  return ctx.provider && ctx.model ? { provider: ctx.provider, model: ctx.model } : void 0;
@@ -25083,6 +25351,14 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
25083
25351
  transition = transition.then(async () => {
25084
25352
  if (stopped) return;
25085
25353
  if (pendingClaim?.sessionId === sessionId) {
25354
+ await pendingClaim.claim.activate({
25355
+ sessionId,
25356
+ ...target,
25357
+ clientType: "webui",
25358
+ pid: process.pid,
25359
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
25360
+ agents: statusTracker.getAgents()
25361
+ });
25086
25362
  pendingClaim = void 0;
25087
25363
  } else {
25088
25364
  await register(sessionId, true, target);
@@ -25106,14 +25382,35 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
25106
25382
  }
25107
25383
  if (sessionId === activeSessionId) return async () => {
25108
25384
  };
25109
- const previousSessionId = activeSessionId;
25110
- const previousTarget = activeTarget;
25111
25385
  const token = Symbol(sessionId);
25112
- await register(sessionId, true, target);
25113
- pendingClaim = { sessionId, previousSessionId, token };
25386
+ if ("reserveResume" in registry && typeof registry.reserveResume === "function") {
25387
+ const reservation = await registry.reserveResume({
25388
+ sessionId,
25389
+ projectSlug: target.projectSlug,
25390
+ projectRoot: target.projectRoot
25391
+ });
25392
+ pendingClaim = { sessionId, token, claim: reservation, target };
25393
+ } else {
25394
+ await register(sessionId, true, target);
25395
+ pendingClaim = {
25396
+ sessionId,
25397
+ token,
25398
+ target,
25399
+ claim: {
25400
+ reservation: {
25401
+ reservationId: "legacy",
25402
+ targetSessionId: sessionId,
25403
+ requesterInstanceId: "legacy",
25404
+ expiresAt: Number.MAX_SAFE_INTEGER
25405
+ },
25406
+ activate: async () => void 0,
25407
+ cancel: async () => register(activeSessionId, true, activeTarget)
25408
+ }
25409
+ };
25410
+ }
25114
25411
  return async () => {
25115
25412
  if (pendingClaim?.token !== token) return;
25116
- await register(previousSessionId, true, previousTarget);
25413
+ await pendingClaim.claim.cancel();
25117
25414
  pendingClaim = void 0;
25118
25415
  };
25119
25416
  };
@@ -25818,11 +26115,11 @@ function buildRoutes(state, deps2, cb) {
25818
26115
  }
25819
26116
 
25820
26117
  // src/server/server-runtime.ts
25821
- import * as path33 from "node:path";
25822
26118
  import { createRequire as createRequire4 } from "node:module";
26119
+ import * as path33 from "node:path";
25823
26120
  import { fileURLToPath } from "node:url";
25824
- import { WebSocketServer } from "ws";
25825
26121
  import { toErrorMessage as toErrorMessage13 } from "@wrongstack/core/utils";
26122
+ import { WebSocketServer } from "ws";
25826
26123
  async function resolvePorts(opts) {
25827
26124
  const surface = opts.surface ?? "webui";
25828
26125
  const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };
@@ -26040,10 +26337,11 @@ function startHttpServer(opts) {
26040
26337
  return httpServer;
26041
26338
  }
26042
26339
  function registerShutdown(deps2) {
26043
- registerShutdownHandlers({
26340
+ return registerShutdownHandlers({
26044
26341
  flushSession: deps2.flushSession,
26045
26342
  clients: deps2.clients,
26046
26343
  servers: deps2.servers,
26344
+ onPreShutdown: deps2.onPreShutdown,
26047
26345
  onShutdown: deps2.onShutdown
26048
26346
  });
26049
26347
  }
@@ -26358,10 +26656,15 @@ async function startWebUI(opts = {}) {
26358
26656
  );
26359
26657
  }
26360
26658
  let _runLock = null;
26659
+ let _runLockSession = null;
26361
26660
  const runLockControl = {
26362
26661
  get: () => _runLock,
26363
26662
  set: (ctrl) => {
26364
26663
  _runLock = ctrl;
26664
+ },
26665
+ getSession: () => _runLockSession,
26666
+ setSession: (id) => {
26667
+ _runLockSession = id;
26365
26668
  }
26366
26669
  };
26367
26670
  const pendingConfirms = /* @__PURE__ */ new Map();
@@ -26486,6 +26789,7 @@ async function startWebUI(opts = {}) {
26486
26789
  if (ctrl) {
26487
26790
  ctrl.abort();
26488
26791
  runLockControl.set(null);
26792
+ runLockControl.setSession(null);
26489
26793
  }
26490
26794
  },
26491
26795
  isRunActive: () => runLockControl.get() !== null,
@@ -26635,6 +26939,7 @@ async function startWebUI(opts = {}) {
26635
26939
  })
26636
26940
  });
26637
26941
  const routes = buildRoutes(state, deps2, cb);
26942
+ let kanbanSupervisorDispose = null;
26638
26943
  const handleMessage = createMessageDispatcher({
26639
26944
  state,
26640
26945
  deps: deps2,
@@ -26642,7 +26947,10 @@ async function startWebUI(opts = {}) {
26642
26947
  promptsCtx,
26643
26948
  codebaseIndexing,
26644
26949
  runLock: runLockControl,
26645
- pendingConfirms
26950
+ pendingConfirms,
26951
+ onDispose: (dispose) => {
26952
+ kanbanSupervisorDispose = dispose;
26953
+ }
26646
26954
  });
26647
26955
  const mailbox = getSharedProjectMailbox5(
26648
26956
  resolveProjectDir4(context.projectRoot, wstackGlobalRoot4()),
@@ -26676,7 +26984,14 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
26676
26984
  priority: "high",
26677
26985
  senderSessionId: session.id
26678
26986
  }).catch((err) => {
26679
- console.warn(`[WebUI] security-rejection mailbox note failed: ${String(err)}`);
26987
+ console.warn(
26988
+ JSON.stringify({
26989
+ level: "warn",
26990
+ event: "webui.security_rejection_mailbox_note_failed",
26991
+ message: String(err),
26992
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
26993
+ })
26994
+ );
26680
26995
  });
26681
26996
  },
26682
26997
  goalHandler,
@@ -26690,7 +27005,9 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
26690
27005
  });
26691
27006
  wssPrimary.on("connection", handleConnection);
26692
27007
  if (wssSecondary) wssSecondary.on("connection", handleConnection);
26693
- registerShutdown({
27008
+ let unregisterShutdown = () => {
27009
+ };
27010
+ unregisterShutdown = registerShutdown({
26694
27011
  flushSession: async () => {
26695
27012
  await session.append({
26696
27013
  type: "session_end",
@@ -26706,7 +27023,12 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
26706
27023
  wssPrimary,
26707
27024
  ...wssSecondary ? [wssSecondary] : []
26708
27025
  ],
27026
+ onPreShutdown: () => {
27027
+ kanbanSupervisorDispose?.();
27028
+ kanbanSupervisorDispose = null;
27029
+ },
26709
27030
  onShutdown: async () => {
27031
+ unregisterShutdown();
26710
27032
  await todosCheckpoint.detach();
26711
27033
  await stopHeapWatchdog();
26712
27034
  credentialWatcherClose?.();